浏览代码

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

共有 7 个文件被更改,包括 95 次插入33 次删除
  1. 8 1
      admin.py
  2. 5 3
      api/views/ticket.py
  3. 3 1
      api/views/ticket_list.py
  4. 1 0
      models/__init__.py
  5. 20 0
      models/event.py
  6. 48 27
      templates/tickets/ticket_detail.html
  7. 10 1
      views/ticket_detail.py

+ 8 - 1
admin.py

@@ -1,7 +1,7 @@
 from django.contrib import admin
 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):
@@ -84,6 +84,12 @@ class CommentAdmin(admin.ModelAdmin):
         "snippet"
     )
 
+class EventAdmin(admin.ModelAdmin):
+    list_display = (
+        "author",
+        "date",
+        "body"
+    )
 
 class AttachmentAdmin(admin.ModelAdmin):
     list_display = (
@@ -102,4 +108,5 @@ admin.site.register(TicketList, TicketListAdmin)
 admin.site.register(Ticket, TicketAdmin)
 admin.site.register(TicketArchive, TicketArchiveAdmin)
 admin.site.register(Comment, CommentAdmin)
+admin.site.register(Event, EventAdmin)
 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.response import Response
+from rest_framework_api_key.permissions import HasAPIKey
 
 from tickets.api.permissions import UserTicketAccessPermission, UserTicketStatusAccessPermission
 from tickets.api.serializers import TicketDetailSerializer, TicketSerializer, TicketStatusSerializer
 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):
     queryset = Ticket.objects.all()
     serializer_class = TicketDetailSerializer
-    permission_classes = [permissions.IsAuthenticated & UserTicketAccessPermission]
+    permission_classes = [permissions.IsAuthenticated & UserTicketAccessPermission | HasAPIKey]
 
     def destroy(self, request, *args, **kwargs):
         instance = self.get_object()
@@ -20,7 +22,7 @@ class TicketDetailAPIView(generics.RetrieveUpdateDestroyAPIView):
 class TicketCreateAPIView(generics.CreateAPIView):
     queryset = Ticket.objects.all()
     serializer_class = TicketSerializer
-    permission_classes = [permissions.IsAuthenticated & UserTicketAccessPermission]
+    permission_classes = [permissions.IsAuthenticated & UserTicketAccessPermission | HasAPIKey]
 
     def perform_create(self, serializer):
         if serializer.is_valid():
@@ -30,4 +32,4 @@ class TicketCreateAPIView(generics.CreateAPIView):
 class TicketStatusAPIView(generics.RetrieveUpdateAPIView):
     queryset = Ticket.objects.all()
     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 rest_framework import generics, permissions
 
+from rest_framework_api_key.permissions import HasAPIKey
+
 from tickets.api.permissions import UserCanReadTicketListPermission
 from tickets.api.serializers import TicketListSerializer, TicketSerializer
 from tickets.models import Ticket, TicketList
 
 
 class TicketListListAPIView(generics.ListAPIView):
-    permission_classes = [permissions.IsAuthenticated]
+    permission_classes = [permissions.IsAuthenticated | HasAPIKey]
     serializer_class = TicketListSerializer
 
     def get_queryset(self):

+ 1 - 0
models/__init__.py

@@ -1,4 +1,5 @@
 from tickets.models.attachment import *
 from tickets.models.comment import *
+from tickets.models.event import *
 from tickets.models.ticket 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>
           </div>
         </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 %}
         <p class="my-3">No available statuses</p>
       {% endif %}
@@ -143,33 +170,6 @@
             </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 %}
     </div>
   </div>
@@ -221,6 +221,27 @@
     </div>
   </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-->
   <h4 class="mt-4">Discussion</h4>
   <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 tickets.forms import TicketForm
-from tickets.models import Attachment, Comment, Ticket
+from tickets.models import Attachment, Comment, Event, Ticket
 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(
             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")
 
         # List with statuses codes & their names
@@ -47,9 +50,15 @@ class TicketDetailView(LoginRequiredMixin, UserCanReadTicketMixin, DetailView):
             status = request.POST.get("ticket-status")
 
             if int(status) in ticket.get_available_statuses():
+                old_status = ticket.status
                 ticket.status = int(status)
                 ticket.save()
                 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:
                 messages.error(request, f"Status change error.")