[Python-modules-commits] [django-fsm-admin] 01/02: Imported Upstream version 1.2.1
Michael Fladischer
fladi at moszumanska.debian.org
Tue Jun 30 09:19:11 UTC 2015
This is an automated email from the git hooks/post-receive script.
fladi pushed a commit to branch master
in repository django-fsm-admin.
commit 931cccc262db1670b1ccddec9834890ce1b2ee5b
Author: Michael Fladischer <FladischerMichael at fladi.at>
Date: Tue Jun 30 10:39:49 2015 +0200
Imported Upstream version 1.2.1
---
.gitignore | 8 +
MANIFEST.in | 4 +
MIT-LICENSE.txt | 21 ++
README.rst | 60 ++++++
example/example/__init__.py | 0
example/example/settings.py | 127 +++++++++++++
example/example/urls.py | 12 ++
example/example/wsgi.py | 14 ++
example/fsm_example/__init__.py | 0
example/fsm_example/admin.py | 22 +++
example/fsm_example/models.py | 113 +++++++++++
.../templates/admin/fsm_example/change_form.html | 10 +
example/fsm_example/tests.py | 3 +
example/fsm_example/views.py | 3 +
example/manage.py | 10 +
fsm_admin/__init__.py | 3 +
fsm_admin/mixins.py | 211 +++++++++++++++++++++
fsm_admin/templates/fsm_admin/change_form.html | 9 +
.../templates/fsm_admin/fsm_submit_button.html | 1 +
.../fsm_admin/fsm_submit_button_grappelli.html | 1 +
.../fsm_admin/fsm_submit_button_suit.html | 1 +
fsm_admin/templates/fsm_admin/fsm_submit_line.html | 17 ++
.../fsm_admin/fsm_submit_line_grappelli.html | 15 ++
.../templates/fsm_admin/fsm_submit_line_suit.html | 15 ++
.../templates/fsm_admin/fsm_transition_hints.html | 18 ++
fsm_admin/templatetags/__init__.py | 0
fsm_admin/templatetags/fsm_admin.py | 80 ++++++++
requirements.txt | 2 +
setup.cfg | 2 +
setup.py | 42 ++++
30 files changed, 824 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..300bebd
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+build
+dist
+*.egg-info
+docs/_build
+*.pyc
+.DS_Store
+db.sqlite3
+*.tmp
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..73fd210
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,4 @@
+include README.md
+include MIT-LICENSE.txt
+recursive-include fsm_admin *
+recursive-exclude fsm_admin *.pyc
\ No newline at end of file
diff --git a/MIT-LICENSE.txt b/MIT-LICENSE.txt
new file mode 100644
index 0000000..f446b1c
--- /dev/null
+++ b/MIT-LICENSE.txt
@@ -0,0 +1,21 @@
+Copyright 2014 G Adventures
+http://www.gadventures.com
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..b23c9a6
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,60 @@
+.. _QuickCast: http://quick.as/aq8fogo
+.. _django-fsm: https://github.com/kmmbvnr/django-fsm
+
+===============================
+django-fsm-admin
+===============================
+
+Mixin and template tags to integrate django-fsm_
+state transitions into the django admin.
+
+Installation
+------------
+::
+
+ $ pip install django-fsm-admin
+
+Or from github:
+
+::
+
+ $ pip install -e git://github.com/gadventures/django-fsm-admin.git#egg=django-fsm-admin
+
+Usage
+-----
+1. Add ``fsm_admin`` to your INSTALLED_APPS
+
+2. In your ``admin.py`` file, use `FSMTransitionMixin` to add behaviour to your ModelAdmin.
+
+::
+
+ from fsm_admin.mixins import FSMTransitionMixin
+
+ class YourModelAdmin(FSMTransitionMixin, admin.ModelAdmin):
+ pass
+
+ admin.site.register(YourModel, YourModelAdmin)
+
+Try the example
+---------------
+
+::
+
+ $ git clone git at github.com:gadventures/django-fsm-admin.git
+ $ cd django-fsm-admin
+ $ mkvirtualenv fsm_admin
+ $ pip install -r requirements.txt
+ $ python fsm_admin/setup.py develop
+ $ cd example
+ $ ./manage.py syncdb
+ $ ./manage.py runserver
+
+Demo
+----
+Watch a QuickCast_ of the django-fsm-admin example
+
+.. image:: http://i.imgur.com/IJuE9Sr.png
+ :width: 728px
+ :height: 346px
+ :target: QuickCast_
+
diff --git a/example/example/__init__.py b/example/example/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/example/example/settings.py b/example/example/settings.py
new file mode 100644
index 0000000..dcdedc3
--- /dev/null
+++ b/example/example/settings.py
@@ -0,0 +1,127 @@
+"""
+Django settings for example project.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/1.6/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/1.6/ref/settings/
+"""
+
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+import os
+BASE_DIR = os.path.dirname(os.path.dirname(__file__))
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/1.6/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = 'nq-ck(53l4ne1p$2w77t6hpt)rvg4_rj1t%%xzphea+bn at i2d$'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+TEMPLATE_DEBUG = True
+
+ALLOWED_HOSTS = []
+
+
+# Application definition
+
+INSTALLED_APPS = (
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+ 'django_fsm',
+ 'fsm_admin',
+ 'fsm_example',
+)
+
+MIDDLEWARE_CLASSES = (
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+)
+
+ROOT_URLCONF = 'example.urls'
+
+WSGI_APPLICATION = 'example.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/1.6/ref/settings/#databases
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
+ }
+}
+
+TEMPLATE_LOADERS = (
+ 'django.template.loaders.filesystem.Loader',
+ 'django.template.loaders.app_directories.Loader',
+)
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/1.6/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/1.6/howto/static-files/
+
+STATIC_URL = '/static/'
+
+
+LOGGING = {
+ 'version': 1,
+ 'disable_existing_loggers': False,
+ 'formatters': {
+ 'verbose': {
+ 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s'
+ },
+ 'simple': {
+ 'format': '%(levelname)s %(message)s'
+ },
+ },
+ 'handlers': {
+ 'null': {
+ 'level': 'DEBUG',
+ 'class': 'logging.NullHandler',
+ },
+ 'console': {
+ 'level': 'DEBUG',
+ 'class': 'logging.StreamHandler',
+ 'formatter': 'simple'
+ },
+ },
+ 'loggers': {
+ 'django': {
+ 'handlers': ['null'],
+ 'propagate': True,
+ 'level': 'INFO',
+ },
+ 'geodata.models': {
+ 'handlers': ['console', ],
+ 'level': 'INFO',
+ }
+ }
+}
diff --git a/example/example/urls.py b/example/example/urls.py
new file mode 100644
index 0000000..dc0789d
--- /dev/null
+++ b/example/example/urls.py
@@ -0,0 +1,12 @@
+from django.conf.urls import patterns, include, url
+
+from django.contrib import admin
+admin.autodiscover()
+
+urlpatterns = patterns('',
+ # Examples:
+ # url(r'^$', 'example.views.home', name='home'),
+ # url(r'^blog/', include('blog.urls')),
+
+ url(r'^admin/', include(admin.site.urls)),
+)
diff --git a/example/example/wsgi.py b/example/example/wsgi.py
new file mode 100644
index 0000000..2cc360a
--- /dev/null
+++ b/example/example/wsgi.py
@@ -0,0 +1,14 @@
+"""
+WSGI config for example project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/1.6/howto/deployment/wsgi/
+"""
+
+import os
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings")
+
+from django.core.wsgi import get_wsgi_application
+application = get_wsgi_application()
diff --git a/example/fsm_example/__init__.py b/example/fsm_example/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/example/fsm_example/admin.py b/example/fsm_example/admin.py
new file mode 100644
index 0000000..6c150c1
--- /dev/null
+++ b/example/fsm_example/admin.py
@@ -0,0 +1,22 @@
+from django.contrib import admin
+
+from fsm_admin.mixins import FSMTransitionMixin
+from fsm_example.models import PublishableModel
+
+
+# Example use of FSMTransitionMixin (order is important!)
+class PublishableModelAdmin(FSMTransitionMixin, admin.ModelAdmin):
+ list_display = (
+ 'name',
+ 'display_from',
+ 'display_until',
+ 'state',
+ )
+ list_filter = (
+ 'state',
+ )
+ readonly_fields = (
+ 'state',
+ )
+
+admin.site.register(PublishableModel, PublishableModelAdmin)
diff --git a/example/fsm_example/models.py b/example/fsm_example/models.py
new file mode 100644
index 0000000..3ce571d
--- /dev/null
+++ b/example/fsm_example/models.py
@@ -0,0 +1,113 @@
+from django.db import models
+from django.utils import timezone
+
+from django_fsm import FSMField, transition
+
+
+class State(object):
+ '''
+ Constants to represent the `state`s of the PublishableModel
+ '''
+ DRAFT = 'draft' # Early stages of content editing
+ APPROVED = 'approved' # Ready to be published
+ PUBLISHED = 'published' # Visible on the website
+ EXPIRED = 'expired' # Period for which the model is set to display has passed
+ DELETED = 'deleted' # Soft delete state
+
+ CHOICES = (
+ (DRAFT, DRAFT),
+ (APPROVED, APPROVED),
+ (PUBLISHED, PUBLISHED),
+ (EXPIRED, EXPIRED),
+ (DELETED, DELETED),
+ )
+
+
+class PublishableModel(models.Model):
+
+ name = models.CharField(max_length=42, blank=False)
+
+ # One state to rule them all
+ state = FSMField(
+ default=State.DRAFT,
+ verbose_name='Publication State',
+ choices=State.CHOICES,
+ protected=True,
+ )
+
+ # For scheduled publishing
+ display_from = models.DateTimeField(blank=True, null=True)
+ display_until = models.DateTimeField(blank=True, null=True)
+
+ class Meta:
+ verbose_name = 'Post'
+ verbose_name_plural = 'Posts'
+
+ def __unicode__(self):
+ return self.name
+
+ ########################################################
+ # Transition Conditions
+ # These must be defined prior to the actual transitions
+ # to be refrenced.
+
+ def has_display_dates(self):
+ return self.display_from and self.display_until
+ has_display_dates.hint = 'Display dates are required to expire a page.'
+
+ def can_display(self):
+ '''
+ The display dates must be valid for the current date
+ '''
+ return self.check_displayable(timezone.now())
+ can_display.hint = 'The display dates may need to be adjusted.'
+
+ def is_expired(self):
+ return self.state == State.EXPIRED
+
+ def check_displayable(self, date):
+ '''
+ Check that the current date falls within this object's display dates,
+ if set, otherwise default to being displayable.
+ '''
+ if not self.has_display_dates():
+ return True
+
+ displayable = self.display_from < date and self.display_until > date
+ # Expired Pages should transition to the expired state
+ if not displayable and not self.is_expired:
+ self.expire() # Calling the expire transition
+ self.save()
+ return displayable
+
+ ########################################################
+ # Workflow (state) Transitions
+
+ @transition(field=state, source=[State.APPROVED, State.EXPIRED],
+ target=State.PUBLISHED,
+ conditions=[can_display])
+ def publish(self):
+ '''
+ Publish the object.
+ '''
+
+ @transition(field=state, source=State.PUBLISHED, target=State.EXPIRED,
+ conditions=[has_display_dates])
+ def expire(self):
+ '''
+ Automatically called when a object is detected as being not
+ displayable. See `check_displayable`
+ '''
+ self.display_until = timezone.now()
+
+ @transition(field=state, source=State.PUBLISHED, target=State.APPROVED)
+ def unpublish(self):
+ '''
+ Revert to the approved state
+ '''
+
+ @transition(field=state, source=State.DRAFT, target=State.APPROVED)
+ def approve(self):
+ '''
+ After reviewed by stakeholders, the Page is approved.
+ '''
diff --git a/example/fsm_example/templates/admin/fsm_example/change_form.html b/example/fsm_example/templates/admin/fsm_example/change_form.html
new file mode 100644
index 0000000..aaeecee
--- /dev/null
+++ b/example/fsm_example/templates/admin/fsm_example/change_form.html
@@ -0,0 +1,10 @@
+{% extends 'admin/change_form.html' %}
+{% load fsm_admin %}
+
+
+{% block submit_buttons_bottom %}{% fsm_submit_row %}{% endblock %}
+
+{% block after_field_sets %}
+ {{ block.super }}
+ {% block transition_hints %}{% fsm_transition_hints %}{% endblock %}
+{% endblock %}
diff --git a/example/fsm_example/tests.py b/example/fsm_example/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/example/fsm_example/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/example/fsm_example/views.py b/example/fsm_example/views.py
new file mode 100644
index 0000000..91ea44a
--- /dev/null
+++ b/example/fsm_example/views.py
@@ -0,0 +1,3 @@
+from django.shortcuts import render
+
+# Create your views here.
diff --git a/example/manage.py b/example/manage.py
new file mode 100755
index 0000000..2605e37
--- /dev/null
+++ b/example/manage.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+import os
+import sys
+
+if __name__ == "__main__":
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings")
+
+ from django.core.management import execute_from_command_line
+
+ execute_from_command_line(sys.argv)
diff --git a/fsm_admin/__init__.py b/fsm_admin/__init__.py
new file mode 100644
index 0000000..c7f88b3
--- /dev/null
+++ b/fsm_admin/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+__version__ = '1.2.1'
+__author__ = 'G Adventures'
diff --git a/fsm_admin/mixins.py b/fsm_admin/mixins.py
new file mode 100644
index 0000000..b7e5a3b
--- /dev/null
+++ b/fsm_admin/mixins.py
@@ -0,0 +1,211 @@
+from __future__ import unicode_literals
+
+from collections import defaultdict
+
+from django.contrib import messages
+from django.utils.translation import ugettext as _
+from django.utils.encoding import force_text
+from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
+from django.http import HttpResponseRedirect
+
+
+class FSMTransitionMixin(object):
+ """
+ Mixin to use with `admin.ModelAdmin` to support transitioning
+ a model from one state to another (workflow style).
+
+ * The change_form.html must be overriden to use the custom submit
+ row template (on a model or global level).
+
+ {% load fsm_admin %}
+ {% block submit_buttons_bottom %}{% fsm_submit_row %}{% endblock %}
+
+ * To optionally display hints to the user about what's needed
+ to transition to other states that aren't available due to unmet
+ pre-conditions, add this to the change_form as well:
+
+ {% block after_field_sets %}
+ {{ block.super }}
+ {% fsm_transition_hints %}
+ {% endblock %}
+
+ * There must be one and only one FSMField on the model.
+ * There must be a corresponding model function to run the transition,
+ generally decorated with the transition decorator. This is what
+ determines the available transitions. Without a function, the action
+ in the submit row will not be available.
+ * In the absence of specific transition permissions, the user must
+ have change permission for the model.
+ """
+ # Each transition input is named with the state field and transition.
+ # e.g. _fsmtransition-publish_state-publish
+ # _fsmtransition-revision_state-delete
+ fsm_input_prefix = '_fsmtransition'
+ # The name of one or more FSMFields on the model to transition
+ fsm_field = ['state',]
+ change_form_template = 'fsm_admin/change_form.html'
+
+ def _fsm_get_transitions(self, obj, request, perms=None):
+ """
+ Gets a list of transitions available to the user.
+
+ Available state transitions are provided by django-fsm
+ following the pattern get_available_FIELD_transitions
+ """
+ user = request.user
+ fsm_fields = self._get_fsm_field_list()
+
+ transitions = {}
+ for field in fsm_fields:
+ transitions_func = 'get_available_user_{0}_transitions'.format(field)
+ transitions[field] = getattr(obj, transitions_func)(user) if obj else []
+
+ return transitions
+
+ def get_redirect_url(self, request, obj):
+ """
+ Hook to adjust the redirect post-save.
+ """
+ return request.path
+
+ def fsm_field_instance(self, fsm_field_name):
+ """
+ Returns the actual state field instance, as opposed to
+ fsm_field attribute representing just the field name.
+ """
+ return self.model._meta.get_field_by_name(fsm_field_name)[0]
+
+ def display_fsm_field(self, obj, fsm_field_name):
+ """
+ Makes sure get_FOO_display() is used for choices-based FSM fields.
+ """
+ field_instance = self.fsm_field_instance(fsm_field_name)
+ if field_instance and field_instance.choices:
+ return getattr(obj, 'get_%s_display' % fsm_field_name)()
+ else:
+ return getattr(obj, fsm_field_name)
+
+ def response_change(self, request, obj):
+ """
+ Override of `ModelAdmin.response_change` to detect the FSM button
+ that was clicked in the submit row and perform the state transtion.
+ """
+ if not getattr(obj, '_fsmtransition_results', None):
+ return super(FSMTransitionMixin, self).response_change(request, obj)
+
+ if obj._fsmtransition_results['status'] == messages.SUCCESS:
+ msg = _('%(obj)s successfully set to %(new_state)s') % obj._fsmtransition_results
+ else:
+ msg = _('Error! %(obj)s failed to %(transition)s') % obj._fsmtransition_results
+
+ self.message_user(request, msg, obj._fsmtransition_results['status'])
+
+ opts = self.model._meta
+ redirect_url = self.get_redirect_url(request=request, obj=obj)
+
+ preserved_filters = self.get_preserved_filters(request)
+ redirect_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, redirect_url)
+ return HttpResponseRedirect(redirect_url)
+
+ def _is_transition_available(self, obj, transition, request):
+ """
+ Checks if the requested transition is available
+ """
+ transitions = []
+ for field, field_transitions in self._fsm_get_transitions(obj, request).iteritems():
+ transitions += [t.name for t in field_transitions]
+ return transitions
+
+ def _get_requested_transition(self, request):
+ """
+ Extracts the name of the transition requested by user
+ """
+ for key in request.POST.keys():
+ if key.startswith(self.fsm_input_prefix):
+ fsm_input = key.split('-')
+ return (fsm_input[1], fsm_input[2])
+ return None, None
+
+ def _do_transition(self, transition, request, obj, form, fsm_field_name):
+ original_state = self.display_fsm_field(obj, fsm_field_name)
+ msg_dict = {
+ 'obj': force_text(obj),
+ 'transition': transition,
+ 'original_state': original_state,
+ }
+ # Ensure the requested transition is available
+ available = self._is_transition_available(obj, transition, request)
+ trans_func = getattr(obj, transition, None)
+ if available and trans_func:
+ # Run the transition
+ try:
+ # Attempt to pass in the by argument if using django-fsm-log
+ trans_func(by=request.user)
+ except TypeError:
+ # If the function does not have a by attribute, just call with no arguments
+ trans_func()
+ new_state = self.display_fsm_field(obj, fsm_field_name)
+
+ # Mark the fsm_field as changed in the form so it will be
+ # picked up when the change message is constructed
+ form.changed_data.append(fsm_field_name)
+
+ msg_dict.update({'new_state': new_state, 'status': messages.SUCCESS})
+ else:
+ msg_dict.update({'status': messages.ERROR})
+
+ # Attach the results of our transition attempt
+ setattr(obj, '_fsmtransition_results', msg_dict)
+
+ def save_model(self, request, obj, form, change):
+ fsm_field, transition = self._get_requested_transition(request)
+ if transition:
+ self._do_transition(transition, request, obj, form, fsm_field)
+ super(FSMTransitionMixin, self).save_model(request, obj, form, change)
+
+ def get_transition_hints(self, obj):
+ """
+ See `fsm_transition_hints` templatetag.
+ """
+ hints = defaultdict(list)
+ transitions = self._get_possible_transitions(obj)
+
+ # Step through the conditions needed to accomplish the legal state
+ # transitions, and alert the user of any missing condition.
+ # TODO?: find a cleaner way to enumerate conditions methods?
+ for transition in transitions:
+ for condition in transition.conditions:
+
+ # If the condition is valid, then we don't need the hint
+ if condition(obj):
+ continue
+
+ hint = getattr(condition, 'hint', '')
+ if hint:
+ hints[transition.name].append(hint)
+
+ return dict(hints)
+
+ def _get_possible_transitions(self, obj):
+ """
+ Get valid state transitions from the current state of `obj`
+ """
+ fsm_fields = self._get_fsm_field_list()
+ for field in fsm_fields:
+ fsmfield = obj._meta.get_field_by_name(field)[0]
+ transitions = fsmfield.get_all_transitions(self.model)
+ for transition in transitions:
+ if transition.source in [getattr(obj, field), '*']:
+ yield transition
+
+ def _get_fsm_field_list(self):
+ """
+ Ensure backward compatibility by converting a single fsm field to
+ a list. While we are guaranteeing compatibility we should use
+ this method to retrieve the fsm field rather than directly
+ accessing the property.
+ """
+ if not isinstance(self.fsm_field, (list, tuple,)):
+ return [self.fsm_field,]
+
+ return self.fsm_field
diff --git a/fsm_admin/templates/fsm_admin/change_form.html b/fsm_admin/templates/fsm_admin/change_form.html
new file mode 100644
index 0000000..3f09886
--- /dev/null
+++ b/fsm_admin/templates/fsm_admin/change_form.html
@@ -0,0 +1,9 @@
+{% extends 'admin/change_form.html' %}
+{% load fsm_admin %}
+
+{% block submit_buttons_bottom %}{% fsm_submit_row %}{% endblock %}
+
+{% block after_field_sets %}
+ {{ block.super }}
+ {% fsm_transition_hints %}
+{% endblock %}
diff --git a/fsm_admin/templates/fsm_admin/fsm_submit_button.html b/fsm_admin/templates/fsm_admin/fsm_submit_button.html
new file mode 100644
index 0000000..af11b14
--- /dev/null
+++ b/fsm_admin/templates/fsm_admin/fsm_submit_button.html
@@ -0,0 +1 @@
+<input type="submit" value="{{ button_value }}" class="default transition-{{ transition_name }}" name="_fsmtransition-{{ fsm_field_name }}-{{ transition_name }}"/>
diff --git a/fsm_admin/templates/fsm_admin/fsm_submit_button_grappelli.html b/fsm_admin/templates/fsm_admin/fsm_submit_button_grappelli.html
new file mode 100644
index 0000000..c8d1027
--- /dev/null
+++ b/fsm_admin/templates/fsm_admin/fsm_submit_button_grappelli.html
@@ -0,0 +1 @@
+<li class="grp-float-left submit-button-container"><input type="submit" value="{{ button_value }}" class="default transition-{{ transition_name }}" name="_fsmtransition-{{ fsm_field_name }}-{{ transition_name }}"/></li>
diff --git a/fsm_admin/templates/fsm_admin/fsm_submit_button_suit.html b/fsm_admin/templates/fsm_admin/fsm_submit_button_suit.html
new file mode 100644
index 0000000..54514d0
--- /dev/null
+++ b/fsm_admin/templates/fsm_admin/fsm_submit_button_suit.html
@@ -0,0 +1 @@
+<input type="submit" value="{{ button_value }}" class="btn default transition-{{ transition_name }}" name="_fsmtransition-{{ fsm_field_name }}-{{ transition_name }}"/>
diff --git a/fsm_admin/templates/fsm_admin/fsm_submit_line.html b/fsm_admin/templates/fsm_admin/fsm_submit_line.html
new file mode 100644
index 0000000..899b0ff
--- /dev/null
+++ b/fsm_admin/templates/fsm_admin/fsm_submit_line.html
@@ -0,0 +1,17 @@
+{% load i18n admin_urls fsm_admin %}
+<div class="submit-row">
+{% if show_save %}<input type="submit" value="{% trans 'Save' %}" class="default" name="_save" />{% endif %}
+{% if show_delete_link %}
+ {% url opts|admin_urlname:'delete' original.pk|admin_urlquote as delete_url %}
+ <p class="deletelink-box"><a href="{% add_preserved_filters delete_url %}" class="deletelink">{% trans "Delete" %}</a></p>
+{% endif %}
+
+{% if show_save_as_new %}<input type="submit" value="{% trans 'Save as new' %}" name="_saveasnew" />{%endif%}
+{% if show_save_and_add_another %}<input type="submit" value="{% trans 'Save and add another' %}" name="_addanother" />{% endif %}
+{% if show_save_and_continue %}<input type="submit" value="{% trans 'Save and continue editing' %}" name="_continue" />{% endif %}
+
+{% for transition in transitions %}
+{% fsm_submit_button transition %}
+{% endfor %}
+
+</div>
diff --git a/fsm_admin/templates/fsm_admin/fsm_submit_line_grappelli.html b/fsm_admin/templates/fsm_admin/fsm_submit_line_grappelli.html
new file mode 100644
index 0000000..70b1ca8
--- /dev/null
+++ b/fsm_admin/templates/fsm_admin/fsm_submit_line_grappelli.html
@@ -0,0 +1,15 @@
+{% load i18n fsm_admin %}
+<footer class="grp-module grp-submit-row grp-fixed-footer">
+ <ul class="submit-row">
+ {% if show_delete_link %}<li class="grp-float-left delete-link-container"><a href="delete/" class="grp-button grp-delete-link">{% trans "Delete" %}</a></li>{% endif %}
+ {% if show_save %}<li class="submit-button-container"><input type="submit" value="{% trans 'Save' %}" class="default" name="_save"/></li>{% endif %}
+ {% if show_save_as_new %}<li class="submit-button-container"><input type="submit" value="{% trans 'Save as new' %}" name="_saveasnew"/></li>{% endif %}
+ {% if show_save_and_add_another %}<li class="submit-button-container"><input type="submit" value="{% trans 'Save and add another' %}" name="_addanother"/></li>{% endif %}
+ {% if show_save_and_continue %}<li class="submit-button-container"><input type="submit" value="{% trans 'Save and continue editing' %}" name="_continue"/></li>{% endif %}
+
+ {% for transition in transitions %}
+ {% fsm_submit_button transition %}
+ {% endfor %}
+
+ </ul><br clear="all" />
+</footer>
diff --git a/fsm_admin/templates/fsm_admin/fsm_submit_line_suit.html b/fsm_admin/templates/fsm_admin/fsm_submit_line_suit.html
new file mode 100644
index 0000000..83d4f1c
--- /dev/null
+++ b/fsm_admin/templates/fsm_admin/fsm_submit_line_suit.html
@@ -0,0 +1,15 @@
+{% load i18n fsm_admin %}
+<div class="submit-row clearfix">
+ {% if show_save %}<button type="submit" class="btn btn-high btn-info" name="_save" {{ onclick_attrib }}>{% trans 'Save' %}</button>{% endif %}
+ {% if show_save_and_continue %}<button type="submit" name="_continue" class=" btn btn-high" {{ onclick_attrib }}>{% trans 'Save and continue editing' %}</button>{% endif %}
+ {% if show_save_as_new %}<button type="submit" name="_saveasnew" class="btn" {{ onclick_attrib }}>{% trans 'Save as new' %}</button>{%endif%}
+ {% if show_save_and_add_another %}
+ <button type="submit" name="_addanother" class="btn" {{ onclick_attrib }} >{% trans 'Save and add another' %}</button>{% endif %}
+
+ {% for transition in transitions %}
+ {% fsm_submit_button transition %}
+ {% endfor %}
+
+ {% if show_delete_link %}<a href="delete/" class="text-error deletelink">{% trans "Delete" %}</a>
+ {% endif %}
+</div>
diff --git a/fsm_admin/templates/fsm_admin/fsm_transition_hints.html b/fsm_admin/templates/fsm_admin/fsm_transition_hints.html
new file mode 100644
index 0000000..df0414c
--- /dev/null
+++ b/fsm_admin/templates/fsm_admin/fsm_transition_hints.html
@@ -0,0 +1,18 @@
+{% if transition_hints %}
+ <div class="module aligned">
+ <h2>Hints in order to...</h2>
+
+ {% for action, hints in transition_hints.items %}
+ {% for hint in hints %}
+ <div class="form-row">
+ <div>
+ {% if forloop.first %}
+ <label><strong>{{ action|title }}</strong></label>
+ {% endif %}
+ <p>{{ hint }}</p>
+ </div>
+ </div>
+ {% endfor %}
+ {% endfor %}
+ </div>
+{% endif %}
diff --git a/fsm_admin/templatetags/__init__.py b/fsm_admin/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/fsm_admin/templatetags/fsm_admin.py b/fsm_admin/templatetags/fsm_admin.py
new file mode 100644
index 0000000..a9d9ec0
--- /dev/null
+++ b/fsm_admin/templatetags/fsm_admin.py
@@ -0,0 +1,80 @@
+from __future__ import unicode_literals
+
+from django import template
+from django.contrib.admin.templatetags.admin_modify import submit_row
+from django.conf import settings
+
+register = template.Library()
+
+import logging
+logger = logging.getLogger(__name__)
+
+FSM_SUBMIT_BUTTON_TEMPLATE = 'fsm_admin/fsm_submit_button.html'
+FSM_SUBMIT_LINE_TEMPLATE = 'fsm_admin/fsm_submit_line.html'
+if 'grappelli' in settings.INSTALLED_APPS:
+ FSM_SUBMIT_BUTTON_TEMPLATE = 'fsm_admin/fsm_submit_button_grappelli.html'
+ FSM_SUBMIT_LINE_TEMPLATE = 'fsm_admin/fsm_submit_line_grappelli.html'
+if 'suit' in settings.INSTALLED_APPS:
+ FSM_SUBMIT_BUTTON_TEMPLATE = 'fsm_admin/fsm_submit_button_suit.html'
+ FSM_SUBMIT_LINE_TEMPLATE = 'fsm_admin/fsm_submit_line_suit.html'
+
+
+ at register.inclusion_tag(FSM_SUBMIT_BUTTON_TEMPLATE)
+def fsm_submit_button(transition):
+ """
+ Render a submit button that requests an fsm state transition for a
+ single state.
+ """
+ fsm_field_name, button_value, transition_name = transition
+ return {
+ 'button_value': button_value,
+ 'fsm_field_name': fsm_field_name,
+ 'transition_name': transition_name,
+ }
+
+
+ at register.inclusion_tag(FSM_SUBMIT_LINE_TEMPLATE, takes_context=True)
+def fsm_submit_row(context):
+ """
+ Additional context added to an overridded submit row that adds links
+ to change the state of an FSMField.
+ """
+ original = context.get('original', None)
+ model_name = original.__class__._meta.verbose_name if original else ''
+
+ def button_name(transition):
+ if hasattr(transition, 'custom') and 'button_name' in transition.custom:
+ return transition.custom['button_name']
+ else:
+ # Make the function name the button title, but prettier
+ return '{0} {1}'.format(transition.name.replace('_',' '), model_name).title()
+
+ # The model admin defines which field we're dealing with
+ # and has some utils for getting the transitions.
+ request = context['request']
+ model_admin = context.get('adminform').model_admin
+ transitions = model_admin._fsm_get_transitions(original, request)
+
+ ctx = submit_row(context)
+ ctx['transitions'] = []
+ for field,field_transitions in transitions.iteritems():
+ ctx['transitions'] += [(field, button_name(t), t.name) for t in field_transitions]
+ ctx['perms'] = context['perms']
+
+ return ctx
+
+
+ at register.inclusion_tag('fsm_admin/fsm_transition_hints.html', takes_context=True)
+def fsm_transition_hints(context):
+ """
+ Displays hints about why a state transition might not be applicable for
+ this the model.
+ """
+ original = context.get('original', None)
+ if not original:
+ return {}
+
+ model_admin = context.get('adminform').model_admin
+ return {
+ 'transition_hints': model_admin.get_transition_hints(original)
+ }
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..67081f8
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+Django>=1.6,<1.7
+django-fsm==2.0.1
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..5e40900
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,2 @@
+[wheel]
+universal = 1
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..bc12849
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+import os
+import sys
+from setuptools import setup, find_packages
+
+import fsm_admin
+
... 34 lines suppressed ...
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/django-fsm-admin.git
More information about the Python-modules-commits
mailing list