Compare commits

..

21 commits

Author SHA1 Message Date
4f93a36546 stash
All checks were successful
/ test (push) Successful in 54s
2024-11-23 01:39:36 +01:00
63e3319f0d stash 2024-11-23 01:39:11 +01:00
ec6bfc0d3f stash 2024-11-23 01:38:03 +01:00
9134f6b95e stash 2024-11-23 01:37:57 +01:00
d73fb90442 stash 2024-11-23 01:36:50 +01:00
ee9d83049c save raw_mails as file 2024-11-23 01:36:50 +01:00
3ce31c1f62 stash 2024-11-23 01:36:41 +01:00
18cb69f4f4 stash 2024-11-23 01:35:29 +01:00
b32645b18e stash 2024-11-23 01:35:29 +01:00
9f1a681762 stash 2024-11-23 01:35:29 +01:00
a76706851d stash 2024-11-23 01:35:29 +01:00
e311f87d5d stash 2024-11-23 01:35:29 +01:00
3e03832867 stash 2024-11-23 01:35:29 +01:00
c5e59a3b2b stash 2024-11-23 01:35:29 +01:00
6ac49458f4 stash 2024-11-23 01:35:29 +01:00
76119de05f stash 2024-11-23 01:35:29 +01:00
d5eadbe4b1 add timeline information to the /items endpoint
All checks were successful
/ test (push) Successful in 52s
/ deploy (push) Successful in 4m37s
2024-11-23 01:22:24 +01:00
e8887fee8b add frontend to edit event details
All checks were successful
/ test (push) Successful in 51s
/ deploy (push) Successful in 4m37s
2024-11-23 01:12:45 +01:00
8786f4b845 ensure creation file date
All checks were successful
/ test (push) Successful in 49s
/ deploy (push) Successful in 5m5s
2024-11-23 00:59:45 +01:00
0eaff2266c fix untested error in /tickets endpoint
All checks were successful
/ test (push) Successful in 48s
/ deploy (push) Successful in 4m45s
2024-11-21 22:37:49 +01:00
81a0959547 Speed up sql
All checks were successful
/ test (pull_request) Successful in 48s
/ test (push) Successful in 49s
/ deploy (push) Successful in 5m13s
2024-11-21 21:26:16 +01:00
26 changed files with 606 additions and 182 deletions

View file

@ -20,9 +20,12 @@ jobs:
- name: Run django tests - name: Run django tests
working-directory: core working-directory: core
run: python3 manage.py test run: python3 manage.py test
- name: Run django coverage
working-directory: core
run: coverage manage.py test
deploy: deploy:
needs: [test] needs: [ test ]
runs-on: docker runs-on: docker
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4

View file

@ -56,6 +56,7 @@ TELEGRAM_GROUP_CHAT_ID = os.getenv('TELEGRAM_GROUP_CHAT_ID', '-1234567890')
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
*(['daphne'] if 'runserver' in sys.argv else []),
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
@ -129,7 +130,7 @@ TEMPLATES = [
}, },
] ]
WSGI_APPLICATION = 'core.wsgi.application' ASGI_APPLICATION = 'core.asgi.application'
# Database # Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases # https://docs.djangoproject.com/en/4.2/ref/settings/#databases

View file

@ -0,0 +1,24 @@
# Generated by Django 4.2.7 on 2024-11-21 22:40
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('files', '0002_alter_file_file'),
]
def set_creation_date(apps, schema_editor):
File = apps.get_model('files', 'File')
for file in File.objects.all():
if file.created_at is None:
if not file.item.created_at is None:
file.created_at = file.item.created_at
else:
file.created_at = max(File.objects.filter(
id__lt=file.id, created_at__isnull=False).values_list('created_at', flat=True))
file.save()
operations = [
migrations.RunPython(set_creation_date),
]

View file

@ -1,32 +0,0 @@
# Generated by Django 4.2.7 on 2024-11-20 01:48
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('inventory', '0006_alter_event_table'),
]
operations = [
migrations.RemoveField(
model_name='item',
name='container',
),
migrations.AlterField(
model_name='item',
name='event',
field=models.ForeignKey(db_column='eid', on_delete=django.db.models.deletion.CASCADE, to='inventory.event'),
),
migrations.CreateModel(
name='ItemPlacement',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('timestamp', models.DateTimeField(auto_now_add=True)),
('container', models.ForeignKey(db_column='cid', on_delete=django.db.models.deletion.CASCADE, related_name='item_history', to='inventory.container')),
('item', models.ForeignKey(db_column='iid', on_delete=django.db.models.deletion.CASCADE, related_name='container_history', to='inventory.item')),
],
),
]

View file

@ -0,0 +1,36 @@
# Generated by Django 4.2.7 on 2024-11-23 00:19
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('inventory', '0006_alter_event_table'),
]
operations = [
migrations.RemoveField(
model_name='item',
name='container',
),
migrations.CreateModel(
name='ItemPlacement',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('timestamp', models.DateTimeField(auto_now_add=True)),
('container', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='item_history', to='inventory.container')),
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='container_history', to='inventory.item')),
],
),
migrations.CreateModel(
name='Comment',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('comment', models.TextField()),
('timestamp', models.DateTimeField(auto_now_add=True)),
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='inventory.item')),
],
),
]

View file

@ -1,29 +0,0 @@
# Generated by Django 4.2.7 on 2024-11-20 01:52
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('inventory', '0007_remove_item_container_alter_item_event_itemplacement'),
]
operations = [
migrations.AlterField(
model_name='item',
name='event',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.event'),
),
migrations.AlterField(
model_name='itemplacement',
name='container',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='item_history', to='inventory.container'),
),
migrations.AlterField(
model_name='itemplacement',
name='item',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='container_history', to='inventory.item'),
),
]

View file

@ -1,7 +1,6 @@
from itertools import groupby from itertools import groupby
from django.core.files.base import ContentFile from django.db import models
from django.db import models, IntegrityError
from django_softdelete.models import SoftDeleteModel, SoftDeleteManager from django_softdelete.models import SoftDeleteModel, SoftDeleteManager
@ -48,8 +47,6 @@ class Item(SoftDeleteModel):
groups = groupby(self.issue_relation_changes.all(), lambda rel: rel.issue_thread.id) groups = groupby(self.issue_relation_changes.all(), lambda rel: rel.issue_thread.id)
return [sorted(v, key=lambda r: r.timestamp)[0].issue_thread for k, v in groups] return [sorted(v, key=lambda r: r.timestamp)[0].issue_thread for k, v in groups]
objects = ItemManager() objects = ItemManager()
all_objects = models.Manager() all_objects = models.Manager()
@ -73,7 +70,7 @@ class Container(SoftDeleteModel):
def items(self): def items(self):
try: try:
history = self.item_history.order_by('-timestamp').all() history = self.item_history.order_by('-timestamp').all()
return [v for k, v in groupby(history, key=lambda item: item.item.id)] return [v for k, v in groupby(history, key=lambda item: item.item.id)]
except AttributeError: except AttributeError:
return [] return []
@ -88,6 +85,16 @@ class ItemPlacement(models.Model):
timestamp = models.DateTimeField(auto_now_add=True) timestamp = models.DateTimeField(auto_now_add=True)
class Comment(models.Model):
id = models.AutoField(primary_key=True)
item = models.ForeignKey(Item, on_delete=models.CASCADE, related_name='comments')
comment = models.TextField()
timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
return str(self.issue_thread) + ' comment #' + str(self.id)
class Event(models.Model): class Event(models.Model):
id = models.AutoField(primary_key=True) id = models.AutoField(primary_key=True)
name = models.CharField(max_length=255) name = models.CharField(max_length=255)

View file

@ -9,24 +9,7 @@ from mail.models import EventAddress
from tickets.shared_serializers import BasicIssueSerializer from tickets.shared_serializers import BasicIssueSerializer
# class EventAdressSerializer(serializers.ModelSerializer):
# class Meta:
# model = EventAddress
# fields = ['address']
# def to_internal_value(self, data):
# if not isinstance(data, str):
# raise serializers.ValidationError('This field must be a string.')
#
# def create(self, validated_data):
# return EventAddress.objects.create(**validated_data)
#
# def validate(self, data):
# return isinstance(data, str)
class EventSerializer(serializers.ModelSerializer): class EventSerializer(serializers.ModelSerializer):
# addresses = EventAdressSerializer(many=True, required=False)
addresses = SlugRelatedField(many=True, slug_field='address', queryset=EventAddress.objects.all()) addresses = SlugRelatedField(many=True, slug_field='address', queryset=EventAddress.objects.all())
class Meta: class Meta:
@ -34,17 +17,13 @@ class EventSerializer(serializers.ModelSerializer):
fields = ['id', 'slug', 'name', 'start', 'end', 'pre_start', 'post_end', 'addresses'] fields = ['id', 'slug', 'name', 'start', 'end', 'pre_start', 'post_end', 'addresses']
read_only_fields = ['id'] read_only_fields = ['id']
def to_internal_value(self, data):
# def update(self, instance, validated_data): data = data.copy()
# addresses = validated_data.pop('addresses', None) addresses = data.pop('addresses', None)
# instance.save(validated_data) dict = super().to_internal_value(data)
# if addresses: if addresses:
# for address in addresses: dict['addresses'] = [EventAddress.objects.get_or_create(address=x)[0] for x in addresses]
# nested_instance, created = EventAddress.objects.get_or_create(address=address) return dict
# instance.addresses.add(nested_instance)
#
# return instance
class ContainerSerializer(serializers.ModelSerializer): class ContainerSerializer(serializers.ModelSerializer):
itemCount = serializers.SerializerMethodField() itemCount = serializers.SerializerMethodField()
@ -59,12 +38,14 @@ class ContainerSerializer(serializers.ModelSerializer):
class ItemSerializer(BasicItemSerializer): class ItemSerializer(BasicItemSerializer):
timeline = serializers.SerializerMethodField()
dataImage = serializers.CharField(write_only=True, required=False) dataImage = serializers.CharField(write_only=True, required=False)
related_issues = BasicIssueSerializer(many=True, read_only=True) related_issues = BasicIssueSerializer(many=True, read_only=True)
class Meta: class Meta:
model = Item model = Item
fields = ['cid', 'box', 'id', 'description', 'file', 'dataImage', 'returned', 'event', 'related_issues'] fields = ['cid', 'box', 'id', 'description', 'file', 'dataImage', 'returned', 'event', 'related_issues',
'timeline']
read_only_fields = ['id'] read_only_fields = ['id']
def to_internal_value(self, data): def to_internal_value(self, data):
@ -106,6 +87,26 @@ class ItemSerializer(BasicItemSerializer):
instance.files.add(file) instance.files.add(file)
return super().update(instance, validated_data) return super().update(instance, validated_data)
@staticmethod
def get_timeline(obj):
timeline = []
for comment in obj.comments.all():
timeline.append({
'type': 'comment',
'id': comment.id,
'timestamp': comment.timestamp,
'comment': comment.comment,
})
for relation in (obj.issue_relation_changes.all()):
timeline.append({
'type': 'issue_relation',
'id': relation.id,
'status': relation.status,
'timestamp': relation.timestamp,
'issue_thread': BasicIssueSerializer(relation.issue_thread).data,
})
return sorted(timeline, key=lambda x: x['timestamp'])
class SearchResultSerializer(serializers.Serializer): class SearchResultSerializer(serializers.Serializer):
search_score = serializers.IntegerField() search_score = serializers.IntegerField()

View file

@ -24,7 +24,7 @@ class BasicItemSerializer(serializers.ModelSerializer):
def get_file(self, instance): def get_file(self, instance):
if len(instance.files.all()) > 0: if len(instance.files.all()) > 0:
return instance.files.all().order_by('-created_at')[0].hash return sorted(instance.files.all(), key=lambda x: x.created_at, reverse=True)[0].hash
return None return None
def get_returned(self, instance): def get_returned(self, instance):

View file

@ -50,14 +50,16 @@ class EventTestCase(TestCase):
def test_update_event(self): def test_update_event(self):
from rest_framework.test import APIClient from rest_framework.test import APIClient
event = Event.objects.create(slug='EVENT1', name='Event 1') event = Event.objects.create(slug='EVENT1', name='Event 1')
response = APIClient().patch(f'/api/2/events/{event.id}/', {'addresses': []})#'foo@bar.baz', 'foo1@bar.baz' response = APIClient().patch(f'/api/2/events/{event.id}/', {'addresses': ['foo@bar.baz', 'foo1@bar.baz']})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['slug'], 'EVENT1') self.assertEqual(response.json()['slug'], 'EVENT1')
self.assertEqual(response.json()['name'], 'Event 1') self.assertEqual(response.json()['name'], 'Event 1')
self.assertEqual(2, len(response.json()['addresses']))
self.assertEqual('foo@bar.baz', response.json()['addresses'][0])
self.assertEqual('foo1@bar.baz', response.json()['addresses'][1])
self.assertEqual(len(Event.objects.all()), 1) self.assertEqual(len(Event.objects.all()), 1)
self.assertEqual(Event.objects.all()[0].slug, 'EVENT1') self.assertEqual(Event.objects.all()[0].slug, 'EVENT1')
self.assertEqual(Event.objects.all()[0].name, 'Event 1') self.assertEqual(Event.objects.all()[0].name, 'Event 1')
#self.assertEqual(1, len(response.json()[0]['addresses']))
def test_remove_event(self): def test_remove_event(self):
event = Event.objects.create(slug='EVENT1', name='Event 1') event = Event.objects.create(slug='EVENT1', name='Event 1')
@ -67,13 +69,26 @@ class EventTestCase(TestCase):
self.assertEqual(response.status_code, 204) self.assertEqual(response.status_code, 204)
self.assertEqual(len(Event.objects.all()), 1) self.assertEqual(len(Event.objects.all()), 1)
def test_items2(self): def test_event_with_address(self):
from mail.models import EventAddress from mail.models import EventAddress
event1 = Event.objects.create(slug='TEST1', name='Event') event1 = Event.objects.create(slug='TEST1', name='Event')
EventAddress.objects.create(event=Event.objects.get(slug='TEST1'), address='foo@bar.baz') EventAddress.objects.create(event=event1, address='foo@bar.baz')
response = self.client.get('/api/2/events/') response = self.client.get('/api/2/events/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(1, len(response.json())) self.assertEqual(1, len(response.json()))
self.assertEqual('TEST1', response.json()[0]['slug']) self.assertEqual('TEST1', response.json()[0]['slug'])
self.assertEqual('Event', response.json()[0]['name']) self.assertEqual('Event', response.json()[0]['name'])
self.assertEqual(1, len(response.json()[0]['addresses'])) self.assertEqual(1, len(response.json()[0]['addresses']))
def test_items_remove_addresss(self):
from mail.models import EventAddress
from rest_framework.test import APIClient
event1 = Event.objects.create(slug='TEST1', name='Event')
EventAddress.objects.create(event=event1, address='foo@bar.baz')
EventAddress.objects.create(event=event1, address='fo1o@bar.baz')
response = APIClient().patch(f'/api/2/events/{event1.id}/', {'addresses': ['foo1@bar.baz']})
self.assertEqual(response.status_code, 200)
self.assertEqual('TEST1', response.json()['slug'])
self.assertEqual('Event', response.json()['name'])
self.assertEqual(1, len(response.json()['addresses']))
self.assertEqual('foo1@bar.baz', response.json()['addresses'][0])

View file

@ -1,3 +1,5 @@
from datetime import datetime, timedelta
from django.utils import timezone from django.utils import timezone
from django.test import TestCase, Client from django.test import TestCase, Client
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
@ -5,10 +7,12 @@ from knox.models import AuthToken
from authentication.models import ExtendedUser from authentication.models import ExtendedUser
from files.models import File from files.models import File
from inventory.models import Event, Container, Item from inventory.models import Event, Container, Item, Comment
from base64 import b64encode from base64 import b64encode
from tickets.models import IssueThread, ItemRelation
class ItemTestCase(TestCase): class ItemTestCase(TestCase):
@ -20,14 +24,29 @@ class ItemTestCase(TestCase):
self.user.user_permissions.add(*Permission.objects.all()) self.user.user_permissions.add(*Permission.objects.all())
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]})
self.issue = IssueThread.objects.create(
name="test issue",
event=self.event,
)
def test_empty(self): def test_empty(self):
response = self.client.get(f'/api/2/{self.event.slug}/item/') response = self.client.get(f'/api/2/{self.event.slug}/item/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, b'[]') self.assertEqual(response.content, b'[]')
def test_members(self): def test_members_and_timeline(self):
now = datetime.now()
item = Item.objects.create(container=self.box, event=self.event, description='1') item = Item.objects.create(container=self.box, event=self.event, description='1')
comment = Comment.objects.create(
item=item,
comment="test",
timestamp=now + timedelta(seconds=3),
)
match = ItemRelation.objects.create(
issue_thread=self.issue,
item = item,
timestamp=now + timedelta(seconds=5),
)
response = self.client.get(f'/api/2/{self.event.slug}/item/') response = self.client.get(f'/api/2/{self.event.slug}/item/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 1) self.assertEqual(len(response.json()), 1)
@ -38,7 +57,24 @@ class ItemTestCase(TestCase):
self.assertEqual(response.json()[0]['file'], None) self.assertEqual(response.json()[0]['file'], None)
self.assertEqual(response.json()[0]['returned'], False) self.assertEqual(response.json()[0]['returned'], False)
self.assertEqual(response.json()[0]['event'], self.event.slug) self.assertEqual(response.json()[0]['event'], self.event.slug)
self.assertEqual(len(response.json()[0]['related_issues']), 0) self.assertEqual(len(response.json()[0]['timeline']), 2)
self.assertEqual(response.json()[0]['timeline'][0]['type'], 'comment')
self.assertEqual(response.json()[0]['timeline'][1]['type'], 'issue_relation')
self.assertEqual(response.json()[0]['timeline'][0]['id'], comment.id)
self.assertEqual(response.json()[0]['timeline'][1]['id'], match.id)
self.assertEqual(response.json()[0]['timeline'][0]['comment'], 'test')
self.assertEqual(response.json()[0]['timeline'][0]['timestamp'],
comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'))
self.assertEqual(response.json()[0]['timeline'][1]['status'], 'possible')
self.assertEqual(response.json()[0]['timeline'][1]['timestamp'],
match.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'))
self.assertEqual(response.json()[0]['timeline'][1]['issue_thread']['name'], "test issue")
self.assertEqual(response.json()[0]['timeline'][1]['issue_thread']['event'], "EVENT")
self.assertEqual(response.json()[0]['timeline'][1]['issue_thread']['state'], "pending_new")
self.assertEqual(len(response.json()[0]['related_issues']), 1)
self.assertEqual(response.json()[0]['related_issues'][0]['name'], "test issue")
self.assertEqual(response.json()[0]['related_issues'][0]['event'], "EVENT")
self.assertEqual(response.json()[0]['related_issues'][0]['state'], "pending_new")
def test_members_with_file(self): def test_members_with_file(self):
import base64 import base64
@ -56,6 +92,23 @@ class ItemTestCase(TestCase):
self.assertEqual(response.json()[0]['event'], self.event.slug) self.assertEqual(response.json()[0]['event'], self.event.slug)
self.assertEqual(len(response.json()[0]['related_issues']), 0) self.assertEqual(len(response.json()[0]['related_issues']), 0)
def test_members_with_two_file(self):
import base64
item = Item.objects.create(container=self.box, event=self.event, description='1')
file1 = File.objects.create(item=item, data="data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8'))
file2 = File.objects.create(item=item, data="data:text/plain;base64," + base64.b64encode(b"bar").decode('utf-8'))
response = self.client.get(f'/api/2/{self.event.slug}/item/')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 1)
self.assertEqual(response.json()[0]['id'], item.id)
self.assertEqual(response.json()[0]['description'], '1')
self.assertEqual(response.json()[0]['box'], 'BOX')
self.assertEqual(response.json()[0]['cid'], self.box.id)
self.assertEqual(response.json()[0]['file'], file2.hash)
self.assertEqual(response.json()[0]['returned'], False)
self.assertEqual(response.json()[0]['event'], self.event.slug)
self.assertEqual(len(response.json()[0]['related_issues']), 0)
def test_multi_members(self): def test_multi_members(self):
Item.objects.create(container=self.box, event=self.event, description='1') Item.objects.create(container=self.box, event=self.event, description='1')
Item.objects.create(container=self.box, event=self.event, description='2') Item.objects.create(container=self.box, event=self.event, description='2')

View file

@ -1,4 +1,4 @@
# Generated by Django 4.2.7 on 2024-11-18 01:50 # Generated by Django 4.2.7 on 2024-11-22 18:57
from django.db import migrations, models from django.db import migrations, models

View file

@ -71,7 +71,7 @@ def make_reply(reply_email, references=None, event=None):
reply["From"] = reply_email.sender reply["From"] = reply_email.sender
reply["To"] = reply_email.recipient reply["To"] = reply_email.recipient
reply["Subject"] = reply_email.subject reply["Subject"] = reply_email.subject
reply["Reply-To"] = f"{event}@{MAIL_DOMAIN}" reply["Reply-To"] = f"{event.lower()}@{MAIL_DOMAIN}"
if reply_email.in_reply_to: if reply_email.in_reply_to:
reply["In-Reply-To"] = reply_email.in_reply_to reply["In-Reply-To"] = reply_email.in_reply_to
if reply_email.reference: if reply_email.reference:

View file

@ -21,7 +21,8 @@ from tickets.shared_serializers import RelationSerializer
class IssueViewSet(viewsets.ModelViewSet): class IssueViewSet(viewsets.ModelViewSet):
serializer_class = IssueSerializer serializer_class = IssueSerializer
queryset = IssueThread.objects.all() queryset = IssueThread.objects.all().prefetch_related('state_changes', 'comments', 'emails', 'emails__attachments',
'assignments', 'item_relation_changes', 'shipping_vouchers')
class RelationViewSet(viewsets.ModelViewSet): class RelationViewSet(viewsets.ModelViewSet):

View file

@ -1,20 +0,0 @@
# Generated by Django 4.2.7 on 2024-11-20 01:48
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('inventory', '0007_remove_item_container_alter_item_event_itemplacement'),
('tickets', '0011_train_old_spam'),
]
operations = [
migrations.AlterField(
model_name='itemrelation',
name='item',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_relations', to='inventory.item'),
),
]

View file

@ -1,4 +1,4 @@
# Generated by Django 4.2.7 on 2024-11-20 23:34 # Generated by Django 4.2.7 on 2024-11-20 23:58
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
@ -7,8 +7,8 @@ import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('inventory', '0008_alter_item_event_alter_itemplacement_container_and_more'), ('inventory', '0006_alter_event_table'),
('tickets', '0012_alter_itemrelation_item'), ('tickets', '0011_train_old_spam'),
] ]
operations = [ operations = [

View file

@ -52,7 +52,11 @@ class IssueThread(SoftDeleteModel):
@property @property
def state(self): def state(self):
try: try:
return self.state_changes.order_by('-timestamp').first().state state_changes = sorted(self.state_changes.all(), key=lambda x: x.timestamp, reverse=True)
if state_changes:
return state_changes[0].state
else:
return None
except AttributeError: except AttributeError:
return 'none' return 'none'
@ -67,7 +71,11 @@ class IssueThread(SoftDeleteModel):
@property @property
def assigned_to(self): def assigned_to(self):
try: try:
return self.assignments.order_by('-timestamp').first().assigned_to assignments = sorted(self.assignments.all(), key=lambda x: x.timestamp, reverse=True)
if assignments:
return assignments[0].assigned_to
else:
return None
except AttributeError: except AttributeError:
return None return None

View file

@ -63,14 +63,12 @@ class IssueSerializer(BasicIssueSerializer):
@staticmethod @staticmethod
def get_last_activity(self): def get_last_activity(self):
try: try:
last_state_change = self.state_changes.order_by('-timestamp').first().timestamp \ last_state_change = max([t.timestamp for t in self.state_changes.all()]) if self.state_changes.exists() else None
if self.state_changes.count() > 0 else None last_comment = max([t.timestamp for t in self.comments.all()]) if self.comments.exists() else None
last_comment = self.comments.order_by('-timestamp').first().timestamp if self.comments.count() > 0 else None last_mail = max([t.timestamp for t in self.emails.all()]) if self.emails.exists() else None
last_mail = self.emails.order_by('-timestamp').first().timestamp if self.emails.count() > 0 else None last_assignment = max([t.timestamp for t in self.assignments.all()]) if self.assignments.exists() else None
last_assignment = self.assignments.order_by('-timestamp').first().timestamp if \
self.assignments.count() > 0 else None last_relation = max([t.timestamp for t in self.item_relation_changes.all()]) if self.item_relation_changes.exists() else None
last_relation = self.item_relation_changes.order_by('-timestamp').first().timestamp if \
self.item_relation_changes.count() > 0 else None
args = [x for x in [last_state_change, last_comment, last_mail, last_assignment, last_relation] if args = [x for x in [last_state_change, last_comment, last_mail, last_assignment, last_relation] if
x is not None] x is not None]
return max(args) return max(args)

View file

@ -5,7 +5,7 @@ from django.test import TestCase, Client
from authentication.models import ExtendedUser from authentication.models import ExtendedUser
from inventory.models import Event, Container, Item from inventory.models import Event, Container, Item
from mail.models import Email, EmailAttachment from mail.models import Email, EmailAttachment
from tickets.models import IssueThread, StateChange, Comment, ItemRelation from tickets.models import IssueThread, StateChange, Comment, ItemRelation, Assignment
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from knox.models import AuthToken from knox.models import AuthToken
@ -53,19 +53,24 @@ class IssueApiTest(TestCase):
in_reply_to=mail1.reference, in_reply_to=mail1.reference,
timestamp=now + timedelta(seconds=2), 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( comment = Comment.objects.create(
issue_thread=issue, issue_thread=issue,
comment="test", comment="test",
timestamp=now + timedelta(seconds=3), timestamp=now + timedelta(seconds=4),
) )
match = ItemRelation.objects.create( match = ItemRelation.objects.create(
issue_thread=issue, issue_thread=issue,
item = self.item, item=self.item,
timestamp=now + timedelta(seconds=5), timestamp=now + timedelta(seconds=5),
) )
self.assertEqual('pending_new', issue.state) self.assertEqual('pending_new', issue.state)
self.assertEqual('test issue', issue.name) self.assertEqual('test issue', issue.name)
self.assertEqual(None, issue.assigned_to) self.assertEqual(self.user, issue.assigned_to)
self.assertEqual(36, len(issue.uuid)) self.assertEqual(36, len(issue.uuid))
response = self.client.get('/api/2/tickets/') response = self.client.get('/api/2/tickets/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@ -74,14 +79,16 @@ class IssueApiTest(TestCase):
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]['event'], "evt")
self.assertEqual(response.json()[0]['assigned_to'], None) self.assertEqual(response.json()[0]['assigned_to'], self.user.username)
self.assertEqual(response.json()[0]['uuid'], issue.uuid) self.assertEqual(response.json()[0]['uuid'], issue.uuid)
self.assertEqual(response.json()[0]['last_activity'], match.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) self.assertEqual(response.json()[0]['last_activity'], match.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'))
self.assertEqual(len(response.json()[0]['timeline']), 5) self.assertEqual(len(response.json()[0]['timeline']), 6)
self.assertEqual(response.json()[0]['timeline'][0]['type'], 'state') self.assertEqual(response.json()[0]['timeline'][0]['type'], 'state')
self.assertEqual(response.json()[0]['timeline'][1]['type'], 'mail') self.assertEqual(response.json()[0]['timeline'][1]['type'], 'mail')
self.assertEqual(response.json()[0]['timeline'][2]['type'], 'mail') self.assertEqual(response.json()[0]['timeline'][2]['type'], 'mail')
self.assertEqual(response.json()[0]['timeline'][3]['type'], 'comment') self.assertEqual(response.json()[0]['timeline'][3]['type'], 'assignment')
self.assertEqual(response.json()[0]['timeline'][4]['type'], 'comment')
self.assertEqual(response.json()[0]['timeline'][5]['type'], 'item_relation')
self.assertEqual(response.json()[0]['timeline'][1]['id'], mail1.id) self.assertEqual(response.json()[0]['timeline'][1]['id'], mail1.id)
self.assertEqual(response.json()[0]['timeline'][2]['id'], mail2.id) self.assertEqual(response.json()[0]['timeline'][2]['id'], mail2.id)
self.assertEqual(response.json()[0]['timeline'][3]['id'], comment.id) self.assertEqual(response.json()[0]['timeline'][3]['id'], comment.id)
@ -98,15 +105,19 @@ class IssueApiTest(TestCase):
self.assertEqual(response.json()[0]['timeline'][2]['body'], 'test') self.assertEqual(response.json()[0]['timeline'][2]['body'], 'test')
self.assertEqual(response.json()[0]['timeline'][2]['timestamp'], self.assertEqual(response.json()[0]['timeline'][2]['timestamp'],
mail2.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) mail2.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'))
self.assertEqual(response.json()[0]['timeline'][3]['comment'], 'test') self.assertEqual(response.json()[0]['timeline'][3]['assigned_to'], self.user.username)
self.assertEqual(response.json()[0]['timeline'][3]['timestamp'], self.assertEqual(response.json()[0]['timeline'][3]['timestamp'],
comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) assignment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'))
self.assertEqual(response.json()[0]['timeline'][4]['status'], 'possible') self.assertEqual(response.json()[0]['timeline'][4]['comment'], 'test')
self.assertEqual(response.json()[0]['timeline'][4]['timestamp'], self.assertEqual(response.json()[0]['timeline'][4]['timestamp'],
comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'))
self.assertEqual(response.json()[0]['timeline'][5]['status'], 'possible')
self.assertEqual(response.json()[0]['timeline'][5]['timestamp'],
match.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) match.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'))
self.assertEqual(response.json()[0]['timeline'][4]['item']['description'], "foo") self.assertEqual(response.json()[0]['timeline'][5]['item']['description'], "foo")
self.assertEqual(response.json()[0]['timeline'][4]['item']['event'], "evt") self.assertEqual(response.json()[0]['timeline'][5]['item']['event'], "evt")
self.assertEqual(response.json()[0]['timeline'][4]['item']['box'], "box1") self.assertEqual(response.json()[0]['timeline'][5]['item']['box'], "box1")
self.assertEqual(len(response.json()[0]['related_items']), 1)
self.assertEqual(response.json()[0]['related_items'][0]['description'], "foo") self.assertEqual(response.json()[0]['related_items'][0]['description'], "foo")
self.assertEqual(response.json()[0]['related_items'][0]['event'], "evt") self.assertEqual(response.json()[0]['related_items'][0]['event'], "evt")
self.assertEqual(response.json()[0]['related_items'][0]['box'], "box1") self.assertEqual(response.json()[0]['related_items'][0]['box'], "box1")
@ -303,16 +314,6 @@ class IssueApiTest(TestCase):
content_type='application/json') content_type='application/json')
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
# def test_post_comment(self):
# issue = IssueThread.objects.create(
# name="test issue",
# )
# response = self.client.post('/api/2/comments/', {'comment': 'test', 'issue_thread': issue.id})
# self.assertEqual(response.status_code, 201)
# self.assertEqual(response.json()['comment'], 'test')
# self.assertEqual(response.json()['issue_thread'], issue.id)
# self.assertEqual(response.json()['timestamp'], response.json()['timestamp'])
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",

View file

@ -12,13 +12,16 @@
field="description" field="description"
:validation-fn="str => str && str.length > 0" :validation-fn="str => str && str.length > 0"
/> />
<InputCombo <div class="form-group">
label="box" <label for="box">box</label>
:model="item" <InputCombo
nameKey="box" label="box"
uniqueKey="cid" :model="item"
:options="boxes" nameKey="box"
/> uniqueKey="cid"
:options="boxes"
/>
</div>
</div> </div>
</template> </template>

View file

@ -0,0 +1,222 @@
<template>
<ol class="timeline">
<li v-for="(item, index) in timeline" :key="index"
:class="{'timeline-item':true, 'extra-space': item.type === 'mail'}">
<span class="timeline-item-icon filled-icon" v-if="item.type === 'mail'">
<font-awesome-icon icon="envelope"/>
</span>
<span class="timeline-item-icon faded-icon" v-else-if="item.type === 'comment'">
<font-awesome-icon icon="comment"/>
</span>
<span class="timeline-item-icon faded-icon" v-else-if="item.type === 'state'"
:class="'bg-' + stateInfo(item.state).color">
<font-awesome-icon :icon="stateInfo(item.state).icon"/>
</span>
<span class="timeline-item-icon faded-icon" v-else-if="item.type === 'assignment'" :class="'bg-secondary'">
<font-awesome-icon icon="user"/>
</span>
<span class="timeline-item-icon faded-icon" v-else-if="item.type === 'item_relation'">
<font-awesome-icon icon="object-group"/>
</span>
<span class="timeline-item-icon faded-icon" v-else-if="item.type === 'shipping_voucher'">
<font-awesome-icon icon="truck"/>
</span>
<span class="timeline-item-icon faded-icon" v-else>
<font-awesome-icon icon="pen"/>
</span>
<TimelineMail v-if="item.type === 'mail'" :item="item"/>
<TimelineComment v-else-if="item.type === 'comment'" :item="item"/>
<TimelineStateChange v-else-if="item.type === 'state'" :item="item"/>
<TimelineAssignment v-else-if="item.type === 'assignment'" :item="item"/>
<TimelineRelatedItem v-else-if="item.type === 'item_relation'" :item="item"/>
<TimelineShippingVoucher v-else-if="item.type === 'shipping_voucher'" :item="item"/>
<p v-else>{{ item }}</p>
</li>
<li class="timeline-item">
<span class="timeline-item-icon | faded-icon">
<font-awesome-icon icon="comment"/>
</span>
<div class="new-comment card bg-dark">
<div class="">
<textarea placeholder="add comment..." v-model="newComment" class="form-control">
</textarea>
<AsyncButton class="btn btn-primary float-right" :task="addCommentAndClear">
<font-awesome-icon icon="comment"/>
Save Comment
</AsyncButton>
</div>
</div>
</li>
</ol>
</template>
<script>
import TimelineMail from "@/components/TimelineMail.vue";
import TimelineComment from "@/components/TimelineComment.vue";
import TimelineStateChange from "@/components/TimelineStateChange.vue";
import {mapActions, mapGetters} from "vuex";
import TimelineAssignment from "@/components/TimelineAssignment.vue";
import TimelineRelatedItem from "@/components/TimelineRelatedItem.vue";
import TimelineShippingVoucher from "@/components/TimelineShippingVoucher.vue";
import AsyncButton from "@/components/inputs/AsyncButton.vue";
export default {
name: 'Timeline',
components: {
TimelineShippingVoucher, AsyncButton,
TimelineRelatedItem, TimelineAssignment, TimelineStateChange, TimelineComment, TimelineMail
},
props: {
timeline: {
type: Array,
default: () => []
}
},
emits: ['sendMail', 'addComment'],
data: () => ({
newMail: "",
newComment: ""
}),
computed: {
...mapGetters(['stateInfo']),
newestMailSubject() {
const mail = this.timeline.filter(item => item.type === 'mail').pop();
return mail ? mail.subject : "";
},
},
methods: {
...mapActions(['sendMail', 'postComment']),
sendMailAndClear: async function () {
await this.sendMail(this.newMail);
this.newMail = "";
},
addCommentAndClear: async function () {
await this.postComment(this.newComment);
this.newComment = "";
}
}
};
</script>
<style lang="scss" scoped>
*,
*:before,
*:after {
box-sizing: border-box;
}
button,
input,
select,
textarea {
font: inherit;
}
a {
color: inherit;
}
img {
display: block;
max-width: 100%;
}
/* End basic CSS override */
.timeline {
width: 85%;
display: flex;
flex-direction: column;
padding: 32px 0 32px 32px;
border-left: 2px solid var(--gray);
font-size: 1.125rem;
margin: 0 auto;
}
.timeline-item {
display: flex;
gap: 24px;
& + * {
margin-top: 24px;
}
& + .extra-space {
margin-top: 48px;
}
}
.new-comment, .new-mail {
width: 100%;
textarea, input {
border: 1px solid var(--gray);
border-radius: 6px;
height: 5em;
padding: 8px 16px;
&::placeholder {
color: var(--gray-dark);
}
&:focus {
border-color: var(--gray-dark);
outline: 0; /* Don't actually do this */
box-shadow: 0 0 0 4px var(--dark);
}
}
}
.timeline-item-icon {
display: flex;
align-items: center;
justify-content: center;
width: 50px;
height: 50px;
border-radius: 50%;
margin-left: -57px;
flex-shrink: 0;
overflow: hidden;
svg {
width: 20px;
height: 20px;
}
&.faded-icon {
background-color: var(--secondary);
color: var(--light);
}
&.filled-icon {
background-color: var(--primary);
color: var(--light);
}
}
.button {
border: 0;
display: inline-flex;
vertical-align: middle;
margin-right: 4px;
margin-top: 12px;
align-items: center;
justify-content: center;
font-size: 1rem;
height: 32px;
padding: 0 8px;
background-color: var(--dark);
flex-shrink: 0;
cursor: pointer;
border-radius: 99em;
&:hover {
background-color: var(--gray);
}
}
</style>

View file

@ -1,6 +1,4 @@
<template> <template>
<div class="form-group">
<label :for="label">{{ label }}</label>
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<button <button
@ -34,7 +32,6 @@
</div> </div>
<Addon type="Combo Box" :is-valid="isValid"/> <Addon type="Combo Box" :is-valid="isValid"/>
</div> </div>
</div>
</template> </template>
<script> <script>

View file

@ -18,6 +18,7 @@ import Settings from "@/views/admin/Settings.vue";
import AccessControl from "@/views/admin/AccessControl.vue"; import AccessControl from "@/views/admin/AccessControl.vue";
import {default as BoxesAdmin} from "@/views/admin/Boxes.vue" import {default as BoxesAdmin} from "@/views/admin/Boxes.vue"
import Shipping from "@/views/admin/Shipping.vue"; import Shipping from "@/views/admin/Shipping.vue";
import Item from "@/views/Item.vue";
import Notifications from "@/views/admin/Notifications.vue"; import Notifications from "@/views/admin/Notifications.vue";
const routes = [ const routes = [
@ -30,7 +31,7 @@ const routes = [
{requiresAuth: true, requiresPermission: 'view_item'} {requiresAuth: true, requiresPermission: 'view_item'}
}, },
{ {
path: '/:event/item/:uid/', name: 'item', component: Items, meta: path: '/:event/item/:id/', name: 'item', component: Item, meta:
{requiresAuth: true, requiresPermission: 'view_item'} {requiresAuth: true, requiresPermission: 'view_item'}
}, },
{ {

View file

@ -151,13 +151,11 @@ const store = createStore({
setItems(state, {slug, items}) { setItems(state, {slug, items}) {
state.loadedItems[slug] = items; state.loadedItems[slug] = items;
state.loadedItems = {...state.loadedItems}; state.loadedItems = {...state.loadedItems};
console.log(state.loadedItems)
}, },
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;
state.loadedItems = {...state.loadedItems}; state.loadedItems = {...state.loadedItems};
console.log(state.loadedItems)
}, },
updateItem(state, updatedItem) { updateItem(state, updatedItem) {
const item = state.loadedItems[updatedItem.event ? updatedItem.event : 'none'].filter( const item = state.loadedItems[updatedItem.event ? updatedItem.event : 'none'].filter(
@ -173,13 +171,11 @@ const store = createStore({
setTickets(state, {slug, tickets}) { setTickets(state, {slug, tickets}) {
state.loadedTickets[slug] = tickets; state.loadedTickets[slug] = tickets;
state.loadedTickets = {...state.loadedTickets}; state.loadedTickets = {...state.loadedTickets};
console.log(state.loadedTickets)
}, },
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;
state.loadedTickets = {...state.loadedTickets}; state.loadedTickets = {...state.loadedTickets};
console.log(state.loadedTickets)
}, },
updateTicket(state, updatedTicket) { updateTicket(state, updatedTicket) {
const ticket = state.loadedTickets[updatedTicket.event ? updatedTicket.event : 'none'].filter( const ticket = state.loadedTickets[updatedTicket.event ? updatedTicket.event : 'none'].filter(

134
web/src/views/Item.vue Normal file
View file

@ -0,0 +1,134 @@
<template>
<AsyncLoader :loaded="!!item.id">
<div class="container-fluid px-xl-5 mt-3">
<div class="row">
<div class="col-xl-8 offset-xl-2">
<div class="card bg-dark text-light mb-2" id="filters">
<div class="card-header">
<h3>Item #{{ item.id }} - {{ item.description }}</h3>
</div>
<div>
{{ item }}
</div>
<ItemTimeline :timeline="item.timeline" @sendMail="handleMail" @addComment="handleComment"/>
<div class="card-footer d-flex justify-content-between">
<button class="btn btn-secondary mr-2" @click="$router.go(-1)">Back</button>
<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="openEditingModalWith(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>
<InputCombo
label="box"
:model="item"
nameKey="box"
uniqueKey="cid"
:options="boxes"
/>
<div class="btn-group">
<select class="form-control" v-model="selected_state">
<option v-for="status in state_options" :value="status.value">{{
status.text
}}
</option>
</select>
<button class="form-control btn btn-success"
@click="changeTicketStatus(item)"
:disabled="(selected_state == item.state)">
Change&nbsp;Status
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</AsyncLoader>
</template>
<script>
import {mapActions, mapGetters, mapState} from 'vuex';
import ItemTimeline from "@/components/ItemTimeline.vue";
import ClipboardButton from "@/components/inputs/ClipboardButton.vue";
import AsyncLoader from "@/components/AsyncLoader.vue";
import InputCombo from "@/components/inputs/InputCombo.vue";
import InputString from "@/components/inputs/InputString.vue";
export default {
name: 'Item',
components: {InputString, InputCombo, AsyncLoader, ClipboardButton, ItemTimeline},
data() {
return {
selected_state: null,
selected_assignee: null,
shipping_voucher_type: null,
}
},
computed: {
...mapState(['state_options', 'users']),
...mapGetters(['availableShippingVoucherTypes', 'getAllItems', 'route', 'getBoxes']),
item() {
const id = parseInt(this.route.params.id)
const ret = this.getAllItems.find(item => item.id === id);
return ret ? ret : {};
},
boxes() {
console.log(this.getBoxes);
return this.getBoxes.map(obj => ({cid: obj.cid, box: obj.name}));
}
},
methods: {
...mapActions(['deleteItem', 'markItemReturned', 'sendMail', 'updateTicketPartial', 'postComment']),
...mapActions(['loadTickets', 'fetchTicketStates', 'loadUsers', 'scheduleAfterInit']),
...mapActions(['claimShippingVoucher', 'fetchShippingVouchers', 'loadEventItems', 'loadBoxes']),
handleMail(mail) {
this.sendMail({
id: this.item.id,
message: mail
})
},
handleComment(comment) {
this.postComment({
id: this.item.id,
message: comment
})
},
changeTicketStatus(item) {
item.state = this.selected_state;
this.updateTicketPartial({
id: item.id,
state: this.selected_state,
})
},
//assignTicket(item) {
// item.assigned_to = this.selected_assignee;
// this.updateTicketPartial({
// id: item.id,
// assigned_to: this.selected_assignee
// })
//},
},
mounted() {
this.scheduleAfterInit(() => [Promise.all([this.loadEventItems(), this.loadBoxes()]).then(() => {
this.selected_state = this.item.state;
this.selected_assignee = this.item.assigned_to
})]);
}
};
</script>
<style scoped>
</style>

View file

@ -20,7 +20,7 @@
:columns="['uid', 'description', 'box']" :columns="['uid', 'description', 'box']"
:items="getEventItems" :items="getEventItems"
:keyName="'uid'" :keyName="'uid'"
@itemActivated="openLightboxModalWith($event)" @itemActivated="showItemDetail"
> >
<template #actions="{ item }"> <template #actions="{ item }">
<div class="btn-group"> <div class="btn-group">
@ -47,7 +47,7 @@
:items="getEventItems" :items="getEventItems"
:keyName="'uid'" :keyName="'uid'"
v-slot="{ item }" v-slot="{ item }"
@itemActivated="openLightboxModalWith($event)" @itemActivated="showItemDetail"
> >
<AuthenticatedImage v-if="item.file" cached <AuthenticatedImage v-if="item.file" cached
:src="`/media/2/256/${item.file}/`" :src="`/media/2/256/${item.file}/`"
@ -88,6 +88,7 @@ import {mapActions, mapGetters, mapState} from 'vuex';
import Lightbox from '../components/Lightbox'; import Lightbox from '../components/Lightbox';
import AuthenticatedImage from "@/components/AuthenticatedImage.vue"; import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
import AsyncLoader from "@/components/AsyncLoader.vue"; import AsyncLoader from "@/components/AsyncLoader.vue";
import router from "@/router";
export default { export default {
name: 'Items', name: 'Items',
@ -102,6 +103,9 @@ export default {
}, },
methods: { methods: {
...mapActions(['deleteItem', 'markItemReturned', 'loadEventItems', 'updateItem', 'scheduleAfterInit']), ...mapActions(['deleteItem', 'markItemReturned', 'loadEventItems', 'updateItem', 'scheduleAfterInit']),
showItemDetail(item) {
router.push({name: 'item', params: {id: item.id}});
},
openLightboxModalWith(item) { openLightboxModalWith(item) {
this.lightboxHash = item.file; this.lightboxHash = item.file;
}, },