8
0
Fork 0
mirror of https://gitlab2.federez.net/re2o/re2o synced 2024-11-22 03:13:12 +00:00

feat: add first part of ticket api

This commit is contained in:
Yoann Pétri 2021-05-10 20:56:34 +02:00
parent 0a1dc9edd8
commit f4f6a70de2
Signed by: nanoy
GPG key ID: DC24C5787C943389
9 changed files with 258 additions and 42 deletions

14
tickets/api/__init__.py Normal file
View file

@ -0,0 +1,14 @@
# Copyright 2021 nanoy
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

View file

@ -0,0 +1,55 @@
# -*- mode: python; coding: utf-8 -*-
# Re2o est un logiciel d'administration développé initiallement au Rézo Metz. Il
# se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics.
#
# Copyright © 2018 Maël Kervella
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from rest_framework import serializers
from tickets.models import Ticket, CommentTicket
from api.serializers import NamespacedHMSerializer
class TicketSerializer(NamespacedHMSerializer):
"""Serialize `tickets.models.Ticket` objects."""
class Meta:
model = Ticket
fields = ("id", "title", "description", "email", "uuid")
class CommentTicketSerializer(NamespacedHMSerializer):
uuid = serializers.UUIDField()
class Meta:
model = CommentTicket
fields = ("comment", "uuid", "parent_ticket", "created_at", "created_by")
read_only_fields = ("parent_ticket", "created_at", "created_by")
extra_kwargs = {
"uuid": {"write_only": True},
}
def create(self, validated_data):
validated_data = {
"comment": validated_data["comment"],
"parent_ticket": Ticket.objects.get(uuid=validated_data["uuid"]),
"created_by": validated_data["created_by"],
}
comment = CommentTicket(**validated_data)
comment.save()
return comment

31
tickets/api/urls.py Normal file
View file

@ -0,0 +1,31 @@
# -*- mode: python; coding: utf-8 -*-
# Re2o est un logiciel d'administration développé initiallement au Rézo Metz. Il
# se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics.
#
# Copyright © 2018 Maël Kervella
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from . import views
urls_viewset = [
(r"tickets/tickets", views.TicketsViewSet, None),
(r"tickets/comments", views.CommentTicketViewSet, None),
]
# urls_view = [
# (r"ticket/tickets", ),
# ]

51
tickets/api/views.py Normal file
View file

@ -0,0 +1,51 @@
# -*- mode: python; coding: utf-8 -*-
# Re2o est un logiciel d'administration développé initiallement au Rézo Metz. Il
# se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics.
#
# Copyright © 2018 Maël Kervella
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from rest_framework import viewsets
from tickets.models import Ticket, CommentTicket
from api.permissions import ACLPermission
from . import serializers
class TicketsViewSet(viewsets.ModelViewSet):
permission_classes = (ACLPermission,)
perms_map = {
"GET": [Ticket.can_view_all],
"POST": [],
}
serializer_class = serializers.TicketSerializer
queryset = Ticket.objects.all()
class CommentTicketViewSet(viewsets.ModelViewSet):
permission_classes = (ACLPermission,)
perms_map = {
"GET": [Ticket.can_view_all],
"POST": [],
}
serializer_class = serializers.CommentTicketSerializer
queryset = CommentTicket.objects.all()
def perform_create(self, serializer):
serializer.save(created_by=self.request.user)

View file

@ -0,0 +1,19 @@
# Generated by Django 2.2.18 on 2021-05-09 17:29
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('tickets', '0002_auto_20210214_1046'),
]
operations = [
migrations.AddField(
model_name='ticket',
name='uuid',
field=models.UUIDField(default=uuid.uuid4, editable=False),
),
]

View file

@ -25,6 +25,9 @@ Ticket model
from __future__ import absolute_import from __future__ import absolute_import
import uuid
from django.core.mail import EmailMessage
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.template import loader from django.template import loader
@ -87,6 +90,7 @@ class Ticket(AclMixin, models.Model):
language = models.CharField( language = models.CharField(
max_length=16, help_text=_("Language of the ticket."), default="en" max_length=16, help_text=_("Language of the ticket."), default="en"
) )
uuid = models.UUIDField(default=uuid.uuid4, editable=False)
request = None request = None
class Meta: class Meta:
@ -96,7 +100,9 @@ class Ticket(AclMixin, models.Model):
def __str__(self): def __str__(self):
if self.user: if self.user:
return _("Ticket from {name}. Date: {date}.").format(name=self.user.get_full_name(),date=self.date) return _("Ticket from {name}. Date: {date}.").format(
name=self.user.get_full_name(), date=self.date
)
else: else:
return _("Anonymous ticket. Date: %s.") % (self.date) return _("Anonymous ticket. Date: %s.") % (self.date)
@ -134,10 +140,10 @@ class Ticket(AclMixin, models.Model):
GeneralOption.get_cached_value("email_from"), GeneralOption.get_cached_value("email_from"),
[to_addr], [to_addr],
reply_to=[self.get_mail], reply_to=[self.get_mail],
headers={"re2o-uuid": self.uuid},
) )
send_mail_object(mail_to_send, self.request) send_mail_object(mail_to_send, self.request)
def can_view(self, user_request, *_args, **_kwargs): def can_view(self, user_request, *_args, **_kwargs):
"""Check that the user has the right to view the ticket """Check that the user has the right to view the ticket
or that it is the author.""" or that it is the author."""
@ -189,9 +195,7 @@ class CommentTicket(AclMixin, models.Model):
blank=False, blank=False,
null=False, null=False,
) )
parent_ticket = models.ForeignKey( parent_ticket = models.ForeignKey("Ticket", on_delete=models.CASCADE)
"Ticket", on_delete=models.CASCADE
)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
created_by = models.ForeignKey( created_by = models.ForeignKey(
"users.User", "users.User",
@ -207,7 +211,16 @@ class CommentTicket(AclMixin, models.Model):
@cached_property @cached_property
def comment_id(self): def comment_id(self):
return CommentTicket.objects.filter(parent_ticket=self.parent_ticket, pk__lt=self.pk).count() + 1 return (
CommentTicket.objects.filter(
parent_ticket=self.parent_ticket, pk__lt=self.pk
).count()
+ 1
)
@property
def uuid(self):
return self.parent_ticket.uuid
def can_view(self, user_request, *_args, **_kwargs): def can_view(self, user_request, *_args, **_kwargs):
"""Check that the user has the right to view the ticket comment """Check that the user has the right to view the ticket comment
@ -218,7 +231,9 @@ class CommentTicket(AclMixin, models.Model):
): ):
return ( return (
False, False,
_("You don't have the right to view other tickets comments than yours."), _(
"You don't have the right to view other tickets comments than yours."
),
("tickets.view_commentticket",), ("tickets.view_commentticket",),
) )
else: else:
@ -227,13 +242,15 @@ class CommentTicket(AclMixin, models.Model):
def can_edit(self, user_request, *_args, **_kwargs): def can_edit(self, user_request, *_args, **_kwargs):
"""Check that the user has the right to edit the ticket comment """Check that the user has the right to edit the ticket comment
or that it is the author.""" or that it is the author."""
if ( if not user_request.has_perm("tickets.change_commentticket") and (
not user_request.has_perm("tickets.change_commentticket") self.parent_ticket.user != user_request
and (self.parent_ticket.user != user_request or self.parent_ticket.user != self.created_by) or self.parent_ticket.user != self.created_by
): ):
return ( return (
False, False,
_("You don't have the right to edit other tickets comments than yours."), _(
"You don't have the right to edit other tickets comments than yours."
),
("tickets.change_commentticket",), ("tickets.change_commentticket",),
) )
else: else:
@ -273,6 +290,7 @@ class CommentTicket(AclMixin, models.Model):
GeneralOption.get_cached_value("email_from"), GeneralOption.get_cached_value("email_from"),
[to_addr, self.parent_ticket.get_mail], [to_addr, self.parent_ticket.get_mail],
reply_to=[to_addr, self.parent_ticket.get_mail], reply_to=[to_addr, self.parent_ticket.get_mail],
headers={"re2o-uuid": self.uuid},
) )
send_mail_object(mail_to_send, self.request) send_mail_object(mail_to_send, self.request)

View file

@ -39,6 +39,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% else %} {% else %}
<span class="badge badge-danger">{% trans "Not solved" %}</span> <span class="badge badge-danger">{% trans "Not solved" %}</span>
{% endif %} {% endif %}
<span class="badge badge-info">{{ ticket.uuid }}</span>
</h2> </h2>
<div class="panel panel-default"> <div class="panel panel-default">

View file

@ -29,17 +29,30 @@ from . import views
from .preferences.views import edit_options from .preferences.views import edit_options
urlpatterns = [ urlpatterns = [
url(r"^$", views.aff_tickets, name="aff-tickets"), path("", views.aff_tickets, name="aff-tickets"),
url(r"^(?P<ticketid>[0-9]+)$", views.aff_ticket, name="aff-ticket"), path("<int:ticketid>", views.aff_ticket, name="aff-ticket"),
url(r"^change_ticket_status/(?P<ticketid>[0-9]+)$", views.change_ticket_status, name="change-ticket-status"), path("by_uuid/<uuid:ticketuuid>", views.aff_ticket_uuid, name="aff-ticket-uuid"),
url(r"^edit_ticket/(?P<ticketid>[0-9]+)$", views.edit_ticket, name="edit-ticket"), path(
url( "change_ticket_status/<int:ticketid>",
views.change_ticket_status,
name="change-ticket-status",
),
path("edit_ticket/<int:ticketid>", views.edit_ticket, name="edit-ticket"),
re_path(
r"^edit_options/(?P<section>TicketOption)$", r"^edit_options/(?P<section>TicketOption)$",
edit_options, edit_options,
name="edit-options", name="edit-options",
), ),
url(r"^new_ticket/$", views.new_ticket, name="new-ticket"), url(r"^new_ticket/$", views.new_ticket, name="new-ticket"),
url(r"^add_comment/(?P<ticketid>[0-9]+)$", views.add_comment, name="add-comment"), url(r"^add_comment/(?P<ticketid>[0-9]+)$", views.add_comment, name="add-comment"),
url(r"^edit_comment/(?P<commentticketid>[0-9]+)$", views.edit_comment, name="edit-comment"), url(
url(r"^del_comment/(?P<commentticketid>[0-9]+)$", views.del_comment, name="del-comment"), r"^edit_comment/(?P<commentticketid>[0-9]+)$",
views.edit_comment,
name="edit-comment",
),
url(
r"^del_comment/(?P<commentticketid>[0-9]+)$",
views.del_comment,
name="del-comment",
),
] ]

View file

@ -22,7 +22,8 @@
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.shortcuts import render, redirect from django.forms import modelformset_factory
from django.shortcuts import get_object_or_404, redirect, render
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.views.decorators.cache import cache_page from django.views.decorators.cache import cache_page
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
@ -32,13 +33,7 @@ from re2o.views import form
from re2o.base import re2o_paginator from re2o.base import re2o_paginator
from re2o.acl import ( from re2o.acl import can_view, can_view_all, can_edit, can_create, can_delete
can_view,
can_view_all,
can_edit,
can_create,
can_delete
)
from preferences.models import GeneralOption from preferences.models import GeneralOption
@ -65,7 +60,9 @@ def new_ticket(request):
reverse("users:profil", kwargs={"userid": str(request.user.id)}) reverse("users:profil", kwargs={"userid": str(request.user.id)})
) )
return form( return form(
{"ticketform": ticketform, 'action_name': ("Create a ticket")}, "tickets/edit.html", request {"ticketform": ticketform, "action_name": ("Create a ticket")},
"tickets/edit.html",
request,
) )
@ -81,15 +78,24 @@ def aff_ticket(request, ticket, ticketid):
) )
def aff_ticket_uuid(request, ticketuuid):
"""View used to display a single ticket."""
ticket = get_object_or_404(Ticket, uuid=ticketuuid)
comments = CommentTicket.objects.filter(parent_ticket=ticket)
return render(
request,
"tickets/aff_ticket.html",
{"ticket": ticket, "comments": comments},
)
@login_required @login_required
@can_edit(Ticket) @can_edit(Ticket)
def change_ticket_status(request, ticket, ticketid): def change_ticket_status(request, ticket, ticketid):
"""View used to change a ticket's status.""" """View used to change a ticket's status."""
ticket.solved = not ticket.solved ticket.solved = not ticket.solved
ticket.save() ticket.save()
return redirect( return redirect(reverse("tickets:aff-ticket", kwargs={"ticketid": str(ticketid)}))
reverse("tickets:aff-ticket", kwargs={"ticketid": str(ticketid)})
)
@login_required @login_required
@ -101,15 +107,15 @@ def edit_ticket(request, ticket, ticketid):
ticketform.save() ticketform.save()
messages.success( messages.success(
request, request,
_( _("Ticket has been updated successfully"),
"Ticket has been updated successfully"
),
) )
return redirect( return redirect(
reverse("tickets:aff-ticket", kwargs={"ticketid": str(ticketid)}) reverse("tickets:aff-ticket", kwargs={"ticketid": str(ticketid)})
) )
return form( return form(
{"ticketform": ticketform, 'action_name': ("Edit this ticket")}, "tickets/edit.html", request {"ticketform": ticketform, "action_name": ("Edit this ticket")},
"tickets/edit.html",
request,
) )
@ -128,7 +134,9 @@ def add_comment(request, ticket, ticketid):
reverse("tickets:aff-ticket", kwargs={"ticketid": str(ticketid)}) reverse("tickets:aff-ticket", kwargs={"ticketid": str(ticketid)})
) )
return form( return form(
{"ticketform": commentticket, "action_name": _("Add a comment")}, "tickets/edit.html", request {"ticketform": commentticket, "action_name": _("Add a comment")},
"tickets/edit.html",
request,
) )
@ -136,7 +144,9 @@ def add_comment(request, ticket, ticketid):
@can_edit(CommentTicket) @can_edit(CommentTicket)
def edit_comment(request, commentticket_instance, **_kwargs): def edit_comment(request, commentticket_instance, **_kwargs):
"""View used to edit a comment of a ticket.""" """View used to edit a comment of a ticket."""
commentticket = CommentTicketForm(request.POST or None, instance=commentticket_instance) commentticket = CommentTicketForm(
request.POST or None, instance=commentticket_instance
)
if commentticket.is_valid(): if commentticket.is_valid():
ticketid = commentticket_instance.parent_ticket.id ticketid = commentticket_instance.parent_ticket.id
if commentticket.changed_data: if commentticket.changed_data:
@ -146,7 +156,9 @@ def edit_comment(request, commentticket_instance, **_kwargs):
reverse("tickets:aff-ticket", kwargs={"ticketid": str(ticketid)}) reverse("tickets:aff-ticket", kwargs={"ticketid": str(ticketid)})
) )
return form( return form(
{"ticketform": commentticket, "action_name": _("Edit")}, "tickets/edit.html", request, {"ticketform": commentticket, "action_name": _("Edit")},
"tickets/edit.html",
request,
) )
@ -162,7 +174,9 @@ def del_comment(request, commentticket, **_kwargs):
reverse("tickets:aff-ticket", kwargs={"ticketid": str(ticketid)}) reverse("tickets:aff-ticket", kwargs={"ticketid": str(ticketid)})
) )
return form( return form(
{"objet": commentticket, "objet_name": _("Ticket Comment")}, "tickets/delete.html", request {"objet": commentticket, "objet_name": _("Ticket Comment")},
"tickets/delete.html",
request,
) )