mirror of
https://gitlab2.federez.net/re2o/re2o
synced 2024-12-23 07:23:46 +00:00
feat: ✨ add first part of ticket api
This commit is contained in:
parent
0a1dc9edd8
commit
f4f6a70de2
9 changed files with 258 additions and 42 deletions
14
tickets/api/__init__.py
Normal file
14
tickets/api/__init__.py
Normal 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.
|
||||
|
55
tickets/api/serializers.py
Normal file
55
tickets/api/serializers.py
Normal 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
31
tickets/api/urls.py
Normal 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
51
tickets/api/views.py
Normal 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)
|
19
tickets/migrations/0003_ticket_uuid.py
Normal file
19
tickets/migrations/0003_ticket_uuid.py
Normal 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),
|
||||
),
|
||||
]
|
|
@ -25,6 +25,9 @@ Ticket model
|
|||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import uuid
|
||||
|
||||
from django.core.mail import EmailMessage
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.template import loader
|
||||
|
@ -87,6 +90,7 @@ class Ticket(AclMixin, models.Model):
|
|||
language = models.CharField(
|
||||
max_length=16, help_text=_("Language of the ticket."), default="en"
|
||||
)
|
||||
uuid = models.UUIDField(default=uuid.uuid4, editable=False)
|
||||
request = None
|
||||
|
||||
class Meta:
|
||||
|
@ -96,7 +100,9 @@ class Ticket(AclMixin, models.Model):
|
|||
|
||||
def __str__(self):
|
||||
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:
|
||||
return _("Anonymous ticket. Date: %s.") % (self.date)
|
||||
|
||||
|
@ -134,10 +140,10 @@ class Ticket(AclMixin, models.Model):
|
|||
GeneralOption.get_cached_value("email_from"),
|
||||
[to_addr],
|
||||
reply_to=[self.get_mail],
|
||||
headers={"re2o-uuid": self.uuid},
|
||||
)
|
||||
send_mail_object(mail_to_send, self.request)
|
||||
|
||||
|
||||
def can_view(self, user_request, *_args, **_kwargs):
|
||||
"""Check that the user has the right to view the ticket
|
||||
or that it is the author."""
|
||||
|
@ -189,9 +195,7 @@ class CommentTicket(AclMixin, models.Model):
|
|||
blank=False,
|
||||
null=False,
|
||||
)
|
||||
parent_ticket = models.ForeignKey(
|
||||
"Ticket", on_delete=models.CASCADE
|
||||
)
|
||||
parent_ticket = models.ForeignKey("Ticket", on_delete=models.CASCADE)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
created_by = models.ForeignKey(
|
||||
"users.User",
|
||||
|
@ -207,7 +211,16 @@ class CommentTicket(AclMixin, models.Model):
|
|||
|
||||
@cached_property
|
||||
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):
|
||||
"""Check that the user has the right to view the ticket comment
|
||||
|
@ -218,7 +231,9 @@ class CommentTicket(AclMixin, models.Model):
|
|||
):
|
||||
return (
|
||||
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",),
|
||||
)
|
||||
else:
|
||||
|
@ -227,13 +242,15 @@ class CommentTicket(AclMixin, models.Model):
|
|||
def can_edit(self, user_request, *_args, **_kwargs):
|
||||
"""Check that the user has the right to edit the ticket comment
|
||||
or that it is the author."""
|
||||
if (
|
||||
not user_request.has_perm("tickets.change_commentticket")
|
||||
and (self.parent_ticket.user != user_request or self.parent_ticket.user != self.created_by)
|
||||
if not user_request.has_perm("tickets.change_commentticket") and (
|
||||
self.parent_ticket.user != user_request
|
||||
or self.parent_ticket.user != self.created_by
|
||||
):
|
||||
return (
|
||||
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",),
|
||||
)
|
||||
else:
|
||||
|
@ -273,6 +290,7 @@ class CommentTicket(AclMixin, models.Model):
|
|||
GeneralOption.get_cached_value("email_from"),
|
||||
[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)
|
||||
|
||||
|
|
|
@ -39,6 +39,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% else %}
|
||||
<span class="badge badge-danger">{% trans "Not solved" %}</span>
|
||||
{% endif %}
|
||||
<span class="badge badge-info">{{ ticket.uuid }}</span>
|
||||
</h2>
|
||||
|
||||
<div class="panel panel-default">
|
||||
|
|
|
@ -29,17 +29,30 @@ from . import views
|
|||
from .preferences.views import edit_options
|
||||
|
||||
urlpatterns = [
|
||||
url(r"^$", views.aff_tickets, name="aff-tickets"),
|
||||
url(r"^(?P<ticketid>[0-9]+)$", views.aff_ticket, name="aff-ticket"),
|
||||
url(r"^change_ticket_status/(?P<ticketid>[0-9]+)$", views.change_ticket_status, name="change-ticket-status"),
|
||||
url(r"^edit_ticket/(?P<ticketid>[0-9]+)$", views.edit_ticket, name="edit-ticket"),
|
||||
url(
|
||||
path("", views.aff_tickets, name="aff-tickets"),
|
||||
path("<int:ticketid>", views.aff_ticket, name="aff-ticket"),
|
||||
path("by_uuid/<uuid:ticketuuid>", views.aff_ticket_uuid, name="aff-ticket-uuid"),
|
||||
path(
|
||||
"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)$",
|
||||
edit_options,
|
||||
name="edit-options",
|
||||
),
|
||||
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"^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"),
|
||||
url(
|
||||
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",
|
||||
),
|
||||
]
|
||||
|
|
|
@ -22,7 +22,8 @@
|
|||
|
||||
from django.contrib import messages
|
||||
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.views.decorators.cache import cache_page
|
||||
from django.utils.translation import ugettext as _
|
||||
|
@ -32,13 +33,7 @@ from re2o.views import form
|
|||
|
||||
from re2o.base import re2o_paginator
|
||||
|
||||
from re2o.acl import (
|
||||
can_view,
|
||||
can_view_all,
|
||||
can_edit,
|
||||
can_create,
|
||||
can_delete
|
||||
)
|
||||
from re2o.acl import can_view, can_view_all, can_edit, can_create, can_delete
|
||||
|
||||
from preferences.models import GeneralOption
|
||||
|
||||
|
@ -65,7 +60,9 @@ def new_ticket(request):
|
|||
reverse("users:profil", kwargs={"userid": str(request.user.id)})
|
||||
)
|
||||
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
|
||||
@can_edit(Ticket)
|
||||
def change_ticket_status(request, ticket, ticketid):
|
||||
"""View used to change a ticket's status."""
|
||||
ticket.solved = not ticket.solved
|
||||
ticket.save()
|
||||
return redirect(
|
||||
reverse("tickets:aff-ticket", kwargs={"ticketid": str(ticketid)})
|
||||
)
|
||||
return redirect(reverse("tickets:aff-ticket", kwargs={"ticketid": str(ticketid)}))
|
||||
|
||||
|
||||
@login_required
|
||||
|
@ -101,15 +107,15 @@ def edit_ticket(request, ticket, ticketid):
|
|||
ticketform.save()
|
||||
messages.success(
|
||||
request,
|
||||
_(
|
||||
"Ticket has been updated successfully"
|
||||
),
|
||||
_("Ticket has been updated successfully"),
|
||||
)
|
||||
return redirect(
|
||||
reverse("tickets:aff-ticket", kwargs={"ticketid": str(ticketid)})
|
||||
)
|
||||
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)})
|
||||
)
|
||||
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)
|
||||
def edit_comment(request, commentticket_instance, **_kwargs):
|
||||
"""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():
|
||||
ticketid = commentticket_instance.parent_ticket.id
|
||||
if commentticket.changed_data:
|
||||
|
@ -146,7 +156,9 @@ def edit_comment(request, commentticket_instance, **_kwargs):
|
|||
reverse("tickets:aff-ticket", kwargs={"ticketid": str(ticketid)})
|
||||
)
|
||||
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)})
|
||||
)
|
||||
return form(
|
||||
{"objet": commentticket, "objet_name": _("Ticket Comment")}, "tickets/delete.html", request
|
||||
{"objet": commentticket, "objet_name": _("Ticket Comment")},
|
||||
"tickets/delete.html",
|
||||
request,
|
||||
)
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue