From f2647f0dbd4fc3c95aeee1c8dba9238b6dd87e47 Mon Sep 17 00:00:00 2001 From: jedi Date: Thu, 21 Nov 2024 00:58:14 +0100 Subject: [PATCH] add /matches endpoint ad return match information in tickets and /item endpoints --- core/core/settings.py | 7 +- core/inventory/models.py | 12 +- core/inventory/serializers.py | 28 +--- core/inventory/shared_serializers.py | 31 ++++ core/inventory/tests/v2/test_items.py | 46 ++++-- core/tickets/api_v2.py | 16 +- ...move_issuethread_related_items_and_more.py | 29 ++++ core/tickets/models.py | 14 +- core/tickets/serializers.py | 21 +-- core/tickets/shared_serializers.py | 23 +++ core/tickets/tests/v2/test_matches.py | 149 ++++++++++++++++++ core/tickets/tests/v2/test_tickets.py | 24 ++- 12 files changed, 339 insertions(+), 61 deletions(-) create mode 100644 core/inventory/shared_serializers.py create mode 100644 core/tickets/migrations/0012_remove_issuethread_related_items_and_more.py create mode 100644 core/tickets/shared_serializers.py create mode 100644 core/tickets/tests/v2/test_matches.py diff --git a/core/core/settings.py b/core/core/settings.py index 6796112..5a8f20f 100644 --- a/core/core/settings.py +++ b/core/core/settings.py @@ -34,7 +34,9 @@ SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', 'django-insecure-tm*$w_14iqbiy-!7(8# # SECURITY WARNING: don't run with debug turned on in production! DEBUG = truthy_str(os.getenv('DEBUG_MODE_ACTIVE', 'False')) -ALLOWED_HOSTS = [os.getenv('HTTP_HOST', 'localhost')] +PRIMARY_HOST = os.getenv('HTTP_HOST', 'localhost') + +ALLOWED_HOSTS = [PRIMARY_HOST] MAIL_DOMAIN = os.getenv('MAIL_DOMAIN', 'localhost') @@ -144,7 +146,8 @@ else: 'USER': os.getenv('DB_USER', 'system3'), 'PASSWORD': os.getenv('DB_PASSWORD', 'system3'), 'OPTIONS': { - 'charset': 'utf8mb4' + 'charset': 'utf8mb4', + 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'" } } } diff --git a/core/inventory/models.py b/core/inventory/models.py index 5a10202..3421680 100644 --- a/core/inventory/models.py +++ b/core/inventory/models.py @@ -1,5 +1,6 @@ -from django.core.files.base import ContentFile -from django.db import models, IntegrityError +from itertools import groupby + +from django.db import models from django_softdelete.models import SoftDeleteModel, SoftDeleteManager @@ -26,6 +27,13 @@ class Item(SoftDeleteModel): created_at = models.DateTimeField(null=True, auto_now_add=True) updated_at = models.DateTimeField(blank=True, null=True) + @property + def related_issues(self): + groups = groupby(self.issue_relation_changes.all(), lambda rel: rel.issue_thread.id) + return [sorted(v, key=lambda r: r.timestamp)[0].issue_thread for k, v in groups] + + + objects = ItemManager() all_objects = models.Manager() diff --git a/core/inventory/serializers.py b/core/inventory/serializers.py index 0230644..941e20f 100644 --- a/core/inventory/serializers.py +++ b/core/inventory/serializers.py @@ -1,9 +1,12 @@ from django.utils import timezone from rest_framework import serializers +from rest_framework.relations import SlugRelatedField from files.models import File from inventory.models import Event, Container, Item +from inventory.shared_serializers import BasicItemSerializer from mail.models import EventAddress +from tickets.shared_serializers import BasicIssueSerializer class EventAdressSerializer(serializers.ModelSerializer): @@ -33,34 +36,15 @@ class ContainerSerializer(serializers.ModelSerializer): return Item.objects.filter(container=instance.id).count() -class ItemSerializer(serializers.ModelSerializer): +class ItemSerializer(BasicItemSerializer): dataImage = serializers.CharField(write_only=True, required=False) - cid = serializers.SerializerMethodField() - box = serializers.SerializerMethodField() - file = serializers.SerializerMethodField() - returned = serializers.SerializerMethodField(required=False) - event = serializers.SlugRelatedField(slug_field='slug', queryset=Event.objects.all(), - allow_null=True, required=False) + related_issues = BasicIssueSerializer(many=True, read_only=True) class Meta: model = Item - fields = ['cid', 'box', 'id', 'description', 'file', 'dataImage', 'returned', 'event'] + fields = ['cid', 'box', 'id', 'description', 'file', 'dataImage', 'returned', 'event', 'related_issues'] read_only_fields = ['id'] - def get_cid(self, instance): - return instance.container.id - - def get_box(self, instance): - return instance.container.name - - def get_file(self, instance): - if len(instance.files.all()) > 0: - return instance.files.all().order_by('-created_at')[0].hash - return None - - def get_returned(self, instance): - return instance.returned_at is not None - def to_internal_value(self, data): container = None returned = False diff --git a/core/inventory/shared_serializers.py b/core/inventory/shared_serializers.py new file mode 100644 index 0000000..0bd44c3 --- /dev/null +++ b/core/inventory/shared_serializers.py @@ -0,0 +1,31 @@ +from rest_framework import serializers + +from inventory.models import Event, Item + + +class BasicItemSerializer(serializers.ModelSerializer): + cid = serializers.SerializerMethodField() + box = serializers.SerializerMethodField() + file = serializers.SerializerMethodField() + returned = serializers.SerializerMethodField(required=False) + event = serializers.SlugRelatedField(slug_field='slug', queryset=Event.objects.all(), + allow_null=True, required=False) + + class Meta: + model = Item + fields = ['cid', 'box', 'id', 'description', 'file', 'returned', 'event'] + read_only_fields = ['id'] + + def get_cid(self, instance): + return instance.container.id if instance.container else None + + def get_box(self, instance): + return instance.container.name if instance.container else None + + def get_file(self, instance): + if len(instance.files.all()) > 0: + return instance.files.all().order_by('-created_at')[0].hash + return None + + def get_returned(self, instance): + return instance.returned_at is not None diff --git a/core/inventory/tests/v2/test_items.py b/core/inventory/tests/v2/test_items.py index 1832872..953f1a3 100644 --- a/core/inventory/tests/v2/test_items.py +++ b/core/inventory/tests/v2/test_items.py @@ -28,9 +28,15 @@ class ItemTestCase(TestCase): item = Item.objects.create(container=self.box, event=self.event, description='1') response = self.client.get(f'/api/2/{self.event.slug}/item/') self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), - [{'id': 1, 'description': '1', 'box': 'BOX', 'cid': self.box.id, 'file': None, - 'returned': False, 'event': self.event.slug}]) + self.assertEqual(len(response.json()), 1) + self.assertEqual(response.json()[0]['id'], item.id) + self.assertEqual(response.json()[0]['description'], '1') + self.assertEqual(response.json()[0]['box'], 'BOX') + self.assertEqual(response.json()[0]['cid'], self.box.id) + self.assertEqual(response.json()[0]['file'], None) + self.assertEqual(response.json()[0]['returned'], False) + self.assertEqual(response.json()[0]['event'], self.event.slug) + self.assertEqual(len(response.json()[0]['related_issues']), 0) def test_members_with_file(self): import base64 @@ -38,9 +44,15 @@ class ItemTestCase(TestCase): file = File.objects.create(item=item, data="data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8')) response = self.client.get(f'/api/2/{self.event.slug}/item/') self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), - [{'id': 1, 'description': '1', 'box': 'BOX', 'cid': self.box.id, 'file': file.hash, - 'returned': False, 'event': self.event.slug}]) + self.assertEqual(len(response.json()), 1) + self.assertEqual(response.json()[0]['id'], item.id) + self.assertEqual(response.json()[0]['description'], '1') + self.assertEqual(response.json()[0]['box'], 'BOX') + self.assertEqual(response.json()[0]['cid'], self.box.id) + self.assertEqual(response.json()[0]['file'], file.hash) + self.assertEqual(response.json()[0]['returned'], False) + self.assertEqual(response.json()[0]['event'], self.event.slug) + self.assertEqual(len(response.json()[0]['related_issues']), 0) def test_multi_members(self): Item.objects.create(container=self.box, event=self.event, description='1') @@ -53,9 +65,14 @@ class ItemTestCase(TestCase): def test_create_item(self): response = self.client.post(f'/api/2/{self.event.slug}/item/', {'cid': self.box.id, 'description': '1'}) self.assertEqual(response.status_code, 201) - self.assertEqual(response.json(), - {'id': 1, 'description': '1', 'box': 'BOX', 'cid': self.box.id, 'file': None, - 'returned': False, 'event': self.event.slug}) + self.assertEqual(response.json()['id'], 1) + self.assertEqual(response.json()['description'], '1') + self.assertEqual(response.json()['box'], 'BOX') + self.assertEqual(response.json()['cid'], self.box.id) + self.assertEqual(response.json()['file'], None) + self.assertEqual(response.json()['returned'], False) + self.assertEqual(response.json()['event'], self.event.slug) + self.assertEqual(len(response.json()['related_issues']), 0) self.assertEqual(len(Item.objects.all()), 1) self.assertEqual(Item.objects.all()[0].id, 1) self.assertEqual(Item.objects.all()[0].description, '1') @@ -92,9 +109,14 @@ class ItemTestCase(TestCase): response = self.client.patch(f'/api/2/{self.event.slug}/item/{item.id}/', {'description': '2'}, content_type='application/json') self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), - {'id': 1, 'description': '2', 'box': 'BOX', 'cid': self.box.id, 'file': None, - 'returned': False, 'event': self.event.slug}) + self.assertEqual(response.json()['id'], item.id) + self.assertEqual(response.json()['description'], '2') + self.assertEqual(response.json()['box'], 'BOX') + self.assertEqual(response.json()['cid'], self.box.id) + self.assertEqual(response.json()['file'], None) + self.assertEqual(response.json()['returned'], False) + self.assertEqual(response.json()['event'], self.event.slug) + self.assertEqual(len(response.json()['related_issues']), 0) self.assertEqual(len(Item.objects.all()), 1) self.assertEqual(Item.objects.all()[0].id, 1) self.assertEqual(Item.objects.all()[0].description, '2') diff --git a/core/tickets/api_v2.py b/core/tickets/api_v2.py index 39404f2..b119bd9 100644 --- a/core/tickets/api_v2.py +++ b/core/tickets/api_v2.py @@ -1,4 +1,4 @@ -import logging +from base64 import b64decode from django.urls import re_path from django.contrib.auth.decorators import permission_required @@ -14,8 +14,9 @@ from inventory.models import Event 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, ShippingVoucher +from tickets.models import IssueThread, Comment, STATE_CHOICES, ShippingVoucher, ItemRelation from tickets.serializers import IssueSerializer, CommentSerializer, ShippingVoucherSerializer +from tickets.shared_serializers import RelationSerializer class IssueViewSet(viewsets.ModelViewSet): @@ -23,6 +24,16 @@ class IssueViewSet(viewsets.ModelViewSet): queryset = IssueThread.objects.all() +class RelationViewSet(viewsets.ModelViewSet): + serializer_class = RelationSerializer + queryset = ItemRelation.objects.all() + + +class CommentViewSet(viewsets.ModelViewSet): + serializer_class = CommentSerializer + queryset = Comment.objects.all() + + class ShippingVoucherViewSet(viewsets.ModelViewSet): serializer_class = ShippingVoucherSerializer queryset = ShippingVoucher.objects.all() @@ -127,6 +138,7 @@ def add_comment(request, pk): router = routers.SimpleRouter() router.register(r'tickets', IssueViewSet, basename='issues') +router.register(r'matches', RelationViewSet, basename='matches') router.register(r'shipping_vouchers', ShippingVoucherViewSet, basename='shipping_vouchers') urlpatterns = ([ diff --git a/core/tickets/migrations/0012_remove_issuethread_related_items_and_more.py b/core/tickets/migrations/0012_remove_issuethread_related_items_and_more.py new file mode 100644 index 0000000..d8a24c7 --- /dev/null +++ b/core/tickets/migrations/0012_remove_issuethread_related_items_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.7 on 2024-11-20 23:58 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0006_alter_event_table'), + ('tickets', '0011_train_old_spam'), + ] + + operations = [ + migrations.RemoveField( + model_name='issuethread', + name='related_items', + ), + migrations.AlterField( + model_name='itemrelation', + name='issue_thread', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='item_relation_changes', to='tickets.issuethread'), + ), + migrations.AlterField( + model_name='itemrelation', + name='item', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_relation_changes', to='inventory.item'), + ), + ] diff --git a/core/tickets/models.py b/core/tickets/models.py index aff5d6c..2c14687 100644 --- a/core/tickets/models.py +++ b/core/tickets/models.py @@ -1,5 +1,7 @@ -from django.db import models +from itertools import groupby + from django.utils import timezone +from django.db import models from django_softdelete.models import SoftDeleteModel from authentication.models import ExtendedUser @@ -43,7 +45,6 @@ class IssueThread(SoftDeleteModel): name = models.CharField(max_length=255) event = models.ForeignKey(Event, null=True, on_delete=models.SET_NULL, related_name='issue_threads') manually_created = models.BooleanField(default=False) - related_items = models.ManyToManyField(Item, through='ItemRelation') def short_uuid(self): return self.uuid[:8] @@ -76,6 +77,11 @@ class IssueThread(SoftDeleteModel): return self.assignments.create(assigned_to=value) + @property + def related_items(self): + groups = groupby(self.item_relation_changes.all(), lambda rel: rel.item.id) + return [sorted(v, key=lambda r: r.timestamp)[0].item for k, v in groups] + def __str__(self): return '[' + str(self.id) + '][' + self.short_uuid() + '] ' + self.name @@ -132,8 +138,8 @@ class Assignment(models.Model): class ItemRelation(models.Model): id = models.AutoField(primary_key=True) - issue_thread = models.ForeignKey(IssueThread, on_delete=models.CASCADE, related_name='item_relations') - item = models.ForeignKey(Item, on_delete=models.CASCADE, related_name='issues') + issue_thread = models.ForeignKey(IssueThread, on_delete=models.CASCADE, related_name='item_relation_changes') + item = models.ForeignKey(Item, on_delete=models.CASCADE, related_name='issue_relation_changes') timestamp = models.DateTimeField(auto_now_add=True) status = models.CharField(max_length=255, choices=RELATION_STATUS_CHOICES, default='possible') diff --git a/core/tickets/serializers.py b/core/tickets/serializers.py index 9f14e7e..2778dd2 100644 --- a/core/tickets/serializers.py +++ b/core/tickets/serializers.py @@ -2,9 +2,10 @@ from rest_framework import serializers from authentication.models import ExtendedUser from inventory.models import Event +from inventory.shared_serializers import BasicItemSerializer from mail.api_v2 import AttachmentSerializer from tickets.models import IssueThread, Comment, STATE_CHOICES, ShippingVoucher -from inventory.serializers import ItemSerializer +from tickets.shared_serializers import BasicIssueSerializer class CommentSerializer(serializers.ModelSerializer): @@ -37,14 +38,10 @@ class ShippingVoucherSerializer(serializers.ModelSerializer): read_only_fields = ('id', 'timestamp', 'used_at') -class IssueSerializer(serializers.ModelSerializer): +class IssueSerializer(BasicIssueSerializer): timeline = serializers.SerializerMethodField() last_activity = serializers.SerializerMethodField() - assigned_to = serializers.SlugRelatedField(slug_field='username', queryset=ExtendedUser.objects.all(), - allow_null=True, required=False) - event = serializers.SlugRelatedField(slug_field='slug', queryset=Event.objects.all(), - allow_null=True, required=False) - related_items = ItemSerializer(many=True, read_only=True) + related_items = BasicItemSerializer(many=True, read_only=True) class Meta: model = IssueThread @@ -72,8 +69,8 @@ class IssueSerializer(serializers.ModelSerializer): last_mail = self.emails.order_by('-timestamp').first().timestamp if self.emails.count() > 0 else None last_assignment = self.assignments.order_by('-timestamp').first().timestamp if \ self.assignments.count() > 0 else None - last_relation = self.item_relations.order_by('-timestamp').first().timestamp if \ - self.item_relations.count() > 0 else None + last_relation = self.item_relation_changes.order_by('-timestamp').first().timestamp if \ + self.item_relation_changes.count() > 0 else None args = [x for x in [last_state_change, last_comment, last_mail, last_assignment, last_relation] if x is not None] return max(args) @@ -115,13 +112,13 @@ class IssueSerializer(serializers.ModelSerializer): 'timestamp': assignment.timestamp, 'assigned_to': assignment.assigned_to.username, }) - for relation in obj.item_relations.all(): + for relation in (obj.item_relation_changes.all()): timeline.append({ 'type': 'item_relation', 'id': relation.id, 'status': relation.status, 'timestamp': relation.timestamp, - 'item': ItemSerializer(relation.item).data, + 'item': BasicItemSerializer(relation.item).data, }) for shipping_voucher in obj.shipping_vouchers.all(): timeline.append({ @@ -133,5 +130,3 @@ class IssueSerializer(serializers.ModelSerializer): }) return sorted(timeline, key=lambda x: x['timestamp']) - def get_queryset(self): - return IssueThread.objects.all().order_by('-last_activity') diff --git a/core/tickets/shared_serializers.py b/core/tickets/shared_serializers.py new file mode 100644 index 0000000..ac16d81 --- /dev/null +++ b/core/tickets/shared_serializers.py @@ -0,0 +1,23 @@ +from rest_framework import serializers + +from authentication.models import ExtendedUser +from inventory.models import Event +from tickets.models import IssueThread, ItemRelation + + +class RelationSerializer(serializers.ModelSerializer): + class Meta: + model = ItemRelation + fields = ('id', 'status', 'timestamp', 'item', 'issue_thread') + + +class BasicIssueSerializer(serializers.ModelSerializer): + assigned_to = serializers.SlugRelatedField(slug_field='username', queryset=ExtendedUser.objects.all(), + allow_null=True, required=False) + event = serializers.SlugRelatedField(slug_field='slug', queryset=Event.objects.all(), + allow_null=True, required=False) + + class Meta: + model = IssueThread + fields = ('id', 'name', 'state', 'assigned_to', 'uuid', 'event') + read_only_fields = ('id', 'timeline', 'last_activity', 'uuid', 'related_items') diff --git a/core/tickets/tests/v2/test_matches.py b/core/tickets/tests/v2/test_matches.py new file mode 100644 index 0000000..7bd7a52 --- /dev/null +++ b/core/tickets/tests/v2/test_matches.py @@ -0,0 +1,149 @@ +from datetime import datetime, timedelta + +from django.test import TestCase, Client + +from authentication.models import ExtendedUser +from inventory.models import Event, Container, Item +from mail.models import Email, EmailAttachment +from tickets.models import IssueThread, StateChange, Comment, ItemRelation +from django.contrib.auth.models import Permission +from knox.models import AuthToken + +from base64 import b64encode + + +class IssueItemMatchApiTest(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.event = Event.objects.create(slug='evt') + self.box = Container.objects.create(name='box1') + self.item = Item.objects.create(container=self.box, description="foo", event=self.event) + self.token = AuthToken.objects.create(user=self.user) + self.client = Client(headers={'Authorization': 'Token ' + self.token[1]}) + now = datetime.now() + self.issue = IssueThread.objects.create( + name="test issue", + event=self.event, + ) + self.mail1 = Email.objects.create( + subject='test', + body='test', + sender='test', + recipient='test', + issue_thread=self.issue, + timestamp=now, + ) + self.comment = Comment.objects.create( + issue_thread=self.issue, + comment="test", + timestamp=now + timedelta(seconds=3), + ) + self.match = ItemRelation.objects.create( + issue_thread=self.issue, + item=self.item, + timestamp=now + timedelta(seconds=5), + ) + + def test_issues(self): + response = self.client.get('/api/2/tickets/') + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.json()), 1) + self.assertEqual(response.json()[0]['id'], self.issue.id) + self.assertEqual(response.json()[0]['name'], "test issue") + self.assertEqual(response.json()[0]['state'], "pending_new") + self.assertEqual(response.json()[0]['event'], "evt") + self.assertEqual(response.json()[0]['assigned_to'], None) + self.assertEqual(response.json()[0]['uuid'], self.issue.uuid) + self.assertEqual(response.json()[0]['last_activity'], self.match.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + self.assertEqual(len(response.json()[0]['timeline']), 4) + self.assertEqual(response.json()[0]['timeline'][0]['type'], 'state') + self.assertEqual(response.json()[0]['timeline'][1]['type'], 'mail') + self.assertEqual(response.json()[0]['timeline'][2]['type'], 'comment') + self.assertEqual(response.json()[0]['timeline'][1]['id'], self.mail1.id) + self.assertEqual(response.json()[0]['timeline'][2]['id'], self.comment.id) + self.assertEqual(response.json()[0]['timeline'][0]['state'], 'pending_new') + self.assertEqual(response.json()[0]['timeline'][1]['sender'], 'test') + self.assertEqual(response.json()[0]['timeline'][1]['recipient'], 'test') + self.assertEqual(response.json()[0]['timeline'][1]['subject'], 'test') + self.assertEqual(response.json()[0]['timeline'][1]['body'], 'test') + self.assertEqual(response.json()[0]['timeline'][1]['timestamp'], + self.mail1.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + self.assertEqual(response.json()[0]['timeline'][2]['comment'], 'test') + self.assertEqual(response.json()[0]['timeline'][2]['timestamp'], + self.comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + self.assertEqual(response.json()[0]['timeline'][3]['status'], 'possible') + self.assertEqual(response.json()[0]['timeline'][3]['timestamp'], + self.match.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + self.assertEqual(response.json()[0]['timeline'][3]['item']['description'], "foo") + self.assertEqual(response.json()[0]['timeline'][3]['item']['event'], "evt") + self.assertEqual(response.json()[0]['timeline'][3]['item']['box'], "box1") + self.assertEqual(response.json()[0]['related_items'][0]['description'], "foo") + self.assertEqual(response.json()[0]['related_items'][0]['event'], "evt") + self.assertEqual(response.json()[0]['related_items'][0]['box'], "box1") + + def test_members(self): + response = self.client.get(f'/api/2/{self.event.slug}/item/') + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.json()), 1) + self.assertEqual(response.json()[0]['id'], self.item.id) + self.assertEqual(response.json()[0]['description'], 'foo') + self.assertEqual(response.json()[0]['box'], 'box1') + self.assertEqual(response.json()[0]['cid'], self.box.id) + self.assertEqual(response.json()[0]['file'], None) + self.assertEqual(response.json()[0]['returned'], False) + self.assertEqual(response.json()[0]['event'], self.event.slug) + self.assertEqual(len(response.json()[0]['related_issues']), 1) + self.assertEqual(response.json()[0]['related_issues'][0]['id'], self.issue.id) + self.assertEqual(response.json()[0]['related_issues'][0]['name'], "test issue") + + def test_add_match(self): + response = self.client.get('/api/2/matches/') + self.assertEqual(1, len(response.json())) + item = Item.objects.create(container=self.box, event=self.event, description='1') + issue = IssueThread.objects.create(name="test issue", event=self.event) + + response = self.client.post(f'/api/2/matches/', + {'item': item.id, 'issue_thread': issue.id}, + content_type='application/json') + self.assertEqual(response.status_code, 201) + + response = self.client.get('/api/2/matches/') + self.assertEqual(2, len(response.json())) + + response = self.client.get('/api/2/tickets/') + self.assertEqual(4, len(response.json()[0]['timeline'])) + self.assertEqual('item_relation', response.json()[0]['timeline'][3]['type']) + self.assertEqual('possible', response.json()[0]['timeline'][3]['status']) + self.assertEqual(1, len(response.json()[0]['related_items'])) + + def test_change_match_state(self): + response = self.client.get('/api/2/matches/') + self.assertEqual(1, len(response.json())) + + response = self.client.get('/api/2/tickets/') + self.assertEqual(4, len(response.json()[0]['timeline'])) + self.assertEqual('item_relation', response.json()[0]['timeline'][3]['type']) + self.assertEqual('possible', response.json()[0]['timeline'][3]['status']) + self.assertEqual(1, len(response.json()[0]['related_items'])) + + response = self.client.post(f'/api/2/matches/', + {'item': self.item.id, 'issue_thread': self.issue.id, 'status': 'confirmed'}, + content_type='application/json') + self.assertEqual(response.status_code, 201) + self.assertEqual(response.json()['status'], 'confirmed') + self.assertEqual(response.json()['id'], 2) + + response = self.client.get('/api/2/matches/') + self.assertEqual(2, len(response.json())) + + response = self.client.get('/api/2/tickets/') + self.assertEqual(5, len(response.json()[0]['timeline'])) + self.assertEqual('item_relation', response.json()[0]['timeline'][3]['type']) + self.assertEqual('possible', response.json()[0]['timeline'][3]['status']) + self.assertEqual('item_relation', response.json()[0]['timeline'][4]['type']) + self.assertEqual('confirmed', response.json()[0]['timeline'][4]['status']) + self.assertEqual(1, len(response.json()[0]['related_items'])) diff --git a/core/tickets/tests/v2/test_tickets.py b/core/tickets/tests/v2/test_tickets.py index 9e89ed5..e0fb004 100644 --- a/core/tickets/tests/v2/test_tickets.py +++ b/core/tickets/tests/v2/test_tickets.py @@ -3,9 +3,9 @@ from datetime import datetime, timedelta from django.test import TestCase, Client from authentication.models import ExtendedUser -from inventory.models import Event +from inventory.models import Event, Container, Item from mail.models import Email, EmailAttachment -from tickets.models import IssueThread, StateChange, Comment +from tickets.models import IssueThread, StateChange, Comment, ItemRelation from django.contrib.auth.models import Permission from knox.models import AuthToken @@ -18,6 +18,8 @@ class IssueApiTest(TestCase): self.user.user_permissions.add(*Permission.objects.all()) self.user.save() self.event = Event.objects.create(slug='evt') + self.box = Container.objects.create(name='box1') + self.item = Item.objects.create(container=self.box, description="foo", event=self.event) self.token = AuthToken.objects.create(user=self.user) self.client = Client(headers={'Authorization': 'Token ' + self.token[1]}) @@ -54,6 +56,11 @@ class IssueApiTest(TestCase): comment="test", timestamp=now + timedelta(seconds=3), ) + match = ItemRelation.objects.create( + issue_thread=issue, + item = self.item, + timestamp=now + timedelta(seconds=5), + ) self.assertEqual('pending_new', issue.state) self.assertEqual('test issue', issue.name) self.assertEqual(None, issue.assigned_to) @@ -67,8 +74,8 @@ class IssueApiTest(TestCase): self.assertEqual(response.json()[0]['event'], "evt") self.assertEqual(response.json()[0]['assigned_to'], None) self.assertEqual(response.json()[0]['uuid'], issue.uuid) - self.assertEqual(response.json()[0]['last_activity'], comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) - self.assertEqual(len(response.json()[0]['timeline']), 4) + self.assertEqual(response.json()[0]['last_activity'], match.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + self.assertEqual(len(response.json()[0]['timeline']), 5) self.assertEqual(response.json()[0]['timeline'][0]['type'], 'state') self.assertEqual(response.json()[0]['timeline'][1]['type'], 'mail') self.assertEqual(response.json()[0]['timeline'][2]['type'], 'mail') @@ -92,6 +99,15 @@ class IssueApiTest(TestCase): self.assertEqual(response.json()[0]['timeline'][3]['comment'], 'test') self.assertEqual(response.json()[0]['timeline'][3]['timestamp'], comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + self.assertEqual(response.json()[0]['timeline'][4]['status'], 'possible') + self.assertEqual(response.json()[0]['timeline'][4]['timestamp'], + match.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + self.assertEqual(response.json()[0]['timeline'][4]['item']['description'], "foo") + self.assertEqual(response.json()[0]['timeline'][4]['item']['event'], "evt") + self.assertEqual(response.json()[0]['timeline'][4]['item']['box'], "box1") + self.assertEqual(response.json()[0]['related_items'][0]['description'], "foo") + self.assertEqual(response.json()[0]['related_items'][0]['event'], "evt") + self.assertEqual(response.json()[0]['related_items'][0]['box'], "box1") def test_issues_incomplete_timeline(self): now = datetime.now()