Création d'un système de Tickets/SAV simple (Django)

5 novembre 2023

image_8.max-220x220


Pour un projet, j'avais besoin de mettre en place un système de Tickets/SAV. L'idée est que l'utilisateur puisse demander une assistance depuis un formulaire.


Les modèles

Pour commencer, dans mon application j'utilise une application à part que j'ai appelé "sav". Lorsqu'un utilisateur envoie un message depuis le formulaire, au moment de la validation un ticket doit d'abord être créé pour pouvoir rattacher le message au ticket.

Ci-dessous les modèles :

class Ticket(models.Model):
    reference = models.CharField(max_length=36, default=uuid4)
    subject = models.CharField(max_length=200, verbose_name="Objet")
    closed = models.BooleanField(default=False)
    user = models.ForeignKey(AUTH_USER_MODEL, verbose_name="Utilisateur", on_delete=models.CASCADE)
    date = models.DateTimeField(verbose_name="Publication", auto_now_add=True)

    def __str__(self):
        return f"{self.date} {self.subject} {self.closed}"

    def get_absolute_url(self):
        return reverse(viewname="sav:ticket", kwargs={"pk": self.pk})

    @property
    def messages_count(self):
        return Message.objects.filter(ticket=self).count()


class Message(models.Model):
    user = models.ForeignKey(AUTH_USER_MODEL, verbose_name="Utilisateur", on_delete=models.CASCADE)
    ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE, verbose_name="Ticket")
    message = models.TextField(verbose_name="Message")
    date = models.DateTimeField(verbose_name="Publication", auto_now_add=True)

    def __str__(self):
        return f"{self.user} {self.ticket} {self.date}"

Ticket

Une référence (utilisation de uuid), un objet, un booléen "closed", un utilisateur et une date. Très simple, si le booléen est coché le ticket est considéré comme clôturé.

J'utilise une propriété pour compter le nombre de messages associés au ticket.

Message

Le message est simplement associé à un ticket via une ForeignKey.

Voici le formulaire en question :

Capture_decran_du_2023-11-04_11-52-49.width-800

Le formulaire

Dans la capture ci-dessus, un formulaire pour ouvrir un nouveau ticket avec un premier message. C'est un ModelForm basé sur le modèle Message, mais j'insère un champ subject qui n'est pas de base dans mon modèle Message.

class MessageForm(forms.ModelForm):
    subject = forms.CharField(label="Objet", help_text="Merci de donner un maximum d'informations.")

    class Meta:
        model = Message
        fields = ["subject", "message"]

Je vais récupérer les informations du champ subject pour créer le ticket avant le message.

La vue

@login_required
def new_ticket(request):
    user = request.user

    if request.method == "POST":
        form = MessageForm(request.POST)
        if form.is_valid():
            ticket = Ticket.objects.create(user=user, subject=form.cleaned_data["subject"])
            form.instance.user = user
            form.instance.ticket = ticket
            form.save()
            messages.add_message(request,
                                 message=f"Ticket {ticket.reference} ouvert. "
                                         f"Vous serez averti par mail lorsqu'une réponse sera donnée",
                                 level=messages.INFO)
            return HttpResponseRedirect(request.path)
    else:
        form = MessageForm()
    return render(request, "sav/new-ticket.html", context={"form": form})

Si le formulaire est valide je crée tout de suite un ticket avec les informations du champ subject, et l'utilisateur.

if form.is_valid():
    ticket = Ticket.objects.create(user=user, subject=form.cleaned_data["subject"])

Ensuite, je rattache le message au ticket.

if form.is_valid():
    ticket = Ticket.objects.create(user=user, subject=form.cleaned_data["subject"])
    form.instance.user = user
    form.instance.ticket = ticket
    form.save()

C'est terminé, un ticket est créé avec le premier message.

Ensuite, j'ai décidé de gérer le système de question/réponse dans le ticket de cette manière :

Capture_decran_du_2023-11-04_12-09-02.width-800

Je réponds à moi-même dans l'exemple, mais vous avez compris le principe.

@login_required
def ticket_view(request, pk):
    user = request.user
    ticket = get_object_or_404(Ticket, pk=pk)
    if user != ticket.user and not user.is_superuser:
        raise PermissionDenied()

    ticket_messages = Message.objects.filter(ticket=ticket)

    if request.method == "POST":
        form = ResponseForm(request.POST)
        if form.is_valid():
            form.instance.user = user
            form.instance.ticket = ticket
            form.save()
            return redirect(ticket)
    else:
        form = ResponseForm()
    return render(request, "sav/ticket.html", context={"ticket": ticket,
                                                       "messages": ticket_messages, "form": form})

Récupérer le ticket, les messages associés au ticket, limiter la vue au propriétaire et aux admins et insérer un formulaire de réponse.

Retour