diff --git a/core/inventory/api_v2.py b/core/inventory/api_v2.py index 0a34140..b539e97 100644 --- a/core/inventory/api_v2.py +++ b/core/inventory/api_v2.py @@ -1,4 +1,4 @@ -from django.urls import path +from django.urls import re_path from django.contrib.auth.decorators import permission_required from rest_framework import routers, viewsets from rest_framework.decorators import api_view, permission_classes @@ -40,7 +40,9 @@ def search_items(request, event_slug, query): @permission_classes([IsAuthenticated]) def item(request, event_slug): try: - event = Event.objects.get(slug=event_slug) + event = None + if event_slug != 'none': + event = Event.objects.get(slug=event_slug) if request.method == 'GET': if not request.user.has_event_perm(event, 'view_item'): return Response(status=403) @@ -99,8 +101,8 @@ 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'), + re_path(r'^(?P[\w-]+)/item/(?P\d+)/$', item_by_id, name='item_by_id'), ] diff --git a/core/inventory/serializers.py b/core/inventory/serializers.py index 5a26623..3c0eb7e 100644 --- a/core/inventory/serializers.py +++ b/core/inventory/serializers.py @@ -39,10 +39,12 @@ class ItemSerializer(serializers.ModelSerializer): 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', 'uid', 'description', 'file', 'dataImage', 'returned'] + fields = ['cid', 'box', 'uid', 'description', 'file', 'dataImage', 'returned', 'event'] read_only_fields = ['uid'] def get_cid(self, instance): diff --git a/core/inventory/tests/v2/test_items.py b/core/inventory/tests/v2/test_items.py index 056b38c..a955161 100644 --- a/core/inventory/tests/v2/test_items.py +++ b/core/inventory/tests/v2/test_items.py @@ -30,7 +30,7 @@ class ItemTestCase(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), [{'uid': 1, 'description': '1', 'box': 'BOX', 'cid': self.box.cid, 'file': None, - 'returned': False}]) + 'returned': False, 'event': self.event.slug}]) def test_members_with_file(self): import base64 @@ -40,7 +40,7 @@ class ItemTestCase(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), [{'uid': 1, 'description': '1', 'box': 'BOX', 'cid': self.box.cid, 'file': file.hash, - 'returned': False}]) + 'returned': False, 'event': self.event.slug}]) def test_multi_members(self): Item.objects.create(container=self.box, event=self.event, description='1') @@ -55,7 +55,7 @@ class ItemTestCase(TestCase): self.assertEqual(response.status_code, 201) self.assertEqual(response.json(), {'uid': 1, 'description': '1', 'box': 'BOX', 'cid': self.box.cid, 'file': None, - 'returned': False}) + 'returned': False, 'event': self.event.slug}) self.assertEqual(len(Item.objects.all()), 1) self.assertEqual(Item.objects.all()[0].uid, 1) self.assertEqual(Item.objects.all()[0].description, '1') @@ -86,7 +86,7 @@ class ItemTestCase(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), {'uid': 1, 'description': '2', 'box': 'BOX', 'cid': self.box.cid, 'file': None, - 'returned': False}) + 'returned': False, 'event': self.event.slug}) self.assertEqual(len(Item.objects.all()), 1) self.assertEqual(Item.objects.all()[0].uid, 1) self.assertEqual(Item.objects.all()[0].description, '2') diff --git a/core/tickets/api_v2.py b/core/tickets/api_v2.py index 596bd9b..39404f2 100644 --- a/core/tickets/api_v2.py +++ b/core/tickets/api_v2.py @@ -56,7 +56,7 @@ def reply(request, pk): @api_view(['POST']) @permission_classes([IsAuthenticated]) @permission_required('tickets.add_issuethread_manual', raise_exception=True) -def manual_ticket(request): +def manual_ticket(request, event_slug): if 'name' not in request.data: return Response({'status': 'error', 'message': 'missing name'}, status=status.HTTP_400_BAD_REQUEST) if 'sender' not in request.data: @@ -66,8 +66,16 @@ def manual_ticket(request): if 'body' not in request.data: return Response({'status': 'error', 'message': 'missing body'}, status=status.HTTP_400_BAD_REQUEST) + event = None + if event_slug != 'none': + try: + event = Event.objects.get(slug=event_slug) + except: + return Response({'status': 'error', 'message': 'invalid event'}, status=status.HTTP_400_BAD_REQUEST) + issue = IssueThread.objects.create( name=request.data['name'], + event=event, manually_created=True, ) email = Email.objects.create( @@ -122,8 +130,8 @@ router.register(r'tickets', IssueViewSet, basename='issues') router.register(r'shipping_vouchers', ShippingVoucherViewSet, basename='shipping_vouchers') urlpatterns = ([ + re_path(r'tickets/states/$', get_available_states, name='get_available_states'), re_path(r'^tickets/(?P\d+)/reply/$', reply, name='reply'), re_path(r'^tickets/(?P\d+)/comment/$', add_comment, name='add_comment'), - re_path(r'^tickets/manual/$', manual_ticket, name='manual_ticket'), - re_path(r'^tickets/states/$', get_available_states, name='get_available_states'), + re_path(r'^(?P[\w-]+)/tickets/manual/$', manual_ticket, name='manual_ticket'), ] + router.urls) diff --git a/core/tickets/serializers.py b/core/tickets/serializers.py index a980b97..f5b7ef6 100644 --- a/core/tickets/serializers.py +++ b/core/tickets/serializers.py @@ -1,6 +1,7 @@ from rest_framework import serializers from authentication.models import ExtendedUser +from inventory.models import Event from mail.api_v2 import AttachmentSerializer from tickets.models import IssueThread, Comment, STATE_CHOICES, ShippingVoucher from inventory.serializers import ItemSerializer @@ -41,11 +42,13 @@ class IssueSerializer(serializers.ModelSerializer): 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) class Meta: model = IssueThread - fields = ('id', 'timeline', 'name', 'state', 'assigned_to', 'last_activity', 'uuid', 'related_items') + fields = ('id', 'timeline', 'name', 'state', 'assigned_to', 'last_activity', 'uuid', 'related_items', 'event') read_only_fields = ('id', 'timeline', 'last_activity', 'uuid', 'related_items') def to_internal_value(self, data): diff --git a/core/tickets/tests/v2/test_tickets.py b/core/tickets/tests/v2/test_tickets.py index 8223506..9e89ed5 100644 --- a/core/tickets/tests/v2/test_tickets.py +++ b/core/tickets/tests/v2/test_tickets.py @@ -3,6 +3,7 @@ from datetime import datetime, timedelta from django.test import TestCase, Client from authentication.models import ExtendedUser +from inventory.models import Event from mail.models import Email, EmailAttachment from tickets.models import IssueThread, StateChange, Comment from django.contrib.auth.models import Permission @@ -16,6 +17,7 @@ class IssueApiTest(TestCase): 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.token = AuthToken.objects.create(user=self.user) self.client = Client(headers={'Authorization': 'Token ' + self.token[1]}) @@ -28,6 +30,7 @@ class IssueApiTest(TestCase): now = datetime.now() issue = IssueThread.objects.create( name="test issue", + event=self.event, ) mail1 = Email.objects.create( subject='test', @@ -61,6 +64,7 @@ class IssueApiTest(TestCase): self.assertEqual(response.json()[0]['id'], 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'], issue.uuid) self.assertEqual(response.json()[0]['last_activity'], comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) @@ -93,12 +97,15 @@ class IssueApiTest(TestCase): now = datetime.now() issue1 = IssueThread.objects.create( name="test issue", + event=self.event, ) issue2 = IssueThread.objects.create( name="test issue", + event=self.event, ) issue3 = IssueThread.objects.create( name="test issue", + event=self.event, ) mail1 = Email.objects.create( subject='test', @@ -118,8 +125,11 @@ class IssueApiTest(TestCase): self.assertEqual(200, response.status_code) self.assertEqual(3, len(response.json())) self.assertEqual(issue1.id, response.json()[0]['id']) + self.assertEqual("evt", response.json()[0]['event']) self.assertEqual(issue2.id, response.json()[1]['id']) + self.assertEqual("evt", response.json()[1]['event']) self.assertEqual(issue3.id, response.json()[2]['id']) + self.assertEqual("evt", response.json()[2]['event']) self.assertEqual(issue1.state_changes.first().timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), response.json()[0]['last_activity']) self.assertEqual(mail1.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), @@ -153,6 +163,7 @@ class IssueApiTest(TestCase): now = datetime.now() issue = IssueThread.objects.create( name="test issue", + event=self.event, ) mail1 = Email.objects.create( subject='test', @@ -189,6 +200,7 @@ class IssueApiTest(TestCase): self.assertEqual(200, response.status_code) self.assertEqual(1, len(response.json())) self.assertEqual(issue.id, response.json()[0]['id']) + self.assertEqual("evt", response.json()[0]['event']) self.assertEqual('pending_new', response.json()[0]['state']) self.assertEqual('test issue', response.json()[0]['name']) self.assertEqual(None, response.json()[0]['assigned_to']) @@ -230,13 +242,14 @@ class IssueApiTest(TestCase): self.assertEqual(file2.hash, response.json()[0]['timeline'][1]['attachments'][1]['hash']) def test_manual_creation(self): - response = self.client.post('/api/2/tickets/manual/', + response = self.client.post('/api/2/evt/tickets/manual/', {'name': 'test issue', 'sender': 'test', 'recipient': 'test', 'body': 'test'}, content_type='application/json') self.assertEqual(response.status_code, 201) self.assertEqual(response.json()['state'], 'pending_new') self.assertEqual(response.json()['name'], 'test issue') self.assertEqual(response.json()['assigned_to'], None) + self.assertEqual("evt", response.json()['event']) timeline = response.json()['timeline'] self.assertEqual(len(timeline), 2) self.assertEqual(timeline[0]['type'], 'state') @@ -247,9 +260,35 @@ class IssueApiTest(TestCase): self.assertEqual(timeline[1]['subject'], 'test issue') self.assertEqual(timeline[1]['body'], 'test') + def test_manual_creation_none(self): + response = self.client.post('/api/2/none/tickets/manual/', + {'name': 'test issue', 'sender': 'test', 'recipient': 'test', 'body': 'test'}, + content_type='application/json') + self.assertEqual(response.status_code, 201) + self.assertEqual(response.json()['state'], 'pending_new') + self.assertEqual(response.json()['name'], 'test issue') + self.assertEqual(response.json()['assigned_to'], None) + self.assertEqual(None, response.json()['event']) + timeline = response.json()['timeline'] + self.assertEqual(len(timeline), 2) + self.assertEqual(timeline[0]['type'], 'state') + self.assertEqual(timeline[0]['state'], 'pending_new') + self.assertEqual(timeline[1]['type'], 'mail') + self.assertEqual(timeline[1]['sender'], 'test') + self.assertEqual(timeline[1]['recipient'], 'test') + self.assertEqual(timeline[1]['subject'], 'test issue') + self.assertEqual(timeline[1]['body'], 'test') + + def test_manual_creation_invalid(self): + response = self.client.post('/api/2/foobar/tickets/manual/', + {'name': 'test issue', 'sender': 'test', 'recipient': 'test', 'body': 'test'}, + content_type='application/json') + self.assertEqual(response.status_code, 400) + def test_post_comment_altenative(self): issue = IssueThread.objects.create( name="test issue", + event=self.event, ) response = self.client.post(f'/api/2/tickets/{issue.id}/comment/', {'comment': 'test'}) self.assertEqual(response.status_code, 201) @@ -260,6 +299,7 @@ class IssueApiTest(TestCase): def test_post_alt_comment_empty(self): issue = IssueThread.objects.create( name="test issue", + event=self.event, ) response = self.client.post(f'/api/2/tickets/{issue.id}/comment/', {'comment': ''}) self.assertEqual(response.status_code, 400) @@ -267,6 +307,7 @@ class IssueApiTest(TestCase): def test_state_change(self): issue = IssueThread.objects.create( name="test issue", + event=self.event, ) response = self.client.patch(f'/api/2/tickets/{issue.id}/', {'state': 'pending_open'}, content_type='application/json') @@ -284,6 +325,7 @@ class IssueApiTest(TestCase): def test_state_change_invalid_state(self): issue = IssueThread.objects.create( name="test issue", + event=self.event, ) response = self.client.patch(f'/api/2/tickets/{issue.id}/', {'state': 'invalid'}, content_type='application/json') @@ -292,12 +334,14 @@ class IssueApiTest(TestCase): def test_assign_user(self): issue = IssueThread.objects.create( name="test issue", + event=self.event, ) response = self.client.patch(f'/api/2/tickets/{issue.id}/', {'assigned_to': self.user.username}, content_type='application/json') self.assertEqual(200, response.status_code) self.assertEqual('pending_new', response.json()['state']) self.assertEqual('test issue', response.json()['name']) + self.assertEqual("evt", response.json()['event']) self.assertEqual(self.user.username, response.json()['assigned_to']) timeline = response.json()['timeline'] self.assertEqual(2, len(timeline)) diff --git a/web/src/components/Navbar.vue b/web/src/components/Navbar.vue index 686f324..eb7504b 100644 --- a/web/src/components/Navbar.vue +++ b/web/src/components/Navbar.vue @@ -6,7 +6,8 @@ {{ getEventSlug }} @@ -115,6 +116,9 @@ export default { computed: { ...mapState(['events']), ...mapGetters(['getEventSlug', 'getActiveView', "checkPermission", "hasPermissions", "layout", "route"]), + selectableEvents() { + return [{slug: 'all'}, ...this.events, {slug: 'none'}]; + } }, methods: { ...mapActions(['changeEvent', 'changeView']), diff --git a/web/src/store.js b/web/src/store.js index 53d01b6..3140a99 100644 --- a/web/src/store.js +++ b/web/src/store.js @@ -21,7 +21,7 @@ const store = createStore({ state_options: [], shippingVouchers: [], - lastEvent: '37C3', + lastEvent: 'all', lastUsed: {}, searchQuery: '', remember: false, @@ -358,7 +358,8 @@ const store = createStore({ async searchEventItems({commit, getters, state}, query) { const encoded_query = base64.encode(utf8.encode(query)); - const {data, success} = await http.get(`/2/${getters.getEventSlug}/items/${encoded_query}/`, state.user.token); + const {data, success} = await http.get(`/2/${getters.getEventSlug}/items/${encoded_query}/`, + state.user.token); if (data && success) commit('replaceLoadedItems', data); }, @@ -410,7 +411,8 @@ const store = createStore({ async searchEventTickets({commit, getters, state}, query) { const encoded_query = base64.encode(utf8.encode(query)); - const {data, success} = await http.get(`/2/${getters.getEventSlug}/tickets/${encoded_query}/`, state.user.token); + const {data, success} = await http.get(`/2/${getters.getEventSlug}/tickets/${encoded_query}/`, + state.user.token); if (data && success) commit('replaceTickets', data); },