Compare commits

..

7 commits

Author SHA1 Message Date
4338d6b03a add container to testdata.py
Some checks failed
/ deploy (push) Failing after 3m12s
/ test (push) Successful in 2m48s
2024-12-27 21:37:59 +01:00
f6455638d7 add container to testdata.py
All checks were successful
/ test (push) Successful in 2m53s
/ deploy (push) Successful in 4m31s
2024-12-27 20:31:04 +01:00
3068597f3d metrics: also export ticket state
All checks were successful
/ test (push) Successful in 2m47s
/ deploy (push) Successful in 4m27s
2024-12-27 15:08:45 +01:00
7208aede8d Merge pull request 'metrics: Add ticket counters' (#110) from lagertonne/add-ticket-metrics into testing
All checks were successful
/ test (push) Successful in 2m39s
/ deploy (push) Successful in 4m32s
2024-12-27 08:41:55 +00:00
c569d29d8a metrics: Add ticket counters
All checks were successful
/ test (push) Successful in 2m41s
/ test (pull_request) Successful in 2m34s
2024-12-27 09:34:54 +01:00
a2f7682c6f Merge pull request 'metrics: Fix bug when running migrate on empty db' (#109) from lagertonne/fix-db-creation-with-metrics into testing
All checks were successful
/ test (push) Successful in 2m52s
/ deploy (push) Successful in 4m37s
2024-12-26 19:52:28 +00:00
e234385802 metrics: Fix bug when running migrate on empty db
All checks were successful
/ test (push) Successful in 2m49s
/ test (pull_request) Successful in 2m47s
2024-12-26 19:49:12 +00:00
15 changed files with 89 additions and 625 deletions

View file

@ -2,6 +2,10 @@ from django.apps import apps
from prometheus_client.core import CounterMetricFamily, REGISTRY from prometheus_client.core import CounterMetricFamily, REGISTRY
from django.db.models import Case, Value, When, BooleanField, Count from django.db.models import Case, Value, When, BooleanField, Count
from inventory.models import Item from inventory.models import Item
from tickets.models import IssueThread
from itertools import groupby
class ItemCountCollector(object): class ItemCountCollector(object):
@ -15,16 +19,16 @@ class ItemCountCollector(object):
queryset = ( queryset = (
Item.all_objects Item.all_objects
.annotate( .annotate(
returned=Case( returned=Case(
When(returned_at__isnull=True, then=Value(False)), When(returned_at__isnull=True, then=Value(False)),
default=Value(True), default=Value(True),
output_field=BooleanField() output_field=BooleanField()
)
) )
.values('event__slug', 'returned', 'event_id') )
.annotate(amount=Count('id')) .values('event__slug', 'returned', 'event_id')
.order_by('event__slug', 'returned') # Optional: order by slug and returned .annotate(amount=Count('id'))
.order_by('event__slug', 'returned') # Optional: order by slug and returned
) )
for e in queryset: for e in queryset:
@ -32,4 +36,25 @@ class ItemCountCollector(object):
yield counter yield counter
REGISTRY.register(ItemCountCollector())
class TicketCountCollector(object):
def collect(self):
counter = CounterMetricFamily("c3lf_ticket_count", "Current number of tickets", labels=['event', 'event_id'])
yield counter
if not apps.models_ready or not apps.apps_ready:
return
g = groupby(IssueThread.objects.all().prefetch_related('event', 'state_changes'),
lambda x: (x.event.slug, x.state))
for (event, state), v in g:
counter.add_metric([event.lower(), state.lower()], len(list(v)))
yield counter
REGISTRY.register(ItemCountCollector())
REGISTRY.register(TicketCountCollector())

View file

@ -48,15 +48,9 @@ def unescape_and_decode_base64(s):
return decoded return decoded
def unescape_simplified_quoted_printable(s, encoding='utf-8'): def unescape_simplified_quoted_printable(s):
import quopri import quopri
return quopri.decodestring(s).decode(encoding) return quopri.decodestring(s).decode('utf-8')
def ascii_strip(s):
if not s:
return None
return ''.join([c for c in str(s) if 128 > ord(c) > 31])
def collect_references(issue_thread): def collect_references(issue_thread):
@ -122,19 +116,6 @@ def find_target_event(address):
return None return None
def decode_email_segment(segment, charset, transfer_encoding):
decode_as = 'utf-8'
if charset == 'windows-1251':
decode_as = 'cp1251'
elif charset == 'iso-8859-1':
decode_as = 'latin1'
segment = unescape_and_decode_quoted_printable(segment)
segment = unescape_and_decode_base64(segment)
if transfer_encoding == 'quoted-printable':
segment = unescape_simplified_quoted_printable(segment, decode_as)
return segment
def parse_email_body(raw, log=None): def parse_email_body(raw, log=None):
import email import email
from hashlib import sha256 from hashlib import sha256
@ -146,24 +127,21 @@ def parse_email_body(raw, log=None):
if parsed.is_multipart(): if parsed.is_multipart():
for part in parsed.walk(): for part in parsed.walk():
ctype = part.get_content_type() ctype = part.get_content_type()
charset = part.get_content_charset()
cdispo = str(part.get('Content-Disposition')) cdispo = str(part.get('Content-Disposition'))
if ctype == 'multipart/mixed':
log.debug("Ignoring Multipart %s %s", ctype, cdispo)
# skip any text/plain (txt) attachments # skip any text/plain (txt) attachments
elif ctype == 'text/plain' and 'attachment' not in cdispo: if ctype == 'text/plain' and 'attachment' not in cdispo:
segment = part.get_payload() segment = part.get_payload()
if not segment: if not segment:
continue continue
segment = decode_email_segment(segment, charset, part.get('Content-Transfer-Encoding')) segment = unescape_and_decode_quoted_printable(segment)
segment = unescape_and_decode_base64(segment)
if part.get('Content-Transfer-Encoding') == 'quoted-printable':
segment = unescape_simplified_quoted_printable(segment)
log.debug(segment) log.debug(segment)
body = body + segment body = body + segment
elif 'attachment' in cdispo or 'inline' in cdispo: elif 'attachment' in cdispo or 'inline' in cdispo:
content = part.get_payload(decode=True) file = ContentFile(part.get_payload(decode=True))
if content is None:
continue
file = ContentFile(content)
chash = sha256(file.read()).hexdigest() chash = sha256(file.read()).hexdigest()
name = part.get_filename() name = part.get_filename()
if name is None: if name is None:
@ -189,7 +167,10 @@ def parse_email_body(raw, log=None):
else: else:
log.warning("Unknown content type %s", parsed.get_content_type()) log.warning("Unknown content type %s", parsed.get_content_type())
body = "Unknown content type" body = "Unknown content type"
body = decode_email_segment(body, parsed.get_content_charset(), parsed.get('Content-Transfer-Encoding')) body = unescape_and_decode_quoted_printable(body)
body = unescape_and_decode_base64(body)
if parsed.get('Content-Transfer-Encoding') == 'quoted-printable':
body = unescape_simplified_quoted_printable(body)
log.debug(body) log.debug(body)
return parsed, body, attachments return parsed, body, attachments
@ -201,8 +182,8 @@ def receive_email(envelope, log=None):
header_from = parsed.get('From') header_from = parsed.get('From')
header_to = parsed.get('To') header_to = parsed.get('To')
header_in_reply_to = ascii_strip(parsed.get('In-Reply-To')) header_in_reply_to = parsed.get('In-Reply-To')
header_message_id = ascii_strip(parsed.get('Message-ID')) header_message_id = parsed.get('Message-ID')
if match(r'^([a-zA-Z ]*<)?MAILER-DAEMON@', header_from) and envelope.mail_from.strip("<>") == "": if match(r'^([a-zA-Z ]*<)?MAILER-DAEMON@', header_from) and envelope.mail_from.strip("<>") == "":
log.warning("Ignoring mailer daemon") log.warning("Ignoring mailer daemon")
@ -214,7 +195,7 @@ def receive_email(envelope, log=None):
recipient = envelope.rcpt_tos[0].lower() if envelope.rcpt_tos else header_to.lower() recipient = envelope.rcpt_tos[0].lower() if envelope.rcpt_tos else header_to.lower()
sender = envelope.mail_from if envelope.mail_from else header_from sender = envelope.mail_from if envelope.mail_from else header_from
subject = ascii_strip(parsed.get('Subject')) subject = parsed.get('Subject')
if not subject: if not subject:
subject = "No subject" subject = "No subject"
subject = unescape_and_decode_quoted_printable(subject) subject = unescape_and_decode_quoted_printable(subject)
@ -228,8 +209,7 @@ def receive_email(envelope, log=None):
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, name=random_filename), in_reply_to=header_in_reply_to, raw_file=ContentFile(envelope.content, name=random_filename), event=target_event,
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)
@ -252,7 +232,7 @@ do not create a new request.
Your c3lf (Cloakroom + Lost&Found) Team'''.format(active_issue_thread.short_uuid()) Your c3lf (Cloakroom + Lost&Found) Team'''.format(active_issue_thread.short_uuid())
reply_email = Email.objects.create( reply_email = Email.objects.create(
sender=recipient, recipient=sender, body=body, subject=ascii_strip(subject), sender=recipient, recipient=sender, body=body, subject=subject,
in_reply_to=header_message_id, event=target_event, issue_thread=active_issue_thread) in_reply_to=header_message_id, event=target_event, issue_thread=active_issue_thread)
reply = make_reply(reply_email, references, event=target_event.slug if target_event else None) reply = make_reply(reply_email, references, event=target_event.slug if target_event else None)
else: else:

View file

@ -142,7 +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) 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
@ -163,7 +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) 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
@ -184,7 +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) 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(
@ -232,7 +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) 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(
@ -285,7 +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) 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(
@ -887,59 +887,6 @@ hello \xe4\xf6\xfc'''
self.assertEqual(1, len(states)) self.assertEqual(1, len(states))
self.assertEqual('pending_new', states[0].state) self.assertEqual('pending_new', states[0].state)
def test_mail_windows_1252(self):
from aiosmtpd.smtp import Envelope
from asgiref.sync import async_to_sync
import aiosmtplib
aiosmtplib.send = make_mocked_coro()
handler = LMTPHandler()
server = mock.Mock()
session = mock.Mock()
envelope = Envelope()
envelope.mail_from = 'test1@test'
envelope.rcpt_tos = ['test2@test']
envelope.content = b'''Subject: test
From: test1@test
To: test2@test
Message-ID: <1@test>
Content-Type: text/html; charset=windows-1252
Content-Transfer-Encoding: quoted-printable
=0D=0Ahello='''
result = async_to_sync(handler.handle_DATA)(server, session, envelope)
self.assertEqual('250 Message accepted for delivery', result)
self.assertEqual(2, len(Email.objects.all()))
self.assertEqual(1, len(IssueThread.objects.all()))
aiosmtplib.send.assert_called_once()
self.assertEqual('test', Email.objects.all()[0].subject)
self.assertEqual('test1@test', Email.objects.all()[0].sender)
self.assertEqual('test2@test', Email.objects.all()[0].recipient)
self.assertEqual('\r\nhello', Email.objects.all()[0].body)
self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[0].issue_thread)
self.assertEqual('<1@test>', Email.objects.all()[0].reference)
self.assertEqual(None, Email.objects.all()[0].in_reply_to)
self.assertEqual(expected_auto_reply_subject.format('test', IssueThread.objects.all()[0].short_uuid()),
Email.objects.all()[1].subject)
self.assertEqual('test2@test', Email.objects.all()[1].sender)
self.assertEqual('test1@test', Email.objects.all()[1].recipient)
self.assertEqual(expected_auto_reply.format(IssueThread.objects.all()[0].short_uuid()),
Email.objects.all()[1].body)
self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[1].issue_thread)
self.assertTrue(Email.objects.all()[1].reference.startswith("<"))
self.assertTrue(Email.objects.all()[1].reference.endswith("@localhost>"))
self.assertEqual("<1@test>", Email.objects.all()[1].in_reply_to)
self.assertEqual('test', IssueThread.objects.all()[0].name)
self.assertEqual('pending_new', IssueThread.objects.all()[0].state)
self.assertEqual(None, IssueThread.objects.all()[0].assigned_to)
states = StateChange.objects.filter(issue_thread=IssueThread.objects.all()[0])
self.assertEqual(1, len(states))
self.assertEqual('pending_new', states[0].state)
def test_mail_quoted_printable_transfer_encoding(self): def test_mail_quoted_printable_transfer_encoding(self):
from aiosmtpd.smtp import Envelope from aiosmtpd.smtp import Envelope
from asgiref.sync import async_to_sync from asgiref.sync import async_to_sync
@ -992,146 +939,3 @@ hello =C3=A4=C3=B6=C3=BC'''
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)
def test_text_with_attachment2(self):
from aiosmtpd.smtp import Envelope
from asgiref.sync import async_to_sync
import aiosmtplib
aiosmtplib.send = make_mocked_coro()
handler = LMTPHandler()
server = mock.Mock()
session = mock.Mock()
envelope = Envelope()
envelope.mail_from = 'test1@test'
envelope.rcpt_tos = ['test2@test']
envelope.content = b'''Subject: test
From: test1@test
To: test2@test
Message-ID: <1@test>
Content-Type: multipart/mixed; boundary="abc"
Content-Disposition: inline
Content-Transfer-Encoding: 8bit
--abc
Content-Type: text/plain; charset=utf-8
Content-Disposition: inline
Content-Transfer-Encoding: 8bit
test1
--abc
Content-Type: image/jpeg; name="test.jpg"
Content-Disposition: attachment; filename="test.jpg"
Content-Transfer-Encoding: base64
Content-ID: <1>
X-Attachment-Id: 1
dGVzdGltYWdl
--abc--'''
result = async_to_sync(handler.handle_DATA)(server, session, envelope)
self.assertEqual(result, '250 Message accepted for delivery')
self.assertEqual(len(Email.objects.all()), 2)
self.assertEqual(len(IssueThread.objects.all()), 1)
aiosmtplib.send.assert_called_once()
self.assertEqual('test', Email.objects.all()[0].subject)
self.assertEqual('test1@test', Email.objects.all()[0].sender)
self.assertEqual('test2@test', Email.objects.all()[0].recipient)
self.assertEqual('test1\n', Email.objects.all()[0].body)
self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[0].issue_thread)
self.assertEqual('<1@test>', Email.objects.all()[0].reference)
self.assertEqual(None, Email.objects.all()[0].in_reply_to)
self.assertEqual(expected_auto_reply_subject.format('test', IssueThread.objects.all()[0].short_uuid()),
Email.objects.all()[1].subject)
self.assertEqual('test2@test', Email.objects.all()[1].sender)
self.assertEqual('test1@test', Email.objects.all()[1].recipient)
self.assertEqual(expected_auto_reply.format(IssueThread.objects.all()[0].short_uuid()),
Email.objects.all()[1].body)
self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[1].issue_thread)
self.assertTrue(Email.objects.all()[1].reference.startswith("<"))
self.assertTrue(Email.objects.all()[1].reference.endswith("@localhost>"))
self.assertEqual("<1@test>", Email.objects.all()[1].in_reply_to)
self.assertEqual('test', IssueThread.objects.all()[0].name)
self.assertEqual('pending_new', IssueThread.objects.all()[0].state)
self.assertEqual(None, IssueThread.objects.all()[0].assigned_to)
states = StateChange.objects.filter(issue_thread=IssueThread.objects.all()[0])
self.assertEqual(1, len(states))
self.assertEqual('pending_new', states[0].state)
self.assertEqual(1, len(EmailAttachment.objects.all()))
self.assertEqual(1, EmailAttachment.objects.all()[0].id)
self.assertEqual('image/jpeg', EmailAttachment.objects.all()[0].mime_type)
self.assertEqual('test.jpg', EmailAttachment.objects.all()[0].name)
file_content = EmailAttachment.objects.all()[0].file.read()
self.assertEqual(b'testimage', file_content)
def test_text_non_utf8_in_multipart(self):
from aiosmtpd.smtp import Envelope
from asgiref.sync import async_to_sync
import aiosmtplib
aiosmtplib.send = make_mocked_coro()
handler = LMTPHandler()
server = mock.Mock()
session = mock.Mock()
envelope = Envelope()
envelope.mail_from = 'test1@test'
envelope.rcpt_tos = ['test2@test']
envelope.content = b'''Subject: test
From: test1@test
To: test2@test
Message-ID: <1@test>
Content-Type: multipart/alternative; boundary="abc"
--abc
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 8bit
test1
--abc
Content-Type: text/plain; charset=iso-8859-1
Content-Transfer-Encoding: quoted-printable
hello =E4
--abc
Content-Type: text/plain; charset=windows-1252
Content-Transfer-Encoding: quoted-printable
=0D=0Ahello
--abc--'''
result = async_to_sync(handler.handle_DATA)(server, session, envelope)
self.assertEqual(result, '250 Message accepted for delivery')
self.assertEqual(len(Email.objects.all()), 2)
self.assertEqual(len(IssueThread.objects.all()), 1)
aiosmtplib.send.assert_called_once()
self.assertEqual('test', Email.objects.all()[0].subject)
self.assertEqual('test1@test', Email.objects.all()[0].sender)
self.assertEqual('test2@test', Email.objects.all()[0].recipient)
self.assertEqual('test1\nhello ä\n\r\nhello\n', Email.objects.all()[0].body)
self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[0].issue_thread)
self.assertEqual('<1@test>', Email.objects.all()[0].reference)
self.assertEqual(None, Email.objects.all()[0].in_reply_to)
self.assertEqual(expected_auto_reply_subject.format('test', IssueThread.objects.all()[0].short_uuid()),
Email.objects.all()[1].subject)
self.assertEqual('test2@test', Email.objects.all()[1].sender)
self.assertEqual('test1@test', Email.objects.all()[1].recipient)
self.assertEqual(expected_auto_reply.format(IssueThread.objects.all()[0].short_uuid()),
Email.objects.all()[1].body)
self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[1].issue_thread)
self.assertTrue(Email.objects.all()[1].reference.startswith("<"))
self.assertTrue(Email.objects.all()[1].reference.endswith("@localhost>"))
self.assertEqual("<1@test>", Email.objects.all()[1].in_reply_to)
self.assertEqual('test', IssueThread.objects.all()[0].name)
self.assertEqual('pending_new', IssueThread.objects.all()[0].state)
self.assertEqual(None, IssueThread.objects.all()[0].assigned_to)
states = StateChange.objects.filter(issue_thread=IssueThread.objects.all()[0])
self.assertEqual(1, len(states))
self.assertEqual('pending_new', states[0].state)

View file

@ -143,36 +143,12 @@ def add_comment(request, pk):
def filter_issues(issues, query): def filter_issues(issues, query):
query_tokens = query.lower().split(' ') query_tokens = query.split(' ')
for issue in issues: for issue in issues:
value = 0 value = 0
if issue.short_uuid() in query:
value += 10
if "T#" + str(issue.id) in query:
value += 10
elif "#" + str(issue.id) in query:
value += 9
for item in issue.related_items:
if "I#" + str(item.id) in query:
value += 8
elif "#" + str(item.id) in query:
value += 5
for token in query_tokens:
if token in item.description.lower():
value += 1
for token in query_tokens: for token in query_tokens:
if token in issue.name.lower(): if token in issue.description:
value += 1 value += 1
for comment in issue.comments.all():
for token in query_tokens:
if token in comment.comment.lower():
value += 1
for email in issue.emails.all():
for token in query_tokens:
if token in email.subject.lower():
value += 1
if token in email.body.lower():
value += 1
if value > 0: if value > 0:
yield {'search_score': value, 'issue': issue} yield {'search_score': value, 'issue': issue}
@ -184,10 +160,7 @@ def search_issues(request, event_slug, query):
event = Event.objects.get(slug=event_slug) event = Event.objects.get(slug=event_slug)
if not request.user.has_event_perm(event, 'view_issuethread'): if not request.user.has_event_perm(event, 'view_issuethread'):
return Response(status=403) return Response(status=403)
serializer = IssueSerializer() items = filter_issues(IssueThread.objects.filter(event=event), b64decode(query).decode('utf-8'))
queryset = IssueThread.objects.filter(event=event)
items = filter_issues(queryset.prefetch_related(*serializer.Meta.prefetch_related_fields),
b64decode(query).decode('utf-8'))
return Response(SearchResultSerializer(items, many=True).data) return Response(SearchResultSerializer(items, many=True).data)
except Event.DoesNotExist: except Event.DoesNotExist:
return Response(status=404) return Response(status=404)

View file

@ -139,10 +139,10 @@ class IssueSerializer(BasicIssueSerializer):
class SearchResultSerializer(serializers.Serializer): class SearchResultSerializer(serializers.Serializer):
search_score = serializers.IntegerField() search_score = serializers.IntegerField()
issue = IssueSerializer() item = IssueSerializer()
def to_representation(self, instance): def to_representation(self, instance):
return {**IssueSerializer(instance['issue']).data, 'search_score': instance['search_score']} return {**IssueSerializer(instance['item']).data, 'search_score': instance['search_score']}
class Meta: class Meta:
model = IssueThread model = IssueThread

View file

@ -383,85 +383,15 @@ class IssueSearchTest(TestCase):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.event = Event.objects.create(slug='EVENT', name='Event')
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='EVENT', name='Event')
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]})
def test_search_empty_result(self): def test_search(self):
search_query = b64encode(b'abc').decode('utf-8') search_query = b64encode(b'abc').decode('utf-8')
response = self.client.get(f'/api/2/{self.event.slug}/tickets/{search_query}/') response = self.client.get(f'/api/2/{self.event.slug}/tickets/{search_query}/')
self.assertEqual(200, response.status_code) self.assertEqual(200, response.status_code)
self.assertEqual([], response.json()) self.assertEqual([], response.json())
def test_search(self):
now = datetime.now()
issue = IssueThread.objects.create(
name="test issue Abc",
event=self.event,
)
mail1 = Email.objects.create(
subject='test',
body='test aBc',
sender='test',
recipient='test',
issue_thread=issue,
timestamp=now,
)
mail2 = Email.objects.create(
subject='test',
body='test',
sender='test',
recipient='test',
issue_thread=issue,
in_reply_to=mail1.reference,
timestamp=now + timedelta(seconds=2),
)
assignment = Assignment.objects.create(
issue_thread=issue,
assigned_to=self.user,
timestamp=now + timedelta(seconds=3),
)
comment = Comment.objects.create(
issue_thread=issue,
comment="test deF",
timestamp=now + timedelta(seconds=4),
)
match = ItemRelation.objects.create(
issue_thread=issue,
item=self.item,
timestamp=now + timedelta(seconds=5),
)
search_query = b64encode(b'abC').decode('utf-8')
response = self.client.get(f'/api/2/{self.event.slug}/tickets/{search_query}/')
self.assertEqual(200, response.status_code)
self.assertEqual(1, len(response.json()))
self.assertEqual(issue.id, response.json()[0]['id'])
score2 = response.json()[0]['search_score']
search_query = b64encode(b'dEf').decode('utf-8')
response = self.client.get(f'/api/2/{self.event.slug}/tickets/{search_query}/')
self.assertEqual(200, response.status_code)
self.assertEqual(1, len(response.json()))
self.assertEqual(issue.id, response.json()[0]['id'])
score1 = response.json()[0]['search_score']
search_query = b64encode(b'ghi').decode('utf-8')
response = self.client.get(f'/api/2/{self.event.slug}/tickets/{search_query}/')
self.assertEqual(200, response.status_code)
self.assertEqual(0, len(response.json()))
search_query = b64encode(b'Abc def').decode('utf-8')
response = self.client.get(f'/api/2/{self.event.slug}/tickets/{search_query}/')
self.assertEqual(200, response.status_code)
self.assertEqual(1, len(response.json()))
self.assertEqual(issue.id, response.json()[0]['id'])
score3 = response.json()[0]['search_score']
self.assertGreater(score3, score2)
self.assertGreater(score2, score1)
self.assertGreater(score1, 0)

View file

@ -7,6 +7,7 @@ services:
environment: environment:
- HTTP_HOST=core - HTTP_HOST=core
- DB_FILE=dev.db - DB_FILE=dev.db
- DEBUG_MODE_ACTIVE=yup
volumes: volumes:
- ../../core:/code - ../../core:/code
- ../testdata.py:/code/testdata.py - ../testdata.py:/code/testdata.py

View file

@ -70,6 +70,15 @@ def setup():
issue_thread=issue_thread, issue_thread=issue_thread,
)[0] )[0]
from inventory.models import Container
Container.objects.get_or_create(
id=1,
name='testcontainer'
)[0]
def main(): def main():
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings")

View file

@ -30,6 +30,7 @@ services:
- DB_USER=system3 - DB_USER=system3
- DB_PASSWORD=system3 - DB_PASSWORD=system3
- MAIL_DOMAIN=mail:1025 - MAIL_DOMAIN=mail:1025
- DEBUG_MODE_ACTIVE=certainly
volumes: volumes:
- ../../core:/code - ../../core:/code
- ../testdata.py:/code/testdata.py - ../testdata.py:/code/testdata.py

View file

@ -115,10 +115,10 @@ export default {
this.$router.push(link); this.$router.push(link);
}, },
isItemView() { isItemView() {
return this.getActiveView === 'items' || this.getActiveView === 'item' || this.getActiveView === 'item_search'; return this.getActiveView === 'items' || this.getActiveView === 'item';
}, },
isTicketView() { isTicketView() {
return this.getActiveView === 'tickets' || this.getActiveView === 'ticket' || this.getActiveView === 'ticket_search'; return this.getActiveView === 'tickets' || this.getActiveView === 'ticket';
}, },
setLayout(layout) { setLayout(layout) {
if (this.route.query.layout === layout) if (this.route.query.layout === layout)

View file

@ -12,7 +12,6 @@
<script> <script>
import {mapActions, mapGetters} from "vuex"; import {mapActions, mapGetters} from "vuex";
import router from "@/router";
export default { export default {
name: 'SearchBox', name: 'SearchBox',
@ -22,34 +21,21 @@ export default {
} }
}, },
computed: { computed: {
...mapGetters(['getActiveView', 'route']), ...mapGetters(['getActiveView'])
},
watch: {
route() {
this.search_query = this.route.params.search || '';
}
}, },
methods: { methods: {
...mapActions(['searchEventItems', 'searchEventTickets']), ...mapActions(['searchEventItems', 'searchEventTickets']),
isItemView() { isItemView() {
return this.getActiveView === 'items' || this.getActiveView === 'item' || this.getActiveView === 'item_search'; return this.getActiveView === 'items' || this.getActiveView === 'item';
}, },
isTicketView() { isTicketView() {
return this.getActiveView === 'tickets' || this.getActiveView === 'ticket' || this.getActiveView === 'ticket_search'; return this.getActiveView === 'tickets' || this.getActiveView === 'ticket';
}, },
dispatchSearch() { dispatchSearch() {
if (this.isItemView()) { if (this.isItemView()) {
router.push({ this.searchEventItems(this.search_query);
name: "item_search",
query: this.route.query,
params: {...this.route.params, search: this.search_query}
});
} else if (this.isTicketView()) { } else if (this.isTicketView()) {
router.push({ this.searchEventTickets(this.search_query);
name: "ticket_search",
query: this.route.query,
params: {...this.route.params, search: this.search_query}
});
} }
} }
} }

View file

@ -2,7 +2,6 @@ import {createRouter, createWebHistory} from 'vue-router'
import store from '@/store'; import store from '@/store';
import Item from "@/views/Item.vue"; import Item from "@/views/Item.vue";
import ItemSearch from "@/views/ItemSearch.vue";
import Items from '@/views/Items'; import Items from '@/views/Items';
import Boxes from '@/views/Boxes'; import Boxes from '@/views/Boxes';
import Files from '@/views/Files'; import Files from '@/views/Files';
@ -11,7 +10,6 @@ import Login from '@/views/Login.vue';
import Register from '@/views/Register.vue'; import Register from '@/views/Register.vue';
import Dashboard from "@/views/admin/Dashboard.vue"; import Dashboard from "@/views/admin/Dashboard.vue";
import Tickets from "@/views/Tickets.vue"; import Tickets from "@/views/Tickets.vue";
import TicketSearch from "@/views/TicketSearch.vue";
import Ticket from "@/views/Ticket.vue"; import Ticket from "@/views/Ticket.vue";
import Admin from "@/views/admin/Admin.vue"; import Admin from "@/views/admin/Admin.vue";
import Empty from "@/views/Empty.vue"; import Empty from "@/views/Empty.vue";
@ -29,10 +27,6 @@ const routes = [
path: '/:event/items/', name: 'items', component: Items, meta: path: '/:event/items/', name: 'items', component: Items, meta:
{requiresAuth: true, requiresPermission: 'view_item'} {requiresAuth: true, requiresPermission: 'view_item'}
}, },
{
path: '/:event/items/:search', name: 'item_search', component: ItemSearch, meta:
{requiresAuth: true, requiresPermission: 'view_item'}
},
{ {
path: '/:event/item/:id/', name: 'item', component: Item, meta: path: '/:event/item/:id/', name: 'item', component: Item, meta:
{requiresAuth: true, requiresPermission: 'view_item'} {requiresAuth: true, requiresPermission: 'view_item'}
@ -49,10 +43,6 @@ const routes = [
path: '/:event/tickets/', name: 'tickets', component: Tickets, meta: path: '/:event/tickets/', name: 'tickets', component: Tickets, meta:
{requiresAuth: true, requiresPermission: 'view_issuethread'} {requiresAuth: true, requiresPermission: 'view_issuethread'}
}, },
{
path: '/:event/tickets/:search', name: 'ticket_search', component: TicketSearch, meta:
{requiresAuth: true, requiresPermission: 'view_issuethread'}
},
{ {
path: '/:event/ticket/:id/', name: 'ticket', component: Ticket, meta: path: '/:event/ticket/:id/', name: 'ticket', component: Ticket, meta:
{requiresAuth: true, requiresPermission: 'view_issuethread'} {requiresAuth: true, requiresPermission: 'view_issuethread'}

View file

@ -9,7 +9,6 @@ import persistentStatePlugin from "@/persistent-state-plugin";
const store = createStore({ const store = createStore({
state: { state: {
//shared
keyIncrement: 0, keyIncrement: 0,
events: [], events: [],
items: [], items: [],
@ -23,12 +22,9 @@ const store = createStore({
loadedItems: {}, loadedItems: {},
loadedTickets: {}, loadedTickets: {},
loadedItemSearchResults: {},
loadedTicketSearchResults: {},
//local
lastEvent: 'all', lastEvent: 'all',
lastUsed: {}, lastUsed: {},
searchQuery: '',
remember: false, remember: false,
user: { user: {
username: null, username: null,
@ -70,17 +66,12 @@ const store = createStore({
route: state => router.currentRoute.value, route: state => router.currentRoute.value,
session: state => http_session(state.user.token), 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,
searchQuery: state => router.currentRoute.value.params.search,
getAllItems: state => Object.values(state.loadedItems).flat(), getAllItems: state => Object.values(state.loadedItems).flat(),
getAllTickets: state => Object.values(state.loadedTickets).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')), 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')), 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), 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), isTicketsLoaded: (state, getters) => (getters.getEventSlug === 'all' || getters.getEventSlug === 'none') ? !!state.loadedTickets : Object.keys(state.loadedTickets).includes(getters.getEventSlug),
getItemsSearchResults: (state, getters) => state.loadedItemSearchResults[getters.getEventSlug + '/' + base64.encode(utf8.encode(getters.searchQuery))] || [],
getTicketsSearchResults: (state, getters) => state.loadedTicketSearchResults[getters.getEventSlug + '/' + base64.encode(utf8.encode(getters.searchQuery))] || [],
isItemsSearchLoaded: (state, getters) => Object.keys(state.loadedItemSearchResults).includes(getters.getEventSlug + '/' + base64.encode(utf8.encode(getters.searchQuery))),
isTicketsSearchLoaded: (state, getters) => Object.keys(state.loadedTicketSearchResults).includes(getters.getEventSlug + '/' + base64.encode(utf8.encode(getters.searchQuery))),
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,
@ -115,9 +106,9 @@ const store = createStore({
layout: (state, getters) => { layout: (state, getters) => {
if (router.currentRoute.value.query.layout) if (router.currentRoute.value.query.layout)
return router.currentRoute.value.query.layout; return router.currentRoute.value.query.layout;
if (getters.getActiveView === 'items' || getters.getActiveView === 'item_search') if (getters.getActiveView === 'items')
return 'cards'; return 'cards';
if (getters.getActiveView === 'tickets' || getters.getActiveView === 'ticket_search') if (getters.getActiveView === 'tickets')
return 'tasks'; return 'tasks';
}, },
isLoggedIn(state) { isLoggedIn(state) {
@ -156,10 +147,6 @@ const store = createStore({
state.loadedItems[slug] = items; state.loadedItems[slug] = items;
state.loadedItems = {...state.loadedItems}; state.loadedItems = {...state.loadedItems};
}, },
setItemSearchResults(state, {slug, query, items}) {
state.loadedItemSearchResults[slug + '/' + query] = items;
state.loadedItemSearchResults = {...state.loadedItemSearchResults};
},
replaceItems(state, items) { replaceItems(state, items) {
const groups = Object.groupBy(items, i => i.event ? i.event : 'none') const groups = Object.groupBy(items, i => i.event ? i.event : 'none')
for (const [key, value] of Object.entries(groups)) state.loadedItems[key] = value; for (const [key, value] of Object.entries(groups)) state.loadedItems[key] = value;
@ -180,10 +167,6 @@ const store = createStore({
state.loadedTickets[slug] = tickets; state.loadedTickets[slug] = tickets;
state.loadedTickets = {...state.loadedTickets}; state.loadedTickets = {...state.loadedTickets};
}, },
setTicketSearchResults(state, {slug, query, items}) {
state.loadedTicketSearchResults[slug + '/' + query] = items;
state.loadedTicketSearchResults = {...state.loadedTicketSearchResults};
},
replaceTickets(state, tickets) { replaceTickets(state, tickets) {
const groups = Object.groupBy(tickets, t => t.event ? t.event : 'none') const groups = Object.groupBy(tickets, t => t.event ? t.event : 'none')
for (const [key, value] of Object.entries(groups)) state.loadedTickets[key] = value; for (const [key, value] of Object.entries(groups)) state.loadedTickets[key] = value;
@ -393,12 +376,11 @@ 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 slug = getters.getEventSlug;
if (Object.keys(state.loadedItemSearchResults).includes(slug + '/' + encoded_query)) return;
const { const {
data, success data, success
} = await getters.session.get(`/2/${slug}/items/${encoded_query}/`); } = await getters.session.get(`/2/${slug}/items/${encoded_query}/`);
if (data && success) { if (data && success) {
commit('setItemSearchResults', {slug, query: encoded_query, items: data}); commit('setItems', {slug, items: data});
} }
}, },
async loadBoxes({commit, state, getters}) { async loadBoxes({commit, state, getters}) {
@ -446,12 +428,11 @@ const store = createStore({
}, },
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 slug = getters.getEventSlug;
if (Object.keys(state.loadedTicketSearchResults).includes(slug + '/' + encoded_query)) return;
const { const {
data, success data, success
} = await getters.session.get(`/2/${slug}/tickets/${encoded_query}/`); } = await getters.session.get(`/2/${getters.getEventSlug}/tickets/${encoded_query}/`);
if (data && success) commit('setTicketSearchResults', {slug, query: encoded_query, items: data}); if (data && success) commit('replaceTickets', data);
}, },
async sendMail({commit, dispatch, state, getters}, {id, message}) { async sendMail({commit, dispatch, state, getters}, {id, message}) {
const {data, success} = await getters.session.post(`/2/tickets/${id}/reply/`, {message}, const {data, success} = await getters.session.post(`/2/tickets/${id}/reply/`, {message},

View file

@ -1,114 +0,0 @@
<template>
<AsyncLoader :loaded="isItemsSearchLoaded">
<div class="container-fluid px-xl-5 mt-3">
<div class="row" v-if="layout === 'table'">
<div class="col-xl-8 offset-xl-2">
<Table
:columns="['id', 'description', 'box']"
:items="getItemsSearchResults"
:keyName="'id'"
@itemActivated="showItemDetail"
>
<template #actions="{ item }">
<div class="btn-group">
<button class="btn btn-success"
@click.stop="confirm('return Item?') && markItemReturned(item)"
title="returned">
<font-awesome-icon icon="check"/>
</button>
<button class="btn btn-secondary" @click.stop="openEditingModalWith(item)" title="edit">
<font-awesome-icon icon="edit"/>
</button>
<button class="btn btn-danger" @click.stop="confirm('delete Item?') && deleteItem(item)"
title="delete">
<font-awesome-icon icon="trash"/>
</button>
</div>
</template>
</Table>
</div>
</div>
<Cards
v-if="layout === 'cards'"
:columns="['id', 'description', 'box']"
:items="getItemsSearchResults"
:keyName="'id'"
v-slot="{ item }"
@itemActivated="item => openLightboxModalWith(item.file)"
>
<AuthenticatedImage v-if="item.file" cached
:src="`/media/2/256/${item.file}/`"
class="card-img-top img-fluid"
/>
<div class="card-body">
<h6 class="card-title">{{ item.description }}</h6>
<h6 class="card-subtitle text-secondary">id: {{ item.id }} box: {{ item.box }}</h6>
<div class="row mx-auto mt-2">
<div class="btn-group">
<button class="btn btn-outline-success"
@click.stop="confirm('return Item?') && markItemReturned(item)" title="returned">
<font-awesome-icon icon="check"/>
</button>
<button class="btn btn-outline-secondary" @click.stop="showItemDetail(item)"
title="edit">
<font-awesome-icon icon="edit"/>
</button>
<button class="btn btn-outline-danger"
@click.stop="confirm('delete Item?') && deleteItem(item)"
title="delete">
<font-awesome-icon icon="trash"/>
</button>
</div>
</div>
</div>
</Cards>
</div>
</AsyncLoader>
</template>
<script>
import {mapActions, mapGetters, mapMutations, mapState} from 'vuex';
import Table from '@/components/Table';
import Cards from '@/components/Cards';
import Modal from '@/components/Modal';
import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
import AsyncLoader from "@/components/AsyncLoader.vue";
import router from "@/router";
export default {
name: 'Items',
data: () => ({
lightboxHash: null,
editingItem: null,
}),
components: {AsyncLoader, AuthenticatedImage, Table, Cards, Modal},
computed: {
...mapGetters(['getItemsSearchResults', 'isItemsSearchLoaded', 'layout', 'getEventSlug', 'searchQuery']),
},
methods: {
...mapActions(['deleteItem', 'markItemReturned', 'searchEventItems', 'updateItem', 'scheduleAfterInit']),
...mapMutations(['openLightboxModalWith']),
showItemDetail(item) {
router.push({name: 'item', params: {id: item.id}});
},
confirm(message) {
return window.confirm(message);
}
},
watch: {
searchQuery() {
this.scheduleAfterInit(() => [this.searchEventItems(this.searchQuery)]);
},
getEventSlug() {
this.scheduleAfterInit(() => [this.searchEventItems(this.searchQuery)]);
}
},
mounted() {
this.scheduleAfterInit(() => [this.searchEventItems(this.searchQuery)]);
}
};
</script>
<style scoped>
</style>

View file

@ -1,102 +0,0 @@
<template>
<AsyncLoader :loaded="isTicketsSearchLoaded">
<div class="container-fluid px-xl-5 mt-3">
<div class="row" v-if="layout === 'table'">
<div class="col-xl-8 offset-xl-2">
<Table
:columns="['id', 'name', 'state', 'last_activity', 'assigned_to',
...(getEventSlug==='all'?['event']:[])]"
:items="getTicketsSearchResults.map(formatTicket)"
:keyName="'id'"
>
<template v-slot:actions="{item}">
<div class="btn-group">
<router-link :to="{name: 'ticket', params: {id: item.id}}" class="btn btn-primary"
title="view">
<font-awesome-icon icon="eye"/>
View
</router-link>
</div>
</template>
</Table>
</div>
</div>
<CollapsableCards v-if="layout === 'tasks'" :items="getTicketsSearchResults"
:columns="['id', 'name', 'last_activity', 'assigned_to',
...(getEventSlug==='all'?['event']:[])]"
:keyName="'state'" :sections="['pending_new', 'pending_open','pending_shipping',
'pending_physical_confirmation','pending_return','pending_postponed'].map(stateInfo)">
<template #section_header="{index, section, count}">
{{ section.text }} <span class="badge badge-light ml-1">{{ count }}</span>
</template>
<template #section_body="{item}">
<tr>
<td>{{ item.id }}</td>
<td>{{ item.name }}</td>
<td>{{ item.last_activity }}</td>
<td>{{ item.assigned_to }}</td>
<td v-if="getEventSlug==='all'">{{ item.event }}</td>
<td>
<div class="btn-group">
<router-link :to="{name: 'ticket', params: {id: item.id}}" class="btn btn-primary"
title="view">
<font-awesome-icon icon="eye"/>
View
</router-link>
</div>
</td>
</tr>
</template>
</CollapsableCards>
</div>
</AsyncLoader>
</template>
<script>
import Cards from '@/components/Cards';
import {mapActions, mapGetters, mapState} from 'vuex';
import Table from '@/components/Table';
import CollapsableCards from "@/components/CollapsableCards.vue";
import AsyncLoader from "@/components/AsyncLoader.vue";
import router from "@/router";
export default {
name: 'TicketSearch',
components: {AsyncLoader, Table, Cards, CollapsableCards},
computed: {
...mapGetters(['getTicketsSearchResults', 'isTicketsSearchLoaded', 'stateInfo', 'getEventSlug', 'layout', 'searchQuery']),
},
methods: {
...mapActions(['searchEventTickets', 'fetchTicketStates', 'scheduleAfterInit']),
gotoDetail(ticket) {
router.push({name: 'ticket', params: {id: ticket.id}});
},
formatTicket(ticket) {
return {
id: ticket.id,
name: ticket.name,
state: this.stateInfo(ticket.state).text,
stateColor: this.stateInfo(ticket.state).color,
last_activity: ticket.last_activity,
assigned_to: ticket.assigned_to,
event: ticket.event
};
}
},
watch: {
searchQuery() {
this.scheduleAfterInit(() => [this.searchEventTickets(this.searchQuery)]);
},
getEventSlug() {
this.scheduleAfterInit(() => [this.searchEventTickets(this.searchQuery)]);
}
},
mounted() {
this.scheduleAfterInit(() => [this.fetchTicketStates(), this.searchEventTickets(this.searchQuery)]);
}
};
</script>
<style scoped>
</style>