diff --git a/core/authentication/admin.py b/core/authentication/admin.py index ed88ace..0024cb0 100644 --- a/core/authentication/admin.py +++ b/core/authentication/admin.py @@ -10,8 +10,5 @@ class ExtendedUserAdmin(UserAdmin): ordering = ('username',) filter_horizontal = ('groups', 'user_permissions', 'permissions') - def permissions(self, obj): - return ', '.join(obj.get_all_permissions()) - admin.site.register(ExtendedUser, ExtendedUserAdmin) diff --git a/core/files/media_v2.py b/core/files/media_v2.py index d62ee98..9cc8f23 100644 --- a/core/files/media_v2.py +++ b/core/files/media_v2.py @@ -33,7 +33,7 @@ def media_urls(request, hash): headers={ 'X-Accel-Redirect': f'/redirect_media/{hash_path}', 'Access-Control-Allow-Origin': '*', - 'Cache-Control': 'max-age=31536000, private', + 'Cache-Control': 'max-age=31536000, private, immutable', 'Expires': datetime.utcnow() + timedelta(days=365), 'Age': 0, 'ETag': file.hash, @@ -74,7 +74,7 @@ def thumbnail_urls(request, size, hash): headers={ 'X-Accel-Redirect': f'/redirect_thumbnail/{size}/{hash_path}', 'Access-Control-Allow-Origin': '*', - 'Cache-Control': 'max-age=31536000, private', + 'Cache-Control': 'max-age=31536000, private, immutable', 'Expires': datetime.utcnow() + timedelta(days=365), 'Age': 0, 'ETag': file.hash + "_" + str(size), diff --git a/core/files/models.py b/core/files/models.py index df46fd3..33a6265 100644 --- a/core/files/models.py +++ b/core/files/models.py @@ -90,4 +90,6 @@ class AbstractFile(models.Model): class File(AbstractFile): item = models.ForeignKey(Item, models.CASCADE, db_column='iid', null=True, blank=True, related_name='files') - pass + + def __str__(self): + return self.hash diff --git a/core/inventory/api_v1.py b/core/inventory/api_v1.py index 52c6ed0..d5a1e29 100644 --- a/core/inventory/api_v1.py +++ b/core/inventory/api_v1.py @@ -1,5 +1,4 @@ -from datetime import datetime - +from django.utils import timezone from django.urls import re_path from rest_framework import routers, viewsets, serializers from rest_framework.decorators import api_view, permission_classes, authentication_classes @@ -87,7 +86,7 @@ class ItemSerializer(serializers.ModelSerializer): def update(self, instance, validated_data): if 'returned' in validated_data: if validated_data['returned']: - validated_data['returned_at'] = datetime.now() + validated_data['returned_at'] = timezone.now() validated_data.pop('returned') if 'dataImage' in validated_data: file = File.objects.create(data=validated_data['dataImage']) diff --git a/core/inventory/models.py b/core/inventory/models.py index ca0aeb7..8b3d018 100644 --- a/core/inventory/models.py +++ b/core/inventory/models.py @@ -35,6 +35,9 @@ class Item(SoftDeleteModel): ('match_item', 'Can match item') ] + def __str__(self): + return '[' + str(self.uid) + ']' + self.description + class Container(SoftDeleteModel): cid = models.AutoField(primary_key=True) @@ -42,6 +45,9 @@ class Container(SoftDeleteModel): created_at = models.DateTimeField(blank=True, null=True) updated_at = models.DateTimeField(blank=True, null=True) + def __str__(self): + return '[' + str(self.cid) + ']' + self.name + class Event(models.Model): eid = models.AutoField(primary_key=True) @@ -53,3 +59,6 @@ class Event(models.Model): post_end = models.DateTimeField(blank=True, null=True) created_at = models.DateTimeField(null=True, auto_now_add=True) updated_at = models.DateTimeField(blank=True, null=True) + + def __str__(self): + return '[' + str(self.slug) + ']' + self.name diff --git a/core/inventory/tests/v2/test_items.py b/core/inventory/tests/v2/test_items.py index ef61333..056b38c 100644 --- a/core/inventory/tests/v2/test_items.py +++ b/core/inventory/tests/v2/test_items.py @@ -1,5 +1,4 @@ -from datetime import datetime - +from django.utils import timezone from django.test import TestCase, Client from django.contrib.auth.models import Permission from knox.models import AuthToken @@ -164,7 +163,7 @@ class ItemTestCase(TestCase): response = self.client.get(f'/api/2/{self.event.slug}/item/') self.assertEqual(response.status_code, 200) self.assertEqual(len(response.json()), 2) - item2.returned_at = datetime.now() + item2.returned_at = timezone.now() item2.save() response = self.client.get(f'/api/2/{self.event.slug}/item/') self.assertEqual(response.status_code, 200) diff --git a/core/mail/protocol.py b/core/mail/protocol.py index bc83230..cfd25ce 100644 --- a/core/mail/protocol.py +++ b/core/mail/protocol.py @@ -1,8 +1,8 @@ import logging import aiosmtplib -from asgiref.sync import sync_to_async from channels.layers import get_channel_layer +from channels.db import database_sync_to_async from django.core.files.base import ContentFile from mail.models import Email, EventAddress, EmailAttachment @@ -82,8 +82,7 @@ def make_reply(reply_email, references=None, event=None): return reply -async def send_smtp(message, log): - log.info('Sending message to %s' % message['To']) +async def send_smtp(message): await aiosmtplib.send(message, hostname="127.0.0.1", port=25, use_tls=False, start_tls=False) @@ -148,9 +147,9 @@ def parse_email_body(raw, log=None): attachments.append(attachment) if 'inline' in cdispo: body = body + f'' - log.info("Image", ctype, attachment.id) + log.info("Image %s %s", ctype, attachment.id) else: - log.info("Attachment", ctype, cdispo) + log.info("Attachment %s %s", ctype, cdispo) else: if parsed.get_content_type() == 'text/plain': body = parsed.get_payload() @@ -161,7 +160,7 @@ def parse_email_body(raw, log=None): soup = BeautifulSoup(body, 'html.parser') body = re.sub(r'([\r\n]+.?)*[\r\n]', r'\n', soup.get_text()).strip('\n') else: - log.warning("Unknown content type", parsed.get_content_type()) + log.warning("Unknown content type %s", parsed.get_content_type()) body = "Unknown content type" body = unescape_and_decode_quoted_printable(body) body = unescape_and_decode_base64(body) @@ -172,6 +171,7 @@ def parse_email_body(raw, log=None): return parsed, body, attachments +@database_sync_to_async def receive_email(envelope, log=None): parsed, body, attachments = parse_email_body(envelope.content, log) @@ -255,9 +255,10 @@ class LMTPHandler: content = None try: content = envelope.content - email, new, reply = await sync_to_async(receive_email)(envelope, log) + email, new, reply = await receive_email(envelope, log) log.info(f"Created email {email.id}") - systemevent = await sync_to_async(SystemEvent.objects.create)(type='email received', reference=email.id) + systemevent = await database_sync_to_async(SystemEvent.objects.create)(type='email received', + reference=email.id) log.info(f"Created system event {systemevent.id}") channel_layer = get_channel_layer() await channel_layer.group_send( @@ -266,14 +267,15 @@ class LMTPHandler: ) log.info(f"Sent message to frontend") if new and reply: - await send_smtp(reply, log) + log.info('Sending message to %s' % reply['To']) + await send_smtp(reply) log.info("Sent auto reply") return '250 Message accepted for delivery' except Exception as e: - import uuid - random_filename = 'mail-' + str(uuid.uuid4()) + from hashlib import sha256 + random_filename = 'mail-' + sha256(content).hexdigest() with open(random_filename, 'wb') as f: f.write(content) - log.error(type(e), e, f"Saved email to {random_filename}") + log.error(f"Saved email to {random_filename} because of error %s (%s)", e, type(e)) return '451 Internal server error' diff --git a/core/tickets/api_v2.py b/core/tickets/api_v2.py index 9b9855d..6557a98 100644 --- a/core/tickets/api_v2.py +++ b/core/tickets/api_v2.py @@ -47,8 +47,7 @@ def reply(request, pk): body=request.data['message'], in_reply_to=first_mail.reference, ) - log = logging.getLogger('mail.log') - async_to_sync(send_smtp)(make_reply(mail, references), log) + async_to_sync(send_smtp)(make_reply(mail, references)) return Response({'status': 'ok'}, status=status.HTTP_201_CREATED) diff --git a/core/tickets/models.py b/core/tickets/models.py index 7d90052..6dd42c3 100644 --- a/core/tickets/models.py +++ b/core/tickets/models.py @@ -64,6 +64,9 @@ class IssueThread(SoftDeleteModel): return self.assignments.create(assigned_to=value) + def __str__(self): + return '[' + str(self.id) + '][' + self.short_uuid() + '] ' + self.name + class Meta: permissions = [ ('send_mail', 'Can send mail'), @@ -91,6 +94,9 @@ class Comment(models.Model): comment = models.TextField() timestamp = models.DateTimeField(auto_now_add=True) + def __str__(self): + return str(self.issue_thread) + ' comment #' + str(self.id) + class StateChange(models.Model): id = models.AutoField(primary_key=True) @@ -98,9 +104,15 @@ class StateChange(models.Model): state = models.CharField(max_length=255, choices=STATE_CHOICES, default='pending_new') timestamp = models.DateTimeField(auto_now_add=True) + def __str__(self): + return str(self.issue_thread) + ' state change to ' + self.state + class Assignment(models.Model): id = models.AutoField(primary_key=True) issue_thread = models.ForeignKey(IssueThread, on_delete=models.CASCADE, related_name='assignments') assigned_to = models.ForeignKey(ExtendedUser, on_delete=models.CASCADE, related_name='assigned_tickets') timestamp = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return str(self.issue_thread) + ' assigned to ' + self.assigned_to.username diff --git a/web/src/App.vue b/web/src/App.vue index ede1d16..83a3bb2 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -28,7 +28,7 @@ export default { }), methods: { ...mapMutations(['removeToast', 'createToast', 'closeAddBoxModal', 'openAddBoxModal']), - ...mapActions(['loadEvents']), + ...mapActions(['loadEvents', 'scheduleAfterInit']), openAddItemModal() { this.addItemModalOpen = true; }, @@ -44,6 +44,7 @@ export default { }, created: function () { document.title = document.location.hostname; + this.scheduleAfterInit(() => [this.loadEvents()]); } }; diff --git a/web/src/components/AddItemModal.vue b/web/src/components/AddItemModal.vue index c90829f..a3c23fd 100644 --- a/web/src/components/AddItemModal.vue +++ b/web/src/components/AddItemModal.vue @@ -27,16 +27,19 @@ export default { computed: { ...mapState(['lastUsed']) }, - created() { - this.item = {box: this.lastUsed.box || '', cid: this.lastUsed.cid || ''}; - }, methods: { - ...mapActions(['postItem']), + ...mapActions(['postItem', 'loadBoxes', 'scheduleAfterInit']), saveNewItem() { this.postItem(this.item).then(() => { this.$emit('close'); }); } + }, + created() { + this.item = {box: this.lastUsed.box || '', cid: this.lastUsed.cid || ''}; + }, + mounted() { + this.scheduleAfterInit(() => [this.loadBoxes()]); } }; diff --git a/web/src/shared-state-plugin/index.js b/web/src/shared-state-plugin/index.js new file mode 100644 index 0000000..67397d0 --- /dev/null +++ b/web/src/shared-state-plugin/index.js @@ -0,0 +1,345 @@ +import {isProxy, toRaw} from 'vue'; + +export default (config) => { + if (!('isLoadedKey' in config)) { + throw new Error("isLoadedKey not defined in config"); + } + if (('asyncFetch' in config) && !('lastfetched' in config)) { + throw new Error("asyncFetch defined but lastfetched not defined in config"); + } + + if (config.debug) console.log('plugin created'); + + const clone = (obj) => { + if (isProxy(obj)) { + obj = toRaw(obj); + } + if (obj === null || typeof obj !== 'object') { + return obj; + } + if (obj.__proto__ === ({}).__proto__) { + return Object.assign({}, obj); + } + if (obj.__proto__ === [].__proto__) { + return obj.slice(); + } + return obj; + } + + const deepEqual = (a, b) => { + if (a === b) { + return true; + } + if (a === null || b === null) { + return false; + } + if (a.__proto__ === ({}).__proto__ && b.__proto__ === ({}).__proto__) { + + if (Object.keys(a).length !== Object.keys(b).length) { + return false; + } + for (let key in b) { + if (!(key in a)) { + return false; + } + } + for (let key in a) { + if (!(key in b)) { + return false; + } + if (!deepEqual(a[key], b[key])) { + return false; + } + } + return true; + } + if (a.__proto__ === [].__proto__ && b.__proto__ === [].__proto__) { + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (!deepEqual(a[i], b[i])) { + return false; + } + } + return true; + } + return false; + } + + const toRawRecursive = (obj) => { + if (isProxy(obj)) { + obj = toRaw(obj); + } + if (obj === null || typeof obj !== 'object') { + return obj; + } + if (obj.__proto__ === ({}).__proto__) { + const new_obj = {}; + for (let key in obj) { + new_obj[key] = toRawRecursive(obj[key]); + } + return new_obj; + } + if (obj.__proto__ === [].__proto__) { + return obj.map((item) => toRawRecursive(item)); + } + return obj; + } + + /** may only be called from worker */ + const worker_fun = function (self, ctx) { + /* globals WebSocket, SharedWorker, onconnect, onmessage, postMessage, close, location */ + + let intialized = false; + let state = {}; + let ports = []; + let notify_socket; + + const tryConnect = () => { + if (self.WebSocket === undefined) { + if (ctx.debug) console.log("no websocket support"); + return; + } + if (!notify_socket || notify_socket.readyState !== WebSocket.OPEN) { + // global location is not useful in worker loaded from data url + const scheme = ctx.location.protocol === "https:" ? "wss" : "ws"; + if (ctx.debug) console.log("connecting to", scheme + '://' + ctx.location.host + '/ws/2/notify/'); + notify_socket = new WebSocket(scheme + '://' + ctx.location.host + '/ws/2/notify/'); + notify_socket.onopen = (e) => { + if (ctx.debug) console.log("open", JSON.stringify(e)); + }; + notify_socket.onclose = (e) => { + if (ctx.debug) console.log("close", JSON.stringify(e)); + setTimeout(() => { + tryConnect(); + }, 1000); + }; + notify_socket.onerror = (e) => { + if (ctx.debug) console.log("error", JSON.stringify(e)); + setTimeout(() => { + tryConnect(); + }, 1000); + }; + notify_socket.onmessage = (e) => { + let data = JSON.parse(e.data); + if (ctx.debug) console.log("message", data); + //this.loadEventItems() + //this.loadTickets() + } + } + } + + const deepEqual = (a, b) => { + if (a === b) { + return true; + } + if (a === null || b === null) { + return false; + } + if (a.__proto__ === ({}).__proto__ && b.__proto__ === ({}).__proto__) { + + if (Object.keys(a).length !== Object.keys(b).length) { + return false; + } + for (let key in b) { + if (!(key in a)) { + return false; + } + } + for (let key in a) { + if (!(key in b)) { + return false; + } + if (!deepEqual(a[key], b[key])) { + return false; + } + } + return true; + } + if (a.__proto__ === [].__proto__ && b.__proto__ === [].__proto__) { + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (!deepEqual(a[i], b[i])) { + return false; + } + } + return true; + } + return false; + } + + const handle_message = (message_data, reply, others, all) => { + switch (message_data.type) { + case 'state_init': + if (!intialized) { + intialized = true; + state = message_data.state; + reply({type: 'state_init', first: true}); + } else { + reply({type: 'state_init', first: false, state: state}); + } + break; + case 'state_diff': + if (message_data.key in state) { + if (!deepEqual(state[message_data.key], message_data.old_value)) { + if (ctx.debug) console.log("state diff old value mismatch | state:", state[message_data.key], " old:", message_data.old_value); + } + if (!deepEqual(state[message_data.key], message_data.new_value)) { + if (ctx.debug) console.log("state diff changed | state:", state[message_data.key], " new:", message_data.new_value); + state[message_data.key] = message_data.new_value; + others(message_data); + } else { + if (ctx.debug) console.log("state diff no change | state:", state[message_data.key], " new:", message_data.new_value); + } + } else { + if (ctx.debug) console.log("state diff key not found", message_data.key); + } + break; + default: + if (ctx.debug) console.log("unknown message", message_data); + } + } + + onconnect = (connect_event) => { + const port = connect_event.ports[0]; + ports.push(port); + port.onmessage = (message_event) => { + const reply = (message_data) => { + port.postMessage(message_data); + } + const others = (message_data) => { + for (let i = 0; i < ports.length; i++) { + if (ports[i] !== port) { + ports[i].postMessage(message_data); + } + } + } + const all = (message_data) => { + for (let i = 0; i < ports.length; i++) { + ports[i].postMessage(message_data); + } + } + handle_message(message_event.data, reply, others, all); + } + port.start(); + if (ctx.debug) console.log("worker connected", JSON.stringify(connect_event)); + tryConnect(); + } + + if (ctx.debug) console.log("worker loaded"); + } + + const worker_context = { + location: { + protocol: location.protocol, host: location.host + }, bug: config.debug + } + const worker_code = '(' + worker_fun.toString() + ')(self,' + JSON.stringify(worker_context) + ')'; + const worker_url = 'data:application/javascript;base64,' + btoa(worker_code); + + const worker = new SharedWorker(worker_url, 'vuex-shared-state-plugin'); + worker.port.start(); + if (config.debug) console.log('worker started'); + + const updateWorkerState = (key, new_value, old_value = null) => { + if (new_value === old_value) { + if (config.debug) console.log('updateWorkerState: no change', key, new_value); + return; + } + if (new_value === undefined) { + if (config.debug) console.log('updateWorkerState: undefined', key, new_value); + return; + } + + worker.port.postMessage({ + type: 'state_diff', + key: key, + new_value: isProxy(new_value) ? toRawRecursive(new_value) : new_value, + old_value: isProxy(old_value) ? toRawRecursive(old_value) : old_value + }); + } + + const registerInitialState = (keys, local_state) => { + const value = keys.reduce((obj, key) => { + obj[key] = isProxy(local_state[key]) ? toRawRecursive(local_state[key]) : local_state[key]; + return obj; + }, {}); + if (config.debug) console.log('registerInitilState', value); + worker.port.postMessage({ + type: 'state_init', state: value + }); + } + + return (store) => { + + worker.port.onmessage = function (e) { + switch (e.data.type) { + case 'state_init': + if (config.debug) console.log('state_init', e.data); + if (e.data.first) { + if (config.debug) console.log('worker state initialized'); + } else { + for (let key in e.data.state) { + if (key in store.state) { + if (config.debug) console.log('worker state init received', key, clone(e.data.state[key])); + if (!deepEqual(store.state[key], e.data.state[key])) { + store.state[key] = e.data.state[key]; + } + } else { + if (config.debug) console.log("state init key not found", key); + } + } + } + store.state[config.isLoadedKey] = true; + if ('afterInit' in config) { + setTimeout(() => { + store.dispatch(config.afterInit); + }, 0); + } + break; + case 'state_diff': + if (config.debug) console.log('state_diff', e.data); + if (e.data.key in store.state) { + if (config.debug) console.log('worker state update', e.data.key, clone(e.data.new_value)); + //TODO this triggers the watcher again, but we don't want that + store.state[e.data.key] = e.data.new_value; + } else { + if (config.debug) console.log("state diff key not found", e.data.key); + } + break; + default: + if (config.debug) console.log("unknown message", e.data); + } + }; + + registerInitialState(config.state, store.state); + + if ('mutations' in config) { + store.subscribe((mutation, state) => { + if (mutation.type in config.mutations) { + console.log(mutation.type, mutation.payload); + console.log(state); + } + }); + } + /*if ('actions' in config) { + store.subscribeAction((action, state) => { + if (action.type in config.actions) { + console.log(action.type, action.payload); + console.log(state); + } + }); + }*/ + if ('state' in config) { + config.watch.forEach((member) => { + store.watch((state, getters) => state[member], (newValue, oldValue) => { + if (config.debug) console.log('watch', member, clone(newValue), clone(oldValue)); + updateWorkerState(member, newValue, oldValue); + }); + }); + } + }; +} \ No newline at end of file diff --git a/web/src/store.js b/web/src/store.js index 94bfc70..7ee6614 100644 --- a/web/src/store.js +++ b/web/src/store.js @@ -4,8 +4,9 @@ import router from './router'; import * as base64 from 'base-64'; import * as utf8 from 'utf8'; import {ticketStateColorLookup, ticketStateIconLookup, http} from "@/utils"; - +import sharedStatePlugin from "@/shared-state-plugin"; import persistentStatePlugin from "@/persistent-state-plugin"; +import {triggerRef} from "vue"; const store = createStore({ state: { @@ -20,6 +21,7 @@ const store = createStore({ groups: [], state_options: [], lastEvent: '37C3', + lastUsed: {}, remember: false, user: { username: null, @@ -30,7 +32,19 @@ const store = createStore({ }, thumbnailCache: {}, + fetchedData: { + events: 0, + items: 0, + boxes: 0, + tickets: 0, + users: 0, + groups: 0, + states: 0, + }, persistent_loaded: false, + shared_loaded: false, + afterInitHandlers: [], + showAddBoxModal: false, }, getters: { @@ -80,26 +94,33 @@ const store = createStore({ }, }, mutations: { + updateLastUsed(state, diff) { + state.lastUsed = {...state.lastUsed, ...diff}; + }, updateLastEvent(state, slug) { state.lastEvent = slug; }, replaceEvents(state, events) { state.events = events; + state.fetchedData = {...state.fetchedData, events: Date.now()}; }, replaceTicketStates(state, states) { state.state_options = states; + state.fetchedData = {...state.fetchedData, states: Date.now()}; }, 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()}; }, updateItem(state, updatedItem) { const item = state.loadedItems.filter(({uid}) => uid === updatedItem.uid)[0]; @@ -113,16 +134,21 @@ const store = createStore({ }, replaceTickets(state, tickets) { state.tickets = tickets; - }, - replaceUsers(state, users) { - state.users = users; - }, - replaceGroups(state, groups) { - state.groups = groups; + state.fetchedData = {...state.fetchedData, tickets: Date.now()}; }, updateTicket(state, updatedTicket) { const ticket = state.tickets.filter(({id}) => id === updatedTicket.id)[0]; Object.assign(ticket, updatedTicket); + //triggerRef(state.tickets); + state.tickets = [...state.tickets]; + }, + replaceUsers(state, users) { + state.users = users; + state.fetchedData = {...state.fetchedData, users: Date.now()}; + }, + replaceGroups(state, groups) { + state.groups = groups; + state.fetchedData = {...state.fetchedData, groups: Date.now()}; }, openAddBoxModal(state) { state.showAddBoxModal = true; @@ -227,6 +253,19 @@ const store = createStore({ } await Promise.all(promises); }, + async afterSharedInit({dispatch, state}) { + const handlers = state.afterInitHandlers; + state.afterInitHandlers = []; + await Promise.all(handlers.map(h => h()).flat()); + }, + scheduleAfterInit({dispatch, state}, handler) { + if (state.shared_loaded) { + Promise.all(handler()).then(() => { + }); + } else { + state.afterInitHandlers.push(handler); + } + }, async fetchImage({state}, url) { return await fetch(url, {headers: {'Authorization': `Token ${state.user.token}`}}); }, @@ -235,11 +274,15 @@ const store = createStore({ commit('setPermissions', data.permissions); }, async loadEvents({commit, state}) { + if (!state.user.token) return; + if (state.fetchedData.events > Date.now() - 1000 * 60 * 60 * 24) return; const {data, success} = await http.get('/2/events/', state.user.token); if (data && success) commit('replaceEvents', data); }, async fetchTicketStates({commit, state}) { + if (!state.user.token) return; + if (state.fetchedData.states > Date.now() - 1000 * 60 * 60 * 24) return; const {data, success} = await http.get('/2/tickets/states/', state.user.token); if (data && success) commit('replaceTicketStates', data); @@ -255,6 +298,8 @@ const store = createStore({ 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; @@ -279,6 +324,8 @@ const store = createStore({ commit('replaceLoadedItems', data); }, async loadBoxes({commit, state}) { + if (!state.user.token) return; + if (state.fetchedData.boxes > Date.now() - 1000 * 60 * 60 * 24) return; const {data, success} = await http.get('/2/boxes/', state.user.token); if (data && success) commit('replaceBoxes', data); @@ -315,6 +362,8 @@ const store = createStore({ commit('appendItem', data); }, async loadTickets({commit, state}) { + if (!state.user.token) return; + 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); @@ -322,6 +371,7 @@ const store = createStore({ 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'); } }, @@ -337,15 +387,20 @@ const store = createStore({ 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}) { + if (!state.user.token) return; + if (state.fetchedData.users > Date.now() - 1000 * 60 * 60 * 24) return; const {data, success} = await http.get('/2/users/', state.user.token); if (data && success) commit('replaceUsers', data); }, async loadGroups({commit, state}) { + if (!state.user.token) return; + if (state.fetchedData.groups > Date.now() - 1000 * 60 * 60 * 24) return; const {data, success} = await http.get('/2/groups/', state.user.token); if (data && success) commit('replaceGroups', data); @@ -368,8 +423,38 @@ const store = createStore({ "remember", "user", "events", + "lastUsed", ] }), + sharedStatePlugin({ + debug: true, + isLoadedKey: "shared_loaded", + clearingMutation: "logout", + afterInit: "afterSharedInit", + state: [ + "test", + "state_options", + "fetchedData", + "tickets", + "users", + "groups", + "loadedBoxes", + "loadedItems", + ], + watch: [ + "test", + "state_options", + "fetchedData", + "tickets", + "users", + "groups", + "loadedBoxes", + "loadedItems", + ], + mutations: [ + //"replaceTickets", + ], + }), ], }); diff --git a/web/src/views/Items.vue b/web/src/views/Items.vue index 2eec96f..5824999 100644 --- a/web/src/views/Items.vue +++ b/web/src/views/Items.vue @@ -93,7 +93,7 @@ export default { ...mapGetters(['layout']), }, methods: { - ...mapActions(['deleteItem', 'markItemReturned', 'loadEventItems', 'updateItem']), + ...mapActions(['deleteItem', 'markItemReturned', 'loadEventItems', 'updateItem', 'scheduleAfterInit']), openLightboxModalWith(item) { this.lightboxHash = item.file; }, @@ -115,7 +115,7 @@ export default { } }, mounted() { - this.loadEventItems(); + this.scheduleAfterInit(() => [this.loadEventItems()]); } }; diff --git a/web/src/views/Ticket.vue b/web/src/views/Ticket.vue index b8c0cf9..b4d1e0e 100644 --- a/web/src/views/Ticket.vue +++ b/web/src/views/Ticket.vue @@ -62,7 +62,7 @@ export default { }, methods: { ...mapActions(['deleteItem', 'markItemReturned', 'sendMail', 'updateTicketPartial', 'postComment']), - ...mapActions(['loadTickets', 'loadUsers', 'fetchTicketStates']), + ...mapActions(['loadTickets', 'fetchTicketStates', 'loadUsers', 'scheduleAfterInit']), handleMail(mail) { this.sendMail({ id: this.ticket.id, @@ -86,12 +86,10 @@ export default { id: ticket.id, assigned_to: ticket.assigned_to }) - } + }, }, - created() { - this.fetchTicketStates() - this.loadTickets() - this.loadUsers() + mounted() { + this.scheduleAfterInit(() => [this.fetchTicketStates(), this.loadTickets(), this.loadUsers()]); } }; diff --git a/web/src/views/Tickets.vue b/web/src/views/Tickets.vue index 0b31e4d..40b2a9c 100644 --- a/web/src/views/Tickets.vue +++ b/web/src/views/Tickets.vue @@ -65,7 +65,7 @@ export default { ...mapGetters(['stateInfo', 'getEventSlug', 'layout']), }, methods: { - ...mapActions(['loadTickets', 'fetchTicketStates']), + ...mapActions(['loadTickets', 'fetchTicketStates', 'scheduleAfterInit']), gotoDetail(ticket) { this.$router.push({name: 'ticket', params: {id: ticket.id}}); }, @@ -80,9 +80,8 @@ export default { }; } }, - created() { - this.fetchTicketStates(); - this.loadTickets(); + mounted() { + this.scheduleAfterInit(() => [this.fetchTicketStates(), this.loadTickets()]); } };