Bienvenue sur IndexError.

Ici vous pouvez poser des questions sur Python et le Framework Django.

Mais aussi sur les technos front comme React, Angular, Typescript et Javascript en général.

Consultez la FAQ pour améliorer vos chances d'avoir des réponses à vos questions.

Django historique des editions d'une table

+1 vote

Voila j'ai un projet d'une Webapp de ticket (support informatique) avec django 1.8 et python 3.3 : https://github.com/hadmagic/Aidez-moi

J'ai une table tickets et j'aimerais pouvoir mettre dans une autre table toutes les éditions du ticket. J'ai déjà essayé django-simple-history et django-reversion et cela ne me convient pas.
En fait j'aimerais pouvoir garder les attributs de chaque colonne du ticket dans l'historique (par exemple les choix du status etc...)
Pour le moment j'ai juste surcharger la méthode save() de TicketForm :

model.py :

class UserProfile(models.Model):

user = models.OneToOneField(User)

class Tickets( models.Model):

title = models.TextField()
content = models.TextField()
create_by = models.ForeignKey(User)
created = models.DateTimeField()
last_edited = models.DateTimeField(auto_now=True)

TYPES_CHOICES = (
    (1, 'Incident'),
    (2, 'Demande'),)

types = models.IntegerField(
    ('Types'),
    choices=TYPES_CHOICES )

OPEN_STATUS = 'OPEN'
RESOLVED_STATUS = 3
CLOSED_STATUS = 4

STATUS_CHOICES = (
    ('OPEN', 'Open'),
    ('RESOLVED', 'Resolved'),
    ('CLOSED', 'Closed'),
)

PRIORITY_CHOICES = (
    ('CRITICAL', 'Critical'),
    ('HIGH', 'High'),
    ('NORMAL', 'Normal'),
    ('LOW', 'Low'),
    ('VERYLOW', 'Very Low'),)

assign_to = models.ForeignKey(
    settings.AUTH_USER_MODEL,
    related_name='assigned_to',
    blank=True,
    null=True,
    verbose_name=('Assigned to'),
    )

status = models.CharField(max_length=15
    ,
    choices=STATUS_CHOICES,
    default=OPEN_STATUS, )

priority = models.CharField(max_length=15
    ,
    choices=PRIORITY_CHOICES,
    default='NORMAL',
    blank='NORMAL',
    help_text=('1 = Highest Priority, 5 = Low Priority'),)



def __str__(self):
    """
    Cette méthode que nous définirons dans tous les modèles
    nous permettra de reconnaître facilement les différents objets que
    nous traiterons plus tard et dans l'administration
    """
    return self.title

class Follow(models.Model):

follow_by = models.ForeignKey(User, related_name='follower')
follow = models.TextField(blank=True, null=True)
ticket = models.ForeignKey(Tickets, related_name='ticket_id')
date_follow = models.DateTimeField(auto_now=True, )

field = models.CharField(max_length=100, null=True)
old_value = models.TextField(null=True)
new_value = models.TextField(null=True)

forms.py :

class ConnexionForm(forms.Form):

username = forms.CharField(label="Nom d'utilisateur", max_length=30,
                           widget=forms.TextInput(attrs={
                                                        'type':"text",
                                                        'placeholder':"Username"}))

password = forms.CharField(label="Mot de passe",
                           widget=forms.PasswordInput(attrs={
                                                        'type':"password",
                                                        'placeholder':"Password"}))

class TicketForm(forms.ModelForm):
"""
Pour ajouter un ticket
"""

title = forms.CharField(label='Titre',widget=forms.TextInput(attrs={'placeholder': 'Titre',
                                                                    'size':'110',
                                                                    }))

content = forms.CharField(label='Ticket',widget=forms.Textarea(attrs={'placeholder': 'Contenu du ticket',
                                                                      'rows':'5',

                                                                      'class':'uk-width-1-1'}))

priority = forms.ChoiceField(
    choices=Tickets.PRIORITY_CHOICES,
    required=True,
    initial='3',
    label=('Urgency'),
    help_text=('Please select a priority carefully.'),
     )

# Pour choisir que les membres du staff
assign_to = forms.ModelChoiceField(queryset=User.objects.all().filter(is_staff=1))

class Meta:
    model = Tickets
    exclude = ('created', 'create_by')

def __init__(self, *args, **kwargs):
    """
    Pour exclure certains champs de la classe TicketForm
    afin d'afficher assign_to et status pour un membre du staff
    """
    user = kwargs.pop('user', None)
    super(TicketForm, self).__init__(*args, **kwargs)
    if user.is_staff is False:
        del self.fields['assign_to']
        del self.fields['status']

def edit(self,ticket_id, user, *args, **kwargs):
    print(args)
    if  Tickets.objects.filter(id=ticket_id).exists():
        if self.has_changed():
            ticket = Tickets.objects.filter(pk=ticket_id)
            for field in self.changed_data:
                oldvalue = ticket.values(field)
                Follow.objects.create(
                                ticket_id=ticket_id,
                                field=field,
                                old_value=oldvalue[0].get(field),
                                new_value=self[field].value(),
                                follow_by=user
                                 )
    else:
        pass

    super(TicketForm, self).save(*args, **kwargs)

class ResponseForm(forms.ModelForm):
follow = forms.CharField(label='Ticket',widget=forms.Textarea(
attrs={'placeholder': 'Réponse au ticket','rows':'4','class':'uk-width-1-1'}))

class Meta:
    model = Follow
    fields = ['follow']
    exclude = ('date_follow', 'ticket_id', 'field', 'new_value', 'old_value', 'follower')

views.py :

def ticketedit(request, id):
ticket = get
objector404(Tickets, id=id)
if request.method=='POST' and 'edit' in request.POST:
form = TicketForm(request.POST, user=request.user, instance=ticket)

    if form.is_valid():
        #form.edit(commit=False, ticket_id=id, user=request.user)
        form.save()
        #messages.add_message(request, messages.INFO, 'Ticket mis à jour OK')
        return redirect(view_ticket, id)
        # If the save was successful, redirect to another page
else:
        form = TicketForm(user=request.user, instance=ticket)
        response = ResponseForm()

return render(request, 'add_ticket.html', locals())

def view_ticket(request, id):

tickets = Tickets.objects.select_related('create_by').get(id=id)
follow_up = Follow.objects.select_related('follow_by', 'ticket').filter(ticket=id)

if request.method == 'POST':
    form = ResponseForm(data=request.POST)
    #if form.is_valid():
    follow = form.save(commit=False)
    follow.ticket_id=id
    follow.follow_by=request.user
    follow.save()
else:
    form = ResponseForm()

return render(request,'ticket.html', locals())
demandé 3-Avr-2015 par hadmagic (112 points)
edité 13-Avr-2015 par max

C'est quoi la question ?

Sinon si vous n'avez pas regardé, en python ,comme système de ticketing il y a "Trac" de EdgeWall avec un paquet d'outils / plugins. Ça resoud pas le "problème" mais c une solution éprouvée ;)

En fait quand je recupere dans la table Follow oldvalue ou newvalue, j'ai pas les propriété de l'ancienne valeur

C'est à dire quand je change par exemple le type, dans new_value cela m'inscrit un 1 au lieu de incident.

Sinon peut être une autre piste pour stocker les historiques (ils sont affiché sur la timeline du ticket)

Sinon pour trac, en fait le but et développer moi-même mon outils pour apprendre

1 Réponse

+1 vote

Nous avons implémenté un système similaire sur notre application en entreprise.

Pour faire simple, nous avons fait les actions suivantes :

  • Tout d'abord, nous avons créé un modèle abstrait dont héritera tous les modèles qui devront être historisés.
  • Nous avons créé deux modèles d'historisation :

    • Le premier contiendra les informations relatives au modèle impacté ainsi que la typologie d'historisation (création / modification / suppression). Pour cela, l'astuce est d'utiliser les GenericForeignKey de Django.
    • Le second contiendra précisément les modifications apportées sur les champs du modèle (un enregistrement par champ)
  • S'il ne s'agit que d'historisation sans se soucier de restauration (comme django-reversion par exemple), pas besoin d'imaginer un truc compliqué : un simple champ "avant" et "après" suffit. Sinon il faudra sauvegarder les instances des modèles sous forme de JSON dans la base.
  • Ensuite il faut créer un ensemble de récepteurs de signaux (presave, postsave, post_delete, etc...) pour récupérer les données des entités et les sauvegarder dans le système d'historisation.

Bien sûr, il s'agit ici que d'une façon de faire, pas forcément la meilleure évidemment, tout dépend de tes besoins. Il y a de nombreuses optimisations possibles pour traiter ce sujet, et le fait que des produits tels que django-reversion existent prouvent que ce n'est pas une mince affaire.

Par exemple nous utilisons Celery pour réaliser les actions d'historisation de manière asynchrone et ne pas gêner le fonctionnement de Django pour les utilisateurs. Il sera peut être nécessaire de prévoir l'historisation dans tous les domaines de votre application, notamment dans les formulaires, les serializers ou l'administration Django, là encore ça dépendra du niveau de détail que tu veux atteindre.

Egalement, ne pas oublier que les ManyToManyField fonctionnent de manière complètement différente dans Django, il faudra prévoir un comportement spécifique si tu veux les historiser également. Ne pas oublier non plus que certaines opérations de l'ORM ne permettent pas d'historiser quoiqu'il arrive car elles exécutent directement du SQL sur la base de données sans passer par les modèles (create, update, etc...).

Voici quelques images de ce que nous avons pu mettre en place sur notre produit à travers l'admin :

Les historiques des modèles
Le détail des modifications

répondu 3-Avr-2015 par debnet (1,014 points)
...