[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