diff --git a/core/tickets/admin.py b/core/tickets/admin.py index d862811..20cb83f 100644 --- a/core/tickets/admin.py +++ b/core/tickets/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from tickets.models import IssueThread, Comment, StateChange +from tickets.models import IssueThread, Comment, StateChange, Assignment, ShippingVoucher class IssueThreadAdmin(admin.ModelAdmin): @@ -15,6 +15,16 @@ class StateChangeAdmin(admin.ModelAdmin): pass +class AssignmentAdmin(admin.ModelAdmin): + pass + + +class ShippingVouchersAdmin(admin.ModelAdmin): + pass + + admin.site.register(IssueThread, IssueThreadAdmin) admin.site.register(Comment, CommentAdmin) admin.site.register(StateChange, StateChangeAdmin) +admin.site.register(Assignment, AssignmentAdmin) +admin.site.register(ShippingVoucher, ShippingVouchersAdmin) diff --git a/core/tickets/api_v2.py b/core/tickets/api_v2.py index cd9cd3e..f8f746e 100644 --- a/core/tickets/api_v2.py +++ b/core/tickets/api_v2.py @@ -13,8 +13,8 @@ from core.settings import MAIL_DOMAIN from mail.models import Email from mail.protocol import send_smtp, make_reply, collect_references from notify_sessions.models import SystemEvent -from tickets.models import IssueThread, Comment, STATE_CHOICES -from tickets.serializers import IssueSerializer, CommentSerializer +from tickets.models import IssueThread, Comment, STATE_CHOICES, ShippingVoucher +from tickets.serializers import IssueSerializer, CommentSerializer, ShippingVoucherSerializer class IssueViewSet(viewsets.ModelViewSet): @@ -22,6 +22,11 @@ class IssueViewSet(viewsets.ModelViewSet): queryset = IssueThread.objects.all() +class ShippingVoucherViewSet(viewsets.ModelViewSet): + serializer_class = ShippingVoucherSerializer + queryset = ShippingVoucher.objects.all() + + @api_view(['POST']) @permission_classes([IsAuthenticated]) @permission_required('tickets.add_issuethread', raise_exception=True) @@ -113,6 +118,7 @@ def add_comment(request, pk): router = routers.SimpleRouter() router.register(r'tickets', IssueViewSet, basename='issues') +router.register(r'shipping_vouchers', ShippingVoucherViewSet, basename='shipping_vouchers') urlpatterns = ([ re_path(r'^tickets/(?P\d+)/reply/$', reply, name='reply'), diff --git a/core/tickets/migrations/0009_shippingvoucher.py b/core/tickets/migrations/0009_shippingvoucher.py new file mode 100644 index 0000000..a857457 --- /dev/null +++ b/core/tickets/migrations/0009_shippingvoucher.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.7 on 2024-06-23 00:47 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('tickets', '0008_alter_issuethread_options_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='ShippingVoucher', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('voucher', models.CharField(max_length=255)), + ('type', models.CharField(max_length=255)), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ('used_at', models.DateTimeField(null=True)), + ('issue_thread', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='shipping_vouchers', to='tickets.issuethread')), + ], + ), + ] diff --git a/core/tickets/models.py b/core/tickets/models.py index 6dd42c3..141cf27 100644 --- a/core/tickets/models.py +++ b/core/tickets/models.py @@ -1,4 +1,5 @@ from django.db import models +from django.utils import timezone from django_softdelete.models import SoftDeleteModel from authentication.models import ExtendedUser @@ -116,3 +117,20 @@ class Assignment(models.Model): def __str__(self): return str(self.issue_thread) + ' assigned to ' + self.assigned_to.username + + +class ShippingVoucher(models.Model): + id = models.AutoField(primary_key=True) + issue_thread = models.ForeignKey(IssueThread, on_delete=models.CASCADE, related_name='shipping_vouchers', null=True) + voucher = models.CharField(max_length=255) + type = models.CharField(max_length=255) + timestamp = models.DateTimeField(auto_now_add=True) + used_at = models.DateTimeField(null=True) + + def __str__(self): + return self.voucher + ' (' + self.type + ')' + + def save(self, *args, **kwargs): + if self.used_at is None and self.issue_thread is not None: + self.used_at = timezone.now() + super().save(*args, **kwargs) diff --git a/core/tickets/serializers.py b/core/tickets/serializers.py index fe3972e..448a902 100644 --- a/core/tickets/serializers.py +++ b/core/tickets/serializers.py @@ -2,7 +2,7 @@ from rest_framework import serializers from authentication.models import ExtendedUser from mail.api_v2 import AttachmentSerializer -from tickets.models import IssueThread, Comment, STATE_CHOICES +from tickets.models import IssueThread, Comment, STATE_CHOICES, ShippingVoucher class CommentSerializer(serializers.ModelSerializer): @@ -28,6 +28,13 @@ class StateSerializer(serializers.Serializer): return obj['value'] +class ShippingVoucherSerializer(serializers.ModelSerializer): + class Meta: + model = ShippingVoucher + fields = ('id', 'voucher', 'type', 'timestamp', 'issue_thread', 'used_at') + read_only_fields = ('id', 'timestamp', 'used_at') + + class IssueSerializer(serializers.ModelSerializer): timeline = serializers.SerializerMethodField() last_activity = serializers.SerializerMethodField() @@ -60,7 +67,10 @@ class IssueSerializer(serializers.ModelSerializer): if self.state_changes.count() > 0 else None last_comment = self.comments.order_by('-timestamp').first().timestamp if self.comments.count() > 0 else None last_mail = self.emails.order_by('-timestamp').first().timestamp if self.emails.count() > 0 else None - args = [x for x in [last_state_change, last_comment, last_mail] if x is not None] + last_assignment = self.assignments.order_by('-timestamp').first().timestamp if \ + self.assignments.count() > 0 else None + args = [x for x in [last_state_change, last_comment, last_mail, last_assignment] if + x is not None] return max(args) except AttributeError: return None @@ -100,6 +110,14 @@ class IssueSerializer(serializers.ModelSerializer): 'timestamp': assignment.timestamp, 'assigned_to': assignment.assigned_to.username, }) + for shipping_voucher in obj.shipping_vouchers.all(): + timeline.append({ + 'type': 'shipping_voucher', + 'id': shipping_voucher.id, + 'timestamp': shipping_voucher.used_at, + 'voucher': shipping_voucher.voucher, + 'voucher_type': shipping_voucher.type, + }) return sorted(timeline, key=lambda x: x['timestamp']) def get_queryset(self): diff --git a/core/tickets/tests/v2/test_shipping_vouchers.py b/core/tickets/tests/v2/test_shipping_vouchers.py new file mode 100644 index 0000000..45fa245 --- /dev/null +++ b/core/tickets/tests/v2/test_shipping_vouchers.py @@ -0,0 +1,41 @@ +from datetime import datetime, timedelta + +from django.test import TestCase, Client + +from authentication.models import ExtendedUser +from mail.models import Email, EmailAttachment +from tickets.models import IssueThread, StateChange, Comment, ShippingVoucher +from django.contrib.auth.models import Permission +from knox.models import AuthToken + + +class ShippingVoucherApiTest(TestCase): + + def setUp(self): + super().setUp() + self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test') + self.user.user_permissions.add(*Permission.objects.all()) + self.user.save() + self.token = AuthToken.objects.create(user=self.user) + self.client = Client(headers={'Authorization': 'Token ' + self.token[1]}) + + def test_issues_empty(self): + response = self.client.get('/api/2/shipping_vouchers/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), []) + + def test_issues_list(self): + ShippingVoucher.objects.create(voucher='1234', type='2kg-eu') + response = self.client.get('/api/2/shipping_vouchers/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()[0]['voucher'], '1234') + self.assertEqual(response.json()[0]['used_at'], None) + self.assertEqual(response.json()[0]['issue_thread'], None) + self.assertEqual(response.json()[0]['type'], '2kg-eu') + + def test_issues_create(self): + response = self.client.post('/api/2/shipping_vouchers/', {'voucher': '1234', 'type': '2kg-eu'}) + self.assertEqual(response.status_code, 201) + self.assertEqual(response.json()['voucher'], '1234') + self.assertEqual(response.json()['used_at'], None) + self.assertEqual(response.json()['issue_thread'], None)