Compare commits

..

No commits in common. "9adaa62c893942c20c6854c59912c8d6177223a5" and "2c609427ec94b48a6336f53651693a432d68cd46" have entirely different histories.

13 changed files with 214 additions and 256 deletions

View file

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

View file

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

View file

@ -39,12 +39,10 @@ class ItemSerializer(serializers.ModelSerializer):
box = serializers.SerializerMethodField()
file = serializers.SerializerMethodField()
returned = serializers.SerializerMethodField(required=False)
event = serializers.SlugRelatedField(slug_field='slug', queryset=Event.objects.all(),
allow_null=True, required=False)
class Meta:
model = Item
fields = ['cid', 'box', 'uid', 'description', 'file', 'dataImage', 'returned', 'event']
fields = ['cid', 'box', 'uid', 'description', 'file', 'dataImage', 'returned']
read_only_fields = ['uid']
def get_cid(self, instance):

View file

@ -30,7 +30,7 @@ class ItemTestCase(TestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(),
[{'uid': 1, 'description': '1', 'box': 'BOX', 'cid': self.box.cid, 'file': None,
'returned': False, 'event': self.event.slug}])
'returned': False}])
def test_members_with_file(self):
import base64
@ -40,7 +40,7 @@ class ItemTestCase(TestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(),
[{'uid': 1, 'description': '1', 'box': 'BOX', 'cid': self.box.cid, 'file': file.hash,
'returned': False, 'event': self.event.slug}])
'returned': False}])
def test_multi_members(self):
Item.objects.create(container=self.box, event=self.event, description='1')
@ -55,7 +55,7 @@ class ItemTestCase(TestCase):
self.assertEqual(response.status_code, 201)
self.assertEqual(response.json(),
{'uid': 1, 'description': '1', 'box': 'BOX', 'cid': self.box.cid, 'file': None,
'returned': False, 'event': self.event.slug})
'returned': False})
self.assertEqual(len(Item.objects.all()), 1)
self.assertEqual(Item.objects.all()[0].uid, 1)
self.assertEqual(Item.objects.all()[0].description, '1')
@ -86,7 +86,7 @@ class ItemTestCase(TestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(),
{'uid': 1, 'description': '2', 'box': 'BOX', 'cid': self.box.cid, 'file': None,
'returned': False, 'event': self.event.slug})
'returned': False})
self.assertEqual(len(Item.objects.all()), 1)
self.assertEqual(Item.objects.all()[0].uid, 1)
self.assertEqual(Item.objects.all()[0].description, '2')

View file

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

View file

@ -1,7 +1,6 @@
from rest_framework import serializers
from authentication.models import ExtendedUser
from inventory.models import Event
from mail.api_v2 import AttachmentSerializer
from tickets.models import IssueThread, Comment, STATE_CHOICES, ShippingVoucher
from inventory.serializers import ItemSerializer
@ -42,19 +41,19 @@ class IssueSerializer(serializers.ModelSerializer):
last_activity = serializers.SerializerMethodField()
assigned_to = serializers.SlugRelatedField(slug_field='username', queryset=ExtendedUser.objects.all(),
allow_null=True, required=False)
event = serializers.SlugRelatedField(slug_field='slug', queryset=Event.objects.all(),
allow_null=True, required=False)
related_items = ItemSerializer(many=True, read_only=True)
class Meta:
model = IssueThread
fields = ('id', 'timeline', 'name', 'state', 'assigned_to', 'last_activity', 'uuid', 'related_items', 'event')
fields = ('id', 'timeline', 'name', 'state', 'assigned_to', 'last_activity', 'uuid', 'related_items')
read_only_fields = ('id', 'timeline', 'last_activity', 'uuid', 'related_items')
def to_internal_value(self, data):
ret = super().to_internal_value(data)
if 'state' in data:
ret['state'] = data['state']
# if 'assigned_to' in data:
# ret['assigned_to'] = data['assigned_to']
return ret
def validate(self, attrs):

View file

@ -3,7 +3,6 @@ from datetime import datetime, timedelta
from django.test import TestCase, Client
from authentication.models import ExtendedUser
from inventory.models import Event
from mail.models import Email, EmailAttachment
from tickets.models import IssueThread, StateChange, Comment
from django.contrib.auth.models import Permission
@ -17,7 +16,6 @@ class IssueApiTest(TestCase):
self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test')
self.user.user_permissions.add(*Permission.objects.all())
self.user.save()
self.event = Event.objects.create(slug='evt')
self.token = AuthToken.objects.create(user=self.user)
self.client = Client(headers={'Authorization': 'Token ' + self.token[1]})
@ -30,7 +28,6 @@ class IssueApiTest(TestCase):
now = datetime.now()
issue = IssueThread.objects.create(
name="test issue",
event=self.event,
)
mail1 = Email.objects.create(
subject='test',
@ -64,7 +61,6 @@ class IssueApiTest(TestCase):
self.assertEqual(response.json()[0]['id'], issue.id)
self.assertEqual(response.json()[0]['name'], "test issue")
self.assertEqual(response.json()[0]['state'], "pending_new")
self.assertEqual(response.json()[0]['event'], "evt")
self.assertEqual(response.json()[0]['assigned_to'], None)
self.assertEqual(response.json()[0]['uuid'], issue.uuid)
self.assertEqual(response.json()[0]['last_activity'], comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'))
@ -97,15 +93,12 @@ class IssueApiTest(TestCase):
now = datetime.now()
issue1 = IssueThread.objects.create(
name="test issue",
event=self.event,
)
issue2 = IssueThread.objects.create(
name="test issue",
event=self.event,
)
issue3 = IssueThread.objects.create(
name="test issue",
event=self.event,
)
mail1 = Email.objects.create(
subject='test',
@ -125,11 +118,8 @@ class IssueApiTest(TestCase):
self.assertEqual(200, response.status_code)
self.assertEqual(3, len(response.json()))
self.assertEqual(issue1.id, response.json()[0]['id'])
self.assertEqual("evt", response.json()[0]['event'])
self.assertEqual(issue2.id, response.json()[1]['id'])
self.assertEqual("evt", response.json()[1]['event'])
self.assertEqual(issue3.id, response.json()[2]['id'])
self.assertEqual("evt", response.json()[2]['event'])
self.assertEqual(issue1.state_changes.first().timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
response.json()[0]['last_activity'])
self.assertEqual(mail1.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
@ -163,7 +153,6 @@ class IssueApiTest(TestCase):
now = datetime.now()
issue = IssueThread.objects.create(
name="test issue",
event=self.event,
)
mail1 = Email.objects.create(
subject='test',
@ -200,7 +189,6 @@ class IssueApiTest(TestCase):
self.assertEqual(200, response.status_code)
self.assertEqual(1, len(response.json()))
self.assertEqual(issue.id, response.json()[0]['id'])
self.assertEqual("evt", response.json()[0]['event'])
self.assertEqual('pending_new', response.json()[0]['state'])
self.assertEqual('test issue', response.json()[0]['name'])
self.assertEqual(None, response.json()[0]['assigned_to'])
@ -242,14 +230,13 @@ class IssueApiTest(TestCase):
self.assertEqual(file2.hash, response.json()[0]['timeline'][1]['attachments'][1]['hash'])
def test_manual_creation(self):
response = self.client.post('/api/2/evt/tickets/manual/',
response = self.client.post('/api/2/tickets/manual/',
{'name': 'test issue', 'sender': 'test', 'recipient': 'test', 'body': 'test'},
content_type='application/json')
self.assertEqual(response.status_code, 201)
self.assertEqual(response.json()['state'], 'pending_new')
self.assertEqual(response.json()['name'], 'test issue')
self.assertEqual(response.json()['assigned_to'], None)
self.assertEqual("evt", response.json()['event'])
timeline = response.json()['timeline']
self.assertEqual(len(timeline), 2)
self.assertEqual(timeline[0]['type'], 'state')
@ -260,35 +247,9 @@ class IssueApiTest(TestCase):
self.assertEqual(timeline[1]['subject'], 'test issue')
self.assertEqual(timeline[1]['body'], 'test')
def test_manual_creation_none(self):
response = self.client.post('/api/2/none/tickets/manual/',
{'name': 'test issue', 'sender': 'test', 'recipient': 'test', 'body': 'test'},
content_type='application/json')
self.assertEqual(response.status_code, 201)
self.assertEqual(response.json()['state'], 'pending_new')
self.assertEqual(response.json()['name'], 'test issue')
self.assertEqual(response.json()['assigned_to'], None)
self.assertEqual(None, response.json()['event'])
timeline = response.json()['timeline']
self.assertEqual(len(timeline), 2)
self.assertEqual(timeline[0]['type'], 'state')
self.assertEqual(timeline[0]['state'], 'pending_new')
self.assertEqual(timeline[1]['type'], 'mail')
self.assertEqual(timeline[1]['sender'], 'test')
self.assertEqual(timeline[1]['recipient'], 'test')
self.assertEqual(timeline[1]['subject'], 'test issue')
self.assertEqual(timeline[1]['body'], 'test')
def test_manual_creation_invalid(self):
response = self.client.post('/api/2/foobar/tickets/manual/',
{'name': 'test issue', 'sender': 'test', 'recipient': 'test', 'body': 'test'},
content_type='application/json')
self.assertEqual(response.status_code, 400)
def test_post_comment_altenative(self):
issue = IssueThread.objects.create(
name="test issue",
event=self.event,
)
response = self.client.post(f'/api/2/tickets/{issue.id}/comment/', {'comment': 'test'})
self.assertEqual(response.status_code, 201)
@ -299,7 +260,6 @@ class IssueApiTest(TestCase):
def test_post_alt_comment_empty(self):
issue = IssueThread.objects.create(
name="test issue",
event=self.event,
)
response = self.client.post(f'/api/2/tickets/{issue.id}/comment/', {'comment': ''})
self.assertEqual(response.status_code, 400)
@ -307,7 +267,6 @@ class IssueApiTest(TestCase):
def test_state_change(self):
issue = IssueThread.objects.create(
name="test issue",
event=self.event,
)
response = self.client.patch(f'/api/2/tickets/{issue.id}/', {'state': 'pending_open'},
content_type='application/json')
@ -325,7 +284,6 @@ class IssueApiTest(TestCase):
def test_state_change_invalid_state(self):
issue = IssueThread.objects.create(
name="test issue",
event=self.event,
)
response = self.client.patch(f'/api/2/tickets/{issue.id}/', {'state': 'invalid'},
content_type='application/json')
@ -334,14 +292,12 @@ class IssueApiTest(TestCase):
def test_assign_user(self):
issue = IssueThread.objects.create(
name="test issue",
event=self.event,
)
response = self.client.patch(f'/api/2/tickets/{issue.id}/', {'assigned_to': self.user.username},
content_type='application/json')
self.assertEqual(200, response.status_code)
self.assertEqual('pending_new', response.json()['state'])
self.assertEqual('test issue', response.json()['name'])
self.assertEqual("evt", response.json()['event'])
self.assertEqual(self.user.username, response.json()['assigned_to'])
timeline = response.json()['timeline']
self.assertEqual(2, len(timeline))

View file

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

View file

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

View file

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

View file

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

View file

@ -80,11 +80,11 @@ export default {
}
},
computed: {
...mapState(['state_options', 'users']),
...mapGetters(['availableShippingVoucherTypes', 'getAllTickets', 'route']),
...mapState(['tickets', 'state_options', 'users']),
...mapGetters(['availableShippingVoucherTypes']),
ticket() {
const id = parseInt(this.route.params.id)
const ret = this.getAllTickets.find(ticket => ticket.id === id);
const id = parseInt(this.$route.params.id)
const ret = this.tickets.find(ticket => ticket.id === id);
return ret ? ret : {};
},
shippingEmail() {
@ -128,8 +128,7 @@ export default {
if (this.ticket.state == "pending_new"){
this.selected_state = "pending_open";
this.changeTicketStatus(this.ticket)
}
;
};
this.selected_state = this.ticket.state;
this.selected_assignee = this.ticket.assigned_to
})]);

View file

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