1
0
Эх сурвалжийг харах

Merge branch 'unstable' of blezz-tech/sharix-open-tickets into master

+ 8 - 1
admin.py

@@ -1,7 +1,7 @@
 from django.contrib import admin
 from django.contrib import admin
 from django.utils.safestring import mark_safe
 from django.utils.safestring import mark_safe
 
 
-from tickets.models import Attachment, Comment, TicketList, Ticket, TicketArchive
+from tickets.models import Attachment, Comment, Event, TicketList, Ticket, TicketArchive
 
 
 
 
 class TicketListAdmin(admin.ModelAdmin):
 class TicketListAdmin(admin.ModelAdmin):
@@ -84,6 +84,12 @@ class CommentAdmin(admin.ModelAdmin):
         "snippet"
         "snippet"
     )
     )
 
 
+class EventAdmin(admin.ModelAdmin):
+    list_display = (
+        "author",
+        "date",
+        "body"
+    )
 
 
 class AttachmentAdmin(admin.ModelAdmin):
 class AttachmentAdmin(admin.ModelAdmin):
     list_display = (
     list_display = (
@@ -102,4 +108,5 @@ admin.site.register(TicketList, TicketListAdmin)
 admin.site.register(Ticket, TicketAdmin)
 admin.site.register(Ticket, TicketAdmin)
 admin.site.register(TicketArchive, TicketArchiveAdmin)
 admin.site.register(TicketArchive, TicketArchiveAdmin)
 admin.site.register(Comment, CommentAdmin)
 admin.site.register(Comment, CommentAdmin)
+admin.site.register(Event, EventAdmin)
 admin.site.register(Attachment, AttachmentAdmin)
 admin.site.register(Attachment, AttachmentAdmin)

+ 5 - 3
api/views/ticket.py

@@ -1,15 +1,17 @@
 from rest_framework import generics, permissions, status
 from rest_framework import generics, permissions, status
 from rest_framework.response import Response
 from rest_framework.response import Response
+from rest_framework_api_key.permissions import HasAPIKey
 
 
 from tickets.api.permissions import UserTicketAccessPermission, UserTicketStatusAccessPermission
 from tickets.api.permissions import UserTicketAccessPermission, UserTicketStatusAccessPermission
 from tickets.api.serializers import TicketDetailSerializer, TicketSerializer, TicketStatusSerializer
 from tickets.api.serializers import TicketDetailSerializer, TicketSerializer, TicketStatusSerializer
 from tickets.models import Ticket
 from tickets.models import Ticket
 
 
+#TODO - maybe it's good idea to add concrete permission checks for handlers with API keys
 
 
 class TicketDetailAPIView(generics.RetrieveUpdateDestroyAPIView):
 class TicketDetailAPIView(generics.RetrieveUpdateDestroyAPIView):
     queryset = Ticket.objects.all()
     queryset = Ticket.objects.all()
     serializer_class = TicketDetailSerializer
     serializer_class = TicketDetailSerializer
-    permission_classes = [permissions.IsAuthenticated & UserTicketAccessPermission]
+    permission_classes = [permissions.IsAuthenticated & UserTicketAccessPermission | HasAPIKey]
 
 
     def destroy(self, request, *args, **kwargs):
     def destroy(self, request, *args, **kwargs):
         instance = self.get_object()
         instance = self.get_object()
@@ -20,7 +22,7 @@ class TicketDetailAPIView(generics.RetrieveUpdateDestroyAPIView):
 class TicketCreateAPIView(generics.CreateAPIView):
 class TicketCreateAPIView(generics.CreateAPIView):
     queryset = Ticket.objects.all()
     queryset = Ticket.objects.all()
     serializer_class = TicketSerializer
     serializer_class = TicketSerializer
-    permission_classes = [permissions.IsAuthenticated & UserTicketAccessPermission]
+    permission_classes = [permissions.IsAuthenticated & UserTicketAccessPermission | HasAPIKey]
 
 
     def perform_create(self, serializer):
     def perform_create(self, serializer):
         if serializer.is_valid():
         if serializer.is_valid():
@@ -30,4 +32,4 @@ class TicketCreateAPIView(generics.CreateAPIView):
 class TicketStatusAPIView(generics.RetrieveUpdateAPIView):
 class TicketStatusAPIView(generics.RetrieveUpdateAPIView):
     queryset = Ticket.objects.all()
     queryset = Ticket.objects.all()
     serializer_class = TicketStatusSerializer
     serializer_class = TicketStatusSerializer
-    permission_classes = [permissions.IsAuthenticated & UserTicketStatusAccessPermission]
+    permission_classes = [permissions.IsAuthenticated & UserTicketStatusAccessPermission | HasAPIKey]

+ 3 - 1
api/views/ticket_list.py

@@ -1,13 +1,15 @@
 from django.shortcuts import get_object_or_404
 from django.shortcuts import get_object_or_404
 from rest_framework import generics, permissions
 from rest_framework import generics, permissions
 
 
+from rest_framework_api_key.permissions import HasAPIKey
+
 from tickets.api.permissions import UserCanReadTicketListPermission
 from tickets.api.permissions import UserCanReadTicketListPermission
 from tickets.api.serializers import TicketListSerializer, TicketSerializer
 from tickets.api.serializers import TicketListSerializer, TicketSerializer
 from tickets.models import Ticket, TicketList
 from tickets.models import Ticket, TicketList
 
 
 
 
 class TicketListListAPIView(generics.ListAPIView):
 class TicketListListAPIView(generics.ListAPIView):
-    permission_classes = [permissions.IsAuthenticated]
+    permission_classes = [permissions.IsAuthenticated | HasAPIKey]
     serializer_class = TicketListSerializer
     serializer_class = TicketListSerializer
 
 
     def get_queryset(self):
     def get_queryset(self):

+ 1 - 0
models/__init__.py

@@ -1,4 +1,5 @@
 from tickets.models.attachment import *
 from tickets.models.attachment import *
 from tickets.models.comment import *
 from tickets.models.comment import *
+from tickets.models.event import *
 from tickets.models.ticket import *
 from tickets.models.ticket import *
 from tickets.models.ticket_list import *
 from tickets.models.ticket_list import *

+ 20 - 0
models/event.py

@@ -0,0 +1,20 @@
+import textwrap
+
+from django.contrib.auth import get_user_model
+from django.db import models
+
+from tickets.models.ticket import Ticket
+
+
+class Event(models.Model):
+    author = models.ForeignKey(
+        get_user_model(), on_delete=models.CASCADE, blank=True, null=True,
+        related_name="tickets_events"
+    )
+    ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE)
+    date = models.DateTimeField(auto_now_add=True)
+
+    body = models.TextField(blank=True)
+
+    def __str__(self):
+        return self.body

+ 48 - 27
templates/tickets/ticket_detail.html

@@ -71,6 +71,33 @@
             <button id="ticket-change-status-btn" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#ticket-change-status-modal">Change</button>
             <button id="ticket-change-status-btn" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#ticket-change-status-modal">Change</button>
           </div>
           </div>
         </form>
         </form>
+
+        <!--ticket change status-->
+        <div class="modal fade" id="ticket-change-status-modal" tabindex="-1">
+          <div class="modal-dialog modal-dialog-centered">
+            <div class="modal-content">
+              <div class="modal-header">
+                <h1 class="modal-title fs-5">
+                  <i class="fa-solid fa-trash-can pe-1"></i>
+                  Change Status
+                </h1>
+              </div>
+              <div class="modal-body">
+                Are you sure you want to change the status of the ticket "{{ ticket.title }}" from <b>{{ ticket.get_status_display }}</b> to <b id="selectedStatusOption"></b>?
+              </div>
+              <div class="modal-footer">
+                <button type="button" class="btn btn-primary" data-bs-dismiss="modal">
+                  <i class="fa-solid fa-xmark pe-1"></i>
+                  Close
+                </button>
+                <button form="ticket-change-status-form" class="btn btn-outline-primary" type="submit">
+                  <i class="fa-solid fa-check pe-1"></i>
+                  Confirm
+                </button>
+              </div>
+            </div>
+          </div>
+        </div>
       {% else %}
       {% else %}
         <p class="my-3">No available statuses</p>
         <p class="my-3">No available statuses</p>
       {% endif %}
       {% endif %}
@@ -143,33 +170,6 @@
             </div>
             </div>
           </div>
           </div>
         </div>
         </div>
-
-        <!--ticket change status-->
-        <div class="modal fade" id="ticket-change-status-modal" tabindex="-1">
-          <div class="modal-dialog modal-dialog-centered">
-            <div class="modal-content">
-              <div class="modal-header">
-                <h1 class="modal-title fs-5">
-                  <i class="fa-solid fa-trash-can pe-1"></i>
-                  Change Status
-                </h1>
-              </div>
-              <div class="modal-body">
-                Are you sure you want to change the status of the ticket "{{ ticket.title }}" from <b>{{ ticket.get_status_display }}</b> to <b id="selectedStatusOption"></b>?
-              </div>
-              <div class="modal-footer">
-                <button type="button" class="btn btn-primary" data-bs-dismiss="modal">
-                  <i class="fa-solid fa-xmark pe-1"></i>
-                  Close
-                </button>
-                <button form="ticket-change-status-form" class="btn btn-outline-primary" type="submit">
-                  <i class="fa-solid fa-check pe-1"></i>
-                  Confirm
-                </button>
-              </div>
-            </div>
-          </div>
-        </div>
       {% endif %}
       {% endif %}
     </div>
     </div>
   </div>
   </div>
@@ -221,6 +221,27 @@
     </div>
     </div>
   </form>
   </form>
 
 
+
+  <!--History block-->
+  <h4 class="mt-4">Status history</h4>
+  
+  {% if event_list %}
+    {% for event in event_list %}
+      <div class="card mb-2 bg-body-secondary">
+        <div class="card-body">
+          <h5>{{ event.author_username }}</h5>
+
+          {{ event.body|safe|urlize|linebreaks }}
+          
+          <div class="text-body-secondary d-flex justify-content-between">
+            <small>{{ event.date }}</small>
+          </div>
+        </div>
+      </div>
+    {% endfor %}
+  {% endif %}
+
+
   <!--Discussion block-->
   <!--Discussion block-->
   <h4 class="mt-4">Discussion</h4>
   <h4 class="mt-4">Discussion</h4>
   <form class="mb-3" method="post">
   <form class="mb-3" method="post">

+ 10 - 1
views/ticket_detail.py

@@ -8,7 +8,7 @@ from django.shortcuts import redirect
 from django.views.generic import DetailView
 from django.views.generic import DetailView
 
 
 from tickets.forms import TicketForm
 from tickets.forms import TicketForm
-from tickets.models import Attachment, Comment, Ticket
+from tickets.models import Attachment, Comment, Event, Ticket
 from tickets.utils import UserCanReadTicketMixin
 from tickets.utils import UserCanReadTicketMixin
 
 
 
 
@@ -27,6 +27,9 @@ class TicketDetailView(LoginRequiredMixin, UserCanReadTicketMixin, DetailView):
         context['comment_list'] = Comment.objects.filter(ticket=self.object.pk).order_by("-date").annotate(
         context['comment_list'] = Comment.objects.filter(ticket=self.object.pk).order_by("-date").annotate(
             author_username=F("author__username"), author_email=F("author__email")
             author_username=F("author__username"), author_email=F("author__email")
         )
         )
+        context['event_list'] = Event.objects.filter(ticket=self.object.pk).order_by("-date").annotate(
+            author_username=F("author__username")
+        )
         context['attachments'] = Attachment.objects.filter(ticket=self.object.pk).select_related("added_by")
         context['attachments'] = Attachment.objects.filter(ticket=self.object.pk).select_related("added_by")
 
 
         # List with statuses codes & their names
         # List with statuses codes & their names
@@ -47,9 +50,15 @@ class TicketDetailView(LoginRequiredMixin, UserCanReadTicketMixin, DetailView):
             status = request.POST.get("ticket-status")
             status = request.POST.get("ticket-status")
 
 
             if int(status) in ticket.get_available_statuses():
             if int(status) in ticket.get_available_statuses():
+                old_status = ticket.status
                 ticket.status = int(status)
                 ticket.status = int(status)
                 ticket.save()
                 ticket.save()
                 messages.success(request, f"Status successfully changed.")
                 messages.success(request, f"Status successfully changed.")
+                Event.objects.create(
+                    author=request.user,
+                    ticket=ticket,
+                    body="Ticket status was changed from "+Ticket.TICKET_STATUSES_NAMES.get(old_status)+" to "+Ticket.TICKET_STATUSES_NAMES.get(int(status))
+                )
             else:
             else:
                 messages.error(request, f"Status change error.")
                 messages.error(request, f"Status change error.")