pax_global_header 0000666 0000000 0000000 00000000064 12437377671 0014533 g ustar 00root root 0000000 0000000 52 comment=10289691c5cc73d17c7dcbeba02d10a098550bcc django-fsm-admin-1.2.1/ 0000775 0000000 0000000 00000000000 12437377671 0014647 5 ustar 00root root 0000000 0000000 django-fsm-admin-1.2.1/.gitignore 0000664 0000000 0000000 00000000103 12437377671 0016631 0 ustar 00root root 0000000 0000000 build dist *.egg-info docs/_build *.pyc .DS_Store db.sqlite3 *.tmp django-fsm-admin-1.2.1/MANIFEST.in 0000664 0000000 0000000 00000000151 12437377671 0016402 0 ustar 00root root 0000000 0000000 include README.md include MIT-LICENSE.txt recursive-include fsm_admin * recursive-exclude fsm_admin *.pyc django-fsm-admin-1.2.1/MIT-LICENSE.txt 0000664 0000000 0000000 00000002067 12437377671 0017126 0 ustar 00root root 0000000 0000000 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. django-fsm-admin-1.2.1/README.rst 0000664 0000000 0000000 00000002357 12437377671 0016345 0 ustar 00root root 0000000 0000000 .. _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@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_ django-fsm-admin-1.2.1/example/ 0000775 0000000 0000000 00000000000 12437377671 0016302 5 ustar 00root root 0000000 0000000 django-fsm-admin-1.2.1/example/example/ 0000775 0000000 0000000 00000000000 12437377671 0017735 5 ustar 00root root 0000000 0000000 django-fsm-admin-1.2.1/example/example/__init__.py 0000664 0000000 0000000 00000000000 12437377671 0022034 0 ustar 00root root 0000000 0000000 django-fsm-admin-1.2.1/example/example/settings.py 0000664 0000000 0000000 00000005656 12437377671 0022163 0 ustar 00root root 0000000 0000000 """ 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@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', } } } django-fsm-admin-1.2.1/example/example/urls.py 0000664 0000000 0000000 00000000452 12437377671 0021275 0 ustar 00root root 0000000 0000000 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)), ) django-fsm-admin-1.2.1/example/example/wsgi.py 0000664 0000000 0000000 00000000605 12437377671 0021261 0 ustar 00root root 0000000 0000000 """ 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() django-fsm-admin-1.2.1/example/fsm_example/ 0000775 0000000 0000000 00000000000 12437377671 0020602 5 ustar 00root root 0000000 0000000 django-fsm-admin-1.2.1/example/fsm_example/__init__.py 0000664 0000000 0000000 00000000000 12437377671 0022701 0 ustar 00root root 0000000 0000000 django-fsm-admin-1.2.1/example/fsm_example/admin.py 0000664 0000000 0000000 00000001006 12437377671 0022241 0 ustar 00root root 0000000 0000000 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) django-fsm-admin-1.2.1/example/fsm_example/models.py 0000664 0000000 0000000 00000006600 12437377671 0022441 0 ustar 00root root 0000000 0000000 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. ''' django-fsm-admin-1.2.1/example/fsm_example/templates/ 0000775 0000000 0000000 00000000000 12437377671 0022600 5 ustar 00root root 0000000 0000000 django-fsm-admin-1.2.1/example/fsm_example/templates/admin/ 0000775 0000000 0000000 00000000000 12437377671 0023670 5 ustar 00root root 0000000 0000000 django-fsm-admin-1.2.1/example/fsm_example/templates/admin/fsm_example/ 0000775 0000000 0000000 00000000000 12437377671 0026170 5 ustar 00root root 0000000 0000000 django-fsm-admin-1.2.1/example/fsm_example/templates/admin/fsm_example/change_form.html 0000664 0000000 0000000 00000000420 12437377671 0031322 0 ustar 00root root 0000000 0000000 {% 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 %} django-fsm-admin-1.2.1/example/fsm_example/tests.py 0000664 0000000 0000000 00000000074 12437377671 0022317 0 ustar 00root root 0000000 0000000 from django.test import TestCase # Create your tests here. django-fsm-admin-1.2.1/example/fsm_example/views.py 0000664 0000000 0000000 00000000077 12437377671 0022315 0 ustar 00root root 0000000 0000000 from django.shortcuts import render # Create your views here. django-fsm-admin-1.2.1/example/manage.py 0000775 0000000 0000000 00000000372 12437377671 0020111 0 ustar 00root root 0000000 0000000 #!/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) django-fsm-admin-1.2.1/fsm_admin/ 0000775 0000000 0000000 00000000000 12437377671 0016604 5 ustar 00root root 0000000 0000000 django-fsm-admin-1.2.1/fsm_admin/__init__.py 0000664 0000000 0000000 00000000112 12437377671 0020707 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*- __version__ = '1.2.1' __author__ = 'G Adventures' django-fsm-admin-1.2.1/fsm_admin/mixins.py 0000664 0000000 0000000 00000020404 12437377671 0020465 0 ustar 00root root 0000000 0000000 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 django-fsm-admin-1.2.1/fsm_admin/templates/ 0000775 0000000 0000000 00000000000 12437377671 0020602 5 ustar 00root root 0000000 0000000 django-fsm-admin-1.2.1/fsm_admin/templates/fsm_admin/ 0000775 0000000 0000000 00000000000 12437377671 0022537 5 ustar 00root root 0000000 0000000 django-fsm-admin-1.2.1/fsm_admin/templates/fsm_admin/change_form.html 0000664 0000000 0000000 00000000343 12437377671 0025675 0 ustar 00root root 0000000 0000000 {% 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 %} django-fsm-admin-1.2.1/fsm_admin/templates/fsm_admin/fsm_submit_button.html 0000664 0000000 0000000 00000000244 12437377671 0027170 0 ustar 00root root 0000000 0000000 django-fsm-admin-1.2.1/fsm_admin/templates/fsm_admin/fsm_submit_button_grappelli.html 0000664 0000000 0000000 00000000334 12437377671 0031227 0 ustar 00root root 0000000 0000000
django-fsm-admin-1.2.1/fsm_admin/templates/fsm_admin/fsm_submit_button_suit.html 0000664 0000000 0000000 00000000250 12437377671 0030231 0 ustar 00root root 0000000 0000000 django-fsm-admin-1.2.1/fsm_admin/templates/fsm_admin/fsm_submit_line.html 0000664 0000000 0000000 00000001566 12437377671 0026614 0 ustar 00root root 0000000 0000000 {% load i18n admin_urls fsm_admin %}{{ hint }}