stash
All checks were successful
/ test (push) Successful in 56s

This commit is contained in:
j3d1 2024-11-28 18:55:18 +01:00
parent 401b71571f
commit 1a3aab4d41
9 changed files with 90 additions and 98 deletions

View file

@ -17,12 +17,12 @@ jobs:
- name: Install dependencies - name: Install dependencies
working-directory: core working-directory: core
run: pip3 install -r requirements.dev.txt run: pip3 install -r requirements.dev.txt
- name: Run django tests - name: Run django tests with coverage
working-directory: core working-directory: core
run: python3 manage.py test run: coverage run manage.py test
- name: Run django coverage - name: Evaluate coverage
working-directory: core working-directory: core
run: coverage manage.py test run: coverage report
deploy: deploy:
needs: [ test ] needs: [ test ]

View file

@ -9,6 +9,9 @@ omit =
*/tests/* */tests/*
*/migrations/* */migrations/*
core/asgi.py core/asgi.py
core/wsgi.py core/globals.py
core/settings.py core/settings.py
manage.py mail/socket.py
manage.py
server.py
helper.py

View file

@ -1,5 +1,6 @@
<template> <template>
<div style="min-height: 100vh; display: flex; flex-direction: column;"> <div style="min-height: 100vh; display: flex; flex-direction: column;">
<Lightbox v-if="lightboxHash" :hash="lightboxHash" @close="openLightboxModalWith(null)"/>
<AddItemModal v-if="addItemModalOpen && isLoggedIn" @close="closeAddItemModal()" isModal="true"/> <AddItemModal v-if="addItemModalOpen && isLoggedIn" @close="closeAddItemModal()" isModal="true"/>
<AddTicketModal v-if="addTicketModalOpen && isLoggedIn" @close="closeAddTicketModal()" isModal="true"/> <AddTicketModal v-if="addTicketModalOpen && isLoggedIn" @close="closeAddTicketModal()" isModal="true"/>
<AddBoxModal v-if="showAddBoxModal && isLoggedIn" @close="closeAddBoxModal()" isModal="true"/> <AddBoxModal v-if="showAddBoxModal && isLoggedIn" @close="closeAddBoxModal()" isModal="true"/>
@ -16,20 +17,22 @@ import {mapState, mapMutations, mapActions, mapGetters} from 'vuex';
import AddTicketModal from "@/components/AddTicketModal.vue"; import AddTicketModal from "@/components/AddTicketModal.vue";
import AddBoxModal from "@/components/AddBoxModal.vue"; import AddBoxModal from "@/components/AddBoxModal.vue";
import AddEventModal from "@/components/AddEventModal.vue"; import AddEventModal from "@/components/AddEventModal.vue";
import Lightbox from "@/components/Lightbox.vue";
export default { export default {
name: 'app', name: 'app',
components: {AddBoxModal, AddEventModal, Navbar, AddItemModal, AddTicketModal}, components: {Lightbox, AddBoxModal, AddEventModal, Navbar, AddItemModal, AddTicketModal},
computed: { computed: {
...mapState(['loadedItems', 'layout', 'toasts', 'showAddBoxModal', 'showAddEventModal']), ...mapState(['loadedItems', 'layout', 'toasts', 'showAddBoxModal', 'showAddEventModal', 'lightboxHash']),
...mapGetters(['isLoggedIn']), ...mapGetters(['isLoggedIn']),
}, },
data: () => ({ data: () => ({
addItemModalOpen: false, addItemModalOpen: false,
addTicketModalOpen: false addTicketModalOpen: false,
}), }),
methods: { methods: {
...mapMutations(['removeToast', 'createToast', 'closeAddBoxModal', 'openAddBoxModal', 'closeAddEventModal']), ...mapMutations(['removeToast', 'createToast', 'closeAddBoxModal', 'openAddBoxModal', 'closeAddEventModal',
'openLightboxModalWith']),
...mapActions(['loadEvents', 'scheduleAfterInit']), ...mapActions(['loadEvents', 'scheduleAfterInit']),
openAddItemModal() { openAddItemModal() {
this.addItemModalOpen = true; this.addItemModalOpen = true;
@ -42,7 +45,7 @@ export default {
}, },
closeAddTicketModal() { closeAddTicketModal() {
this.addTicketModalOpen = false; this.addTicketModalOpen = false;
} },
}, },
created: function () { created: function () {
document.title = document.location.hostname; document.title = document.location.hostname;

View file

@ -43,11 +43,11 @@ export default {
props: ['label', 'model', 'nameKey', 'uniqueKey', 'options', 'onOptionAdd'], props: ['label', 'model', 'nameKey', 'uniqueKey', 'options', 'onOptionAdd'],
data: ({options, model, nameKey, uniqueKey}) => ({ data: ({options, model, nameKey, uniqueKey}) => ({
internalName: model[nameKey], internalName: model[nameKey],
selectedOption: options.filter(e => e[uniqueKey] == model[uniqueKey])[0], selectedOption: options.filter(e => e[uniqueKey] === model[uniqueKey])[0],
addingOption: false addingOption: false
}), }),
computed: { computed: {
isValid: ({options, nameKey, internalName}) => options.some(e => e[nameKey] == internalName), isValid: ({options, nameKey, internalName}) => options.some(e => e[nameKey] === internalName),
sortedOptions: ({ sortedOptions: ({
options, options,
nameKey nameKey
@ -56,7 +56,7 @@ export default {
watch: { watch: {
internalName(newValue) { internalName(newValue) {
if (this.isValid) { if (this.isValid) {
if (!this.selectedOption || newValue != this.selectedOption[this.nameKey]) { if (!this.selectedOption || newValue !== this.selectedOption[this.nameKey]) {
this.selectedOption = this.options.filter(e => e[this.nameKey] === newValue)[0]; this.selectedOption = this.options.filter(e => e[this.nameKey] === newValue)[0];
} }
this.model[this.nameKey] = this.selectedOption[this.nameKey]; this.model[this.nameKey] = this.selectedOption[this.nameKey];

View file

@ -37,6 +37,7 @@ const store = createStore({
expiry: null, expiry: null,
}, },
lightboxHash: null,
thumbnailCache: {}, thumbnailCache: {},
fetchedData: { fetchedData: {
events: 0, events: 0,
@ -191,6 +192,9 @@ const store = createStore({
state.groups = groups; state.groups = groups;
state.fetchedData = {...state.fetchedData, groups: Date.now()}; state.fetchedData = {...state.fetchedData, groups: Date.now()};
}, },
openLightboxModalWith(state, hash) {
state.lightboxHash = hash;
},
openAddBoxModal(state) { openAddBoxModal(state) {
state.showAddBoxModal = true; state.showAddBoxModal = true;
}, },
@ -355,7 +359,7 @@ const store = createStore({
commit('replaceEvents', [...state.events.filter(e => e.id !== event_id)]) commit('replaceEvents', [...state.events.filter(e => e.id !== event_id)])
} }
}, },
async updateEvent({commit, dispatch, state}, {id, partial_event}){ async updateEvent({commit, dispatch, state}, {id, partial_event}) {
const {data, success} = await http.patch(`/2/events/${id}/`, partial_event, state.user.token); const {data, success} = await http.patch(`/2/events/${id}/`, partial_event, state.user.token);
if (success) { if (success) {
commit('replaceEvents', [...state.events.filter(e => e.id !== id), data]) commit('replaceEvents', [...state.events.filter(e => e.id !== id), data])
@ -369,7 +373,6 @@ const store = createStore({
}, },
async changeEvent({dispatch, getters, commit}, eventName) { async changeEvent({dispatch, getters, commit}, eventName) {
await router.push({path: `/${eventName.slug}/${getters.getActiveView}/`}); await router.push({path: `/${eventName.slug}/${getters.getActiveView}/`});
//dispatch('loadEventItems');
}, },
async changeView({getters}, link) { async changeView({getters}, link) {
await router.push({path: `/${getters.getEventSlug}/${link.path}/`}); await router.push({path: `/${getters.getEventSlug}/${link.path}/`});

View file

@ -8,15 +8,25 @@
<AuthenticatedImage v-if="item.file" cached <AuthenticatedImage v-if="item.file" cached
:src="`/media/2/256/${item.file}/`" :src="`/media/2/256/${item.file}/`"
class="d-block card-img" class="d-block card-img"
@click="openLightboxModalWith(item)" @click="openLightboxModalWith(item.file)"
/> />
<div class="card-body"> <div class="card-body">
<h6 class="card-subtitle text-secondary">id: {{ item.id }} box: {{ <h6 class="card-subtitle text-secondary">id: {{ item.id }} box: {{ item.box }}</h6>
item.box
}}</h6>
<h6 class="card-title">{{ item.description }}</h6> <h6 class="card-title">{{ item.description }}</h6>
</div> </div>
<div class="card-footer">
<InputPhoto
:model="editingItem"
field="file"
:on-capture="storeImage"
/>
<InputString
label="description"
:model="editingItem"
field="description"
:validation-fn="str => str && str.length > 0"
/>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -26,7 +36,7 @@
<h3>Item #{{ item.id }} - {{ item.description }}</h3> <h3>Item #{{ item.id }} - {{ item.description }}</h3>
</div> </div>
<Timeline :timeline="item.timeline"> <Timeline :timeline="item.timeline">
<template v-slot:timeline_action1> <!--template v-slot:timeline_action1>
<span class="timeline-item-icon | faded-icon"> <span class="timeline-item-icon | faded-icon">
<font-awesome-icon icon="comment"/> <font-awesome-icon icon="comment"/>
</span> </span>
@ -41,47 +51,35 @@
</AsyncButton> </AsyncButton>
</div> </div>
</div> </div>
</template> </template-->
</Timeline> </Timeline>
<div class="card-footer d-flex justify-content-between"> <div class="card-footer d-flex justify-content-between">
<button class="btn btn-secondary mr-2" @click="$router.go(-1)">Back</button> <!--button class="btn btn-secondary mr-2" @click="$router.go(-1)">Back</button-->
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-outline-success" <button class="btn btn-outline-success"
@click.stop="confirm('return Item?') && markItemReturned(item)" @click.stop="confirm('return Item?') && markItemReturned(item)"
title="returned"> title="returned">
<font-awesome-icon icon="check"/> <font-awesome-icon icon="check"/>&nbsp;mark&nbsp;returned
</button>
<button class="btn btn-outline-secondary" @click.stop="openEditingModalWith(item)"
title="edit">
<font-awesome-icon icon="edit"/>
</button> </button>
<button class="btn btn-outline-danger" <button class="btn btn-outline-danger"
@click.stop="confirm('delete Item?') && deleteItem(item)" @click.stop="confirm('delete Item?') && deleteItem(item)"
title="delete"> title="delete">
<font-awesome-icon icon="trash"/> <font-awesome-icon icon="trash"/>&nbsp;delete
</button> </button>
</div> </div>
<InputCombo <InputCombo
label="box" label="box"
:model="item" :model="editingItem"
nameKey="box" nameKey="box"
uniqueKey="cid" uniqueKey="cid"
:options="boxes" :options="boxes"
style="width: auto;"
/> />
<div class="btn-group">
<select class="form-control" v-model="selected_state"> <button type="button" class="btn btn-success" @click="saveEditingItem()">Save Changes
<option v-for="status in state_options" :value="status.value">{{ </button>
status.text {{ editingItem}}
}}
</option>
</select>
<button class="form-control btn btn-success"
@click="changeTicketStatus(item)"
:disabled="(selected_state == item.state)">
Change&nbsp;Status
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -105,21 +103,29 @@
</AsyncLoader> </AsyncLoader>
</template> </template>
<script> <script>
import {mapActions, mapGetters, mapState} from 'vuex'; import {mapActions, mapGetters, mapMutations, mapState} from 'vuex';
import Timeline from "@/components/Timeline.vue"; import Timeline from "@/components/Timeline.vue";
import ClipboardButton from "@/components/inputs/ClipboardButton.vue"; import ClipboardButton from "@/components/inputs/ClipboardButton.vue";
import AsyncLoader from "@/components/AsyncLoader.vue"; import AsyncLoader from "@/components/AsyncLoader.vue";
import InputCombo from "@/components/inputs/InputCombo.vue"; import InputCombo from "@/components/inputs/InputCombo.vue";
import InputString from "@/components/inputs/InputString.vue"; import InputString from "@/components/inputs/InputString.vue";
import AuthenticatedImage from "@/components/AuthenticatedImage.vue"; import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
import InputPhoto from "@/components/inputs/InputPhoto.vue";
import Modal from "@/components/Modal.vue";
import EditItem from "@/components/EditItem.vue";
export default { export default {
name: 'Item', name: 'Item',
components: {AuthenticatedImage, InputString, InputCombo, AsyncLoader, ClipboardButton, Timeline}, components: {
EditItem,
Modal, InputPhoto, AuthenticatedImage, InputString, InputCombo, AsyncLoader, ClipboardButton, Timeline
},
data() { data() {
return { return {
newComment: "" newComment: "",
editingItem: {},
} }
}, },
computed: { computed: {
@ -131,33 +137,40 @@ export default {
return ret ? ret : {}; return ret ? ret : {};
}, },
boxes() { boxes() {
console.log(this.getBoxes);
return this.getBoxes.map(obj => ({cid: obj.cid, box: obj.name})); return this.getBoxes.map(obj => ({cid: obj.cid, box: obj.name}));
} }
}, },
methods: { methods: {
...mapActions(['deleteItem', 'markItemReturned', 'updateTicketPartial', 'postComment']), ...mapActions(['deleteItem', 'markItemReturned', 'updateTicketPartial', 'postComment']),
...mapActions(['loadTickets', 'fetchTicketStates', 'loadUsers', 'scheduleAfterInit']), ...mapActions(['loadTickets', 'fetchTicketStates', 'loadUsers', 'scheduleAfterInit', 'updateItem']),
...mapActions(['claimShippingVoucher', 'fetchShippingVouchers', 'loadEventItems', 'loadBoxes']), ...mapActions(['claimShippingVoucher', 'fetchShippingVouchers', 'loadEventItems', 'loadBoxes']),
changeTicketStatus(item) { ...mapMutations(['openLightboxModalWith']),
item.state = this.selected_state;
this.updateTicketPartial({
id: item.id,
state: this.selected_state,
})
},
addCommentAndClear: async function () { addCommentAndClear: async function () {
await this.postComment({ await this.postComment({
id: this.ticket.id, id: this.ticket.id,
message: this.newComment message: this.newComment
}) })
this.newComment = ""; this.newComment = "";
} },
closeEditingModal() {
this.editingItem = null;
},
async saveEditingItem() { // Saves the edited copy of the item.
await this.updateItem(this.editingItem);
this.editingItem = {...this.item}
},
storeImage(image) {
this.item.dataImage = image;
},
confirm(message) {
return window.confirm(message);
},
}, },
mounted() { mounted() {
this.scheduleAfterInit(() => [Promise.all([this.loadEventItems(), this.loadBoxes()]).then(() => { this.scheduleAfterInit(() => [Promise.all([this.loadEventItems(), this.loadBoxes()]).then(() => {
this.selected_state = this.item.state; this.selected_state = this.item.state;
this.selected_assignee = this.item.assigned_to this.selected_assignee = this.item.assigned_to
this.editingItem = {...this.item}
})]); })]);
} }
}; };

View file

@ -1,19 +1,6 @@
<template> <template>
<AsyncLoader :loaded="isItemsLoaded"> <AsyncLoader :loaded="isItemsLoaded">
<div class="container-fluid px-xl-5 mt-3"> <div class="container-fluid px-xl-5 mt-3">
<Modal title="Edit Item" v-if="editingItem" @close="closeEditingModal()">
<template #body>
<EditItem
:item="editingItem"
badge="id"
/>
</template>
<template #buttons>
<button type="button" class="btn btn-secondary" @click="closeEditingModal()">Cancel</button>
<button type="button" class="btn btn-success" @click="saveEditingItem()">Save Changes</button>
</template>
</Modal>
<Lightbox v-if="lightboxHash" :hash="lightboxHash" @close="closeLightboxModal()"/>
<div class="row" v-if="layout === 'table'"> <div class="row" v-if="layout === 'table'">
<div class="col-xl-8 offset-xl-2"> <div class="col-xl-8 offset-xl-2">
<Table <Table
@ -47,7 +34,7 @@
:items="getEventItems" :items="getEventItems"
:keyName="'id'" :keyName="'id'"
v-slot="{ item }" v-slot="{ item }"
@itemActivated="showItemDetail" @itemActivated="item => openLightboxModalWith(item.file)"
> >
<AuthenticatedImage v-if="item.file" cached <AuthenticatedImage v-if="item.file" cached
:src="`/media/2/256/${item.file}/`" :src="`/media/2/256/${item.file}/`"
@ -62,7 +49,7 @@
@click.stop="confirm('return Item?') && markItemReturned(item)" title="returned"> @click.stop="confirm('return Item?') && markItemReturned(item)" title="returned">
<font-awesome-icon icon="check"/> <font-awesome-icon icon="check"/>
</button> </button>
<button class="btn btn-outline-secondary" @click.stop="openEditingModalWith(item)" <button class="btn btn-outline-secondary" @click.stop="showItemDetail(item)"
title="edit"> title="edit">
<font-awesome-icon icon="edit"/> <font-awesome-icon icon="edit"/>
</button> </button>
@ -84,8 +71,7 @@ import Table from '@/components/Table';
import Cards from '@/components/Cards'; import Cards from '@/components/Cards';
import Modal from '@/components/Modal'; import Modal from '@/components/Modal';
import EditItem from '@/components/EditItem'; import EditItem from '@/components/EditItem';
import {mapActions, mapGetters, mapState} from 'vuex'; import {mapActions, mapGetters, mapMutations} from 'vuex';
import Lightbox from '../components/Lightbox';
import AuthenticatedImage from "@/components/AuthenticatedImage.vue"; import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
import AsyncLoader from "@/components/AsyncLoader.vue"; import AsyncLoader from "@/components/AsyncLoader.vue";
import router from "@/router"; import router from "@/router";
@ -96,32 +82,16 @@ export default {
lightboxHash: null, lightboxHash: null,
editingItem: null, editingItem: null,
}), }),
components: {AsyncLoader, AuthenticatedImage, Lightbox, Table, Cards, Modal, EditItem}, components: {AsyncLoader, AuthenticatedImage, Table, Cards, Modal, EditItem},
computed: { computed: {
...mapState([]),
...mapGetters(['getEventItems', 'isItemsLoaded', 'layout']), ...mapGetters(['getEventItems', 'isItemsLoaded', 'layout']),
}, },
methods: { methods: {
...mapActions(['deleteItem', 'markItemReturned', 'loadEventItems', 'updateItem', 'scheduleAfterInit']), ...mapActions(['deleteItem', 'markItemReturned', 'loadEventItems', 'updateItem', 'scheduleAfterInit']),
...mapMutations(['openLightboxModalWith']),
showItemDetail(item) { showItemDetail(item) {
router.push({name: 'item', params: {id: item.id}}); router.push({name: 'item', params: {id: item.id}});
}, },
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();
},
confirm(message) { confirm(message) {
return window.confirm(message); return window.confirm(message);
} }

View file

@ -105,7 +105,7 @@
<AuthenticatedImage v-if="item.file" cached <AuthenticatedImage v-if="item.file" cached
:src="`/media/2/256/${item.file}/`" :src="`/media/2/256/${item.file}/`"
class="d-block card-img" class="d-block card-img"
@click="openLightboxModalWith(item)" @click="openLightboxModalWith(item.file)"
/> />
<div class="card-body"> <div class="card-body">
<!--h6 class="card-title text-info"><span class="badge badge-primary">{{ item.relation_status }}</span></--h6--> <!--h6 class="card-title text-info"><span class="badge badge-primary">{{ item.relation_status }}</span></--h6-->
@ -126,7 +126,7 @@
</template> </template>
<script> <script>
import {mapActions, mapGetters, mapState} from 'vuex'; import {mapActions, mapGetters, mapMutations, mapState} from 'vuex';
import Timeline from "@/components/Timeline.vue"; import Timeline from "@/components/Timeline.vue";
import ClipboardButton from "@/components/inputs/ClipboardButton.vue"; import ClipboardButton from "@/components/inputs/ClipboardButton.vue";
import AsyncLoader from "@/components/AsyncLoader.vue"; import AsyncLoader from "@/components/AsyncLoader.vue";
@ -166,6 +166,7 @@ export default {
...mapActions(['deleteItem', 'markItemReturned', 'sendMail', 'updateTicketPartial', 'postComment']), ...mapActions(['deleteItem', 'markItemReturned', 'sendMail', 'updateTicketPartial', 'postComment']),
...mapActions(['loadTickets', 'fetchTicketStates', 'loadUsers', 'scheduleAfterInit']), ...mapActions(['loadTickets', 'fetchTicketStates', 'loadUsers', 'scheduleAfterInit']),
...mapActions(['claimShippingVoucher', 'fetchShippingVouchers']), ...mapActions(['claimShippingVoucher', 'fetchShippingVouchers']),
...mapMutations(['openLightboxModalWith']),
changeTicketStatus() { changeTicketStatus() {
this.ticket.state = this.selected_state; this.ticket.state = this.selected_state;
this.updateTicketPartial({ this.updateTicketPartial({

View file

@ -55,24 +55,23 @@
<script> <script>
import Cards from '@/components/Cards'; import Cards from '@/components/Cards';
import Modal from '@/components/Modal';
import EditItem from '@/components/EditItem';
import {mapActions, mapGetters, mapState} from 'vuex'; import {mapActions, mapGetters, mapState} from 'vuex';
import Lightbox from '../components/Lightbox'; import Lightbox from '../components/Lightbox';
import SlotTable from "@/components/SlotTable.vue"; import SlotTable from "@/components/SlotTable.vue";
import CollapsableCards from "@/components/CollapsableCards.vue"; import CollapsableCards from "@/components/CollapsableCards.vue";
import AsyncLoader from "@/components/AsyncLoader.vue"; import AsyncLoader from "@/components/AsyncLoader.vue";
import router from "@/router";
export default { export default {
name: 'Tickets', name: 'Tickets',
components: {AsyncLoader, Lightbox, SlotTable, Cards, Modal, EditItem, CollapsableCards}, components: {AsyncLoader, Lightbox, SlotTable, Cards, CollapsableCards},
computed: { computed: {
...mapGetters(['getEventTickets', 'isTicketsLoaded', 'stateInfo', 'getEventSlug', 'layout']), ...mapGetters(['getEventTickets', 'isTicketsLoaded', 'stateInfo', 'getEventSlug', 'layout']),
}, },
methods: { methods: {
...mapActions(['loadTickets', 'fetchTicketStates', 'scheduleAfterInit']), ...mapActions(['loadTickets', 'fetchTicketStates', 'scheduleAfterInit']),
gotoDetail(ticket) { gotoDetail(ticket) {
this.$router.push({name: 'ticket', params: {id: ticket.id}}); router.push({name: 'ticket', params: {id: ticket.id}});
}, },
formatTicket(ticket) { formatTicket(ticket) {
return { return {