diff --git a/core/inventory/api_v2.py b/core/inventory/api_v2.py
index 64f9b27..326c049 100644
--- a/core/inventory/api_v2.py
+++ b/core/inventory/api_v2.py
@@ -26,13 +26,16 @@ class ContainerViewSet(viewsets.ModelViewSet):
class ItemViewSet(viewsets.ModelViewSet):
serializer_class = ItemSerializer
- def get_queryset(self):
- queryset = Item.objects.all()
+ def prefetch_queryset(self, queryset):
serializer = self.get_serializer_class()
if hasattr(serializer, 'Meta') and hasattr(serializer.Meta, 'prefetch_related_fields'):
queryset = queryset.prefetch_related(*serializer.Meta.prefetch_related_fields)
return queryset
+ def get_queryset(self):
+ queryset = Item.objects.all()
+ return self.prefetch_queryset(queryset)
+
def filter_items(items, query):
query_tokens = query.split(' ')
@@ -69,7 +72,7 @@ def item(request, event_slug):
if request.method == 'GET':
if not request.user.has_event_perm(event, 'view_item'):
return Response(status=403)
- return Response(ItemSerializer(vs.get_queryset().filter(event=event), many=True).data)
+ return Response(ItemSerializer(vs.prefetch_queryset(Item.objects.filter(event=event)), many=True).data)
elif request.method == 'POST':
if not request.user.has_event_perm(event, 'add_item'):
return Response(status=403)
@@ -108,7 +111,7 @@ def add_comment(request, event_slug, id):
def item_by_id(request, event_slug, id):
try:
event = Event.objects.get(slug=event_slug)
- item = ItemViewSet().get_queryset().get(event=event, id=id)
+ item = Item.objects.get(event=event, id=id)
if request.method == 'GET':
if not request.user.has_event_perm(event, 'view_item'):
return Response(status=403)
diff --git a/core/inventory/models.py b/core/inventory/models.py
index 10796e1..231094e 100644
--- a/core/inventory/models.py
+++ b/core/inventory/models.py
@@ -32,7 +32,11 @@ class Item(SoftDeleteModel):
@property
def container(self):
try:
- return self.container_history.order_by('-timestamp').first().container
+ history = sorted(self.container_history.all(), key=lambda x: x.timestamp, reverse=True)
+ if history:
+ return history[0].container
+ else:
+ return None
except AttributeError:
return None
diff --git a/core/inventory/serializers.py b/core/inventory/serializers.py
index 90a75aa..26a5be4 100644
--- a/core/inventory/serializers.py
+++ b/core/inventory/serializers.py
@@ -60,7 +60,12 @@ class ItemSerializer(BasicItemSerializer):
fields = ['cid', 'box', 'id', 'description', 'file', 'dataImage', 'returned', 'event', 'related_issues',
'timeline']
read_only_fields = ['id']
- prefetch_related_fields = ['comments', 'issue_relation_changes', 'container_history']
+ prefetch_related_fields = ['comments', 'issue_relation_changes', 'container_history',
+ 'container_history__container', 'files', 'event',
+ 'issue_relation_changes__issue_thread',
+ 'issue_relation_changes__issue_thread__event',
+ 'issue_relation_changes__issue_thread__state_changes',
+ 'issue_relation_changes__issue_thread__assignments']
def to_internal_value(self, data):
container = None
diff --git a/core/tickets/serializers.py b/core/tickets/serializers.py
index b382b38..50cdb72 100644
--- a/core/tickets/serializers.py
+++ b/core/tickets/serializers.py
@@ -48,7 +48,11 @@ class IssueSerializer(BasicIssueSerializer):
fields = ('id', 'timeline', 'name', 'state', 'assigned_to', 'last_activity', 'uuid', 'related_items', 'event')
read_only_fields = ('id', 'timeline', 'last_activity', 'uuid', 'related_items')
prefetch_related_fields = ['state_changes', 'comments', 'emails', 'emails__attachments', 'assignments',
- 'item_relation_changes', 'shipping_vouchers']
+ 'item_relation_changes', 'shipping_vouchers', 'item_relation_changes__item',
+ 'item_relation_changes__item__container_history', 'event',
+ 'item_relation_changes__item__container_history__container',
+ 'item_relation_changes__item__files', 'item_relation_changes__item__event',
+ 'item_relation_changes__item__event']
def to_internal_value(self, data):
ret = super().to_internal_value(data)
diff --git a/web/src/App.vue b/web/src/App.vue
index bd4956f..d2c9f7d 100644
--- a/web/src/App.vue
+++ b/web/src/App.vue
@@ -1,5 +1,6 @@
+
@@ -16,20 +17,22 @@ import {mapState, mapMutations, mapActions, mapGetters} from 'vuex';
import AddTicketModal from "@/components/AddTicketModal.vue";
import AddBoxModal from "@/components/AddBoxModal.vue";
import AddEventModal from "@/components/AddEventModal.vue";
+import Lightbox from "@/components/Lightbox.vue";
export default {
name: 'app',
- components: {AddBoxModal, AddEventModal, Navbar, AddItemModal, AddTicketModal},
+ components: {Lightbox, AddBoxModal, AddEventModal, Navbar, AddItemModal, AddTicketModal},
computed: {
- ...mapState(['loadedItems', 'layout', 'toasts', 'showAddBoxModal', 'showAddEventModal']),
+ ...mapState(['loadedItems', 'layout', 'toasts', 'showAddBoxModal', 'showAddEventModal', 'lightboxHash']),
...mapGetters(['isLoggedIn']),
},
data: () => ({
addItemModalOpen: false,
- addTicketModalOpen: false
+ addTicketModalOpen: false,
}),
methods: {
- ...mapMutations(['removeToast', 'createToast', 'closeAddBoxModal', 'openAddBoxModal', 'closeAddEventModal']),
+ ...mapMutations(['removeToast', 'createToast', 'closeAddBoxModal', 'openAddBoxModal', 'closeAddEventModal',
+ 'openLightboxModalWith']),
...mapActions(['loadEvents', 'scheduleAfterInit']),
openAddItemModal() {
this.addItemModalOpen = true;
@@ -42,7 +45,7 @@ export default {
},
closeAddTicketModal() {
this.addTicketModalOpen = false;
- }
+ },
},
created: function () {
document.title = document.location.hostname;
diff --git a/web/src/components/AuthenticatedImage.vue b/web/src/components/AuthenticatedImage.vue
index 9e1a963..8b463b0 100644
--- a/web/src/components/AuthenticatedImage.vue
+++ b/web/src/components/AuthenticatedImage.vue
@@ -42,19 +42,27 @@ export default {
url: this.src,
data: this.image_data
});
+ },
+ deferImage() {
+ setTimeout(() => {
+ if (this.cached) {
+ const c = this.getThumbnail(this.src);
+ if (c) {
+ this.image_data = c;
+ return;
+ }
+ }
+ this.loadImage();
+ }, 0);
+ }
+ },
+ watch: {
+ src: function (newVal, oldVal) {
+ this.deferImage()
}
},
mounted() {
- setTimeout(() => {
- if (this.cached) {
- const c = this.getThumbnail(this.src);
- if (c) {
- this.image_data = c;
- return;
- }
- }
- this.loadImage();
- }, 0);
+ this.deferImage();
}
}
\ No newline at end of file
diff --git a/web/src/components/CollapsableCards.vue b/web/src/components/CollapsableCards.vue
index d1edab7..d38206a 100644
--- a/web/src/components/CollapsableCards.vue
+++ b/web/src/components/CollapsableCards.vue
@@ -52,7 +52,7 @@ export default {
};
},
created() {
- const query = this.$router.currentRoute ? (this.$router.currentRoute.query ? this.$router.currentRoute.query.collapsed : null) : null;
+ const query = this.route ? (this.route.query ? this.route.query.collapsed : null) : null;
if (query !== null && query !== undefined) {
this.collapsed = this.unpackInt(parseInt(query), this.sections.length);
} else {
@@ -84,8 +84,8 @@ export default {
const encoded = this.packInt(this.collapsed).toString()
if (this.route.query.collapsed !== encoded)
this.$router.push({
- ...this.$router.currentRoute,
- query: {...this.$router.currentRoute.query, collapsed: encoded}
+ ...this.route,
+ query: {...this.route.query, collapsed: encoded}
});
},
deep: true,
diff --git a/web/src/components/EditItem.vue b/web/src/components/EditItem.vue
index 9bea6f9..833bfd5 100644
--- a/web/src/components/EditItem.vue
+++ b/web/src/components/EditItem.vue
@@ -12,13 +12,16 @@
field="description"
:validation-fn="str => str && str.length > 0"
/>
-
+
+
+
+
diff --git a/web/src/components/Timeline.vue b/web/src/components/Timeline.vue
index 88bfa94..41e1274 100644
--- a/web/src/components/Timeline.vue
+++ b/web/src/components/Timeline.vue
@@ -21,6 +21,9 @@
+
+
+
@@ -30,40 +33,15 @@
+
+
{{ item }}
-
-
-
-
+
-
-
-
-
+
@@ -78,12 +56,20 @@ import TimelineAssignment from "@/components/TimelineAssignment.vue";
import TimelineRelatedItem from "@/components/TimelineRelatedItem.vue";
import TimelineShippingVoucher from "@/components/TimelineShippingVoucher.vue";
import AsyncButton from "@/components/inputs/AsyncButton.vue";
+import TimelinePlacement from "@/components/TimelinePlacement.vue";
+import TimelineRelatedTicket from "@/components/TimelineRelatedTicket.vue";
export default {
name: 'Timeline',
components: {
- TimelineShippingVoucher, AsyncButton,
- TimelineRelatedItem, TimelineAssignment, TimelineStateChange, TimelineComment, TimelineMail
+ TimelineRelatedTicket,
+ TimelinePlacement,
+ TimelineShippingVoucher,
+ TimelineRelatedItem,
+ TimelineAssignment,
+ TimelineStateChange,
+ TimelineComment,
+ TimelineMail
},
props: {
timeline: {
@@ -91,33 +77,13 @@ export default {
default: () => []
}
},
- emits: ['sendMail', 'addComment'],
- data: () => ({
- newMail: "",
- newComment: ""
- }),
computed: {
- ...mapGetters(['stateInfo']),
- newestMailSubject() {
- const mail = this.timeline.filter(item => item.type === 'mail').pop();
- return mail ? mail.subject : "";
- },
+ ...mapGetters(['stateInfo'])
},
- methods: {
- ...mapActions(['sendMail', 'postComment']),
- sendMailAndClear: async function () {
- await this.sendMail(this.newMail);
- this.newMail = "";
- },
- addCommentAndClear: async function () {
- await this.postComment(this.newComment);
- this.newComment = "";
- }
- }
};
-
\ No newline at end of file
diff --git a/web/src/components/TimelineRelatedItem.vue b/web/src/components/TimelineRelatedItem.vue
index c17a3a8..2670215 100644
--- a/web/src/components/TimelineRelatedItem.vue
+++ b/web/src/components/TimelineRelatedItem.vue
@@ -1,12 +1,15 @@
-
- linked item #{{ item.item.uid }} on as {{ item.status }}
+ linked item #{{
+ item.item.id
+ }} on as {{
+ item.status
+ }}
-
uid: {{ item.item.uid }} box: {{ item.item.box }}
- {{ item.item.description }}
-
+ id: {{ item.item.id }} box: {{ item.item.box }}
+
+ {{ item.item.description }}
+
-
@@ -72,16 +38,11 @@
import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
import AuthenticatedDataLink from "@/components/AuthenticatedDataLink.vue";
-import Lightbox from "@/components/Lightbox.vue";
+import {mapMutations} from "vuex";
export default {
name: 'TimelineRelatedItem',
- components: {Lightbox, AuthenticatedImage, AuthenticatedDataLink},
- data() {
- return {
- lightboxHash: null,
- }
- },
+ components: {AuthenticatedImage, AuthenticatedDataLink},
props: {
'item': {
type: Object,
@@ -98,13 +59,8 @@ export default {
},
methods: {
- openLightboxModalWith(attachment) {
- this.lightboxHash = attachment.hash;
- },
- closeLightboxModal() { // Closes the editing modal and discards the edited copy of the item.
- this.lightboxHash = null;
- },
- },
+ ...mapMutations(['openLightboxModalWith'])
+ }
};
diff --git a/web/src/components/TimelineRelatedTicket.vue b/web/src/components/TimelineRelatedTicket.vue
new file mode 100644
index 0000000..694a4e0
--- /dev/null
+++ b/web/src/components/TimelineRelatedTicket.vue
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+ linked ticket #{{ item.issue_thread.id }} on
+ as
+ {{ item.status }}
+
+
+
+
+ Ticket #{{ item.issue_thread.id }} - {{ item.issue_thread.name }}
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/src/components/inputs/InputCombo.vue b/web/src/components/inputs/InputCombo.vue
index 50b5459..fc64d42 100644
--- a/web/src/components/inputs/InputCombo.vue
+++ b/web/src/components/inputs/InputCombo.vue
@@ -1,39 +1,36 @@
-
-
-
-
-
-
-
-
-
diff --git a/web/src/router.js b/web/src/router.js
index bad2970..eb059c8 100644
--- a/web/src/router.js
+++ b/web/src/router.js
@@ -1,10 +1,11 @@
import {createRouter, createWebHistory} from 'vue-router'
import store from '@/store';
-import Items from './views/Items';
-import Boxes from './views/Boxes';
-import Files from './views/Files';
-import HowTo from './views/HowTo';
+import Item from "@/views/Item.vue";
+import Items from '@/views/Items';
+import Boxes from '@/views/Boxes';
+import Files from '@/views/Files';
+import HowTo from '@/views/HowTo';
import Login from '@/views/Login.vue';
import Register from '@/views/Register.vue';
import Dashboard from "@/views/admin/Dashboard.vue";
@@ -27,7 +28,7 @@ const routes = [
{requiresAuth: true, requiresPermission: 'view_item'}
},
{
- path: '/:event/item/:uid/', name: 'item', component: Items, meta:
+ path: '/:event/item/:id/', name: 'item', component: Item, meta:
{requiresAuth: true, requiresPermission: 'view_item'}
},
{
diff --git a/web/src/store.js b/web/src/store.js
index 60dba61..2f5a60d 100644
--- a/web/src/store.js
+++ b/web/src/store.js
@@ -34,6 +34,7 @@ const store = createStore({
expiry: null,
},
+ lightboxHash: null,
thumbnailCache: {},
fetchedData: {
events: 0,
@@ -145,38 +146,34 @@ const store = createStore({
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[updatedItem.event ? updatedItem.event : 'none'].filter(
+ ({id}) => id === updatedItem.id)[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[item.event ? item.event : 'none'] = state.loadedItems[item.event].filter(it => it !== item);
},
appendItem(state, item) {
- state.loadedItems[item.event?item.event:'none'].push(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)
},
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)
},
updateTicket(state, updatedTicket) {
- const ticket = state.loadedTickets[updatedTicket.event?updatedTicket.event:'none'].filter(
+ const ticket = state.loadedTickets[updatedTicket.event ? updatedTicket.event : 'none'].filter(
({id}) => id === updatedTicket.id)[0];
Object.assign(ticket, updatedTicket);
state.loadedTickets = {...state.loadedTickets};
@@ -189,6 +186,9 @@ const store = createStore({
state.groups = groups;
state.fetchedData = {...state.fetchedData, groups: Date.now()};
},
+ openLightboxModalWith(state, hash) {
+ state.lightboxHash = hash;
+ },
openAddBoxModal(state) {
state.showAddBoxModal = true;
},
@@ -336,14 +336,13 @@ const store = createStore({
const {data, success} = await getters.session.delete(`/2/events/${event_id}/`);
if (success) {
await dispatch('loadEvents')
- commit('replaceEvents', [...state.events.filter(e => e.eid !== event_id)])
+ commit('replaceEvents', [...state.events.filter(e => e.id !== event_id)])
}
},
async updateEvent({commit, dispatch, state}, {id, partial_event}){
- console.log(id, partial_event);
const {data, success} = await http.patch(`/2/events/${id}/`, partial_event, state.user.token);
if (success) {
- commit('replaceEvents', [...state.events.filter(e => e.eid !== id), data])
+ commit('replaceEvents', [...state.events.filter(e => e.id !== id), data])
}
},
async fetchTicketStates({commit, state, getters}) {
@@ -354,7 +353,6 @@ const store = createStore({
},
async changeEvent({dispatch, getters, commit}, eventName) {
await router.push({path: `/${eventName.slug}/${getters.getActiveView}/`});
- //dispatch('loadEventItems');
},
async changeView({getters}, link) {
await router.push({path: `/${getters.getEventSlug}/${link.path}/`});
@@ -405,16 +403,16 @@ const store = createStore({
async updateItem({commit, getters, state}, item) {
const {
data, success
- } = await getters.session.put(`/2/${getters.getEventSlug}/item/${item.uid}/`, item);
+ } = await getters.session.put(`/2/${getters.getEventSlug}/item/${item.id}/`, item);
commit('updateItem', data);
},
async markItemReturned({commit, getters, state}, item) {
- await getters.session.patch(`/2/${getters.getEventSlug}/item/${item.uid}/`, {returned: true},
+ await getters.session.patch(`/2/${getters.getEventSlug}/item/${item.id}/`, {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 getters.session.delete(`/2/${getters.getEventSlug}/item/${item.id}/`, item);
commit('removeItem', item);
},
async postItem({commit, getters, state}, item) {
diff --git a/web/src/views/Item.vue b/web/src/views/Item.vue
new file mode 100644
index 0000000..91cbb61
--- /dev/null
+++ b/web/src/views/Item.vue
@@ -0,0 +1,181 @@
+
+
+
+
+
+
+
+
+
+
id: {{ item.id }} box: {{ item.box }}
+ {{ item.description }}
+
+
+
+
+
+
+
+
+
+
Related
+
+
+
+
+ Ticket #{{ issue.id }} - {{ issue.name }}
+
+
state: {{ issue.state }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/Items.vue b/web/src/views/Items.vue
index 2f9e5ed..21b11d1 100644
--- a/web/src/views/Items.vue
+++ b/web/src/views/Items.vue
@@ -1,26 +1,13 @@
-
-
-
-
-
-
-
-
-
-
@@ -43,11 +30,11 @@
openLightboxModalWith(item.file)"
>
{{ item.description }}
-
uid: {{ item.uid }} box: {{ item.box }}
+
id: {{ item.id }} box: {{ item.box }}
-
@@ -84,10 +71,10 @@ import Table from '@/components/Table';
import Cards from '@/components/Cards';
import Modal from '@/components/Modal';
import EditItem from '@/components/EditItem';
-import {mapActions, mapGetters, mapState} from 'vuex';
-import Lightbox from '../components/Lightbox';
+import {mapActions, mapGetters, mapMutations} from 'vuex';
import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
import AsyncLoader from "@/components/AsyncLoader.vue";
+import router from "@/router";
export default {
name: 'Items',
@@ -95,28 +82,15 @@ export default {
lightboxHash: null,
editingItem: null,
}),
- components: {AsyncLoader, AuthenticatedImage, Lightbox, Table, Cards, Modal, EditItem},
+ components: {AsyncLoader, AuthenticatedImage, Table, Cards, Modal, EditItem},
computed: {
- ...mapState([]),
...mapGetters(['getEventItems', 'isItemsLoaded', 'layout']),
},
methods: {
...mapActions(['deleteItem', 'markItemReturned', 'loadEventItems', 'updateItem', 'scheduleAfterInit']),
- openLightboxModalWith(item) {
- this.lightboxHash = item.file;
- },
- closeLightboxModal() { // Closes the editing modal and discards the edited copy of the item.
- this.lightboxHash = null;
- },
- openEditingModalWith(item) { // Opens the editing modal with a copy of the selected item.
- this.editingItem = item;
- },
- closeEditingModal() {
- this.editingItem = null;
- },
- saveEditingItem() { // Saves the edited copy of the item.
- this.updateItem(this.editingItem);
- this.closeEditingModal();
+ ...mapMutations(['openLightboxModalWith']),
+ showItemDetail(item) {
+ router.push({name: 'item', params: {id: item.id}});
},
confirm(message) {
return window.confirm(message);
diff --git a/web/src/views/Ticket.vue b/web/src/views/Ticket.vue
index d08fb2e..dd0b413 100644
--- a/web/src/views/Ticket.vue
+++ b/web/src/views/Ticket.vue
@@ -1,5 +1,5 @@
-
+
@@ -7,7 +7,42 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -58,25 +96,53 @@
+
+
+
+
Related
+
+
+
+
+
id: {{ item.id }} box: {{
+ item.box
+ }}
+
+ {{ item.description }}
+
+
+
+
+
+