Browse Source

Tickets API v1 release

TonyKurts 1 year ago
parent
commit
593dff465d

+ 0 - 40
_apiviews.py

@@ -1,40 +0,0 @@
-from tickets.serializer import *
-from rest_framework import viewsets, permissions, exceptions
-from rest_framework.authentication import TokenAuthentication
-from rest_framework.decorators import action
-from tickets.models import *
-
-class TaskMVS(viewsets.ModelViewSet):
-    queryset = Task.objects.all()
-    serializer_class = TaskSerializer
-    permission_classes = [permissions.IsAuthenticated]
-
-    def get_queryset(self):
-        queryset = Task.objects.all()
-        task_list = self.request.query_params.get('list_id')
-        task_status = self.request.query_params.get('status')
-        if task_list is not None:
-            queryset = queryset.filter(task_list__pk=task_list)
-        if task_status is not None:
-            queryset = queryset.filter(status=task_status)
-        return queryset
-
-class TaskListMVS(viewsets.ModelViewSet):
-    queryset = TaskList.objects.all()
-    serializer_class = TaskListSerializer
-    permission_classes = [permissions.IsAuthenticated]
-
-class TicketTypeMVS(viewsets.ModelViewSet):
-    queryset = TicketType.objects.all()
-    serializer_class = TicketTypeSerializer
-    permission_classes = [permissions.IsAuthenticated]
-
-class CommentMVS(viewsets.ModelViewSet):
-    queryset = Comment.objects.all()
-    serializer_class = CommentSerializer
-    permission_classes = [permissions.IsAuthenticated]
-
-class AttachmentMVS(viewsets.ModelViewSet):
-    queryset = Attachment.objects.all()
-    serializer_class = AttachmentSerializer
-    permission_classes = [permissions.IsAuthenticated]

+ 0 - 27
_serializer.py

@@ -1,27 +0,0 @@
-from rest_framework import serializers
-from tickets.models import *
-
-class TaskSerializer(serializers.ModelSerializer):
-    class Meta:
-        model = Task
-        fields = "__all__"
-
-class TaskListSerializer(serializers.ModelSerializer):
-    class Meta:
-        model = TaskList
-        fields = "__all__"
-
-class TicketTypeSerializer(serializers.ModelSerializer):
-    class Meta:
-        model = TicketType
-        fields = "__all__"
-
-class CommentSerializer(serializers.ModelSerializer):
-    class Meta:
-        model = Comment
-        fields = "__all__"
-
-class AttachmentSerializer(serializers.ModelSerializer):
-    class Meta:
-        model = Attachment
-        fields = "__all__"

+ 0 - 0
api/__init__.py


+ 19 - 0
api/permissions.py

@@ -0,0 +1,19 @@
+from django.shortcuts import get_object_or_404
+
+from rest_framework import permissions
+
+from tickets.models import Ticket, TicketList
+
+
+class UserCanReadTicketListPermission(permissions.BasePermission):
+    def has_object_permission(serf, request, view, obj):
+        return request.user.is_superuser or obj.group in request.user.groups.all()
+
+
+class UserTicketAccessPermission(permissions.BasePermission):
+    def has_object_permission(self, request, view, obj):
+        if request.method in permissions.SAFE_METHODS:
+            return request.user.is_superuser or obj.ticket_list.group in request.user.groups.all() or obj.assigned_to == request.user
+
+        return request.user.is_superuser or request.user.is_staff or obj.created_by == request.user
+    

+ 2 - 0
api/serializers/__init__.py

@@ -0,0 +1,2 @@
+from tickets.api.serializers.ticket import TicketSerializer, TicketDetailSerializer
+from tickets.api.serializers.ticket_list import TicketListSerializer

+ 35 - 0
api/serializers/ticket.py

@@ -0,0 +1,35 @@
+from django.shortcuts import get_object_or_404
+
+from rest_framework import serializers
+
+from tickets.models import Ticket
+
+
+class BaseTicketSerializer(serializers.ModelSerializer):
+    def validate(self, data):
+        user = self.context['request'].user
+
+        if not (user.is_superuser or data["ticket_list"].group in user.groups.all()):
+            raise serializers.ValidationError("You don't have access to this list.")
+        
+        return data
+
+
+class TicketSerializer(BaseTicketSerializer):
+    status = serializers.IntegerField(read_only=True)
+    note = serializers.CharField(write_only=True, required=False)    
+
+    class Meta():
+        model = Ticket
+        exclude = ["updated_at"]
+
+
+class TicketDetailSerializer(BaseTicketSerializer):
+    available_statuses = serializers.SerializerMethodField()
+
+    def get_available_statuses(self, obj):
+        return obj.get_available_statuses()
+
+    class Meta():
+        model = Ticket
+        fields = "__all__"

+ 11 - 0
api/serializers/ticket_list.py

@@ -0,0 +1,11 @@
+from rest_framework import serializers
+
+from tickets.models import TicketList
+
+
+class TicketListSerializer(serializers.ModelSerializer):
+    group_name = serializers.CharField(source='group.name', read_only=True)
+    
+    class Meta:
+        model = TicketList
+        fields = "__all__"

+ 2 - 0
api/views/__init__.py

@@ -0,0 +1,2 @@
+from tickets.api.views.ticket_list import TicketListDetailAPIView, TicketListListAPIView
+from tickets.api.views.ticket import TicketDetailAPIView, TicketCreateAPIView

+ 22 - 0
api/views/ticket.py

@@ -0,0 +1,22 @@
+from rest_framework import generics, permissions
+from rest_framework.exceptions import NotFound
+
+from tickets.models import Ticket
+from tickets.api.serializers import TicketDetailSerializer, TicketSerializer
+from tickets.api.permissions import UserTicketAccessPermission
+
+
+class TicketDetailAPIView(generics.RetrieveUpdateDestroyAPIView):
+    queryset = Ticket.objects.all()
+    serializer_class = TicketDetailSerializer
+    permission_classes = [permissions.IsAuthenticated & UserTicketAccessPermission]
+
+
+class TicketCreateAPIView(generics.CreateAPIView):
+    queryset = Ticket.objects.all()
+    serializer_class = TicketSerializer
+    permission_classes = [permissions.IsAuthenticated & UserTicketAccessPermission]
+
+    def perform_create(self, serializer):
+        if serializer.is_valid():
+            serializer.save(created_by=self.request.user) 

+ 38 - 0
api/views/ticket_list.py

@@ -0,0 +1,38 @@
+from django.shortcuts import get_object_or_404
+
+from rest_framework import generics, permissions
+
+from tickets.models import Ticket, TicketList
+from tickets.api.serializers import TicketListSerializer, TicketSerializer
+from tickets.api.permissions import UserCanReadTicketListPermission
+
+
+class TicketListListAPIView(generics.ListAPIView):
+    permission_classes = [permissions.IsAuthenticated]
+    serializer_class = TicketListSerializer
+
+    def get_queryset(self):
+        user = self.request.user
+        user_groups_ids = user.groups.all().values_list("pk", flat=True)
+        ticket_lists  = TicketList.objects.select_related("group").order_by("group__name", "name")
+        
+        if not user.is_superuser:
+            if user_groups_ids:
+                ticket_lists = ticket_lists.filter(group__id__in=user_groups_ids)
+            else:
+                raise NotFound("You do not yet belong to any groups. Ask your administrator to add you to one.")
+
+        return ticket_lists
+
+
+class TicketListDetailAPIView(generics.ListAPIView):
+    permission_classes = [permissions.IsAuthenticated & UserCanReadTicketListPermission]
+    serializer_class = TicketSerializer
+
+    def get_queryset(self):
+        return Ticket.objects.filter(ticket_list=self.get_object())   
+
+    def get_object(self):
+        obj = get_object_or_404(TicketList.objects.filter(pk=self.kwargs['pk']))
+        self.check_object_permissions(self.request, obj)
+        return obj   

+ 1 - 1
forms/ticket.py

@@ -9,7 +9,7 @@ from tickets.models import Ticket, TicketList
 
 
 class TicketForm(forms.ModelForm):
 class TicketForm(forms.ModelForm):
     ticket_type = forms.ChoiceField(
     ticket_type = forms.ChoiceField(
-        choices=Ticket.TICKET_TYPES,
+        choices=Ticket.TICKET_TYPES_CHOICES,
         widget=forms.Select(attrs={
         widget=forms.Select(attrs={
                 'class': 'form-control'
                 'class': 'form-control'
         }),
         }),

+ 27 - 33
models/ticket.py

@@ -1,5 +1,5 @@
 from django.db import models
 from django.db import models
-from django.conf import settings
+from django.contrib.auth import get_user_model
 from django.urls import reverse
 from django.urls import reverse
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
 
 
@@ -8,33 +8,26 @@ from tickets.models.ticket_list import TicketList
 
 
 class Ticket(models.Model):
 class Ticket(models.Model):
     TICKET_TYPES = (
     TICKET_TYPES = (
-        (
-            "111-121-149-159,110-121-149-159,121-131-149-159,131-141-149,141-151-110,149-151-110,159,151",
-            "ST_REQUEST"
-        ),
-        (
-            "210-211-251,211-212-220-238-249,212-221-229-238-249,221-222-238-249,220-211-238-249,229-211-251,222-231-238-249,231-241-238-249,238-231-239-211-212-221-220-222-249,239-231-239-211-212-221-220-222-249,241-251,249-251,251",
-            "SERVICE_REQUEST"
-        ),
-        (
-            "320-321-359,321,359",
-            "ACCESS_REQUEST"
-        ),
-        (
-            "420-421-459,421,459",
-            "NEG_REQUEST"
-        )
+        (1, "ST_REQUEST", [[111, 121, 149, 159], [110, 121, 149, 159], [121, 131, 149, 159], [131, 141, 149], [141, 151, 110], [149, 151, 110], [159], [151]]),
+        (2, "SERVICE_REQUEST", [[210, 211, 251], [211, 212, 220, 238, 249], [212, 221, 229, 238, 249], [221, 222, 238, 249], [220, 211, 238, 249], [229, 211, 251], [222, 231, 238, 249], [231, 241, 238, 249], [238, 231, 239, 211, 212, 221, 220, 222, 249], [239, 231, 239, 211, 212, 221, 220, 222, 249], [241, 251], [249, 251], [251]]),
+        (3, "ACCESS_REQUEST", [[320, 321, 359], [321], [359]]),
+        (4, "NEG_REQUEST", [[420, 421, 459], [421], [459]])
     )
     )
-    
+
+    TICKET_TYPES_CHOICES = tuple((item[0], item[1]) for item in TICKET_TYPES)
+
+    LIFE_CYCLE_DICT = dict((item[0], item[2]) for item in TICKET_TYPES)
+    TICKET_TYPES_DICT = dict(TICKET_TYPES_CHOICES)
+
     title = models.CharField(max_length=128)
     title = models.CharField(max_length=128)
     ticket_list = models.ForeignKey(TicketList, on_delete=models.CASCADE)
     ticket_list = models.ForeignKey(TicketList, on_delete=models.CASCADE)
-    ticket_type = models.CharField(max_length=1024, choices=TICKET_TYPES)
+    ticket_type = models.PositiveSmallIntegerField(choices=TICKET_TYPES_CHOICES)
     status = models.PositiveSmallIntegerField(null=True)
     status = models.PositiveSmallIntegerField(null=True)
     created_at = models.DateTimeField(auto_now_add=True, editable=False)
     created_at = models.DateTimeField(auto_now_add=True, editable=False)
     updated_at = models.DateTimeField(auto_now=True)
     updated_at = models.DateTimeField(auto_now=True)
     due_date = models.DateField()
     due_date = models.DateField()
-    created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, related_name="created_by", editable=False)
-    assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="assigned_to")
+    created_by = models.ForeignKey(get_user_model(), on_delete=models.SET_NULL, null=True, related_name="created_by", editable=False)
+    assigned_to = models.ForeignKey(get_user_model(), on_delete=models.SET_NULL, null=True, blank=True, related_name="assigned_to")
     note = models.TextField(blank=True, null=True)
     note = models.TextField(blank=True, null=True)
     priority = models.PositiveSmallIntegerField(default=0)
     priority = models.PositiveSmallIntegerField(default=0)
 
 
@@ -42,26 +35,27 @@ class Ticket(models.Model):
         return self.title
         return self.title
 
 
     def _get_statuses(self) -> list:
     def _get_statuses(self) -> list:
-        ticket_life_cycle = self.ticket_type
-        ticket_life_cycle = ticket_life_cycle.split(",")
-        return [part.split("-") for part in ticket_life_cycle]
+        return self.LIFE_CYCLE_DICT.get(self.ticket_type)
 
 
     def get_available_statuses(self) -> list:
     def get_available_statuses(self) -> list:
         for status_scenario in self._get_statuses():
         for status_scenario in self._get_statuses():
-            if int(status_scenario[0]) == self.status:
+            if status_scenario[0] == int(self.status):
                 return status_scenario[1:] if len(status_scenario) > 1 else None
                 return status_scenario[1:] if len(status_scenario) > 1 else None
 
 
-    def set_next_successful_status(self):
-        available_statuses = self.get_available_statuses()
-        if available_statuses:
-            self.status = available_statuses[0]
-            self.save()
-        else:
-            raise ObjectDoesNotExist("Ticket closed")
+    def set_first_status(self):
+        self.status = self._get_statuses()[0][0]
 
 
     def save(self, *args, **kwargs):
     def save(self, *args, **kwargs):
         if not self.status:
         if not self.status:
-            self.status = self._get_statuses()[0][0]
+            self.set_first_status()
+        else:
+            unique_statuses = set()
+            for status_group in self._get_statuses():
+                for status in status_group:
+                    unique_statuses.add(status)
+
+            if self.status not in unique_statuses:
+                self.set_first_status()
         
         
         super(Ticket, self).save(*args, **kwargs)
         super(Ticket, self).save(*args, **kwargs)
 
 

+ 1 - 1
templates/tickets/ticket_detail.html

@@ -76,7 +76,7 @@
         <p class="my-3">No available statuses</p>
         <p class="my-3">No available statuses</p>
       {% endif %}
       {% endif %}
       
       
-      {% if user.is_staff or user.is_superuser %}
+      {% if user.is_staff or user.is_superuser or ticket.created_by == user %}
         <div class="d-flex justify-content-between">
         <div class="d-flex justify-content-between">
           <button type="button" data-bs-toggle="modal" data-bs-target="#ticket-edit-modal" class="btn btn-primary">
           <button type="button" data-bs-toggle="modal" data-bs-target="#ticket-edit-modal" class="btn btn-primary">
             <i class="fa-solid fa-pen-to-square pe-1"></i>
             <i class="fa-solid fa-pen-to-square pe-1"></i>

+ 7 - 11
urls.py

@@ -1,17 +1,9 @@
 from django.urls import path, include
 from django.urls import path, include
 
 
 from tickets.views import *
 from tickets.views import *
-# from .apiviews import *
-# from rest_framework import routers
+from tickets.api.views import TicketListDetailAPIView, TicketListListAPIView, TicketDetailAPIView, TicketCreateAPIView
 
 
 
 
-# router = routers.DefaultRouter()
-# router.register(r'tickets', ticketMVS)
-# router.register(r'list', ticketListMVS)
-# router.register(r'type', TicketTypeMVS)
-# router.register(r'comment', CommentMVS)
-# router.register(r'attachments', AttachmentMVS)
-
 app_name = "tickets"
 app_name = "tickets"
 
 
 urlpatterns = [
 urlpatterns = [
@@ -20,7 +12,7 @@ urlpatterns = [
     path("ticket_list_create/", TicketListCreateView.as_view(), name="ticket_list_create"),
     path("ticket_list_create/", TicketListCreateView.as_view(), name="ticket_list_create"),
     path("my_tickets/", ticket_list_detail, {"my_tickets": True}, name="my_tickets"),
     path("my_tickets/", ticket_list_detail, {"my_tickets": True}, name="my_tickets"),
     path("assignments/", ticket_list_detail, {"assignments": True}, name="assignments"),
     path("assignments/", ticket_list_detail, {"assignments": True}, name="assignments"),
-        
+
     path("<int:pk>/", ticket_list_detail, name="ticket_list_detail"),
     path("<int:pk>/", ticket_list_detail, name="ticket_list_detail"),
     path("<int:pk>/ticket_create/", TicketCreateView.as_view(), name="ticket_create"),
     path("<int:pk>/ticket_create/", TicketCreateView.as_view(), name="ticket_create"),
     path("<int:pk>/delete", TicketListDeleteView.as_view(), name="ticket_list_delete"),
     path("<int:pk>/delete", TicketListDeleteView.as_view(), name="ticket_list_delete"),
@@ -30,5 +22,9 @@ urlpatterns = [
     path("ticket/<int:pk>/delete", TicketDeleteView.as_view(), name="ticket_delete"),
     path("ticket/<int:pk>/delete", TicketDeleteView.as_view(), name="ticket_delete"),
     path("attachment/remove/<int:attachment_id>/", remove_attachment, name="remove_attachment"),
     path("attachment/remove/<int:attachment_id>/", remove_attachment, name="remove_attachment"),
 
 
-    # path("api/", include(router.urls))
+    # API
+    path("api/v1/ticket_list/", TicketListListAPIView.as_view()),
+    path("api/v1/ticket_list/<int:pk>", TicketListDetailAPIView.as_view()),
+    path("api/v1/ticket/", TicketCreateAPIView.as_view()),
+    path("api/v1/ticket/<int:pk>", TicketDetailAPIView.as_view()),
 ]
 ]

+ 6 - 0
utils.py

@@ -27,6 +27,12 @@ class UserCanReadTicketMixin(UserPassesTestMixin):
         return self.request.user.is_superuser or ticket.ticket_list.group in self.request.user.groups.all() or ticket.assigned_to == self.request.user
         return self.request.user.is_superuser or ticket.ticket_list.group in self.request.user.groups.all() or ticket.assigned_to == self.request.user
 
 
 
 
+class UserCanWriteTicketMixin(UserPassesTestMixin):
+    def test_func(self):
+        ticket = get_object_or_404(Ticket.objects.all(), pk=self.kwargs.get('pk'))
+        return self.request.user.is_superuser or self.request.user.is_staff or ticket.created_by == self.request.user
+
+
 def remove_attachment_file(attachment_id: int) -> bool:
 def remove_attachment_file(attachment_id: int) -> bool:
     """Delete an Attachment object and its corresponding file from the filesystem."""
     """Delete an Attachment object and its corresponding file from the filesystem."""
     try:
     try:

+ 4 - 4
views/delete.py

@@ -6,10 +6,10 @@ from django.shortcuts import get_object_or_404, redirect
 from django.views import View
 from django.views import View
 
 
 from tickets.models import TicketList, Ticket
 from tickets.models import TicketList, Ticket
-from tickets.utils import SuperuserStaffRequiredMixin, UserCanReadTicketListMixin, UserCanReadTicketMixin
+from tickets.utils import SuperuserStaffRequiredMixin, UserCanReadTicketListMixin, UserCanReadTicketMixin, UserCanWriteTicketMixin
 
 
 
 
-class BaseDeleteView(LoginRequiredMixin, SuperuserStaffRequiredMixin, View):
+class BaseDeleteView(LoginRequiredMixin, View):
     model = None
     model = None
     success_message = None
     success_message = None
     redirect_url = None
     redirect_url = None
@@ -24,13 +24,13 @@ class BaseDeleteView(LoginRequiredMixin, SuperuserStaffRequiredMixin, View):
         return redirect(self.redirect_url)
         return redirect(self.redirect_url)
 
 
 
 
-class TicketListDeleteView(BaseDeleteView, UserCanReadTicketListMixin):
+class TicketListDeleteView(BaseDeleteView, SuperuserStaffRequiredMixin, UserCanReadTicketListMixin):
     model = TicketList
     model = TicketList
     success_message = 'The "{0.name}" list has been successfully deleted from "{0.group.name}" group.'
     success_message = 'The "{0.name}" list has been successfully deleted from "{0.group.name}" group.'
     redirect_url = "tickets:ticket_list_list"
     redirect_url = "tickets:ticket_list_list"
 
 
 
 
-class TicketDeleteView(BaseDeleteView, UserCanReadTicketMixin):
+class TicketDeleteView(BaseDeleteView, UserCanReadTicketMixin, UserCanWriteTicketMixin):
     model = Ticket
     model = Ticket
     success_message = 'The "{0.title}" ticket has been successfully deleted from {0.ticket_list} > {0.ticket_list.group}.'
     success_message = 'The "{0.title}" ticket has been successfully deleted from {0.ticket_list} > {0.ticket_list.group}.'
     redirect_url = "tickets:ticket_list_list"
     redirect_url = "tickets:ticket_list_list"

+ 2 - 2
views/ticket_detail.py

@@ -38,8 +38,8 @@ class TicketDetailView(LoginRequiredMixin, UserCanReadTicketMixin, DetailView):
         if request.POST.get("ticket-status"):
         if request.POST.get("ticket-status"):
             status = request.POST.get("ticket-status")
             status = request.POST.get("ticket-status")
             
             
-            if status in ticket.get_available_statuses():
-                ticket.status = status
+            if int(status) in ticket.get_available_statuses():
+                ticket.status = int(status)
                 ticket.save()
                 ticket.save()
                 messages.success(request, f"Status successfully changed.")
                 messages.success(request, f"Status successfully changed.")
             else:
             else:

+ 3 - 2
views/ticket_edit.py

@@ -7,10 +7,11 @@ from django.shortcuts import redirect, get_object_or_404
 from django.views import View
 from django.views import View
 
 
 from tickets.forms import TicketForm
 from tickets.forms import TicketForm
-from tickets.utils import SuperuserStaffRequiredMixin, UserCanReadTicketMixin
+from tickets.utils import UserCanWriteTicketMixin
 from tickets.models import Ticket
 from tickets.models import Ticket
 
 
-class TicketEditView(LoginRequiredMixin, SuperuserStaffRequiredMixin, UserCanReadTicketMixin, View):
+
+class TicketEditView(LoginRequiredMixin, UserCanWriteTicketMixin, View):
     def post(self, request, pk):
     def post(self, request, pk):
         ticket = get_object_or_404(Ticket, pk=pk)
         ticket = get_object_or_404(Ticket, pk=pk)
         form = TicketForm(request.user, request.POST, instance=ticket)
         form = TicketForm(request.user, request.POST, instance=ticket)

+ 1 - 4
views/ticket_list_detail.py

@@ -34,16 +34,13 @@ def ticket_list_detail(request, pk=None, my_tickets=False, assignments=False):
     tickets = tickets.annotate(created_by_username=F("created_by__username"))
     tickets = tickets.annotate(created_by_username=F("created_by__username"))
     tickets = tickets.annotate(assigned_to_username=F("assigned_to__username"))
     tickets = tickets.annotate(assigned_to_username=F("assigned_to__username"))
     
     
-    ticket_types = tickets.values('ticket_type')
-    ticket_types_list = dict(Ticket.TICKET_TYPES).values
-
     context = {
     context = {
         "ticket_list": ticket_list,
         "ticket_list": ticket_list,
         "tickets": tickets,
         "tickets": tickets,
         "form": form,
         "form": form,
         "my_tickets": my_tickets,
         "my_tickets": my_tickets,
         "assignments": assignments,
         "assignments": assignments,
-        "ticket_types": dict(Ticket.TICKET_TYPES).values
+        "ticket_types": Ticket.TICKET_TYPES_DICT.values
     }
     }
 
 
     return render(request, "tickets/ticket_list_detail.html", context)
     return render(request, "tickets/ticket_list_detail.html", context)

+ 1 - 1
views/ticket_list_list.py

@@ -22,7 +22,7 @@ class TicketListView(LoginRequiredMixin, ListView):
                 ticket_lists = ticket_lists.filter(group__id__in=user_groups_ids)
                 ticket_lists = ticket_lists.filter(group__id__in=user_groups_ids)
             else:
             else:
                 messages.warning(self.request, "You do not yet belong to any groups. Ask your administrator to add you to one.")
                 messages.warning(self.request, "You do not yet belong to any groups. Ask your administrator to add you to one.")
-                return None
+                return TicketList.objects.none()
 
 
         return ticket_lists
         return ticket_lists