diff --git a/core/inventory/api_v2.py b/core/inventory/api_v2.py index f2de844..60f0292 100644 --- a/core/inventory/api_v2.py +++ b/core/inventory/api_v2.py @@ -114,10 +114,6 @@ router.register(r'boxes', ContainerViewSet, basename='boxes') router.register(r'box', ContainerViewSet, basename='boxes') urlpatterns = router.urls + [ - # path('/items/', item), - # path('/items//', search_items), - # path('/item/', item), - # path('/item//', item_by_id), re_path(r'^(?P[\w-]+)/items/$', item, name='item'), re_path(r'^(?P[\w-]+)/items/(?P[-A-Za-z0-9+/]*={0,3})/$', search_items, name='search_items'), re_path(r'^(?P[\w-]+)/item/$', item, name='item'), diff --git a/core/inventory/models.py b/core/inventory/models.py index bd1e1bb..50c1a5f 100644 --- a/core/inventory/models.py +++ b/core/inventory/models.py @@ -43,6 +43,13 @@ class Item(SoftDeleteModel): return self.container_history.create(container=value) + @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 5a7b6ac..e4d3252 100644 --- a/core/inventory/serializers.py +++ b/core/inventory/serializers.py @@ -4,7 +4,9 @@ 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): @@ -56,34 +58,15 @@ class ContainerSerializer(serializers.ModelSerializer): return len(instance.items) -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 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 - 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 e66adbf..144797a 100644 --- a/core/inventory/tests/v2/test_items.py +++ b/core/inventory/tests/v2/test_items.py @@ -30,9 +30,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 @@ -40,9 +46,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') @@ -55,9 +67,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') @@ -94,9 +111,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') @@ -215,7 +237,7 @@ class ItemSearchTestCase(TestCase): self.assertEqual(self.item1.id, response.json()[0]['id']) self.assertEqual('abc def', response.json()[0]['description']) self.assertEqual('BOX', response.json()[0]['box']) - self.assertEqual(self.box.id, response.json()[0]['id']) + self.assertEqual(self.box.id, response.json()[0]['cid']) self.assertEqual(1, response.json()[0]['search_score']) self.assertEqual(self.item2.id, response.json()[1]['id']) self.assertEqual('def ghi', response.json()[1]['description']) @@ -242,7 +264,7 @@ class ItemSearchTestCase(TestCase): self.assertEqual(self.item1.id, response.json()[0]['id']) self.assertEqual('abc def', response.json()[0]['description']) self.assertEqual('BOX', response.json()[0]['box']) - self.assertEqual(self.box.id, response.json()[0]['id']) + self.assertEqual(self.box.id, response.json()[0]['cid']) self.assertEqual(2, response.json()[0]['search_score']) self.assertEqual(self.item2.id, response.json()[1]['id']) self.assertEqual('def ghi', response.json()[1]['description']) diff --git a/core/tickets/api_v2.py b/core/tickets/api_v2.py index 382e350..008e0af 100644 --- a/core/tickets/api_v2.py +++ b/core/tickets/api_v2.py @@ -1,4 +1,3 @@ -import logging from base64 import b64decode from django.urls import re_path @@ -15,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, SearchResultSerializer +from tickets.shared_serializers import RelationSerializer class IssueViewSet(viewsets.ModelViewSet): @@ -24,6 +24,11 @@ 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() @@ -157,10 +162,9 @@ def search_issues(request, event_slug, query): router = routers.SimpleRouter() router.register(r'tickets', IssueViewSet, basename='issues') -# router.register(r'comments', CommentViewSet, basename='comments') +router.register(r'matches', RelationViewSet, basename='matches') router.register(r'shipping_vouchers', ShippingVoucherViewSet, basename='shipping_vouchers') - # [-A-Za-z0-9+/]*={0,3} urlpatterns = ([ re_path(r'tickets/states/$', get_available_states, name='get_available_states'), @@ -168,5 +172,5 @@ urlpatterns = ([ re_path(r'^tickets/(?P\d+)/comment/$', add_comment, name='add_comment'), re_path(r'^(?P[\w-]+)/tickets/manual/$', manual_ticket, name='manual_ticket'), re_path(r'^(?P[\w-]+)/tickets/(?P[-A-Za-z0-9+/]*={0,3})/$', search_issues, - name='search_issues'), + name='search_issues'), ] + router.urls) diff --git a/core/tickets/migrations/0013_remove_issuethread_related_items_and_more.py b/core/tickets/migrations/0013_remove_issuethread_related_items_and_more.py new file mode 100644 index 0000000..29da48f --- /dev/null +++ b/core/tickets/migrations/0013_remove_issuethread_related_items_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.7 on 2024-11-20 23:34 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0008_alter_item_event_alter_itemplacement_container_and_more'), + ('tickets', '0012_alter_itemrelation_item'), + ] + + 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 03b528c..2c14687 100644 --- a/core/tickets/models.py +++ b/core/tickets/models.py @@ -1,3 +1,5 @@ +from itertools import groupby + from django.utils import timezone from django.db import models from django_softdelete.models import SoftDeleteModel @@ -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='issue_relations') + 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 9779bc5..4539283 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 @@ -55,8 +52,6 @@ class IssueSerializer(serializers.ModelSerializer): ret = super().to_internal_value(data) if 'state' in data: ret['state'] = data['state'] - # if 'assigned_to' in data: - # ret['assigned_to'] = data['assigned_to'] return ret def validate(self, attrs): @@ -74,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) @@ -117,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({ @@ -135,9 +130,6 @@ class IssueSerializer(serializers.ModelSerializer): }) return sorted(timeline, key=lambda x: x['timestamp']) - def get_queryset(self): - return IssueThread.objects.all().order_by('-last_activity') - class SearchResultSerializer(serializers.Serializer): search_score = serializers.IntegerField() 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 6b7f90d..54bd3fe 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 @@ -20,6 +20,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]}) @@ -56,6 +58,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) @@ -69,8 +76,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') @@ -94,6 +101,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() @@ -287,8 +303,7 @@ class IssueApiTest(TestCase): content_type='application/json') self.assertEqual(response.status_code, 400) - - #def test_post_comment(self): + # def test_post_comment(self): # issue = IssueThread.objects.create( # name="test issue", # )