TonyKurts 1 жил өмнө
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):
     ticket_type = forms.ChoiceField(
-        choices=Ticket.TICKET_TYPES,
+        choices=Ticket.TICKET_TYPES_CHOICES,
         widget=forms.Select(attrs={
                 'class': 'form-control'
         }),

+ 27 - 33
models/ticket.py

@@ -1,5 +1,5 @@
 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.core.exceptions import ValidationError
 
@@ -8,33 +8,26 @@ from tickets.models.ticket_list import TicketList
 
 class Ticket(models.Model):
     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)
     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)
     created_at = models.DateTimeField(auto_now_add=True, editable=False)
     updated_at = models.DateTimeField(auto_now=True)
     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)
     priority = models.PositiveSmallIntegerField(default=0)
 
@@ -42,26 +35,27 @@ class Ticket(models.Model):
         return self.title
 
     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:
         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
 
-    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):
         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)
 

+ 1 - 1
templates/tickets/ticket_detail.html

@@ -76,7 +76,7 @@
         <p class="my-3">No available statuses</p>
       {% 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">
           <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>

+ 7 - 11
urls.py

@@ -1,17 +1,9 @@
 from django.urls import path, include
 
 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"
 
 urlpatterns = [
@@ -20,7 +12,7 @@ urlpatterns = [
     path("ticket_list_create/", TicketListCreateView.as_view(), name="ticket_list_create"),
     path("my_tickets/", ticket_list_detail, {"my_tickets": True}, name="my_tickets"),
     path("assignments/", ticket_list_detail, {"assignments": True}, name="assignments"),
-        
+
     path("<int:pk>/", ticket_list_detail, name="ticket_list_detail"),
     path("<int:pk>/ticket_create/", TicketCreateView.as_view(), name="ticket_create"),
     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("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
 
 
+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:
     """Delete an Attachment object and its corresponding file from the filesystem."""
     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 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
     success_message = None
     redirect_url = None
@@ -24,13 +24,13 @@ class BaseDeleteView(LoginRequiredMixin, SuperuserStaffRequiredMixin, View):
         return redirect(self.redirect_url)
 
 
-class TicketListDeleteView(BaseDeleteView, UserCanReadTicketListMixin):
+class TicketListDeleteView(BaseDeleteView, SuperuserStaffRequiredMixin, UserCanReadTicketListMixin):
     model = TicketList
     success_message = 'The "{0.name}" list has been successfully deleted from "{0.group.name}" group.'
     redirect_url = "tickets:ticket_list_list"
 
 
-class TicketDeleteView(BaseDeleteView, UserCanReadTicketMixin):
+class TicketDeleteView(BaseDeleteView, UserCanReadTicketMixin, UserCanWriteTicketMixin):
     model = Ticket
     success_message = 'The "{0.title}" ticket has been successfully deleted from {0.ticket_list} > {0.ticket_list.group}.'
     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"):
             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()
                 messages.success(request, f"Status successfully changed.")
             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 tickets.forms import TicketForm
-from tickets.utils import SuperuserStaffRequiredMixin, UserCanReadTicketMixin
+from tickets.utils import UserCanWriteTicketMixin
 from tickets.models import Ticket
 
-class TicketEditView(LoginRequiredMixin, SuperuserStaffRequiredMixin, UserCanReadTicketMixin, View):
+
+class TicketEditView(LoginRequiredMixin, UserCanWriteTicketMixin, View):
     def post(self, request, pk):
         ticket = get_object_or_404(Ticket, pk=pk)
         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(assigned_to_username=F("assigned_to__username"))
     
-    ticket_types = tickets.values('ticket_type')
-    ticket_types_list = dict(Ticket.TICKET_TYPES).values
-
     context = {
         "ticket_list": ticket_list,
         "tickets": tickets,
         "form": form,
         "my_tickets": my_tickets,
         "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)

+ 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)
             else:
                 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