Compare commits
16 commits
b9ff51fc74
...
f00afb86d6
Author | SHA1 | Date | |
---|---|---|---|
f00afb86d6 | |||
4f08a2c265 | |||
f51c367ead | |||
124713b98c | |||
f9e8ce1178 | |||
6a99e7420f | |||
6a81b1f832 | |||
b6a8e3acd4 | |||
420d22bb43 | |||
f785189304 | |||
9320765bc5 | |||
33d368b5f4 | |||
7083f4f7b7 | |||
75ea0f4b46 | |||
fdf5ab8ad1 | |||
f2647f0dbd |
12 changed files with 327 additions and 70 deletions
|
@ -114,10 +114,6 @@ router.register(r'boxes', ContainerViewSet, basename='boxes')
|
||||||
router.register(r'box', ContainerViewSet, basename='boxes')
|
router.register(r'box', ContainerViewSet, basename='boxes')
|
||||||
|
|
||||||
urlpatterns = router.urls + [
|
urlpatterns = router.urls + [
|
||||||
# path('<event_slug>/items/', item),
|
|
||||||
# path('<event_slug>/items/<query>/', search_items),
|
|
||||||
# path('<event_slug>/item/', item),
|
|
||||||
# path('<event_slug>/item/<id>/', item_by_id),
|
|
||||||
re_path(r'^(?P<event_slug>[\w-]+)/items/$', item, name='item'),
|
re_path(r'^(?P<event_slug>[\w-]+)/items/$', item, name='item'),
|
||||||
re_path(r'^(?P<event_slug>[\w-]+)/items/(?P<query>[-A-Za-z0-9+/]*={0,3})/$', search_items, name='search_items'),
|
re_path(r'^(?P<event_slug>[\w-]+)/items/(?P<query>[-A-Za-z0-9+/]*={0,3})/$', search_items, name='search_items'),
|
||||||
re_path(r'^(?P<event_slug>[\w-]+)/item/$', item, name='item'),
|
re_path(r'^(?P<event_slug>[\w-]+)/item/$', item, name='item'),
|
||||||
|
|
|
@ -43,6 +43,13 @@ class Item(SoftDeleteModel):
|
||||||
return
|
return
|
||||||
self.container_history.create(container=value)
|
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()
|
objects = ItemManager()
|
||||||
all_objects = models.Manager()
|
all_objects = models.Manager()
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,9 @@ from rest_framework.relations import SlugRelatedField
|
||||||
|
|
||||||
from files.models import File
|
from files.models import File
|
||||||
from inventory.models import Event, Container, Item
|
from inventory.models import Event, Container, Item
|
||||||
|
from inventory.shared_serializers import BasicItemSerializer
|
||||||
from mail.models import EventAddress
|
from mail.models import EventAddress
|
||||||
|
from tickets.shared_serializers import BasicIssueSerializer
|
||||||
|
|
||||||
|
|
||||||
# class EventAdressSerializer(serializers.ModelSerializer):
|
# class EventAdressSerializer(serializers.ModelSerializer):
|
||||||
|
@ -56,34 +58,15 @@ class ContainerSerializer(serializers.ModelSerializer):
|
||||||
return len(instance.items)
|
return len(instance.items)
|
||||||
|
|
||||||
|
|
||||||
class ItemSerializer(serializers.ModelSerializer):
|
class ItemSerializer(BasicItemSerializer):
|
||||||
dataImage = serializers.CharField(write_only=True, required=False)
|
dataImage = serializers.CharField(write_only=True, required=False)
|
||||||
cid = serializers.SerializerMethodField()
|
related_issues = BasicIssueSerializer(many=True, read_only=True)
|
||||||
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:
|
class Meta:
|
||||||
model = Item
|
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']
|
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):
|
def to_internal_value(self, data):
|
||||||
container = None
|
container = None
|
||||||
returned = False
|
returned = False
|
||||||
|
|
31
core/inventory/shared_serializers.py
Normal file
31
core/inventory/shared_serializers.py
Normal file
|
@ -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
|
|
@ -30,9 +30,15 @@ class ItemTestCase(TestCase):
|
||||||
item = Item.objects.create(container=self.box, event=self.event, description='1')
|
item = Item.objects.create(container=self.box, event=self.event, description='1')
|
||||||
response = self.client.get(f'/api/2/{self.event.slug}/item/')
|
response = self.client.get(f'/api/2/{self.event.slug}/item/')
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(response.json(),
|
self.assertEqual(len(response.json()), 1)
|
||||||
[{'id': 1, 'description': '1', 'box': 'BOX', 'cid': self.box.id, 'file': None,
|
self.assertEqual(response.json()[0]['id'], item.id)
|
||||||
'returned': False, 'event': self.event.slug}])
|
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):
|
def test_members_with_file(self):
|
||||||
import base64
|
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'))
|
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/')
|
response = self.client.get(f'/api/2/{self.event.slug}/item/')
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(response.json(),
|
self.assertEqual(len(response.json()), 1)
|
||||||
[{'id': 1, 'description': '1', 'box': 'BOX', 'cid': self.box.id, 'file': file.hash,
|
self.assertEqual(response.json()[0]['id'], item.id)
|
||||||
'returned': False, 'event': self.event.slug}])
|
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):
|
def test_multi_members(self):
|
||||||
Item.objects.create(container=self.box, event=self.event, description='1')
|
Item.objects.create(container=self.box, event=self.event, description='1')
|
||||||
|
@ -55,9 +67,14 @@ class ItemTestCase(TestCase):
|
||||||
def test_create_item(self):
|
def test_create_item(self):
|
||||||
response = self.client.post(f'/api/2/{self.event.slug}/item/', {'cid': self.box.id, 'description': '1'})
|
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.status_code, 201)
|
||||||
self.assertEqual(response.json(),
|
self.assertEqual(response.json()['id'], 1)
|
||||||
{'id': 1, 'description': '1', 'box': 'BOX', 'cid': self.box.id, 'file': None,
|
self.assertEqual(response.json()['description'], '1')
|
||||||
'returned': False, 'event': self.event.slug})
|
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(len(Item.objects.all()), 1)
|
||||||
self.assertEqual(Item.objects.all()[0].id, 1)
|
self.assertEqual(Item.objects.all()[0].id, 1)
|
||||||
self.assertEqual(Item.objects.all()[0].description, '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'},
|
response = self.client.patch(f'/api/2/{self.event.slug}/item/{item.id}/', {'description': '2'},
|
||||||
content_type='application/json')
|
content_type='application/json')
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(response.json(),
|
self.assertEqual(response.json()['id'], item.id)
|
||||||
{'id': 1, 'description': '2', 'box': 'BOX', 'cid': self.box.id, 'file': None,
|
self.assertEqual(response.json()['description'], '2')
|
||||||
'returned': False, 'event': self.event.slug})
|
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(len(Item.objects.all()), 1)
|
||||||
self.assertEqual(Item.objects.all()[0].id, 1)
|
self.assertEqual(Item.objects.all()[0].id, 1)
|
||||||
self.assertEqual(Item.objects.all()[0].description, '2')
|
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(self.item1.id, response.json()[0]['id'])
|
||||||
self.assertEqual('abc def', response.json()[0]['description'])
|
self.assertEqual('abc def', response.json()[0]['description'])
|
||||||
self.assertEqual('BOX', response.json()[0]['box'])
|
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(1, response.json()[0]['search_score'])
|
||||||
self.assertEqual(self.item2.id, response.json()[1]['id'])
|
self.assertEqual(self.item2.id, response.json()[1]['id'])
|
||||||
self.assertEqual('def ghi', response.json()[1]['description'])
|
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(self.item1.id, response.json()[0]['id'])
|
||||||
self.assertEqual('abc def', response.json()[0]['description'])
|
self.assertEqual('abc def', response.json()[0]['description'])
|
||||||
self.assertEqual('BOX', response.json()[0]['box'])
|
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(2, response.json()[0]['search_score'])
|
||||||
self.assertEqual(self.item2.id, response.json()[1]['id'])
|
self.assertEqual(self.item2.id, response.json()[1]['id'])
|
||||||
self.assertEqual('def ghi', response.json()[1]['description'])
|
self.assertEqual('def ghi', response.json()[1]['description'])
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import logging
|
|
||||||
from base64 import b64decode
|
from base64 import b64decode
|
||||||
|
|
||||||
from django.urls import re_path
|
from django.urls import re_path
|
||||||
|
@ -15,8 +14,9 @@ from inventory.models import Event
|
||||||
from mail.models import Email
|
from mail.models import Email
|
||||||
from mail.protocol import send_smtp, make_reply, collect_references
|
from mail.protocol import send_smtp, make_reply, collect_references
|
||||||
from notify_sessions.models import SystemEvent
|
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.serializers import IssueSerializer, CommentSerializer, ShippingVoucherSerializer, SearchResultSerializer
|
||||||
|
from tickets.shared_serializers import RelationSerializer
|
||||||
|
|
||||||
|
|
||||||
class IssueViewSet(viewsets.ModelViewSet):
|
class IssueViewSet(viewsets.ModelViewSet):
|
||||||
|
@ -24,6 +24,11 @@ class IssueViewSet(viewsets.ModelViewSet):
|
||||||
queryset = IssueThread.objects.all()
|
queryset = IssueThread.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class RelationViewSet(viewsets.ModelViewSet):
|
||||||
|
serializer_class = RelationSerializer
|
||||||
|
queryset = ItemRelation.objects.all()
|
||||||
|
|
||||||
|
|
||||||
class CommentViewSet(viewsets.ModelViewSet):
|
class CommentViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = CommentSerializer
|
serializer_class = CommentSerializer
|
||||||
queryset = Comment.objects.all()
|
queryset = Comment.objects.all()
|
||||||
|
@ -157,10 +162,9 @@ def search_issues(request, event_slug, query):
|
||||||
|
|
||||||
router = routers.SimpleRouter()
|
router = routers.SimpleRouter()
|
||||||
router.register(r'tickets', IssueViewSet, basename='issues')
|
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')
|
router.register(r'shipping_vouchers', ShippingVoucherViewSet, basename='shipping_vouchers')
|
||||||
|
|
||||||
|
|
||||||
# [-A-Za-z0-9+/]*={0,3}
|
# [-A-Za-z0-9+/]*={0,3}
|
||||||
urlpatterns = ([
|
urlpatterns = ([
|
||||||
re_path(r'tickets/states/$', get_available_states, name='get_available_states'),
|
re_path(r'tickets/states/$', get_available_states, name='get_available_states'),
|
||||||
|
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,3 +1,5 @@
|
||||||
|
from itertools import groupby
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django_softdelete.models import SoftDeleteModel
|
from django_softdelete.models import SoftDeleteModel
|
||||||
|
@ -43,7 +45,6 @@ class IssueThread(SoftDeleteModel):
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
event = models.ForeignKey(Event, null=True, on_delete=models.SET_NULL, related_name='issue_threads')
|
event = models.ForeignKey(Event, null=True, on_delete=models.SET_NULL, related_name='issue_threads')
|
||||||
manually_created = models.BooleanField(default=False)
|
manually_created = models.BooleanField(default=False)
|
||||||
related_items = models.ManyToManyField(Item, through='ItemRelation')
|
|
||||||
|
|
||||||
def short_uuid(self):
|
def short_uuid(self):
|
||||||
return self.uuid[:8]
|
return self.uuid[:8]
|
||||||
|
@ -76,6 +77,11 @@ class IssueThread(SoftDeleteModel):
|
||||||
return
|
return
|
||||||
self.assignments.create(assigned_to=value)
|
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):
|
def __str__(self):
|
||||||
return '[' + str(self.id) + '][' + self.short_uuid() + '] ' + self.name
|
return '[' + str(self.id) + '][' + self.short_uuid() + '] ' + self.name
|
||||||
|
|
||||||
|
@ -132,8 +138,8 @@ class Assignment(models.Model):
|
||||||
|
|
||||||
class ItemRelation(models.Model):
|
class ItemRelation(models.Model):
|
||||||
id = models.AutoField(primary_key=True)
|
id = models.AutoField(primary_key=True)
|
||||||
issue_thread = models.ForeignKey(IssueThread, on_delete=models.CASCADE, related_name='item_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_relations')
|
item = models.ForeignKey(Item, on_delete=models.CASCADE, related_name='issue_relation_changes')
|
||||||
timestamp = models.DateTimeField(auto_now_add=True)
|
timestamp = models.DateTimeField(auto_now_add=True)
|
||||||
status = models.CharField(max_length=255, choices=RELATION_STATUS_CHOICES, default='possible')
|
status = models.CharField(max_length=255, choices=RELATION_STATUS_CHOICES, default='possible')
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,10 @@ from rest_framework import serializers
|
||||||
|
|
||||||
from authentication.models import ExtendedUser
|
from authentication.models import ExtendedUser
|
||||||
from inventory.models import Event
|
from inventory.models import Event
|
||||||
|
from inventory.shared_serializers import BasicItemSerializer
|
||||||
from mail.api_v2 import AttachmentSerializer
|
from mail.api_v2 import AttachmentSerializer
|
||||||
from tickets.models import IssueThread, Comment, STATE_CHOICES, ShippingVoucher
|
from tickets.models import IssueThread, Comment, STATE_CHOICES, ShippingVoucher
|
||||||
from inventory.serializers import ItemSerializer
|
from tickets.shared_serializers import BasicIssueSerializer
|
||||||
|
|
||||||
|
|
||||||
class CommentSerializer(serializers.ModelSerializer):
|
class CommentSerializer(serializers.ModelSerializer):
|
||||||
|
@ -37,14 +38,10 @@ class ShippingVoucherSerializer(serializers.ModelSerializer):
|
||||||
read_only_fields = ('id', 'timestamp', 'used_at')
|
read_only_fields = ('id', 'timestamp', 'used_at')
|
||||||
|
|
||||||
|
|
||||||
class IssueSerializer(serializers.ModelSerializer):
|
class IssueSerializer(BasicIssueSerializer):
|
||||||
timeline = serializers.SerializerMethodField()
|
timeline = serializers.SerializerMethodField()
|
||||||
last_activity = serializers.SerializerMethodField()
|
last_activity = serializers.SerializerMethodField()
|
||||||
assigned_to = serializers.SlugRelatedField(slug_field='username', queryset=ExtendedUser.objects.all(),
|
related_items = BasicItemSerializer(many=True, read_only=True)
|
||||||
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)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueThread
|
model = IssueThread
|
||||||
|
@ -55,8 +52,6 @@ class IssueSerializer(serializers.ModelSerializer):
|
||||||
ret = super().to_internal_value(data)
|
ret = super().to_internal_value(data)
|
||||||
if 'state' in data:
|
if 'state' in data:
|
||||||
ret['state'] = data['state']
|
ret['state'] = data['state']
|
||||||
# if 'assigned_to' in data:
|
|
||||||
# ret['assigned_to'] = data['assigned_to']
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def validate(self, attrs):
|
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_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 \
|
last_assignment = self.assignments.order_by('-timestamp').first().timestamp if \
|
||||||
self.assignments.count() > 0 else None
|
self.assignments.count() > 0 else None
|
||||||
last_relation = self.item_relations.order_by('-timestamp').first().timestamp if \
|
last_relation = self.item_relation_changes.order_by('-timestamp').first().timestamp if \
|
||||||
self.item_relations.count() > 0 else None
|
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
|
args = [x for x in [last_state_change, last_comment, last_mail, last_assignment, last_relation] if
|
||||||
x is not None]
|
x is not None]
|
||||||
return max(args)
|
return max(args)
|
||||||
|
@ -117,13 +112,13 @@ class IssueSerializer(serializers.ModelSerializer):
|
||||||
'timestamp': assignment.timestamp,
|
'timestamp': assignment.timestamp,
|
||||||
'assigned_to': assignment.assigned_to.username,
|
'assigned_to': assignment.assigned_to.username,
|
||||||
})
|
})
|
||||||
for relation in obj.item_relations.all():
|
for relation in (obj.item_relation_changes.all()):
|
||||||
timeline.append({
|
timeline.append({
|
||||||
'type': 'item_relation',
|
'type': 'item_relation',
|
||||||
'id': relation.id,
|
'id': relation.id,
|
||||||
'status': relation.status,
|
'status': relation.status,
|
||||||
'timestamp': relation.timestamp,
|
'timestamp': relation.timestamp,
|
||||||
'item': ItemSerializer(relation.item).data,
|
'item': BasicItemSerializer(relation.item).data,
|
||||||
})
|
})
|
||||||
for shipping_voucher in obj.shipping_vouchers.all():
|
for shipping_voucher in obj.shipping_vouchers.all():
|
||||||
timeline.append({
|
timeline.append({
|
||||||
|
@ -135,9 +130,6 @@ class IssueSerializer(serializers.ModelSerializer):
|
||||||
})
|
})
|
||||||
return sorted(timeline, key=lambda x: x['timestamp'])
|
return sorted(timeline, key=lambda x: x['timestamp'])
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return IssueThread.objects.all().order_by('-last_activity')
|
|
||||||
|
|
||||||
|
|
||||||
class SearchResultSerializer(serializers.Serializer):
|
class SearchResultSerializer(serializers.Serializer):
|
||||||
search_score = serializers.IntegerField()
|
search_score = serializers.IntegerField()
|
||||||
|
|
23
core/tickets/shared_serializers.py
Normal file
23
core/tickets/shared_serializers.py
Normal file
|
@ -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')
|
149
core/tickets/tests/v2/test_matches.py
Normal file
149
core/tickets/tests/v2/test_matches.py
Normal file
|
@ -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']))
|
|
@ -3,9 +3,9 @@ from datetime import datetime, timedelta
|
||||||
from django.test import TestCase, Client
|
from django.test import TestCase, Client
|
||||||
|
|
||||||
from authentication.models import ExtendedUser
|
from authentication.models import ExtendedUser
|
||||||
from inventory.models import Event
|
from inventory.models import Event, Container, Item
|
||||||
from mail.models import Email, EmailAttachment
|
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 django.contrib.auth.models import Permission
|
||||||
from knox.models import AuthToken
|
from knox.models import AuthToken
|
||||||
|
|
||||||
|
@ -20,6 +20,8 @@ class IssueApiTest(TestCase):
|
||||||
self.user.user_permissions.add(*Permission.objects.all())
|
self.user.user_permissions.add(*Permission.objects.all())
|
||||||
self.user.save()
|
self.user.save()
|
||||||
self.event = Event.objects.create(slug='evt')
|
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.token = AuthToken.objects.create(user=self.user)
|
||||||
self.client = Client(headers={'Authorization': 'Token ' + self.token[1]})
|
self.client = Client(headers={'Authorization': 'Token ' + self.token[1]})
|
||||||
|
|
||||||
|
@ -56,6 +58,11 @@ class IssueApiTest(TestCase):
|
||||||
comment="test",
|
comment="test",
|
||||||
timestamp=now + timedelta(seconds=3),
|
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('pending_new', issue.state)
|
||||||
self.assertEqual('test issue', issue.name)
|
self.assertEqual('test issue', issue.name)
|
||||||
self.assertEqual(None, issue.assigned_to)
|
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]['event'], "evt")
|
||||||
self.assertEqual(response.json()[0]['assigned_to'], None)
|
self.assertEqual(response.json()[0]['assigned_to'], None)
|
||||||
self.assertEqual(response.json()[0]['uuid'], issue.uuid)
|
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(response.json()[0]['last_activity'], match.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'))
|
||||||
self.assertEqual(len(response.json()[0]['timeline']), 4)
|
self.assertEqual(len(response.json()[0]['timeline']), 5)
|
||||||
self.assertEqual(response.json()[0]['timeline'][0]['type'], 'state')
|
self.assertEqual(response.json()[0]['timeline'][0]['type'], 'state')
|
||||||
self.assertEqual(response.json()[0]['timeline'][1]['type'], 'mail')
|
self.assertEqual(response.json()[0]['timeline'][1]['type'], 'mail')
|
||||||
self.assertEqual(response.json()[0]['timeline'][2]['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]['comment'], 'test')
|
||||||
self.assertEqual(response.json()[0]['timeline'][3]['timestamp'],
|
self.assertEqual(response.json()[0]['timeline'][3]['timestamp'],
|
||||||
comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'))
|
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):
|
def test_issues_incomplete_timeline(self):
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
|
@ -287,8 +303,7 @@ class IssueApiTest(TestCase):
|
||||||
content_type='application/json')
|
content_type='application/json')
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
|
# def test_post_comment(self):
|
||||||
#def test_post_comment(self):
|
|
||||||
# issue = IssueThread.objects.create(
|
# issue = IssueThread.objects.create(
|
||||||
# name="test issue",
|
# name="test issue",
|
||||||
# )
|
# )
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue