Initial commit master
authorEric Mc Sween <eric.mcsween@auf.org>
Thu, 13 Oct 2011 18:28:07 +0000 (14:28 -0400)
committerEric Mc Sween <eric.mcsween@auf.org>
Thu, 13 Oct 2011 18:28:07 +0000 (14:28 -0400)
14 files changed:
.gitignore [new file with mode: 0644]
CHANGES [new file with mode: 0644]
auf/__init__.py [new file with mode: 0644]
auf/django/__init__.py [new file with mode: 0644]
auf/django/workflow/__init__.py [new file with mode: 0644]
auf/django/workflow/admin.py [new file with mode: 0644]
auf/django/workflow/forms.py [new file with mode: 0644]
auf/django/workflow/migrations/0001_initial.py [new file with mode: 0644]
auf/django/workflow/migrations/__init__.py [new file with mode: 0644]
auf/django/workflow/models.py [new file with mode: 0644]
auf/django/workflow/tests.py [new file with mode: 0644]
auf/django/workflow/views.py [new file with mode: 0644]
setup.cfg [new file with mode: 0644]
setup.py [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..499a075
--- /dev/null
@@ -0,0 +1,4 @@
+*.pyc
+*egg-info
+build
+dist
diff --git a/CHANGES b/CHANGES
new file mode 100644 (file)
index 0000000..b0ef91a
--- /dev/null
+++ b/CHANGES
@@ -0,0 +1,35 @@
+
+0.14dev (2011-05-27)
+-------------------
+- Ajout d'accesseurs pour les états disponibles à partir du WorkflowFormMixin
+
+0.13dev (2011-05-24)
+-------------------
+- Ajout d'accesseurs pour le commentaire de worflow 
+
+0.12dev (2011-05-24)
+-------------------
+- Amélioration du WorkflowFormMixin pour qu'il puisse générer les changements d'états disponibles
+  sous forme de bouton submit
+
+0.11dev (2011-05-20)
+-------------------
+- textes du champs en unicode
+
+0.10dev (2011-05-10)
+-------------------
+- Ajout d'une méthode d'affichage d'un commentaire cohérent avec les libellés des états de workflow
+
+0.9dev (2011-04-28)
+-------------------
+- Ajout d'un modèle pour tracer les changements d'états (personne, date, etats, commentaire)
+
+- Création d'un formulaire Mixin pour changer un état agrémenté d'un commentaire
+
+0.8dev (2011-03-12)
+-------------------
+- Fix transfert des paramètres à la classe parente pour un modelForm
+
+0.7dev (2011-03-11)
+-------------------
+- Fix initialisation des valeurs par défaut d'un formulaire
diff --git a/auf/__init__.py b/auf/__init__.py
new file mode 100644 (file)
index 0000000..35cf25b
--- /dev/null
@@ -0,0 +1,5 @@
+try:
+    __import__('pkg_resources').declare_namespace(__name__)
+except:
+    # bootstrapping
+    pass
diff --git a/auf/django/__init__.py b/auf/django/__init__.py
new file mode 100644 (file)
index 0000000..35cf25b
--- /dev/null
@@ -0,0 +1,5 @@
+try:
+    __import__('pkg_resources').declare_namespace(__name__)
+except:
+    # bootstrapping
+    pass
diff --git a/auf/django/workflow/__init__.py b/auf/django/workflow/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/auf/django/workflow/admin.py b/auf/django/workflow/admin.py
new file mode 100644 (file)
index 0000000..c15603c
--- /dev/null
@@ -0,0 +1,16 @@
+# -*- encoding: utf-8 -*-
+
+from django.forms import widgets
+from django.contrib import admin
+from forms import form_initialiser_etats
+
+class WorkflowAdmin(admin.ModelAdmin):
+    
+    def get_form(self, request, obj=None, **kwargs):
+        """
+        filtrage de la liste de choix d'état au moment où on a accès à l'objet request,
+        pour le rendre disponible dans le workflow.
+        """
+        form = super(WorkflowAdmin, self).get_form(request, obj, **kwargs)
+        form_initialiser_etats(form, request, obj)
+        return form
diff --git a/auf/django/workflow/forms.py b/auf/django/workflow/forms.py
new file mode 100644 (file)
index 0000000..5173d85
--- /dev/null
@@ -0,0 +1,89 @@
+# -*- encoding: utf-8 -*-
+
+from django import forms
+from django.forms.widgets import Select
+from models import NOM_CHAMPS_ETAT, TEXTE_CHAMPS_ETAT, HELP_CHAMPS_ETAT, WorkflowCommentaire
+
+def form_initialiser_etats(form, request, instance=None):
+    """
+    Modifier le champs 'etat' à travers le form, pour lui fournir la liste
+    des états en fonction du contexte.
+    """
+    # ModelForm
+    fields = getattr(form, 'fields', None)
+    # AdminForm
+    if not fields:
+        fields = getattr(form, 'base_fields', None)
+    if instance:
+        choices = instance.get_etats_disponibles(request)
+        fields[NOM_CHAMPS_ETAT].choices = choices
+        fields[NOM_CHAMPS_ETAT].widget = Select(choices=choices)
+    else:
+        etat_initial = form.Meta.model.etat_initial
+        libelle_etat_initial = form.Meta.model.etats[etat_initial]
+        fields[NOM_CHAMPS_ETAT].initial = etat_initial
+        choices = [(etat_initial, libelle_etat_initial), ]
+
+        fields[NOM_CHAMPS_ETAT].choices = choices
+        fields[NOM_CHAMPS_ETAT].widget = Select(choices=choices)
+
+
+class WorkflowFormMixin(forms.ModelForm):
+    """
+    Mixin pour crééer un formulaire avec l'état dans le frontend.
+    Pour faire un formulaire dans l'application, spécifier le modèle ainsi que les
+    champs à afficher.
+    """
+    bouton_libelles = {}
+    etat = forms.ChoiceField(label=TEXTE_CHAMPS_ETAT, help_text=HELP_CHAMPS_ETAT, required=False)
+    commentaire = forms.CharField(widget=forms.Textarea, required=False)
+    
+    def __init__(self, *args, **kwargs):
+        self.request = kwargs.pop('request', None)
+        if self.request is None:
+            raise Exception("Workflow form doit est construit en connaissant le request")
+        instance = kwargs.get('instance', None)
+        super(WorkflowFormMixin, self).__init__(*args, **kwargs)
+        form_initialiser_etats(self, request=self.request, instance=instance)
+
+    def clean(self):
+        """
+        Substitution des valeurs de boutons au champs 'état'
+        """
+        etats = [k for k,v in self.fields[NOM_CHAMPS_ETAT].choices]
+        for k, v in self.data.items():
+            if k in etats:
+                self.cleaned_data[NOM_CHAMPS_ETAT] = k
+        return self.cleaned_data
+
+    def save(self, *args, **kwargs):
+       commentaire = WorkflowCommentaire()
+       commentaire.content_object = self.instance
+       commentaire.texte = self.data.get('commentaire', '')
+       commentaire.etat_initial = self.instance._etat_courant
+       commentaire.etat_final = self.instance.etat
+       commentaire.owner = self.request.user
+       commentaire.save()
+       return super(forms.ModelForm, self).save(*args, **kwargs)
+
+    def get_etats_disponibles(self, sans_etat_courant=True):
+        return [(k, v) for k, v in self.fields[NOM_CHAMPS_ETAT].choices \
+                if sans_etat_courant and self.instance.etat != k]
+
+    def get_input_etats_as_buttons(self,):
+        html = ""
+        # on oblige la validation, le commentaire sans changement
+        # d'étape n'est pas permis
+        etats = self.get_etats_disponibles()
+        if len(etats) == 0:
+            return u"Aucune action n'est disponible pour vous à cette étape"
+        for k, v in etats :
+            if self.bouton_libelles.has_key(k):
+                label = self.bouton_libelles[k]
+            else:
+                label = v
+            html += u"<input type='submit' name='%s' value='%s' />" % (k, label)
+        return html
+
+        
+
diff --git a/auf/django/workflow/migrations/0001_initial.py b/auf/django/workflow/migrations/0001_initial.py
new file mode 100644 (file)
index 0000000..0f52a41
--- /dev/null
@@ -0,0 +1,81 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+    
+    def forwards(self, orm):
+        
+        # Adding model 'WorkflowCommentaire'
+        db.create_table('workflow_workflowcommentaire', (
+            ('etat_initial', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
+            ('etat_final', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
+            ('texte', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
+            ('object_id', self.gf('django.db.models.fields.PositiveIntegerField')()),
+            ('owner', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
+            ('content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'])),
+            ('date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+        ))
+        db.send_create_signal('workflow', ['WorkflowCommentaire'])
+    
+    
+    def backwards(self, orm):
+        
+        # Deleting model 'WorkflowCommentaire'
+        db.delete_table('workflow_workflowcommentaire')
+    
+    
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'max_length': '30', 'unique': 'True'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'workflow.workflowcommentaire': {
+            'Meta': {'object_name': 'WorkflowCommentaire'},
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'etat_final': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'etat_initial': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+            'texte': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        }
+    }
+    
+    complete_apps = ['workflow']
diff --git a/auf/django/workflow/migrations/__init__.py b/auf/django/workflow/migrations/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/auf/django/workflow/models.py b/auf/django/workflow/models.py
new file mode 100644 (file)
index 0000000..57c3147
--- /dev/null
@@ -0,0 +1,214 @@
+# -*- encoding: utf-8 -*-
+
+from django.utils.functional import curry
+from django.forms import widgets
+from django.db import models
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.contenttypes import generic
+
+NOM_CHAMPS_ETAT = 'etat'
+TEXTE_CHAMPS_ETAT = u'Statut (étape)'
+HELP_CHAMPS_ETAT = u"Ce champs montre par défaut l'étape où est rendu le contenu. La liste déroulante présente les changements d'états autorisés."
+
+class WorkflowCommentaire(models.Model):
+    """
+    Modèle permettant de stocker un commentaire avec le changement d'état.
+    """
+    texte = models.TextField(blank=True, null=True)
+    etat_initial = models.CharField(max_length=255, blank=True, null=True)
+    etat_final = models.CharField(max_length=255, blank=True, null=True)
+    date = models.DateTimeField(auto_now_add=True)
+    owner = models.ForeignKey("auth.User")
+
+    content_type = models.ForeignKey(ContentType)
+    object_id = models.PositiveIntegerField()
+    content_object = generic.GenericForeignKey('content_type', 'object_id')
+
+    def get_etat_label(self, code_etat):
+        """
+        Recherche la traduction d'un code état en inspectant le champs 'etat'
+        de l'objet associé au commentaire.
+        """
+        f_etat = self.content_object._meta.get_field_by_name(NOM_CHAMPS_ETAT)[0]
+        for code, label in f_etat.choices:
+            if code_etat == code:
+                return label
+        return code_etat
+
+    def get_etat_final_label(self):
+        return self.get_etat_label(self.etat_final)
+
+    def get_etat_initial_label(self):
+        return self.get_etat_label(self.etat_initial)
+
+    def __unicode__(self):
+        if self.etat_initial is None:
+            etats = u"%s" % get_etat_final_label()
+        else:
+            etats = u"%s => %s" % (self.get_etat_initial_label(), self.get_etat_final_label())
+        if self.texte is not "":
+            texte = u" : %s" % self.texte
+        else:
+            texte = u""
+
+        return u"[%s %s] (%s) %s" % (self.date, self.owner, etats, texte)
+
+class WorkflowMixin(models.Model):
+    """
+    Classe abstraite pour implanter un workflow simple.
+    """
+    # conserve le request pour connaitre l'utilisateur connecté, et le rendre
+    # disponible dans les fonctions de d'accès et d'avertissement
+    _request = None
+
+    # conserve une trace de l'état courant car la variable 'etat' prend la nouvelle
+    # valeur après validation mais on veut conserver cette information pour faire des
+    # fonctions d'avertissements plus précise en fonction de l'action
+    # (caractérisée par un état initial et un état final).
+    _etat_courant = None
+
+    # variable utilisées pour définir le workflow
+    etat_initial = None
+    etats = {}
+    actions = {}
+
+    # nouveau champ ajouté dans la classe fille
+    etat = models.CharField(verbose_name=TEXTE_CHAMPS_ETAT, help_text=HELP_CHAMPS_ETAT, max_length=20, default=etat_initial, blank = True, null = True)
+
+    # accéder aux commentaires
+    commentaires = generic.GenericRelation(WorkflowCommentaire)
+
+
+    class Meta:
+        abstract = True
+
+    def __init__(self, *args, **kwargs):
+        super(WorkflowMixin, self).__init__(*args, **kwargs)
+        
+        field_object, model, direct, m2m = self._meta.get_field_by_name(NOM_CHAMPS_ETAT)
+
+        # test si les états définis respectent les critères de validations du champs 'état'
+        for e in self.etats.keys():
+            if len(e) >= field_object.max_length:
+                raise Exception(u"Nom de l'état trop long :'%s' (max: %s)" % (e, field_object.max_length))
+
+        # genère l'accesseur pour l'objet
+        setattr(self.__class__, 'get_%s_display' % NOM_CHAMPS_ETAT, curry(self.__class__._get_FIELD_display, field=field_object))
+
+        field_object.default = self.etat_initial
+        # fournir tous les états disponibles pour que la validation soit acceptée.
+        # le filtrage s'opère dans le rendu du form dans l'admin du Worflow
+        if self.id:
+            field_object._choices = [(k, v) for k,v in self.etats.items()]
+        # dans le cas d'un nouvel objet, limiter la liste à son état initial
+        else:
+            field_object._choices = [(k, v) for k,v in self.etats.items() if k == self.etat_initial]
+        field_object.widget = widgets.Select()
+
+        # initialisation de l'état par l'état initial prédéfini
+        if self.etat is None:
+            self.etat = self.etat_initial
+
+    def test_droit_action(self, code_action, action, request):
+        """
+        Fonction utilisée pour savoir si une action est disponible.
+        Elle s'assure tout d'abord que l'action testée possède un état
+        initial identique à l'état courant de l'objet.
+        Ensuite, elle appelle une fonction respectant une convention de nommage,
+        pour savoir si l'action est disponible.
+        """
+        
+        # recherche et appel de la fonction qui déterminera si l'action est 
+        # véritablement disponible, autrement une exception est propagée
+        nom_fonction_droit = "acces_%s" % code_action
+        nom_fonction_droit_lower = nom_fonction_droit.lower()
+        fonction_droit = getattr(self, nom_fonction_droit_lower, None)
+        if fonction_droit:
+            return fonction_droit(action, request)
+        else:
+            raise Exception("Méthode manquante dans la classe de Workflow : '%s'" % nom_fonction_droit_lower)
+
+    def get_action(self, etat_initial, etat_final):
+        """
+        Rechercher l'action en fonction des états.
+        Si les états sont identiques, None est retourné (pas d'action).
+        """
+        # pas d'action sans changement d'état
+        if etat_initial == etat_final:
+            return None
+
+        for action, proprietes in self.actions.items():
+            match_initial = self.est_dans_etat_initial(etat_initial, action)
+            if match_initial and  proprietes['etat_final'] == etat_final:
+               return action
+        raise Exception(u"Ces 2 états ne permettent pas d'identifier une action (initial:%s final:%s)" % (etat_initial, etat_final))
+
+    def est_dans_etat_initial(self, etat, nom_action):
+        """Test si un etat est présent dans les états initiaux possible de l'action"""
+        proprietes = self.actions[nom_action]
+        if getattr(proprietes['etat_initial'], '__iter__', False):
+            match_initial = etat in proprietes['etat_initial']
+        else:
+            match_initial = etat == proprietes['etat_initial']
+        return match_initial
+
+    def get_etats_disponibles(self, request=None):
+        """
+        Retourne la liste des états suivants possibles, en tenant compte des permissions.
+        """
+        self._request = request
+        prochains_etats = [proprietes['etat_final'] for action, proprietes in self.actions.items() if self.est_dans_etat_initial(self.etat, action) and self.test_droit_action(action, proprietes, request)]
+        if not self.etats.has_key(self.etat):
+            aide = u"""Si la valeur non permise est 'None', vous avez peut-être introduit le Workflow après que du contenu était déjà présent.
+            Dans ce cas, vous devez migrer ces anciens contenus en leur spécifiant une étape de workflow valide"""
+            raise Exception("valeur non permise '%s' (%s)" % (self.etat, aide))
+
+        # en premier, garder l'état courant
+        etats = [(self.etat,  self.etats[self.etat]), ]
+
+        #puis cumuler les états suivantes disponible à partir de l'état courant
+        for e in prochains_etats:
+            etats.append((e, self.etats[e]))
+        return etats
+
+    def __setattr__(self, name, value):
+        """
+        Contrôle de l'assignation des variables, et traitement spécial pour l'état
+        afin qu'il puisse être conservé.
+        """
+        if name == NOM_CHAMPS_ETAT and self.__dict__.has_key(name) and self.__dict__[name] != value:
+            self._etat_courant = self.__dict__[name]
+            action_valide = self.get_action(self._etat_courant, value)
+        return object.__setattr__(self, name, value)
+
+
+    def save(self, force_insert=False, force_update=False, using=None):
+        """
+        Surcharge de la sauvegarde pour éventuellement exécuter la fonction de changement d'état.
+        """
+        # s'assurer que l'état est valide
+        # (devrait être testé au moment de l'assignation)
+
+        super(WorkflowMixin, self).save(force_insert, force_update, using)
+
+        # execution de traitement spécial de changement d'état (optionnel)
+        nom_fonction_transition = "passage_de_%s_a_%s" % (self._etat_courant , self.etat)
+        nom_fonction_transition_lower = nom_fonction_transition.lower()
+
+        
+        try:
+            fonction_transition = getattr(self, nom_fonction_transition_lower, None)
+        except:
+            raise Exception(u"le nom généré de la fonction de transition est incorrect : %s" % fonction_transition)
+            
+        if fonction_transition:
+            fonction_transition(self.get_action(self._etat_courant, self.etat), self._request)
+        
+        # sauvegarde l'état courant
+        self._etat_courant = self.etat
+
+    def historique_asc(self):
+        return self.commentaires.order_by("date").all()
+
+    def historique_desc(self):
+        return self.commentaires.order_by("-date").all()
diff --git a/auf/django/workflow/tests.py b/auf/django/workflow/tests.py
new file mode 100644 (file)
index 0000000..81ef449
--- /dev/null
@@ -0,0 +1,204 @@
+# -*- encoding: utf-8 -*-
+
+
+from django.test import TestCase
+from django.db import models
+from models import WorkflowMixin
+
+
+#codes actions
+ACTION_0 = 'action_0'
+ACTION_1 = 'action_1'
+ACTION_2 = 'action_2'
+ACTION_3 = 'action_3'
+ACTION_4 = 'action_4'
+ACTION_5 = 'action_5'
+
+# codes états
+ETAT_0 = 'etat_0'
+ETAT_1 = 'etat_1'
+ETAT_2 = 'etat_2'
+ETAT_3 = 'etat_3'
+ETAT_4 = 'etat_4'
+
+#libellés états
+ETATS = {
+    ETAT_0 : ETAT_0,
+    ETAT_1 : ETAT_1,
+    ETAT_2 : ETAT_2,
+    ETAT_3 : ETAT_3,
+    ETAT_4 : ETAT_4,
+    }
+
+# définition du worflow séquentiel
+ACTIONS = {
+    ACTION_0 : {
+        'nom' : ACTION_0,
+        'etat_initial' : None,
+        'etat_final' : ETAT_0,
+    },
+    ACTION_1 : {
+        'nom' : ACTION_1,
+        'etat_initial' : ETAT_0,
+        'etat_final' : ETAT_1,
+    },
+    ACTION_2 : {
+        'nom' : ACTION_2,
+        'etat_initial' : ETAT_1,
+        'etat_final' : ETAT_2,
+    },
+    ACTION_3 : {
+        'nom' : ACTION_3,
+        'etat_initial' : ETAT_2,
+        'etat_final' : ETAT_3,
+    },
+    ACTION_4 : {
+        'nom' : ACTION_4,
+        'etat_initial' : ETAT_0,
+        'etat_final' : ETAT_3,
+    },
+    ACTION_5 : {
+        'nom' : ACTION_5,
+        'etat_initial' : (ETAT_0, ETAT_1,),
+        'etat_final' : ETAT_4,
+    },
+}
+
+class KlassWorkflow(WorkflowMixin):
+    """
+    Classe décrivant un workflow
+    """
+    etat_initial = ETAT_0
+    etats = ETATS
+    actions = ACTIONS
+    passage = None
+
+    def acces_action_0(self, request, action):
+        return True
+
+    def acces_action_1(self, request, action):
+        return True
+
+    def acces_action_2(self, request, action):
+        return True
+
+    def acces_action_3(self, request, action):
+        return True
+
+    def acces_action_4(self, request, action):
+        return True
+
+    def acces_action_5(self, request, action):
+        return True
+
+    def passage_de_none_a_etat_0(self, request, action):
+        self.passage = 0
+
+    def passage_de_etat_0_a_etat_1(self, request, action):
+        self.passage = 1
+
+    def passage_de_etat_0_a_etat_0(self, request, action):
+        self.passage = 2
+
+    class Meta:
+        abstract = True
+
+
+class WFKlass(KlassWorkflow, models.Model):
+    """
+    Modèle django classique héritant d'un classe de workflow
+    """
+    titre = models.CharField(max_length=10,)
+
+    def __unicode__(self, ):
+        return self.titre
+
+class WorkFlowTest(TestCase):
+
+    def test_passage_initial(self):
+        """
+        test de l'initialisation de l'état initial
+        """
+        obj = WFKlass(titre="1")
+        obj.save()
+        self.assertEqual(obj.etat, ETAT_0)
+        self.assertEqual(obj.passage, 0)
+
+    def test_passage_etat(self):
+        """
+        test d'un passage d'état permis
+        """
+        obj = WFKlass(titre="2")
+        obj.save()
+        self.assertEqual(obj.etat, ETAT_0)
+        self.assertEqual(obj.passage, 0)
+        obj.etat = ETAT_1
+        obj.save()
+        self.assertEqual(obj.etat, ETAT_1)
+        self.assertEqual(obj.passage, 1)
+
+    def test_passage_interdit(self):
+        """
+        WF impossible de 0 à 2
+        """
+        obj = WFKlass(titre="3")
+        obj.save()
+        self.assertRaises(Exception, setattr, obj, 'etat', ETAT_2)
+
+    def test_fixer_au_meme_etat(self):
+        """
+        test que le fonction de sauvegarde au même état est appelée
+        """
+        obj = WFKlass(titre="4")
+        obj.save()
+        self.assertEqual(obj.etat, ETAT_0)
+        obj.save()
+        self.assertEqual(obj.etat, ETAT_0)
+        self.assertEqual(obj.passage, 2)
+
+    def test_etats_initiaux_multiples(self):
+        """
+        test avec provenances multiples pour une action
+        """
+        obj = WFKlass(titre="5")
+        obj.save()
+        self.assertEqual(obj.etat, ETAT_0)
+        obj.etat = ETAT_4
+        obj.save()
+        self.assertEqual(obj.etat, ETAT_4)
+
+        obj = WFKlass(titre="6")
+        obj.save()
+        obj.etat = ETAT_1
+        self.assertEqual(obj.etat, ETAT_1)
+        obj.etat = ETAT_4
+        obj.save()
+        self.assertEqual(obj.etat, ETAT_4)
+
+    def test_etats_initiaux_validation(self):
+        """
+        test si l'action à états initiaux multiples conserve la validation
+        """
+        obj = WFKlass(titre="7")
+        obj.etat = ETAT_1
+        obj.etat = ETAT_2
+        self.assertRaises(Exception, setattr, obj, 'etat', ETAT_4)
+
+    def test_actions_a_la_volee(self):
+        """
+        test si la coherence des etats est respectée sans sauvegarder l'objet
+        """
+        obj = WFKlass(titre="8")
+        obj.etat = ETAT_1
+        obj.etat = ETAT_2
+        self.assertRaises(Exception, setattr, obj, 'etat', ETAT_1)
+
+    def test_etat_dispo_liste(self):
+        """
+        test si la liste d'états possible reflète le workflow défini
+        """
+        obj = WFKlass(titre="9")
+        self.assertEqual(obj.etat, ETAT_0)
+        etats_dispo = [k for k, v in obj.get_etats_disponibles()].sort()
+        etats_attendus = [ETAT_0, ETAT_1, ETAT_3, ETAT_4].sort()
+        self.assertEqual(etats_dispo, etats_attendus)
diff --git a/auf/django/workflow/views.py b/auf/django/workflow/views.py
new file mode 100644 (file)
index 0000000..60f00ef
--- /dev/null
@@ -0,0 +1 @@
+# Create your views here.
diff --git a/setup.cfg b/setup.cfg
new file mode 100644 (file)
index 0000000..01bb954
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,3 @@
+[egg_info]
+tag_build = dev
+tag_svn_revision = true
diff --git a/setup.py b/setup.py
new file mode 100644 (file)
index 0000000..db1e81c
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,30 @@
+# -*- encoding: utf-8 -*-
+
+from setuptools import setup, find_packages
+import sys, os
+
+name = 'auf.django.workflow'
+version = '0.14'
+
+setup(name=name,
+      version=version,
+      description="Toolkit pour mettre en place un mécanisme simple de workflow sur un modèle Django",
+      long_description="""\
+""",
+      classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
+      keywords='AUF django workflow',
+      author='Olivier Larchev\xc3\xaaque',
+      author_email='olivier.larcheveque@auf.org',
+      url='http://pypi.auf.org/%s' % name,
+      license='GPL',
+      namespace_packages = ['auf'],
+      packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
+      include_package_data=True,
+      zip_safe=False,
+      install_requires=[
+          # -*- Extra requirements: -*-
+      ],
+      entry_points="""
+      # -*- Entry points: -*-
+      """,
+      )