Compare commits

...

4 commits

Author SHA1 Message Date
7443e27e68 match incoming mail to event
Some checks failed
/ test (push) Successful in 51s
/ deploy (push) Has been cancelled
2024-11-18 02:02:02 +01:00
1408aa1713 test on every push
All checks were successful
/ test (push) Successful in 52s
/ deploy (push) Successful in 4m57s
2024-11-17 22:07:52 +01:00
5da9278a96 show tickets filtered by active event 2024-11-17 22:06:42 +01:00
147fc9cf86 partition tickets by event 2024-11-14 03:08:22 +01:00
19 changed files with 272 additions and 236 deletions

View file

@ -1,5 +1,6 @@
on: on:
pull_request: pull_request:
push:
jobs: jobs:
test: test:

View file

@ -15,9 +15,11 @@ import sys
import dotenv import dotenv
from pathlib import Path from pathlib import Path
def truthy_str(s): def truthy_str(s):
return s.lower() in ['true', '1', 't', 'y', 'yes', 'yeah', 'yup', 'certainly', 'sure', 'positive', 'uh-huh', '👍'] return s.lower() in ['true', '1', 't', 'y', 'yes', 'yeah', 'yup', 'certainly', 'sure', 'positive', 'uh-huh', '👍']
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent

View file

@ -1,16 +0,0 @@
"""
WSGI config for core project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
application = get_wsgi_application()

View file

@ -1,4 +1,4 @@
from django.urls import path from django.urls import re_path
from django.contrib.auth.decorators import permission_required from django.contrib.auth.decorators import permission_required
from rest_framework import routers, viewsets from rest_framework import routers, viewsets
from rest_framework.decorators import api_view, permission_classes from rest_framework.decorators import api_view, permission_classes
@ -40,6 +40,8 @@ def search_items(request, event_slug, query):
@permission_classes([IsAuthenticated]) @permission_classes([IsAuthenticated])
def item(request, event_slug): def item(request, event_slug):
try: try:
event = None
if event_slug != 'none':
event = Event.objects.get(slug=event_slug) event = Event.objects.get(slug=event_slug)
if request.method == 'GET': if request.method == 'GET':
if not request.user.has_event_perm(event, 'view_item'): if not request.user.has_event_perm(event, 'view_item'):
@ -99,8 +101,8 @@ 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), re_path(r'^(?P<event_slug>[\w-]+)/items/$', item, name='item'),
path('<event_slug>/items/<query>/', search_items), re_path(r'^(?P<event_slug>[\w-]+)/items/(?P<query>[-A-Za-z0-9+/]*={0,3})/$', search_items, name='search_items'),
path('<event_slug>/item/', item), re_path(r'^(?P<event_slug>[\w-]+)/item/$', item, name='item'),
path('<event_slug>/item/<id>/', item_by_id), re_path(r'^(?P<event_slug>[\w-]+)/item/(?P<id>\d+)/$', item_by_id, name='item_by_id'),
] ]

View file

@ -39,10 +39,12 @@ class ItemSerializer(serializers.ModelSerializer):
box = serializers.SerializerMethodField() box = serializers.SerializerMethodField()
file = serializers.SerializerMethodField() file = serializers.SerializerMethodField()
returned = serializers.SerializerMethodField(required=False) 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', 'uid', 'description', 'file', 'dataImage', 'returned'] fields = ['cid', 'box', 'uid', 'description', 'file', 'dataImage', 'returned', 'event']
read_only_fields = ['uid'] read_only_fields = ['uid']
def get_cid(self, instance): def get_cid(self, instance):

View file

@ -30,7 +30,7 @@ class ItemTestCase(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), self.assertEqual(response.json(),
[{'uid': 1, 'description': '1', 'box': 'BOX', 'cid': self.box.cid, 'file': None, [{'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): def test_members_with_file(self):
import base64 import base64
@ -40,7 +40,7 @@ class ItemTestCase(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), self.assertEqual(response.json(),
[{'uid': 1, 'description': '1', 'box': 'BOX', 'cid': self.box.cid, 'file': file.hash, [{'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): 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,7 +55,7 @@ class ItemTestCase(TestCase):
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
self.assertEqual(response.json(), self.assertEqual(response.json(),
{'uid': 1, 'description': '1', 'box': 'BOX', 'cid': self.box.cid, 'file': None, {'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(len(Item.objects.all()), 1)
self.assertEqual(Item.objects.all()[0].uid, 1) self.assertEqual(Item.objects.all()[0].uid, 1)
self.assertEqual(Item.objects.all()[0].description, '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.status_code, 200)
self.assertEqual(response.json(), self.assertEqual(response.json(),
{'uid': 1, 'description': '2', 'box': 'BOX', 'cid': self.box.cid, 'file': None, {'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(len(Item.objects.all()), 1)
self.assertEqual(Item.objects.all()[0].uid, 1) self.assertEqual(Item.objects.all()[0].uid, 1)
self.assertEqual(Item.objects.all()[0].description, '2') self.assertEqual(Item.objects.all()[0].description, '2')

View file

@ -29,13 +29,13 @@ class Email(SoftDeleteModel):
self.save() self.save()
def train_spam(self): def train_spam(self):
if ACTIVE_SPAM_TRAINING: if ACTIVE_SPAM_TRAINING and self.raw_file.path:
import subprocess import subprocess
path = self.raw_file.path path = self.raw_file.path
subprocess.run(["rspamc", "learn_spam", path]) subprocess.run(["rspamc", "learn_spam", path])
def train_ham(self): def train_ham(self):
if ACTIVE_SPAM_TRAINING: if ACTIVE_SPAM_TRAINING and self.raw_file.path:
import subprocess import subprocess
path = self.raw_file.path path = self.raw_file.path
subprocess.run(["rspamc", "learn_ham", path]) subprocess.run(["rspamc", "learn_ham", path])

View file

@ -91,7 +91,7 @@ async def send_smtp(message):
await aiosmtplib.send(message, hostname="127.0.0.1", port=25, use_tls=False, start_tls=False) await aiosmtplib.send(message, hostname="127.0.0.1", port=25, use_tls=False, start_tls=False)
def find_active_issue_thread(in_reply_to, address, subject): def find_active_issue_thread(in_reply_to, address, subject, event):
from re import match from re import match
uuid_match = match(r'^ticket\+([a-f0-9-]{36})@', address) uuid_match = match(r'^ticket\+([a-f0-9-]{36})@', address)
if uuid_match: if uuid_match:
@ -102,7 +102,7 @@ def find_active_issue_thread(in_reply_to, address, subject):
if reply_to.exists(): if reply_to.exists():
return reply_to.first().issue_thread, False return reply_to.first().issue_thread, False
else: else:
issue = IssueThread.objects.create(name=subject) issue = IssueThread.objects.create(name=subject, event=event)
return issue, True return issue, True
@ -202,11 +202,14 @@ def receive_email(envelope, log=None):
subject = unescape_and_decode_base64(subject) subject = unescape_and_decode_base64(subject)
target_event = find_target_event(recipient) target_event = find_target_event(recipient)
active_issue_thread, new = find_active_issue_thread(header_in_reply_to, recipient, subject) active_issue_thread, new = find_active_issue_thread(header_in_reply_to, recipient, subject, target_event)
from hashlib import sha256
random_filename = 'mail-' + sha256(envelope.content).hexdigest()
email = Email.objects.create( email = Email.objects.create(
sender=sender, recipient=recipient, body=body, subject=subject, reference=header_message_id, sender=sender, recipient=recipient, body=body, subject=subject, reference=header_message_id,
in_reply_to=header_in_reply_to, raw_file=ContentFile(envelope.content), event=target_event, in_reply_to=header_in_reply_to, raw_file=ContentFile(envelope.content, name=random_filename), event=target_event,
issue_thread=active_issue_thread) issue_thread=active_issue_thread)
for attachment in attachments: for attachment in attachments:
email.attachments.add(attachment) email.attachments.add(attachment)

View file

@ -142,6 +142,7 @@ class LMTPHandlerTestCase(TestCase): # TODO replace with less hacky test
aiosmtplib.send.assert_called_once() aiosmtplib.send.assert_called_once()
self.assertEqual('test ä', Email.objects.all()[0].subject) self.assertEqual('test ä', Email.objects.all()[0].subject)
self.assertEqual('Text mit Quoted-Printable-Kodierung: äöüß', Email.objects.all()[0].body) self.assertEqual('Text mit Quoted-Printable-Kodierung: äöüß', Email.objects.all()[0].body)
self.assertTrue( Email.objects.all()[0].raw_file.path)
def test_handle_quoted_printable_2(self): def test_handle_quoted_printable_2(self):
from aiosmtpd.smtp import Envelope from aiosmtpd.smtp import Envelope
@ -162,6 +163,7 @@ class LMTPHandlerTestCase(TestCase): # TODO replace with less hacky test
aiosmtplib.send.assert_called_once() aiosmtplib.send.assert_called_once()
self.assertEqual('suche_Mütze', Email.objects.all()[0].subject) self.assertEqual('suche_Mütze', Email.objects.all()[0].subject)
self.assertEqual('Text mit Quoted-Printable-Kodierung: äöüß', Email.objects.all()[0].body) self.assertEqual('Text mit Quoted-Printable-Kodierung: äöüß', Email.objects.all()[0].body)
self.assertTrue( Email.objects.all()[0].raw_file.path)
def test_handle_base64(self): def test_handle_base64(self):
from aiosmtpd.smtp import Envelope from aiosmtpd.smtp import Envelope
@ -182,6 +184,7 @@ class LMTPHandlerTestCase(TestCase): # TODO replace with less hacky test
aiosmtplib.send.assert_called_once() aiosmtplib.send.assert_called_once()
self.assertEqual('test', Email.objects.all()[0].subject) self.assertEqual('test', Email.objects.all()[0].subject)
self.assertEqual('Text mit Base64-Kodierung: äöüß', Email.objects.all()[0].body) self.assertEqual('Text mit Base64-Kodierung: äöüß', Email.objects.all()[0].body)
self.assertTrue( Email.objects.all()[0].raw_file.path)
def test_handle_client_reply(self): def test_handle_client_reply(self):
issue_thread = IssueThread.objects.create( issue_thread = IssueThread.objects.create(
@ -229,6 +232,7 @@ class LMTPHandlerTestCase(TestCase): # TODO replace with less hacky test
self.assertEqual(IssueThread.objects.all()[0].name, 'test') self.assertEqual(IssueThread.objects.all()[0].name, 'test')
self.assertEqual(IssueThread.objects.all()[0].state, 'pending_new') self.assertEqual(IssueThread.objects.all()[0].state, 'pending_new')
self.assertEqual(IssueThread.objects.all()[0].assigned_to, None) self.assertEqual(IssueThread.objects.all()[0].assigned_to, None)
self.assertTrue( Email.objects.all()[2].raw_file.path)
def test_handle_client_reply_2(self): def test_handle_client_reply_2(self):
issue_thread = IssueThread.objects.create( issue_thread = IssueThread.objects.create(
@ -281,6 +285,7 @@ class LMTPHandlerTestCase(TestCase): # TODO replace with less hacky test
self.assertEqual(IssueThread.objects.all()[0].name, 'test') self.assertEqual(IssueThread.objects.all()[0].name, 'test')
self.assertEqual(IssueThread.objects.all()[0].state, 'pending_open') self.assertEqual(IssueThread.objects.all()[0].state, 'pending_open')
self.assertEqual(IssueThread.objects.all()[0].assigned_to, None) self.assertEqual(IssueThread.objects.all()[0].assigned_to, None)
self.assertTrue( Email.objects.all()[2].raw_file.path)
def test_mail_reply(self): def test_mail_reply(self):
issue_thread = IssueThread.objects.create( issue_thread = IssueThread.objects.create(
@ -384,6 +389,7 @@ class LMTPHandlerTestCase(TestCase): # TODO replace with less hacky test
states = StateChange.objects.filter(issue_thread=IssueThread.objects.all()[0]) states = StateChange.objects.filter(issue_thread=IssueThread.objects.all()[0])
self.assertEqual(1, len(states)) self.assertEqual(1, len(states))
self.assertEqual('pending_new', states[0].state) self.assertEqual('pending_new', states[0].state)
self.assertEqual(event, IssueThread.objects.all()[0].event)
def test_mail_html_body(self): def test_mail_html_body(self):
from aiosmtpd.smtp import Envelope from aiosmtpd.smtp import Envelope

View file

@ -56,7 +56,7 @@ def reply(request, pk):
@api_view(['POST']) @api_view(['POST'])
@permission_classes([IsAuthenticated]) @permission_classes([IsAuthenticated])
@permission_required('tickets.add_issuethread_manual', raise_exception=True) @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: if 'name' not in request.data:
return Response({'status': 'error', 'message': 'missing name'}, status=status.HTTP_400_BAD_REQUEST) return Response({'status': 'error', 'message': 'missing name'}, status=status.HTTP_400_BAD_REQUEST)
if 'sender' not in request.data: if 'sender' not in request.data:
@ -66,8 +66,16 @@ def manual_ticket(request):
if 'body' not in request.data: if 'body' not in request.data:
return Response({'status': 'error', 'message': 'missing body'}, status=status.HTTP_400_BAD_REQUEST) 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( issue = IssueThread.objects.create(
name=request.data['name'], name=request.data['name'],
event=event,
manually_created=True, manually_created=True,
) )
email = Email.objects.create( email = Email.objects.create(
@ -122,8 +130,8 @@ router.register(r'tickets', IssueViewSet, basename='issues')
router.register(r'shipping_vouchers', ShippingVoucherViewSet, basename='shipping_vouchers') router.register(r'shipping_vouchers', ShippingVoucherViewSet, basename='shipping_vouchers')
urlpatterns = ([ urlpatterns = ([
re_path(r'tickets/states/$', get_available_states, name='get_available_states'),
re_path(r'^tickets/(?P<pk>\d+)/reply/$', reply, name='reply'), re_path(r'^tickets/(?P<pk>\d+)/reply/$', reply, name='reply'),
re_path(r'^tickets/(?P<pk>\d+)/comment/$', add_comment, name='add_comment'), re_path(r'^tickets/(?P<pk>\d+)/comment/$', add_comment, name='add_comment'),
re_path(r'^tickets/manual/$', manual_ticket, name='manual_ticket'), re_path(r'^(?P<event_slug>[\w-]+)/tickets/manual/$', manual_ticket, name='manual_ticket'),
re_path(r'^tickets/states/$', get_available_states, name='get_available_states'),
] + router.urls) ] + router.urls)

View file

@ -1,6 +1,7 @@
from rest_framework import serializers from rest_framework import serializers
from authentication.models import ExtendedUser from authentication.models import ExtendedUser
from inventory.models import Event
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 inventory.serializers import ItemSerializer
@ -41,19 +42,19 @@ class IssueSerializer(serializers.ModelSerializer):
last_activity = serializers.SerializerMethodField() last_activity = serializers.SerializerMethodField()
assigned_to = serializers.SlugRelatedField(slug_field='username', queryset=ExtendedUser.objects.all(), assigned_to = serializers.SlugRelatedField(slug_field='username', queryset=ExtendedUser.objects.all(),
allow_null=True, required=False) 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 = ItemSerializer(many=True, read_only=True)
class Meta: class Meta:
model = IssueThread 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') read_only_fields = ('id', 'timeline', 'last_activity', 'uuid', 'related_items')
def to_internal_value(self, data): def to_internal_value(self, data):
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):

View file

@ -3,6 +3,7 @@ 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 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
from django.contrib.auth.models import Permission 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 = ExtendedUser.objects.create_user('testuser', 'test', 'test')
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.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]})
@ -28,6 +30,7 @@ class IssueApiTest(TestCase):
now = datetime.now() now = datetime.now()
issue = IssueThread.objects.create( issue = IssueThread.objects.create(
name="test issue", name="test issue",
event=self.event,
) )
mail1 = Email.objects.create( mail1 = Email.objects.create(
subject='test', subject='test',
@ -61,6 +64,7 @@ class IssueApiTest(TestCase):
self.assertEqual(response.json()[0]['id'], issue.id) self.assertEqual(response.json()[0]['id'], issue.id)
self.assertEqual(response.json()[0]['name'], "test issue") self.assertEqual(response.json()[0]['name'], "test issue")
self.assertEqual(response.json()[0]['state'], "pending_new") 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]['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'], comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'))
@ -93,12 +97,15 @@ class IssueApiTest(TestCase):
now = datetime.now() now = datetime.now()
issue1 = IssueThread.objects.create( issue1 = IssueThread.objects.create(
name="test issue", name="test issue",
event=self.event,
) )
issue2 = IssueThread.objects.create( issue2 = IssueThread.objects.create(
name="test issue", name="test issue",
event=self.event,
) )
issue3 = IssueThread.objects.create( issue3 = IssueThread.objects.create(
name="test issue", name="test issue",
event=self.event,
) )
mail1 = Email.objects.create( mail1 = Email.objects.create(
subject='test', subject='test',
@ -118,8 +125,11 @@ class IssueApiTest(TestCase):
self.assertEqual(200, response.status_code) self.assertEqual(200, response.status_code)
self.assertEqual(3, len(response.json())) self.assertEqual(3, len(response.json()))
self.assertEqual(issue1.id, response.json()[0]['id']) 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(issue2.id, response.json()[1]['id'])
self.assertEqual("evt", response.json()[1]['event'])
self.assertEqual(issue3.id, response.json()[2]['id']) 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'), self.assertEqual(issue1.state_changes.first().timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
response.json()[0]['last_activity']) response.json()[0]['last_activity'])
self.assertEqual(mail1.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), self.assertEqual(mail1.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
@ -153,6 +163,7 @@ class IssueApiTest(TestCase):
now = datetime.now() now = datetime.now()
issue = IssueThread.objects.create( issue = IssueThread.objects.create(
name="test issue", name="test issue",
event=self.event,
) )
mail1 = Email.objects.create( mail1 = Email.objects.create(
subject='test', subject='test',
@ -189,6 +200,7 @@ class IssueApiTest(TestCase):
self.assertEqual(200, response.status_code) self.assertEqual(200, response.status_code)
self.assertEqual(1, len(response.json())) self.assertEqual(1, len(response.json()))
self.assertEqual(issue.id, response.json()[0]['id']) 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('pending_new', response.json()[0]['state'])
self.assertEqual('test issue', response.json()[0]['name']) self.assertEqual('test issue', response.json()[0]['name'])
self.assertEqual(None, response.json()[0]['assigned_to']) 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']) self.assertEqual(file2.hash, response.json()[0]['timeline'][1]['attachments'][1]['hash'])
def test_manual_creation(self): 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'}, {'name': 'test issue', 'sender': 'test', 'recipient': 'test', 'body': 'test'},
content_type='application/json') content_type='application/json')
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
self.assertEqual(response.json()['state'], 'pending_new') self.assertEqual(response.json()['state'], 'pending_new')
self.assertEqual(response.json()['name'], 'test issue') self.assertEqual(response.json()['name'], 'test issue')
self.assertEqual(response.json()['assigned_to'], None) self.assertEqual(response.json()['assigned_to'], None)
self.assertEqual("evt", response.json()['event'])
timeline = response.json()['timeline'] timeline = response.json()['timeline']
self.assertEqual(len(timeline), 2) self.assertEqual(len(timeline), 2)
self.assertEqual(timeline[0]['type'], 'state') self.assertEqual(timeline[0]['type'], 'state')
@ -247,9 +260,35 @@ class IssueApiTest(TestCase):
self.assertEqual(timeline[1]['subject'], 'test issue') self.assertEqual(timeline[1]['subject'], 'test issue')
self.assertEqual(timeline[1]['body'], 'test') 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): def test_post_comment_altenative(self):
issue = IssueThread.objects.create( issue = IssueThread.objects.create(
name="test issue", name="test issue",
event=self.event,
) )
response = self.client.post(f'/api/2/tickets/{issue.id}/comment/', {'comment': 'test'}) response = self.client.post(f'/api/2/tickets/{issue.id}/comment/', {'comment': 'test'})
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
@ -260,6 +299,7 @@ class IssueApiTest(TestCase):
def test_post_alt_comment_empty(self): def test_post_alt_comment_empty(self):
issue = IssueThread.objects.create( issue = IssueThread.objects.create(
name="test issue", name="test issue",
event=self.event,
) )
response = self.client.post(f'/api/2/tickets/{issue.id}/comment/', {'comment': ''}) response = self.client.post(f'/api/2/tickets/{issue.id}/comment/', {'comment': ''})
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
@ -267,6 +307,7 @@ class IssueApiTest(TestCase):
def test_state_change(self): def test_state_change(self):
issue = IssueThread.objects.create( issue = IssueThread.objects.create(
name="test issue", name="test issue",
event=self.event,
) )
response = self.client.patch(f'/api/2/tickets/{issue.id}/', {'state': 'pending_open'}, response = self.client.patch(f'/api/2/tickets/{issue.id}/', {'state': 'pending_open'},
content_type='application/json') content_type='application/json')
@ -284,6 +325,7 @@ class IssueApiTest(TestCase):
def test_state_change_invalid_state(self): def test_state_change_invalid_state(self):
issue = IssueThread.objects.create( issue = IssueThread.objects.create(
name="test issue", name="test issue",
event=self.event,
) )
response = self.client.patch(f'/api/2/tickets/{issue.id}/', {'state': 'invalid'}, response = self.client.patch(f'/api/2/tickets/{issue.id}/', {'state': 'invalid'},
content_type='application/json') content_type='application/json')
@ -292,12 +334,14 @@ class IssueApiTest(TestCase):
def test_assign_user(self): def test_assign_user(self):
issue = IssueThread.objects.create( issue = IssueThread.objects.create(
name="test issue", name="test issue",
event=self.event,
) )
response = self.client.patch(f'/api/2/tickets/{issue.id}/', {'assigned_to': self.user.username}, response = self.client.patch(f'/api/2/tickets/{issue.id}/', {'assigned_to': self.user.username},
content_type='application/json') content_type='application/json')
self.assertEqual(200, response.status_code) self.assertEqual(200, response.status_code)
self.assertEqual('pending_new', response.json()['state']) self.assertEqual('pending_new', response.json()['state'])
self.assertEqual('test issue', response.json()['name']) self.assertEqual('test issue', response.json()['name'])
self.assertEqual("evt", response.json()['event'])
self.assertEqual(self.user.username, response.json()['assigned_to']) self.assertEqual(self.user.username, response.json()['assigned_to'])
timeline = response.json()['timeline'] timeline = response.json()['timeline']
self.assertEqual(2, len(timeline)) self.assertEqual(2, len(timeline))

View file

@ -6,7 +6,8 @@
{{ getEventSlug }} {{ getEventSlug }}
</button> </button>
<div class="dropdown-menu bg-dark" aria-labelledby="dropdownMenuButton"> <div class="dropdown-menu bg-dark" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item text-light" href="#" v-for="(event, index) in events" v-bind:key="index" <a class="dropdown-item text-light" href="#" v-for="(event, index) in selectableEvents"
v-bind:key="index"
:class="{ active: event.slug === getEventSlug }" @click="changeEvent(event)">{{ event.slug }}</a> :class="{ active: event.slug === getEventSlug }" @click="changeEvent(event)">{{ event.slug }}</a>
</div> </div>
</div> </div>
@ -48,12 +49,12 @@
</button> </button>
</div> </div>
<button type="button" class="btn text-nowrap btn-success mr-1" @click="$emit('addItemClicked')" <button type="button" class="btn text-nowrap btn-success mr-1" @click="$emit('addItemClicked')"
v-if="isItemView()"> v-if="isItemView() && getEventSlug !== 'all'">
<font-awesome-icon icon="plus"/> <font-awesome-icon icon="plus"/>
<span class="d-none d-md-inline">&nbsp;Add Item</span> <span class="d-none d-md-inline">&nbsp;Add Item</span>
</button> </button>
<button type="button" class="btn text-nowrap btn-success mr-1" @click="$emit('addTicketClicked')" <button type="button" class="btn text-nowrap btn-success mr-1" @click="$emit('addTicketClicked')"
v-if="isTicketView()"> v-if="isTicketView() && getEventSlug !== 'all'">
<font-awesome-icon icon="plus"/> <font-awesome-icon icon="plus"/>
<span class="d-none d-md-inline">&nbsp;Add Ticket</span> <span class="d-none d-md-inline">&nbsp;Add Ticket</span>
</button> </button>
@ -64,19 +65,6 @@
</button> </button>
<div class="collapse navbar-collapse" id="navbarSupportedContent"> <div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ml-auto"> <ul class="navbar-nav ml-auto">
<!--li class="nav-item dropdown">
<button class="btn nav-link dropdown-toggle" type="button" id="dropdownMenuButton2"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ getActiveView }}
</button>
<ul class="dropdown-menu bg-dark" aria-labelledby="dropdownMenuButton2">
<li class="" v-for="(link, index) in views" v-bind:key="index"
:class="{ active: link.path === getActiveView }">
<a class="nav-link text-nowrap" href="#" @click="changeView(link)">{{ link.title }}</a>
</li>
</ul>
</li-->
<li class="nav-item" v-for="(link, index) in links" v-bind:key="index"> <li class="nav-item" v-for="(link, index) in links" v-bind:key="index">
<a class="nav-link text-nowrap" :href="link.path" @click.prevent="navigateTo(link.path)"> <a class="nav-link text-nowrap" :href="link.path" @click.prevent="navigateTo(link.path)">
{{ link.title }} {{ link.title }}
@ -115,6 +103,9 @@ export default {
computed: { computed: {
...mapState(['events']), ...mapState(['events']),
...mapGetters(['getEventSlug', 'getActiveView', "checkPermission", "hasPermissions", "layout", "route"]), ...mapGetters(['getEventSlug', 'getActiveView', "checkPermission", "hasPermissions", "layout", "route"]),
selectableEvents() {
return [{slug: 'all'}, ...this.events, {slug: 'none'}];
}
}, },
methods: { methods: {
...mapActions(['changeEvent', 'changeView']), ...mapActions(['changeEvent', 'changeView']),

View file

@ -3,7 +3,7 @@ import router from './router';
import * as base64 from 'base-64'; import * as base64 from 'base-64';
import * as utf8 from 'utf8'; import * as utf8 from 'utf8';
import {ticketStateColorLookup, ticketStateIconLookup, http} from "@/utils"; import {ticketStateColorLookup, ticketStateIconLookup, http, http_session} from "@/utils";
import sharedStatePlugin from "@/shared-state-plugin"; import sharedStatePlugin from "@/shared-state-plugin";
import persistentStatePlugin from "@/persistent-state-plugin"; import persistentStatePlugin from "@/persistent-state-plugin";
@ -11,17 +11,18 @@ const store = createStore({
state: { state: {
keyIncrement: 0, keyIncrement: 0,
events: [], events: [],
loadedItems: [], items: [],
itemCache: {},
loadedBoxes: [], loadedBoxes: [],
toasts: [], toasts: [],
tickets: [],
users: [], users: [],
groups: [], groups: [],
state_options: [], state_options: [],
shippingVouchers: [], shippingVouchers: [],
lastEvent: '37C3', loadedItems: {},
loadedTickets: {},
lastEvent: 'all',
lastUsed: {}, lastUsed: {},
searchQuery: '', searchQuery: '',
remember: false, remember: false,
@ -62,7 +63,14 @@ const store = createStore({
}, },
getters: { getters: {
route: state => router.currentRoute.value, route: state => router.currentRoute.value,
session: state => http_session(state.user.token),
getEventSlug: state => router.currentRoute.value.params.event ? router.currentRoute.value.params.event : state.lastEvent, getEventSlug: state => router.currentRoute.value.params.event ? router.currentRoute.value.params.event : state.lastEvent,
getAllItems: state => Object.values(state.loadedItems).flat(),
getAllTickets: state => Object.values(state.loadedTickets).flat(),
getEventItems: (state, getters) => getters.getEventSlug === 'all' ? getters.getAllItems : getters.getAllItems.filter(t => t.event === getters.getEventSlug || (t.event == null && getters.getEventSlug === 'none')),
getEventTickets: (state, getters) => getters.getEventSlug === 'all' ? getters.getAllTickets : getters.getAllTickets.filter(t => t.event === getters.getEventSlug || (t.event == null && getters.getEventSlug === 'none')),
isItemsLoaded: (state, getters) => (getters.getEventSlug === 'all' || getters.getEventSlug === 'none') ? !!state.loadedItems : Object.keys(state.loadedItems).includes(getters.getEventSlug),
isTicketsLoaded: (state, getters) => (getters.getEventSlug === 'all' || getters.getEventSlug === 'none') ? !!state.loadedTickets : Object.keys(state.loadedTickets).includes(getters.getEventSlug),
getActiveView: state => router.currentRoute.value.name || 'items', getActiveView: state => router.currentRoute.value.name || 'items',
getFilters: state => router.currentRoute.value.query, getFilters: state => router.currentRoute.value.query,
getBoxes: state => state.loadedBoxes, getBoxes: state => state.loadedBoxes,
@ -130,35 +138,48 @@ const store = createStore({
changeView(state, {view, slug}) { changeView(state, {view, slug}) {
router.push({path: `/${slug}/${view}`}); router.push({path: `/${slug}/${view}`});
}, },
replaceLoadedItems(state, newItems) {
state.loadedItems = newItems;
state.fetchedData = {...state.fetchedData, items: Date.now()}; // TODO: manage caching items for different events and search results correctly
},
setItemCache(state, {slug, items}) {
state.itemCache[slug] = items;
},
replaceBoxes(state, loadedBoxes) { replaceBoxes(state, loadedBoxes) {
state.loadedBoxes = loadedBoxes; state.loadedBoxes = loadedBoxes;
state.fetchedData = {...state.fetchedData, boxes: Date.now()}; state.fetchedData = {...state.fetchedData, boxes: Date.now()};
}, },
setItems(state, {slug, items}) {
state.loadedItems[slug] = items;
state.loadedItems = {...state.loadedItems};
console.log(state.loadedItems)
},
replaceItems(state, items) {
const groups = Object.groupBy(items, i => i.event ? i.event : 'none')
for (const [key, value] of Object.entries(groups)) state.loadedItems[key] = value;
state.loadedItems = {...state.loadedItems};
console.log(state.loadedItems)
},
updateItem(state, updatedItem) { updateItem(state, updatedItem) {
const item = state.loadedItems.filter(({uid}) => uid === updatedItem.uid)[0]; const item = state.loadedItems[updatedItem.event?updatedItem.event:'none'].filter(
({uid}) => uid === updatedItem.uid)[0];
Object.assign(item, updatedItem); Object.assign(item, updatedItem);
}, },
removeItem(state, item) { removeItem(state, item) {
state.loadedItems = state.loadedItems.filter(it => it !== item); state.loadedItems[item.event?item.event:'none'] = state.loadedItems[item.event].filter(it => it !== item);
}, },
appendItem(state, item) { appendItem(state, item) {
state.loadedItems.push(item); state.loadedItems[item.event?item.event:'none'].push(item);
},
setTickets(state, {slug, tickets}) {
state.loadedTickets[slug] = tickets;
state.loadedTickets = {...state.loadedTickets};
console.log(state.loadedTickets)
}, },
replaceTickets(state, tickets) { replaceTickets(state, tickets) {
state.tickets = tickets; const groups = Object.groupBy(tickets, t => t.event ? t.event : 'none')
state.fetchedData = {...state.fetchedData, tickets: Date.now()}; for (const [key, value] of Object.entries(groups)) state.loadedTickets[key] = value;
state.loadedTickets = {...state.loadedTickets};
console.log(state.loadedTickets)
}, },
updateTicket(state, updatedTicket) { updateTicket(state, updatedTicket) {
const ticket = state.tickets.filter(({id}) => id === updatedTicket.id)[0]; const ticket = state.loadedTickets[updatedTicket.event?updatedTicket.event:'none'].filter(
({id}) => id === updatedTicket.id)[0];
Object.assign(ticket, updatedTicket); Object.assign(ticket, updatedTicket);
state.tickets = [...state.tickets]; state.loadedTickets = {...state.loadedTickets};
}, },
replaceUsers(state, users) { replaceUsers(state, users) {
state.users = users; state.users = users;
@ -249,7 +270,7 @@ const store = createStore({
return false; return false;
} }
}, },
async reloadToken({commit, state, getters}) { async reloadToken({commit, state}) {
try { try {
if (state.user.username && state.user.password) { if (state.user.username && state.user.password) {
const data = await fetch('/api/2/login/', { const data = await fetch('/api/2/login/', {
@ -297,59 +318,51 @@ const store = createStore({
async fetchImage({state}, url) { async fetchImage({state}, url) {
return await fetch(url, {headers: {'Authorization': `Token ${state.user.token}`}}); return await fetch(url, {headers: {'Authorization': `Token ${state.user.token}`}});
}, },
async loadUserInfo({commit, state}) { async loadUserInfo({commit, getters}) {
const {data, success} = await http.get('/2/self/', state.user.token); const {data, success} = await getters.session.get('/2/self/');
commit('setPermissions', data.permissions); commit('setPermissions', data.permissions);
}, },
async loadEvents({commit, state}) { async loadEvents({commit, state, getters}) {
if (!state.user.token) return; if (!state.user.token) return;
if (state.fetchedData.events > Date.now() - 1000 * 60 * 60 * 24) return; if (state.fetchedData.events > Date.now() - 1000 * 60 * 60 * 24) return;
const {data, success} = await http.get('/2/events/', state.user.token); const {data, success} = await getters.session.get('/2/events/');
if (data && success) if (data && success) commit('replaceEvents', data);
commit('replaceEvents', data);
}, },
async createEvent({commit, dispatch, state}, event) { async createEvent({commit, dispatch, state, getters}, event) {
const {data, success} = await http.post('/2/events/', event, state.user.token); const {data, success} = await getters.session.post('/2/events/', event);
if (data && success) if (data && success) commit('replaceEvents', [...state.events, data]);
commit('replaceEvents', [...state.events, data]);
}, },
async deleteEvent({commit, dispatch, state}, event_id) { async deleteEvent({commit, dispatch, state, getters}, event_id) {
const {data, success} = await http.delete(`/2/events/${event_id}/`, state.user.token); const {data, success} = await getters.session.delete(`/2/events/${event_id}/`);
if (success) { if (success) {
await dispatch('loadEvents') await dispatch('loadEvents')
commit('replaceEvents', [...state.events.filter(e => e.eid !== event_id)]) commit('replaceEvents', [...state.events.filter(e => e.eid !== event_id)])
} }
}, },
async fetchTicketStates({commit, state}) { async fetchTicketStates({commit, state, getters}) {
if (!state.user.token) return; if (!state.user.token) return;
if (state.fetchedData.states > Date.now() - 1000 * 60 * 60 * 24) return; if (state.fetchedData.states > Date.now() - 1000 * 60 * 60 * 24) return;
const {data, success} = await http.get('/2/tickets/states/', state.user.token); const {data, success} = await getters.session.get('/2/tickets/states/');
if (data && success) if (data && success) commit('replaceTicketStates', data);
commit('replaceTicketStates', data);
}, },
changeEvent({dispatch, getters, commit}, eventName) { async changeEvent({dispatch, getters, commit}, eventName) {
router.push({path: `/${eventName.slug}/${getters.getActiveView}/`}); await router.push({path: `/${eventName.slug}/${getters.getActiveView}/`});
dispatch('loadEventItems'); //dispatch('loadEventItems');
}, },
changeView({getters}, link) { async changeView({getters}, link) {
router.push({path: `/${getters.getEventSlug}/${link.path}/`}); await router.push({path: `/${getters.getEventSlug}/${link.path}/`});
}, },
showBoxContent({getters}, box) { async showBoxContent({getters}, box) {
router.push({path: `/${getters.getEventSlug}/items/`, query: {box}}); await router.push({path: `/${getters.getEventSlug}/items/`, query: {box}});
}, },
async loadEventItems({commit, getters, state}) { async loadEventItems({commit, getters, state}) {
if (!state.user.token) return; if (!state.user.token) return;
if (state.fetchedData.items > Date.now() - 1000 * 60 * 60 * 24) return; if (state.fetchedData.items > Date.now() - 1000 * 60 * 60 * 24) return;
try { try {
commit('replaceLoadedItems', []);
const slug = getters.getEventSlug; const slug = getters.getEventSlug;
if (slug in state.itemCache) { const {data, success} = await getters.session.get(`/2/${slug}/items/`);
commit('replaceLoadedItems', state.itemCache[slug]);
}
const {data, success} = await http.get(`/2/${slug}/items/`, state.user.token);
if (data && success) { if (data && success) {
commit('replaceLoadedItems', data); commit('setItems', {slug, items: data});
commit('setItemCache', {slug, items: data});
} }
} catch (e) { } catch (e) {
console.error("Error loading items"); console.error("Error loading items");
@ -357,129 +370,124 @@ const store = createStore({
}, },
async searchEventItems({commit, getters, state}, query) { async searchEventItems({commit, getters, state}, query) {
const encoded_query = base64.encode(utf8.encode(query)); const encoded_query = base64.encode(utf8.encode(query));
const slug = getters.getEventSlug;
const {data, success} = await http.get(`/2/${getters.getEventSlug}/items/${encoded_query}/`, state.user.token); const {
if (data && success) data, success
commit('replaceLoadedItems', data); } = await getters.session.get(`/2/${slug}/items/${encoded_query}/`);
if (data && success) {
commit('setItems', {slug, items: data});
}
}, },
async loadBoxes({commit, state}) { async loadBoxes({commit, state, getters}) {
if (!state.user.token) return; if (!state.user.token) return;
if (state.fetchedData.boxes > Date.now() - 1000 * 60 * 60 * 24) return; if (state.fetchedData.boxes > Date.now() - 1000 * 60 * 60 * 24) return;
const {data, success} = await http.get('/2/boxes/', state.user.token); const {data, success} = await getters.session.get('/2/boxes/');
if (data && success) if (data && success) commit('replaceBoxes', data);
commit('replaceBoxes', data);
}, },
async createBox({commit, dispatch, state}, box) { async createBox({commit, dispatch, state, getters}, box) {
const {data, success} = await http.post('/2/boxes/', box, state.user.token); const {data, success} = await getters.session.post('/2/boxes/', box);
commit('replaceBoxes', data); commit('replaceBoxes', data);
dispatch('loadBoxes').then(() => { dispatch('loadBoxes').then(() => {
commit('closeAddBoxModal'); commit('closeAddBoxModal');
}); });
}, },
async deleteBox({commit, dispatch, state}, box_id) { async deleteBox({commit, dispatch, state, getters}, box_id) {
await http.delete(`/2/boxes/${box_id}/`, state.user.token); await getters.session.delete(`/2/boxes/${box_id}/`);
dispatch('loadBoxes'); dispatch('loadBoxes');
}, },
async updateItem({commit, getters, state}, item) { async updateItem({commit, getters, state}, item) {
const { const {
data, data, success
success } = await getters.session.put(`/2/${getters.getEventSlug}/item/${item.uid}/`, item);
} = await http.put(`/2/${getters.getEventSlug}/item/${item.uid}/`, item, state.user.token);
commit('updateItem', data); commit('updateItem', data);
}, },
async markItemReturned({commit, getters, state}, item) { async markItemReturned({commit, getters, state}, item) {
await http.patch(`/2/${getters.getEventSlug}/item/${item.uid}/`, {returned: true}, state.user.token); await getters.session.patch(`/2/${getters.getEventSlug}/item/${item.uid}/`, {returned: true},
state.user.token);
commit('removeItem', item); commit('removeItem', item);
}, },
async deleteItem({commit, getters, state}, item) { async deleteItem({commit, getters, state}, item) {
await http.delete(`/2/${getters.getEventSlug}/item/${item.uid}/`, item, state.user.token); await getters.session.delete(`/2/${getters.getEventSlug}/item/${item.uid}/`, item);
commit('removeItem', item); commit('removeItem', item);
}, },
async postItem({commit, getters, state}, item) { async postItem({commit, getters, state}, item) {
commit('updateLastUsed', {box: item.box, cid: item.cid}); commit('updateLastUsed', {box: item.box, cid: item.cid});
const {data, success} = await http.post(`/2/${getters.getEventSlug}/item/`, item, state.user.token); const {data, success} = await getters.session.post(`/2/${getters.getEventSlug}/item/`, item);
commit('appendItem', data); commit('appendItem', data);
}, },
async loadTickets({commit, state}) { async loadTickets({commit, state, getters}) {
if (!state.user.token) return; if (!state.user.token) return;
if (state.fetchedData.tickets > Date.now() - 1000 * 60 * 60 * 24) return; //if (state.fetchedData.tickets > Date.now() - 1000 * 60 * 60 * 24) return;
const {data, success} = await http.get('/2/tickets/', state.user.token); const {data, success} = await getters.session.get('/2/tickets/');
if (data && success) if (data && success) commit('replaceTickets', data);
commit('replaceTickets', data);
}, },
async searchEventTickets({commit, getters, state}, query) { async searchEventTickets({commit, getters, state}, query) {
const encoded_query = base64.encode(utf8.encode(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 {
if (data && success) data, success
commit('replaceTickets', data); } = await getters.session.get(`/2/${getters.getEventSlug}/tickets/${encoded_query}/`);
if (data && success) commit('replaceTickets', data);
}, },
async sendMail({commit, dispatch, state}, {id, message}) { async sendMail({commit, dispatch, state, getters}, {id, message}) {
const {data, success} = await http.post(`/2/tickets/${id}/reply/`, {message}, state.user.token); const {data, success} = await getters.session.post(`/2/tickets/${id}/reply/`, {message},
state.user.token);
if (data && success) { if (data && success) {
state.fetchedData.tickets = 0; state.fetchedData.tickets = 0;
await dispatch('loadTickets'); await dispatch('loadTickets');
} }
}, },
async postManualTicket({commit, dispatch, state}, {sender, message, title,}) { async postManualTicket({commit, dispatch, state, getters}, {sender, message, title,}) {
const {data, success} = await http.post(`/2/tickets/manual/`, { const {data, success} = await getters.session.post(`/2/tickets/manual/`, {
name: title, name: title, sender, body: message, recipient: 'mail@c3lf.de'
sender, });
body: message,
recipient: 'mail@c3lf.de'
}, state.user.token);
await dispatch('loadTickets'); await dispatch('loadTickets');
}, },
async postComment({commit, dispatch, state}, {id, message}) { async postComment({commit, dispatch, state, getters}, {id, message}) {
const {data, success} = await http.post(`/2/tickets/${id}/comment/`, {comment: message}, state.user.token); const {data, success} = await getters.session.post(`/2/tickets/${id}/comment/`, {comment: message});
if (data && success) { if (data && success) {
state.fetchedData.tickets = 0; state.fetchedData.tickets = 0;
await dispatch('loadTickets'); await dispatch('loadTickets');
} }
}, },
async loadUsers({commit, state}) { async loadUsers({commit, state, getters}) {
if (!state.user.token) return; if (!state.user.token) return;
if (state.fetchedData.users > Date.now() - 1000 * 60 * 60 * 24) return; if (state.fetchedData.users > Date.now() - 1000 * 60 * 60 * 24) return;
const {data, success} = await http.get('/2/users/', state.user.token); const {data, success} = await getters.session.get('/2/users/');
if (data && success) if (data && success) commit('replaceUsers', data);
commit('replaceUsers', data);
}, },
async loadGroups({commit, state}) { async loadGroups({commit, state, getters}) {
if (!state.user.token) return; if (!state.user.token) return;
if (state.fetchedData.groups > Date.now() - 1000 * 60 * 60 * 24) return; if (state.fetchedData.groups > Date.now() - 1000 * 60 * 60 * 24) return;
const {data, success} = await http.get('/2/groups/', state.user.token); const {data, success} = await getters.session.get('/2/groups/');
if (data && success) if (data && success) commit('replaceGroups', data);
commit('replaceGroups', data);
}, },
async updateTicket({commit, state}, ticket) { async updateTicket({commit, state, getters}, ticket) {
const {data, success} = await http.put(`/2/tickets/${ticket.id}/`, ticket, state.user.token); const {data, success} = await getters.session.put(`/2/tickets/${ticket.id}/`, ticket);
commit('updateTicket', data); commit('updateTicket', data);
}, },
async updateTicketPartial({commit, state}, {id, ...ticket}) { async updateTicketPartial({commit, state, getters}, {id, ...ticket}) {
const {data, success} = await http.patch(`/2/tickets/${id}/`, ticket, state.user.token); const {data, success} = await getters.session.patch(`/2/tickets/${id}/`, ticket);
commit('updateTicket', data); commit('updateTicket', data);
}, },
async fetchShippingVouchers({commit, state}) { async fetchShippingVouchers({commit, state, getters}) {
if (!state.user.token) return; if (!state.user.token) return;
if (state.fetchedData.shippingVouchers > Date.now() - 1000 * 60 * 60 * 24) return; if (state.fetchedData.shippingVouchers > Date.now() - 1000 * 60 * 60 * 24) return;
const {data, success} = await http.get('/2/shipping_vouchers/', state.user.token); const {data, success} = await getters.session.get('/2/shipping_vouchers/');
if (data && success) { if (data && success) {
commit('setShippingVouchers', data); commit('setShippingVouchers', data);
} }
}, },
async createShippingVoucher({dispatch, state}, code) { async createShippingVoucher({dispatch, state, getters}, code) {
const {data, success} = await http.post('/2/shipping_vouchers/', code, state.user.token); const {data, success} = await getters.session.post('/2/shipping_vouchers/', code);
if (data && success) { if (data && success) {
state.fetchedData.shippingVouchers = 0; state.fetchedData.shippingVouchers = 0;
dispatch('fetchShippingVouchers'); dispatch('fetchShippingVouchers');
} }
}, },
async claimShippingVoucher({dispatch, state}, {ticket, shipping_voucher_type}) { async claimShippingVoucher({dispatch, state, getters}, {ticket, shipping_voucher_type}) {
const id = state.shippingVouchers.filter(voucher => voucher.type === shipping_voucher_type && voucher.issue_thread === null)[0].id; const id = state.shippingVouchers.filter(voucher => voucher.type === shipping_voucher_type && voucher.issue_thread === null)[0].id;
const { const {data, success} = await getters.session.patch(`/2/shipping_vouchers/${id}/`, {issue_thread: ticket});
data,
success
} = await http.patch(`/2/shipping_vouchers/${id}/`, {issue_thread: ticket}, state.user.token);
if (data && success) { if (data && success) {
state.fetchedData.shippingVouchers = 0; state.fetchedData.shippingVouchers = 0;
state.fetchedData.tickets = 0; state.fetchedData.tickets = 0;
@ -487,58 +495,30 @@ const store = createStore({
} }
} }
}, },
plugins: [ plugins: [persistentStatePlugin({ // TODO change remember to some kind of enable field
persistentStatePlugin({ // TODO change remember to some kind of enable field
prefix: "lf_", prefix: "lf_",
debug: false, debug: false,
isLoadedKey: "persistent_loaded", isLoadedKey: "persistent_loaded",
state: [ state: ["remember", "user", "events", "lastUsed",]
"remember", }), sharedStatePlugin({
"user",
"events",
"lastUsed",
]
}),
sharedStatePlugin({
debug: false, debug: false,
isLoadedKey: "shared_loaded", isLoadedKey: "shared_loaded",
clearingMutation: "logout", clearingMutation: "logout",
afterInit: "afterSharedInit", afterInit: "afterSharedInit",
state: [ state: ["test", "state_options", "fetchedData", "loadedItems", "users", "groups", "loadedBoxes", "loadedTickets", "shippingVouchers",],
"test", watch: ["test", "state_options", "fetchedData", "loadedItems", "users", "groups", "loadedBoxes", "loadedTickets", "shippingVouchers",],
"state_options", mutations: [//"replaceTickets",
"fetchedData",
"tickets",
"users",
"groups",
"loadedBoxes",
"loadedItems",
"shippingVouchers",
],
watch: [
"test",
"state_options",
"fetchedData",
"tickets",
"users",
"groups",
"loadedBoxes",
"loadedItems",
"shippingVouchers",
],
mutations: [
//"replaceTickets",
],
}),
], ],
}),],
}); });
store.watch((state) => state.user, (user) => { store.watch((state) => state.user, (user) => {
if (store.getters.isLoggedIn) { if (store.getters.isLoggedIn) {
if (router.currentRoute.value.name === 'login' && router.currentRoute.value.query.redirect) if (router.currentRoute.value.name === 'login' && router.currentRoute.value.query.redirect) {
router.push(router.currentRoute.value.query.redirect); router.push(router.currentRoute.value.query.redirect);
else if (router.currentRoute.value.name === 'login') } else if (router.currentRoute.value.name === 'login') {
router.push('/'); router.push('/');
}
} else { } else {
if (router.currentRoute.value.name !== 'login') { if (router.currentRoute.value.name !== 'login') {
router.push({ router.push({

View file

@ -100,4 +100,12 @@ const http = {
} }
} }
export {ticketStateColorLookup, ticketStateIconLookup, http}; const http_session = token => ({
get: async (url) => await http.get(url, token),
post: async (url, data) => await http.post(url, data, token),
put: async (url, data) => await http.put(url, data, token),
patch: async (url, data) => await http.patch(url, data, token),
delete: async (url) => await http.delete(url, token),
});
export {ticketStateColorLookup, ticketStateIconLookup, http, http_session};

View file

@ -1,5 +1,5 @@
<template> <template>
<AsyncLoader :loaded="loadedItems.length > 0"> <AsyncLoader :loaded="isItemsLoaded">
<div class="container-fluid px-xl-5 mt-3"> <div class="container-fluid px-xl-5 mt-3">
<Modal title="Edit Item" v-if="editingItem" @close="closeEditingModal()"> <Modal title="Edit Item" v-if="editingItem" @close="closeEditingModal()">
<template #body> <template #body>
@ -18,7 +18,7 @@
<div class="col-xl-8 offset-xl-2"> <div class="col-xl-8 offset-xl-2">
<Table <Table
:columns="['uid', 'description', 'box']" :columns="['uid', 'description', 'box']"
:items="loadedItems" :items="getEventItems"
:keyName="'uid'" :keyName="'uid'"
@itemActivated="openLightboxModalWith($event)" @itemActivated="openLightboxModalWith($event)"
> >
@ -44,7 +44,7 @@
<Cards <Cards
v-if="layout === 'cards'" v-if="layout === 'cards'"
:columns="['uid', 'description', 'box']" :columns="['uid', 'description', 'box']"
:items="loadedItems" :items="getEventItems"
:keyName="'uid'" :keyName="'uid'"
v-slot="{ item }" v-slot="{ item }"
@itemActivated="openLightboxModalWith($event)" @itemActivated="openLightboxModalWith($event)"
@ -97,8 +97,8 @@ export default {
}), }),
components: {AsyncLoader, AuthenticatedImage, Lightbox, Table, Cards, Modal, EditItem}, components: {AsyncLoader, AuthenticatedImage, Lightbox, Table, Cards, Modal, EditItem},
computed: { computed: {
...mapState(['loadedItems']), ...mapState([]),
...mapGetters(['layout']), ...mapGetters(['getEventItems', 'isItemsLoaded', 'layout']),
}, },
methods: { methods: {
...mapActions(['deleteItem', 'markItemReturned', 'loadEventItems', 'updateItem', 'scheduleAfterInit']), ...mapActions(['deleteItem', 'markItemReturned', 'loadEventItems', 'updateItem', 'scheduleAfterInit']),

View file

@ -80,11 +80,11 @@ export default {
} }
}, },
computed: { computed: {
...mapState(['tickets', 'state_options', 'users']), ...mapState(['state_options', 'users']),
...mapGetters(['availableShippingVoucherTypes']), ...mapGetters(['availableShippingVoucherTypes', 'getAllTickets', 'route']),
ticket() { ticket() {
const id = parseInt(this.$route.params.id) const id = parseInt(this.route.params.id)
const ret = this.tickets.find(ticket => ticket.id === id); const ret = this.getAllTickets.find(ticket => ticket.id === id);
return ret ? ret : {}; return ret ? ret : {};
}, },
shippingEmail() { shippingEmail() {
@ -124,11 +124,12 @@ export default {
}, },
}, },
mounted() { mounted() {
this.scheduleAfterInit(() => [Promise.all([this.fetchTicketStates(), this.loadTickets(), this.loadUsers(), this.fetchShippingVouchers()]).then(()=>{ this.scheduleAfterInit(() => [Promise.all([this.fetchTicketStates(), this.loadTickets(), this.loadUsers(), this.fetchShippingVouchers()]).then(() => {
if (this.ticket.state == "pending_new"){ if (this.ticket.state == "pending_new") {
this.selected_state = "pending_open"; this.selected_state = "pending_open";
this.changeTicketStatus(this.ticket) this.changeTicketStatus(this.ticket)
}; }
;
this.selected_state = this.ticket.state; this.selected_state = this.ticket.state;
this.selected_assignee = this.ticket.assigned_to this.selected_assignee = this.ticket.assigned_to
})]); })]);

View file

@ -1,11 +1,12 @@
<template> <template>
<AsyncLoader :loaded="tickets.length > 0"> <AsyncLoader :loaded="isTicketsLoaded">
<div class="container-fluid px-xl-5 mt-3"> <div class="container-fluid px-xl-5 mt-3">
<div class="row"> <div class="row">
<div class="col-xl-8 offset-xl-2"> <div class="col-xl-8 offset-xl-2">
<Table <Table
:columns="['id', 'name', 'state', 'last_activity', 'assigned_to', 'actions', 'actions2']" :columns="['id', 'name', 'state', 'last_activity', 'assigned_to',
:items="tickets.map(formatTicket)" ...(getEventSlug==='all'?['event']:[])]"
:items="getEventTickets.map(formatTicket)"
:keyName="'id'" :keyName="'id'"
v-if="layout === 'table'" v-if="layout === 'table'"
> >
@ -21,8 +22,9 @@
</Table> </Table>
</div> </div>
</div> </div>
<CollapsableCards v-if="layout === 'tasks'" :items="tickets" <CollapsableCards v-if="layout === 'tasks'" :items="getEventTickets"
:columns="['id', 'name', 'last_activity', 'assigned_to']" :columns="['id', 'name', 'last_activity', 'assigned_to',
...(getEventSlug==='all'?['event']:[])]"
:keyName="'state'" :sections="['pending_new', 'pending_open','pending_shipping', :keyName="'state'" :sections="['pending_new', 'pending_open','pending_shipping',
'pending_physical_confirmation','pending_return','pending_postponed'].map(stateInfo)"> 'pending_physical_confirmation','pending_return','pending_postponed'].map(stateInfo)">
<template #section_header="{index, section, count}"> <template #section_header="{index, section, count}">
@ -34,6 +36,7 @@
<td>{{ item.name }}</td> <td>{{ item.name }}</td>
<td>{{ item.last_activity }}</td> <td>{{ item.last_activity }}</td>
<td>{{ item.assigned_to }}</td> <td>{{ item.assigned_to }}</td>
<td v-if="getEventSlug==='all'">{{ item.event }}</td>
<td> <td>
<div class="btn-group"> <div class="btn-group">
<a class="btn btn-primary" :href="'/'+ getEventSlug + '/ticket/' + item.id" title="view" <a class="btn btn-primary" :href="'/'+ getEventSlug + '/ticket/' + item.id" title="view"
@ -64,8 +67,7 @@ export default {
name: 'Tickets', name: 'Tickets',
components: {AsyncLoader, Lightbox, Table, Cards, Modal, EditItem, CollapsableCards}, components: {AsyncLoader, Lightbox, Table, Cards, Modal, EditItem, CollapsableCards},
computed: { computed: {
...mapState(['tickets']), ...mapGetters(['getEventTickets', 'isTicketsLoaded', 'stateInfo', 'getEventSlug', 'layout']),
...mapGetters(['stateInfo', 'getEventSlug', 'layout']),
}, },
methods: { methods: {
...mapActions(['loadTickets', 'fetchTicketStates', 'scheduleAfterInit']), ...mapActions(['loadTickets', 'fetchTicketStates', 'scheduleAfterInit']),
@ -79,7 +81,8 @@ export default {
state: this.stateInfo(ticket.state).text, state: this.stateInfo(ticket.state).text,
stateColor: this.stateInfo(ticket.state).color, stateColor: this.stateInfo(ticket.state).color,
last_activity: ticket.last_activity, last_activity: ticket.last_activity,
assigned_to: ticket.assigned_to assigned_to: ticket.assigned_to,
event: ticket.event
}; };
} }
}, },