This commit is contained in:
j3d1 2024-11-28 19:20:36 +01:00
parent 1a3aab4d41
commit f9a7abdbb1
6 changed files with 58 additions and 40 deletions

View file

@ -1,15 +1,20 @@
from django.urls import re_path from django.urls import re_path
from django.contrib.auth.decorators import permission_required from django.contrib.auth.decorators import permission_required
from rest_framework import routers, viewsets from rest_framework import routers, viewsets, status
from rest_framework.decorators import api_view, permission_classes from rest_framework.decorators import api_view, permission_classes
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from inventory.models import Event, Container, Item from inventory.models import Event, Container, Item, Comment
from inventory.serializers import EventSerializer, ContainerSerializer, ItemSerializer, SearchResultSerializer from inventory.serializers import EventSerializer, ContainerSerializer, ItemSerializer, SearchResultSerializer, \
CommentSerializer
from base64 import b64decode from base64 import b64decode
from notify_sessions.models import SystemEvent
class EventViewSet(viewsets.ModelViewSet): class EventViewSet(viewsets.ModelViewSet):
serializer_class = EventSerializer serializer_class = EventSerializer
@ -71,6 +76,31 @@ def item(request, event_slug):
return Response(status=400) return Response(status=400)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
@permission_required('tickets.add_comment', raise_exception=True)
def add_comment(request, event_slug, id):
event = None
if event_slug != 'none':
event = Event.objects.get(slug=event_slug)
item = Item.objects.get(event=event, id=id)
if not request.user.has_event_perm(event, 'view_item'):
return Response(status=403)
if 'comment' not in request.data or request.data['comment'] == '':
return Response({'status': 'error', 'message': 'missing comment'}, status=status.HTTP_400_BAD_REQUEST)
comment = Comment.objects.create(
item=item,
comment=request.data['comment'],
)
systemevent = SystemEvent.objects.create(type='comment added', reference=comment.id)
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
'general', {"type": "generic.event", "name": "send_message_to_frontend", "event_id": systemevent.id,
"message": "comment added"}
)
return Response(CommentSerializer(comment).data, status=status.HTTP_201_CREATED)
@api_view(['GET', 'PUT', 'DELETE', 'PATCH']) @api_view(['GET', 'PUT', 'DELETE', 'PATCH'])
@permission_classes([IsAuthenticated]) @permission_classes([IsAuthenticated])
def item_by_id(request, event_slug, id): def item_by_id(request, event_slug, id):
@ -117,5 +147,6 @@ urlpatterns = router.urls + [
re_path(r'^(?P<event_slug>[\w-]+)/items/$', item, name='item'), re_path(r'^(?P<event_slug>[\w-]+)/items/$', item, name='item'),
re_path(r'^(?P<event_slug>[\w-]+)/items/(?P<query>[-A-Za-z0-9+/]*={0,3})/$', search_items, name='search_items'), re_path(r'^(?P<event_slug>[\w-]+)/items/(?P<query>[-A-Za-z0-9+/]*={0,3})/$', search_items, name='search_items'),
re_path(r'^(?P<event_slug>[\w-]+)/item/$', item, name='item'), re_path(r'^(?P<event_slug>[\w-]+)/item/$', item, name='item'),
re_path(r'^(?P<event_slug>[\w-]+)/item/(?P<id>\d+)/comment/$', add_comment, name='add_comment'),
re_path(r'^(?P<event_slug>[\w-]+)/item/(?P<id>\d+)/$', item_by_id, name='item_by_id'), re_path(r'^(?P<event_slug>[\w-]+)/item/(?P<id>\d+)/$', item_by_id, name='item_by_id'),
] ]

View file

@ -3,7 +3,7 @@ from rest_framework import serializers
from rest_framework.relations import SlugRelatedField from rest_framework.relations import SlugRelatedField
from files.models import File from files.models import File
from inventory.models import Event, Container, Item from inventory.models import Event, Container, Item, Comment
from inventory.shared_serializers import BasicItemSerializer from inventory.shared_serializers import BasicItemSerializer
from mail.models import EventAddress from mail.models import EventAddress
from tickets.shared_serializers import BasicIssueSerializer from tickets.shared_serializers import BasicIssueSerializer
@ -25,6 +25,7 @@ class EventSerializer(serializers.ModelSerializer):
dict['addresses'] = [EventAddress.objects.get_or_create(address=x)[0] for x in addresses] dict['addresses'] = [EventAddress.objects.get_or_create(address=x)[0] for x in addresses]
return dict return dict
class ContainerSerializer(serializers.ModelSerializer): class ContainerSerializer(serializers.ModelSerializer):
itemCount = serializers.SerializerMethodField() itemCount = serializers.SerializerMethodField()
@ -37,6 +38,18 @@ class ContainerSerializer(serializers.ModelSerializer):
return len(instance.items) return len(instance.items)
class CommentSerializer(serializers.ModelSerializer):
def validate(self, attrs):
if 'comment' not in attrs or attrs['comment'] == '':
raise serializers.ValidationError('comment cannot be empty')
return attrs
class Meta:
model = Comment
fields = ('id', 'comment', 'timestamp', 'item')
class ItemSerializer(BasicItemSerializer): class ItemSerializer(BasicItemSerializer):
timeline = serializers.SerializerMethodField() timeline = serializers.SerializerMethodField()
dataImage = serializers.CharField(write_only=True, required=False) dataImage = serializers.CharField(write_only=True, required=False)

0
core/testdata.py Normal file
View file

View file

@ -1,6 +1,5 @@
<template> <template>
<div class="timeline-item-wrapper"> <div class="timeline-item-wrapper">
<Lightbox v-if="lightboxHash" :hash="lightboxHash" @close="closeLightboxModal()"/>
<div class="timeline-item-description"> <div class="timeline-item-description">
<i class="avatar | small"> <i class="avatar | small">
<font-awesome-icon icon="user"/> <font-awesome-icon icon="user"/>
@ -23,7 +22,7 @@
</div> </div>
<div class="card-footer" v-if="item.attachments.length"> <div class="card-footer" v-if="item.attachments.length">
<ul> <ul>
<li v-for="attachment in item.attachments" @click="openLightboxModalWith(attachment)"> <li v-for="attachment in item.attachments" @click="openLightboxModalWith(attachment.hash)">
<AuthenticatedImage :src="`/media/2/256/${attachment.hash}/`" :alt="attachment.name" <AuthenticatedImage :src="`/media/2/256/${attachment.hash}/`" :alt="attachment.name"
v-if="attachment.mime_type.startsWith('image/')" cached/> v-if="attachment.mime_type.startsWith('image/')" cached/>
<AuthenticatedDataLink :href="`/media/2/${attachment.hash}/`" :download="attachment.name" <AuthenticatedDataLink :href="`/media/2/${attachment.hash}/`" :download="attachment.name"
@ -59,16 +58,11 @@
import AuthenticatedImage from "@/components/AuthenticatedImage.vue"; import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
import AuthenticatedDataLink from "@/components/AuthenticatedDataLink.vue"; import AuthenticatedDataLink from "@/components/AuthenticatedDataLink.vue";
import Lightbox from "@/components/Lightbox.vue"; import {mapMutations} from "vuex";
export default { export default {
name: 'TimelineMail', name: 'TimelineMail',
components: {Lightbox, AuthenticatedImage, AuthenticatedDataLink}, components: {AuthenticatedImage, AuthenticatedDataLink},
data() {
return {
lightboxHash: null,
}
},
props: { props: {
'item': { 'item': {
type: Object, type: Object,
@ -85,12 +79,7 @@ export default {
}, },
methods: { methods: {
openLightboxModalWith(attachment) { ...mapMutations(['openLightboxModalWith'])
this.lightboxHash = attachment.hash;
},
closeLightboxModal() { // Closes the editing modal and discards the edited copy of the item.
this.lightboxHash = null;
},
}, },
}; };
</script> </script>

View file

@ -1,6 +1,5 @@
<template> <template>
<div class="timeline-item-wrapper"> <div class="timeline-item-wrapper">
<Lightbox v-if="lightboxHash" :hash="lightboxHash" @close="closeLightboxModal()"/>
<div class="timeline-item-description"> <div class="timeline-item-description">
<i class="avatar | small"> <i class="avatar | small">
<font-awesome-icon icon="user"/> <font-awesome-icon icon="user"/>
@ -19,7 +18,7 @@
<AuthenticatedImage v-if="item.item.file" cached <AuthenticatedImage v-if="item.item.file" cached
:src="`/media/2/256/${item.item.file}/`" :src="`/media/2/256/${item.item.file}/`"
class="d-block card-img-left" class="d-block card-img-left"
@click="openLightboxModalWith(item.item)" @click="openLightboxModalWith(item.item.file)"
/> />
</div> </div>
<div class="col"> <div class="col">
@ -78,16 +77,11 @@
import AuthenticatedImage from "@/components/AuthenticatedImage.vue"; import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
import AuthenticatedDataLink from "@/components/AuthenticatedDataLink.vue"; import AuthenticatedDataLink from "@/components/AuthenticatedDataLink.vue";
import Lightbox from "@/components/Lightbox.vue"; import {mapMutations} from "vuex";
export default { export default {
name: 'TimelineRelatedItem', name: 'TimelineRelatedItem',
components: {Lightbox, AuthenticatedImage, AuthenticatedDataLink}, components: {AuthenticatedImage, AuthenticatedDataLink},
data() {
return {
lightboxHash: null,
}
},
props: { props: {
'item': { 'item': {
type: Object, type: Object,
@ -104,13 +98,8 @@ export default {
}, },
methods: { methods: {
openLightboxModalWith(attachment) { ...mapMutations(['openLightboxModalWith'])
this.lightboxHash = attachment.hash; }
},
closeLightboxModal() { // Closes the editing modal and discards the edited copy of the item.
this.lightboxHash = null;
},
},
}; };
</script> </script>

View file

@ -19,13 +19,9 @@
<script> <script>
import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
import AuthenticatedDataLink from "@/components/AuthenticatedDataLink.vue";
import Lightbox from "@/components/Lightbox.vue";
export default { export default {
name: 'TimelineRelatedTicket', name: 'TimelineRelatedTicket',
components: {Lightbox}, components: {},
props: { props: {
'item': { 'item': {
type: Object, type: Object,