Compare commits

...

2 commits

16 changed files with 500 additions and 50 deletions

View file

@ -10,8 +10,5 @@ class ExtendedUserAdmin(UserAdmin):
ordering = ('username',) ordering = ('username',)
filter_horizontal = ('groups', 'user_permissions', 'permissions') filter_horizontal = ('groups', 'user_permissions', 'permissions')
def permissions(self, obj):
return ', '.join(obj.get_all_permissions())
admin.site.register(ExtendedUser, ExtendedUserAdmin) admin.site.register(ExtendedUser, ExtendedUserAdmin)

View file

@ -33,7 +33,7 @@ def media_urls(request, hash):
headers={ headers={
'X-Accel-Redirect': f'/redirect_media/{hash_path}', 'X-Accel-Redirect': f'/redirect_media/{hash_path}',
'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Origin': '*',
'Cache-Control': 'max-age=31536000, private', 'Cache-Control': 'max-age=31536000, private, immutable',
'Expires': datetime.utcnow() + timedelta(days=365), 'Expires': datetime.utcnow() + timedelta(days=365),
'Age': 0, 'Age': 0,
'ETag': file.hash, 'ETag': file.hash,
@ -74,7 +74,7 @@ def thumbnail_urls(request, size, hash):
headers={ headers={
'X-Accel-Redirect': f'/redirect_thumbnail/{size}/{hash_path}', 'X-Accel-Redirect': f'/redirect_thumbnail/{size}/{hash_path}',
'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Origin': '*',
'Cache-Control': 'max-age=31536000, private', 'Cache-Control': 'max-age=31536000, private, immutable',
'Expires': datetime.utcnow() + timedelta(days=365), 'Expires': datetime.utcnow() + timedelta(days=365),
'Age': 0, 'Age': 0,
'ETag': file.hash + "_" + str(size), 'ETag': file.hash + "_" + str(size),

View file

@ -90,4 +90,6 @@ class AbstractFile(models.Model):
class File(AbstractFile): class File(AbstractFile):
item = models.ForeignKey(Item, models.CASCADE, db_column='iid', null=True, blank=True, related_name='files') item = models.ForeignKey(Item, models.CASCADE, db_column='iid', null=True, blank=True, related_name='files')
pass
def __str__(self):
return self.hash

View file

@ -1,5 +1,4 @@
from datetime import datetime from django.utils import timezone
from django.urls import re_path from django.urls import re_path
from rest_framework import routers, viewsets, serializers from rest_framework import routers, viewsets, serializers
from rest_framework.decorators import api_view, permission_classes, authentication_classes 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): def update(self, instance, validated_data):
if 'returned' in validated_data: if 'returned' in validated_data:
if validated_data['returned']: if validated_data['returned']:
validated_data['returned_at'] = datetime.now() validated_data['returned_at'] = timezone.now()
validated_data.pop('returned') validated_data.pop('returned')
if 'dataImage' in validated_data: if 'dataImage' in validated_data:
file = File.objects.create(data=validated_data['dataImage']) file = File.objects.create(data=validated_data['dataImage'])

View file

@ -35,6 +35,9 @@ class Item(SoftDeleteModel):
('match_item', 'Can match item') ('match_item', 'Can match item')
] ]
def __str__(self):
return '[' + str(self.uid) + ']' + self.description
class Container(SoftDeleteModel): class Container(SoftDeleteModel):
cid = models.AutoField(primary_key=True) cid = models.AutoField(primary_key=True)
@ -42,6 +45,9 @@ class Container(SoftDeleteModel):
created_at = models.DateTimeField(blank=True, null=True) created_at = models.DateTimeField(blank=True, null=True)
updated_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): class Event(models.Model):
eid = models.AutoField(primary_key=True) eid = models.AutoField(primary_key=True)
@ -53,3 +59,6 @@ class Event(models.Model):
post_end = models.DateTimeField(blank=True, null=True) post_end = models.DateTimeField(blank=True, null=True)
created_at = models.DateTimeField(null=True, auto_now_add=True) created_at = models.DateTimeField(null=True, auto_now_add=True)
updated_at = models.DateTimeField(blank=True, null=True) updated_at = models.DateTimeField(blank=True, null=True)
def __str__(self):
return '[' + str(self.slug) + ']' + self.name

View file

@ -1,5 +1,4 @@
from datetime import datetime 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
from knox.models import AuthToken from knox.models import AuthToken
@ -164,7 +163,7 @@ class ItemTestCase(TestCase):
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()), 2) self.assertEqual(len(response.json()), 2)
item2.returned_at = datetime.now() item2.returned_at = timezone.now()
item2.save() item2.save()
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)

View file

@ -1,8 +1,8 @@
import logging import logging
import aiosmtplib import aiosmtplib
from asgiref.sync import sync_to_async
from channels.layers import get_channel_layer from channels.layers import get_channel_layer
from channels.db import database_sync_to_async
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from mail.models import Email, EventAddress, EmailAttachment from mail.models import Email, EventAddress, EmailAttachment
@ -82,8 +82,7 @@ def make_reply(reply_email, references=None, event=None):
return reply return reply
async def send_smtp(message, log): async def send_smtp(message):
log.info('Sending message to %s' % message['To'])
await aiosmtplib.send(message, hostname="127.0.0.1", port=25, use_tls=False, start_tls=False) 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) attachments.append(attachment)
if 'inline' in cdispo: if 'inline' in cdispo:
body = body + f'<img src="cid:{attachment.id}">' body = body + f'<img src="cid:{attachment.id}">'
log.info("Image", ctype, attachment.id) log.info("Image %s %s", ctype, attachment.id)
else: else:
log.info("Attachment", ctype, cdispo) log.info("Attachment %s %s", ctype, cdispo)
else: else:
if parsed.get_content_type() == 'text/plain': if parsed.get_content_type() == 'text/plain':
body = parsed.get_payload() body = parsed.get_payload()
@ -161,7 +160,7 @@ def parse_email_body(raw, log=None):
soup = BeautifulSoup(body, 'html.parser') soup = BeautifulSoup(body, 'html.parser')
body = re.sub(r'([\r\n]+.?)*[\r\n]', r'\n', soup.get_text()).strip('\n') body = re.sub(r'([\r\n]+.?)*[\r\n]', r'\n', soup.get_text()).strip('\n')
else: 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 = "Unknown content type"
body = unescape_and_decode_quoted_printable(body) body = unescape_and_decode_quoted_printable(body)
body = unescape_and_decode_base64(body) body = unescape_and_decode_base64(body)
@ -172,6 +171,7 @@ def parse_email_body(raw, log=None):
return parsed, body, attachments return parsed, body, attachments
@database_sync_to_async
def receive_email(envelope, log=None): def receive_email(envelope, log=None):
parsed, body, attachments = parse_email_body(envelope.content, log) parsed, body, attachments = parse_email_body(envelope.content, log)
@ -255,9 +255,10 @@ class LMTPHandler:
content = None content = None
try: try:
content = envelope.content 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}") 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}") log.info(f"Created system event {systemevent.id}")
channel_layer = get_channel_layer() channel_layer = get_channel_layer()
await channel_layer.group_send( await channel_layer.group_send(
@ -266,14 +267,15 @@ class LMTPHandler:
) )
log.info(f"Sent message to frontend") log.info(f"Sent message to frontend")
if new and reply: 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") log.info("Sent auto reply")
return '250 Message accepted for delivery' return '250 Message accepted for delivery'
except Exception as e: except Exception as e:
import uuid from hashlib import sha256
random_filename = 'mail-' + str(uuid.uuid4()) random_filename = 'mail-' + sha256(content).hexdigest()
with open(random_filename, 'wb') as f: with open(random_filename, 'wb') as f:
f.write(content) 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' return '451 Internal server error'

View file

@ -47,8 +47,7 @@ def reply(request, pk):
body=request.data['message'], body=request.data['message'],
in_reply_to=first_mail.reference, in_reply_to=first_mail.reference,
) )
log = logging.getLogger('mail.log') async_to_sync(send_smtp)(make_reply(mail, references))
async_to_sync(send_smtp)(make_reply(mail, references), log)
return Response({'status': 'ok'}, status=status.HTTP_201_CREATED) return Response({'status': 'ok'}, status=status.HTTP_201_CREATED)

View file

@ -64,6 +64,9 @@ class IssueThread(SoftDeleteModel):
return return
self.assignments.create(assigned_to=value) self.assignments.create(assigned_to=value)
def __str__(self):
return '[' + str(self.id) + '][' + self.short_uuid() + '] ' + self.name
class Meta: class Meta:
permissions = [ permissions = [
('send_mail', 'Can send mail'), ('send_mail', 'Can send mail'),
@ -91,6 +94,9 @@ class Comment(models.Model):
comment = models.TextField() comment = models.TextField()
timestamp = models.DateTimeField(auto_now_add=True) timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
return str(self.issue_thread) + ' comment #' + str(self.id)
class StateChange(models.Model): class StateChange(models.Model):
id = models.AutoField(primary_key=True) 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') state = models.CharField(max_length=255, choices=STATE_CHOICES, default='pending_new')
timestamp = models.DateTimeField(auto_now_add=True) timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
return str(self.issue_thread) + ' state change to ' + self.state
class Assignment(models.Model): class Assignment(models.Model):
id = models.AutoField(primary_key=True) id = models.AutoField(primary_key=True)
issue_thread = models.ForeignKey(IssueThread, on_delete=models.CASCADE, related_name='assignments') 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') assigned_to = models.ForeignKey(ExtendedUser, on_delete=models.CASCADE, related_name='assigned_tickets')
timestamp = models.DateTimeField(auto_now_add=True) timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
return str(self.issue_thread) + ' assigned to ' + self.assigned_to.username

View file

@ -28,7 +28,7 @@ export default {
}), }),
methods: { methods: {
...mapMutations(['removeToast', 'createToast', 'closeAddBoxModal', 'openAddBoxModal']), ...mapMutations(['removeToast', 'createToast', 'closeAddBoxModal', 'openAddBoxModal']),
...mapActions(['loadEvents']), ...mapActions(['loadEvents', 'scheduleAfterInit']),
openAddItemModal() { openAddItemModal() {
this.addItemModalOpen = true; this.addItemModalOpen = true;
}, },
@ -44,6 +44,7 @@ export default {
}, },
created: function () { created: function () {
document.title = document.location.hostname; document.title = document.location.hostname;
this.scheduleAfterInit(() => [this.loadEvents()]);
} }
}; };
</script> </script>

View file

@ -27,16 +27,19 @@ export default {
computed: { computed: {
...mapState(['lastUsed']) ...mapState(['lastUsed'])
}, },
created() {
this.item = {box: this.lastUsed.box || '', cid: this.lastUsed.cid || ''};
},
methods: { methods: {
...mapActions(['postItem']), ...mapActions(['postItem', 'loadBoxes', 'scheduleAfterInit']),
saveNewItem() { saveNewItem() {
this.postItem(this.item).then(() => { this.postItem(this.item).then(() => {
this.$emit('close'); this.$emit('close');
}); });
} }
},
created() {
this.item = {box: this.lastUsed.box || '', cid: this.lastUsed.cid || ''};
},
mounted() {
this.scheduleAfterInit(() => [this.loadBoxes()]);
} }
}; };
</script> </script>

View file

@ -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);
});
});
}
};
}

View file

@ -4,8 +4,9 @@ import router from './router';
import * as base64 from 'base-64'; import * as base64 from 'base-64';
import * as utf8 from 'utf8'; import * as utf8 from 'utf8';
import {ticketStateColorLookup, ticketStateIconLookup, http} from "@/utils"; import {ticketStateColorLookup, ticketStateIconLookup, http} from "@/utils";
import sharedStatePlugin from "@/shared-state-plugin";
import persistentStatePlugin from "@/persistent-state-plugin"; import persistentStatePlugin from "@/persistent-state-plugin";
import {triggerRef} from "vue";
const store = createStore({ const store = createStore({
state: { state: {
@ -20,6 +21,7 @@ const store = createStore({
groups: [], groups: [],
state_options: [], state_options: [],
lastEvent: '37C3', lastEvent: '37C3',
lastUsed: {},
remember: false, remember: false,
user: { user: {
username: null, username: null,
@ -30,7 +32,19 @@ const store = createStore({
}, },
thumbnailCache: {}, thumbnailCache: {},
fetchedData: {
events: 0,
items: 0,
boxes: 0,
tickets: 0,
users: 0,
groups: 0,
states: 0,
},
persistent_loaded: false, persistent_loaded: false,
shared_loaded: false,
afterInitHandlers: [],
showAddBoxModal: false, showAddBoxModal: false,
}, },
getters: { getters: {
@ -80,26 +94,33 @@ const store = createStore({
}, },
}, },
mutations: { mutations: {
updateLastUsed(state, diff) {
state.lastUsed = {...state.lastUsed, ...diff};
},
updateLastEvent(state, slug) { updateLastEvent(state, slug) {
state.lastEvent = slug; state.lastEvent = slug;
}, },
replaceEvents(state, events) { replaceEvents(state, events) {
state.events = events; state.events = events;
state.fetchedData = {...state.fetchedData, events: Date.now()};
}, },
replaceTicketStates(state, states) { replaceTicketStates(state, states) {
state.state_options = states; state.state_options = states;
state.fetchedData = {...state.fetchedData, states: Date.now()};
}, },
changeView(state, {view, slug}) { changeView(state, {view, slug}) {
router.push({path: `/${slug}/${view}`}); router.push({path: `/${slug}/${view}`});
}, },
replaceLoadedItems(state, newItems) { replaceLoadedItems(state, newItems) {
state.loadedItems = 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}) { setItemCache(state, {slug, items}) {
state.itemCache[slug] = items; state.itemCache[slug] = items;
}, },
replaceBoxes(state, loadedBoxes) { replaceBoxes(state, loadedBoxes) {
state.loadedBoxes = loadedBoxes; state.loadedBoxes = loadedBoxes;
state.fetchedData = {...state.fetchedData, boxes: Date.now()};
}, },
updateItem(state, updatedItem) { updateItem(state, updatedItem) {
const item = state.loadedItems.filter(({uid}) => uid === updatedItem.uid)[0]; const item = state.loadedItems.filter(({uid}) => uid === updatedItem.uid)[0];
@ -113,16 +134,21 @@ const store = createStore({
}, },
replaceTickets(state, tickets) { replaceTickets(state, tickets) {
state.tickets = tickets; state.tickets = tickets;
}, state.fetchedData = {...state.fetchedData, tickets: Date.now()};
replaceUsers(state, users) {
state.users = users;
},
replaceGroups(state, groups) {
state.groups = groups;
}, },
updateTicket(state, updatedTicket) { updateTicket(state, updatedTicket) {
const ticket = state.tickets.filter(({id}) => id === updatedTicket.id)[0]; const ticket = state.tickets.filter(({id}) => id === updatedTicket.id)[0];
Object.assign(ticket, updatedTicket); 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) { openAddBoxModal(state) {
state.showAddBoxModal = true; state.showAddBoxModal = true;
@ -227,6 +253,19 @@ const store = createStore({
} }
await Promise.all(promises); 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) { async fetchImage({state}, url) {
return await fetch(url, {headers: {'Authorization': `Token ${state.user.token}`}}); return await fetch(url, {headers: {'Authorization': `Token ${state.user.token}`}});
}, },
@ -235,11 +274,15 @@ const store = createStore({
commit('setPermissions', data.permissions); commit('setPermissions', data.permissions);
}, },
async loadEvents({commit, state}) { 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); const {data, success} = await http.get('/2/events/', state.user.token);
if (data && success) if (data && success)
commit('replaceEvents', data); commit('replaceEvents', data);
}, },
async fetchTicketStates({commit, state}) { 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); const {data, success} = await http.get('/2/tickets/states/', state.user.token);
if (data && success) if (data && success)
commit('replaceTicketStates', data); commit('replaceTicketStates', data);
@ -255,6 +298,8 @@ const store = createStore({
router.push({path: `/${getters.getEventSlug}/items/`, query: {box}}); router.push({path: `/${getters.getEventSlug}/items/`, query: {box}});
}, },
async loadEventItems({commit, getters, state}) { async loadEventItems({commit, getters, state}) {
if (!state.user.token) return;
if (state.fetchedData.items > Date.now() - 1000 * 60 * 60 * 24) return;
try { try {
commit('replaceLoadedItems', []); commit('replaceLoadedItems', []);
const slug = getters.getEventSlug; const slug = getters.getEventSlug;
@ -279,6 +324,8 @@ const store = createStore({
commit('replaceLoadedItems', data); commit('replaceLoadedItems', data);
}, },
async loadBoxes({commit, state}) { 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); const {data, success} = await http.get('/2/boxes/', state.user.token);
if (data && success) if (data && success)
commit('replaceBoxes', data); commit('replaceBoxes', data);
@ -315,6 +362,8 @@ const store = createStore({
commit('appendItem', data); commit('appendItem', data);
}, },
async loadTickets({commit, state}) { 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); const {data, success} = await http.get('/2/tickets/', state.user.token);
if (data && success) if (data && success)
commit('replaceTickets', data); commit('replaceTickets', data);
@ -322,6 +371,7 @@ const store = createStore({
async sendMail({commit, dispatch, state}, {id, message}) { async sendMail({commit, dispatch, state}, {id, message}) {
const {data, success} = await http.post(`/2/tickets/${id}/reply/`, {message}, state.user.token); const {data, success} = await http.post(`/2/tickets/${id}/reply/`, {message}, state.user.token);
if (data && success) { if (data && success) {
state.fetchedData.tickets = 0;
await dispatch('loadTickets'); await dispatch('loadTickets');
} }
}, },
@ -337,15 +387,20 @@ const store = createStore({
async postComment({commit, dispatch, state}, {id, message}) { async postComment({commit, dispatch, state}, {id, message}) {
const {data, success} = await http.post(`/2/tickets/${id}/comment/`, {comment: message}, state.user.token); const {data, success} = await http.post(`/2/tickets/${id}/comment/`, {comment: message}, state.user.token);
if (data && success) { if (data && success) {
state.fetchedData.tickets = 0;
await dispatch('loadTickets'); await dispatch('loadTickets');
} }
}, },
async loadUsers({commit, state}) { 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); const {data, success} = await http.get('/2/users/', state.user.token);
if (data && success) if (data && success)
commit('replaceUsers', data); commit('replaceUsers', data);
}, },
async loadGroups({commit, state}) { 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); const {data, success} = await http.get('/2/groups/', state.user.token);
if (data && success) if (data && success)
commit('replaceGroups', data); commit('replaceGroups', data);
@ -368,8 +423,38 @@ const store = createStore({
"remember", "remember",
"user", "user",
"events", "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",
],
}),
], ],
}); });

View file

@ -93,7 +93,7 @@ export default {
...mapGetters(['layout']), ...mapGetters(['layout']),
}, },
methods: { methods: {
...mapActions(['deleteItem', 'markItemReturned', 'loadEventItems', 'updateItem']), ...mapActions(['deleteItem', 'markItemReturned', 'loadEventItems', 'updateItem', 'scheduleAfterInit']),
openLightboxModalWith(item) { openLightboxModalWith(item) {
this.lightboxHash = item.file; this.lightboxHash = item.file;
}, },
@ -115,7 +115,7 @@ export default {
} }
}, },
mounted() { mounted() {
this.loadEventItems(); this.scheduleAfterInit(() => [this.loadEventItems()]);
} }
}; };
</script> </script>

View file

@ -62,7 +62,7 @@ export default {
}, },
methods: { methods: {
...mapActions(['deleteItem', 'markItemReturned', 'sendMail', 'updateTicketPartial', 'postComment']), ...mapActions(['deleteItem', 'markItemReturned', 'sendMail', 'updateTicketPartial', 'postComment']),
...mapActions(['loadTickets', 'loadUsers', 'fetchTicketStates']), ...mapActions(['loadTickets', 'fetchTicketStates', 'loadUsers', 'scheduleAfterInit']),
handleMail(mail) { handleMail(mail) {
this.sendMail({ this.sendMail({
id: this.ticket.id, id: this.ticket.id,
@ -86,12 +86,10 @@ export default {
id: ticket.id, id: ticket.id,
assigned_to: ticket.assigned_to assigned_to: ticket.assigned_to
}) })
} },
}, },
created() { mounted() {
this.fetchTicketStates() this.scheduleAfterInit(() => [this.fetchTicketStates(), this.loadTickets(), this.loadUsers()]);
this.loadTickets()
this.loadUsers()
} }
}; };
</script> </script>

View file

@ -65,7 +65,7 @@ export default {
...mapGetters(['stateInfo', 'getEventSlug', 'layout']), ...mapGetters(['stateInfo', 'getEventSlug', 'layout']),
}, },
methods: { methods: {
...mapActions(['loadTickets', 'fetchTicketStates']), ...mapActions(['loadTickets', 'fetchTicketStates', 'scheduleAfterInit']),
gotoDetail(ticket) { gotoDetail(ticket) {
this.$router.push({name: 'ticket', params: {id: ticket.id}}); this.$router.push({name: 'ticket', params: {id: ticket.id}});
}, },
@ -80,9 +80,8 @@ export default {
}; };
} }
}, },
created() { mounted() {
this.fetchTicketStates(); this.scheduleAfterInit(() => [this.fetchTicketStates(), this.loadTickets()]);
this.loadTickets();
} }
}; };
</script> </script>