Compare commits

..

12 commits

Author SHA1 Message Date
df781b15ac stash 2024-11-06 01:00:03 +01:00
2d69731e99 stash 2024-11-06 01:00:03 +01:00
38d8b35084 stash 2024-11-06 01:00:03 +01:00
ecbe989f43 stash 2024-11-06 01:00:03 +01:00
87cd630605 stash 2024-11-06 01:00:03 +01:00
781d733197 stash 2024-11-06 01:00:03 +01:00
b45a4af01c stash 2024-11-06 01:00:03 +01:00
7df9a3f8cc stash 2024-11-06 00:59:59 +01:00
8e04cea448 stash 2024-11-06 00:58:24 +01:00
0cb45c73db stash 2024-11-06 00:58:24 +01:00
51f0a90bc7 stash 2024-11-06 00:58:20 +01:00
ec3579e7c1 stash 2024-11-06 00:57:41 +01:00
11 changed files with 64 additions and 297 deletions

View file

@ -141,8 +141,7 @@ else:
'USER': os.getenv('DB_USER', 'system3'), 'USER': os.getenv('DB_USER', 'system3'),
'PASSWORD': os.getenv('DB_PASSWORD', 'system3'), 'PASSWORD': os.getenv('DB_PASSWORD', 'system3'),
'OPTIONS': { 'OPTIONS': {
'charset': 'utf8mb4', 'charset': 'utf8mb4'
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'"
} }
} }
} }

View file

@ -1,49 +1,25 @@
from django.utils import timezone from django.utils import timezone
from rest_framework import serializers from rest_framework import serializers
from rest_framework.relations import SlugRelatedField
from files.models import File from files.models import File
from inventory.models import Event, Container, Item from inventory.models import Event, Container, Item
from mail.models import EventAddress from mail.models import EventAddress
#class EventAdressSerializer(serializers.ModelSerializer): class EventAdressSerializer(serializers.ModelSerializer):
# class Meta: class Meta:
# model = EventAddress model = EventAddress
# fields = ['address'] fields = ['address']
# def to_internal_value(self, data):
# if not isinstance(data, str):
# raise serializers.ValidationError('This field must be a string.')
#
# def create(self, validated_data):
# return EventAddress.objects.create(**validated_data)
#
# def validate(self, data):
# return isinstance(data, str)
class EventSerializer(serializers.ModelSerializer): class EventSerializer(serializers.ModelSerializer):
#addresses = EventAdressSerializer(many=True, required=False) addresses = EventAdressSerializer(many=True, required=False)
addresses = SlugRelatedField(many=True, slug_field='address', queryset=EventAddress.objects.all())
class Meta: class Meta:
model = Event model = Event
fields = ['eid', 'slug', 'name', 'start', 'end', 'pre_start', 'post_end', 'addresses'] fields = ['eid', 'slug', 'name', 'start', 'end', 'pre_start', 'post_end', 'addresses']
read_only_fields = ['eid'] read_only_fields = ['eid']
# def update(self, instance, validated_data):
# addresses = validated_data.pop('addresses', None)
# instance.save(validated_data)
# if addresses:
# for address in addresses:
# nested_instance, created = EventAddress.objects.get_or_create(address=address)
# instance.addresses.add(nested_instance)
#
# return instance
class ContainerSerializer(serializers.ModelSerializer): class ContainerSerializer(serializers.ModelSerializer):
itemCount = serializers.SerializerMethodField() itemCount = serializers.SerializerMethodField()

View file

@ -47,18 +47,6 @@ class EventTestCase(TestCase):
self.assertEqual(Event.objects.all()[0].slug, 'EVENT2') self.assertEqual(Event.objects.all()[0].slug, 'EVENT2')
self.assertEqual(Event.objects.all()[0].name, 'Event 2 new') self.assertEqual(Event.objects.all()[0].name, 'Event 2 new')
def test_update_event(self):
from rest_framework.test import APIClient
event = Event.objects.create(slug='EVENT1', name='Event 1')
response = APIClient().patch(f'/api/2/events/{event.eid}/', {'addresses': ['foo@bar.baz', 'foo1@bar.baz']})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['slug'], 'EVENT1')
self.assertEqual(response.json()['name'], 'Event 1')
self.assertEqual(len(Event.objects.all()), 1)
self.assertEqual(Event.objects.all()[0].slug, 'EVENT1')
self.assertEqual(Event.objects.all()[0].name, 'Event 1')
self.assertEqual(1, len(response.json()[0]['addresses']))
def test_remove_event(self): def test_remove_event(self):
event = Event.objects.create(slug='EVENT1', name='Event 1') event = Event.objects.create(slug='EVENT1', name='Event 1')
Event.objects.create(slug='EVENT2', name='Event 2') Event.objects.create(slug='EVENT2', name='Event 2')
@ -77,3 +65,4 @@ class EventTestCase(TestCase):
self.assertEqual('TEST1', response.json()[0]['slug']) self.assertEqual('TEST1', response.json()[0]['slug'])
self.assertEqual('Event', response.json()[0]['name']) self.assertEqual('Event', response.json()[0]['name'])
self.assertEqual(1, len(response.json()[0]['addresses'])) self.assertEqual(1, len(response.json()[0]['addresses']))

View file

@ -1,18 +0,0 @@
# Generated by Django 4.2.7 on 2024-11-06 06:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mail', '0005_alter_eventaddress_event'),
]
operations = [
migrations.AlterField(
model_name='eventaddress',
name='address',
field=models.CharField(max_length=255, unique=True),
),
]

View file

@ -33,9 +33,12 @@ class Email(SoftDeleteModel):
class EventAddress(models.Model): class EventAddress(models.Model):
id = models.AutoField(primary_key=True) id = models.AutoField(primary_key=True)
event = models.ForeignKey(Event, models.SET_NULL, null=True, related_name='addresses') event = models.ForeignKey(Event, models.SET_NULL, null=True, related_name='addresses')
address = models.CharField(max_length=255, unique=True) address = models.CharField(max_length=255)
class EmailAttachment(AbstractFile): class EmailAttachment(AbstractFile):
email = models.ForeignKey(Email, models.CASCADE, related_name='attachments', null=True) email = models.ForeignKey(Email, models.CASCADE, related_name='attachments', null=True)
name = models.CharField(max_length=255) name = models.CharField(max_length=255)

View file

@ -6,17 +6,11 @@ services:
command: bash -c 'python manage.py migrate && python manage.py runserver 0.0.0.0:8000' command: bash -c 'python manage.py migrate && python manage.py runserver 0.0.0.0:8000'
environment: environment:
- HTTP_HOST=core - HTTP_HOST=core
- DB_HOST=db #- DATABASE_URL
- DB_PORT=3306
- DB_NAME=system3
- DB_USER=system3
- DB_PASSWORD=system3
volumes: volumes:
- ../../core:/code - ../../core:/code
ports: ports:
- "8000:8000" - "8000:8000"
depends_on:
- db
frontend: frontend:
build: build:
@ -29,8 +23,6 @@ services:
- ./vue.config.js:/web/vue.config.js - ./vue.config.js:/web/vue.config.js
ports: ports:
- "8080:8080" - "8080:8080"
depends_on:
- core
db: db:
image: mariadb image: mariadb
@ -38,11 +30,4 @@ services:
MARIADB_RANDOM_ROOT_PASSWORD: true MARIADB_RANDOM_ROOT_PASSWORD: true
MARIADB_DATABASE: system3 MARIADB_DATABASE: system3
MARIADB_USER: system3 MARIADB_USER: system3
MARIADB_PASSWORD: system3 MARIADB_PASSWORD: system3
volumes:
- mariadb_data:/var/lib/mysql
ports:
- "3306:3306"
volumes:
mariadb_data:

View file

@ -73,15 +73,23 @@ export default {
data() { data() {
return { return {
collapsed: [], collapsed: [],
symbols: ['▲', '▼', '▶', '◀'],
}; };
}, },
created() { created() {
const query = this.route ? (this.route.query ? this.route.query.collapsed : null) : null; const query = this.$router.currentRoute ? (this.$router.currentRoute.query ? this.$router.currentRoute.query.collapsed : null) : null;
if (query !== null && query !== undefined) { if (query !== null && query !== undefined) {
this.collapsed = this.unpackInt(parseInt(query), this.sections.length); this.collapsed = this.unpackInt(parseInt(query), this.sections.length);
} else { } else {
this.collapsed = this.sections.map(() => true); this.collapsed = this.sections.map(() => true);
} }
//this.$router.push({...this.$router.currentRoute, query: {...this.$router.currentRoute.query, layout}});
//this.collapsed = this.sections.map(() => true);
/*this.columns.map(e => ({
k: e,
v: this.$store.getters.getFilters[e]
})).filter(e => e.v).forEach(e => this.setFilter(e.k, e.v));*/
}, },
computed: { computed: {
grouped_items() { grouped_items() {
@ -108,8 +116,8 @@ export default {
const encoded = this.packInt(this.collapsed).toString() const encoded = this.packInt(this.collapsed).toString()
if (this.route.query.collapsed !== encoded) if (this.route.query.collapsed !== encoded)
this.$router.push({ this.$router.push({
...this.route, ...this.$router.currentRoute,
query: {...this.route.query, collapsed: encoded} query: {...this.$router.currentRoute.query, collapsed: encoded}
}); });
}, },
deep: true, deep: true,

View file

@ -1,124 +0,0 @@
<template>
<table class="table table-striped table-dark">
<thead>
<tr>
<th>
</th>
<th scope="col" v-for="(column, index) in columns" :key="index">
<div class="input-group">
<div class="input-group-prepend">
<button
:class="[ 'btn', column === sortBy ? 'btn-outline-info' : 'btn-outline-secondary' ]"
@click="toggleSort(column)"
>
{{ column }}
<span :class="{ 'text-info': column === sortBy }">
<font-awesome-icon :icon="getSortIcon(column)"/>
</span>
</button>
</div>
<input
type="text"
class="form-control"
placeholder="filter"
:value="filters[column]"
@input="changeFilter(column, $event.target.value)"
>
</div>
</th>
<th>
<slot name="header_actions"/>
</th>
</tr>
</thead>
<tbody v-for="(item, index) in internalItems" :key="item[keyName]" style="border-top: none">
<tr @click="toggle(index)">
<td>
<font-awesome-icon icon="angle-right" style="width: 1em;" v-if="collapsed[index]"/>
<font-awesome-icon icon="angle-down" style="width: 1em;" v-else/>
</td>
<td v-for="(column, index) in columns" :key="index">{{ item[column] }}</td>
<td>
<slot v-bind:id="index" v-bind:item="item" name="actions"/>
</td>
</tr>
<tr v-if="!collapsed[index]">
<td :colspan="columns.length + 2">
<slot v-bind:id="index" v-bind:item="item" name="detail"/>
</td>
</tr>
</tbody>
</table>
</template>
<script>
import DataContainer from '@/mixins/data-container';
import router from '../router';
import {mapGetters} from "vuex";
export default {
name: 'ExpandableTable',
mixins: [DataContainer],
created() {
this.columns.map(e => ({
k: e,
v: this.$store.getters.getFilters[e]
})).filter(e => e.v).forEach(e => this.setFilter(e.k, e.v));
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.internalItems.length);
} else {
this.collapsed = this.internalItems.map(() => true);
}
},
data() {
return {
collapsed: [],
};
},
computed: {
...mapGetters(['route']),
},
methods: {
changeFilter(col, val) {
this.setFilter(col, val);
let newquery = Object.entries({
...this.$store.getters.getFilters,
[col]: val
}).reduce((a, [k, v]) => (v ? {...a, [k]: v} : a), {});
router.push({query: newquery});
},
packInt(arr) {
return arr.reduce((a, e, i) => a + (e ? 0 : 2 ** i), 0);
},
unpackInt(n, l) {
return [...Array(l)].map((e, i) => (n & 2 ** i) === 0);
},
toggle(index) {
const collapsed = [...this.collapsed]
collapsed[index] = !collapsed[index];
this.collapsed = collapsed;
},
},
watch: {
collapsed: {
handler() {
const encoded = this.packInt(this.collapsed).toString()
if (this.route.query.collapsed !== encoded)
this.$router.push({
...this.route,
query: {...this.route.query, collapsed: encoded}
});
},
deep: true,
},
},
};
</script>
<style>
.table-body-move {
transition: transform 1s;
}
</style>

View file

@ -340,13 +340,6 @@ const store = createStore({
commit('replaceEvents', [...state.events.filter(e => e.eid !== event_id)]) commit('replaceEvents', [...state.events.filter(e => e.eid !== 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])
}
},
async fetchTicketStates({commit, state}) { async fetchTicketStates({commit, state}) {
if (!state.user.token) return; if (!state.user.token) return;
if (state.fetchedData.states > Date.now() - 1000 * 60 * 60 * 24) return; if (state.fetchedData.states > Date.now() - 1000 * 60 * 60 * 24) return;

View file

@ -79,6 +79,16 @@ export default {
shipping_voucher_type: null, shipping_voucher_type: null,
} }
}, },
watch: {
ticket(val) {
if (this.selected_state == null) {
this.selected_state = val.state;
}
if (this.selected_assignee == null) {
this.selected_assignee = val.assigned_to
}
}
},
computed: { computed: {
...mapState(['tickets', 'state_options', 'users']), ...mapState(['tickets', 'state_options', 'users']),
...mapGetters(['availableShippingVoucherTypes']), ...mapGetters(['availableShippingVoucherTypes']),
@ -124,14 +134,12 @@ export default {
}, },
}, },
mounted() { mounted() {
this.scheduleAfterInit(() => [Promise.all([this.fetchTicketStates(), this.loadTickets(), this.loadUsers(), this.fetchShippingVouchers()]).then(()=>{ this.scheduleAfterInit(() => [this.fetchTicketStates(), this.loadTickets(), this.loadUsers(),
this.selected_state = this.ticket.state; this.fetchShippingVouchers()]);
this.selected_assignee = this.ticket.assigned_to
})]);
} }
}; };
</script> </script>
<style scoped> <style scoped>
</style> </style>

View file

@ -1,102 +1,50 @@
<template> <template>
<AsyncLoader :loaded="events.length > 0"> <Table
<ExpandableTable v-if="!!events" :columns="['slug', 'name']" :items="events" :keyName="'slug'"> :columns="['slug', 'name']"
<template v-slot:header_actions> :items="events"
<button class="btn btn-success" @click.prevent="openAddEventModal"> :keyName="'slug'"
<font-awesome-icon icon="plus"/> >
Create Event <template v-slot:header_actions>
<button class="btn btn-success" @click.prevent="openAddEventModal">
<font-awesome-icon icon="plus"/>
Create Event
</button>
</template>
<template v-slot:actions="{ item }">
<div class="btn-group">
<button class="btn btn-secondary" @click.stop="changeEvent(item)">
<font-awesome-icon icon="archive"/>
use
</button> </button>
</template> <button class="btn btn-danger" @click.stop="safeDeleteEvent(item.eid)">
<template v-slot:actions="{ item }"> <font-awesome-icon icon="trash"/>
<div class="btn-group"> delete
<button class="btn btn-secondary" @click.stop="changeEvent(item)" style="white-space: nowrap;"> </button>
<font-awesome-icon icon="archive"/>&nbsp;use </div>
</button> </template>
<button class="btn btn-danger" @click.stop="safeDeleteEvent(item.eid)" style="white-space: nowrap;"> </Table>
<font-awesome-icon icon="trash"/>&nbsp;delete
</button>
</div>
</template>
<template v-slot:detail="{id, item }">
<div class="row">
<div class="col">
<input type="date" class="form-control form-control-sm" title="Buildup Start"
v-model="item.pre_start" disabled style="opacity: 1"
:max="item.start" @focus="prepare_date_field">
</div>
<div class="col">
<input type="date" class="form-control form-control-sm" title="Official Event Start"
v-model="item.start" disabled style="opacity: 1"
:min="item.pre_start" :max="item.end" @focus="prepare_date_field">
</div>
<div class="col">
<input type="date" class="form-control form-control-sm" title="Official Event End"
v-model="item.end" disabled style="opacity: 1"
:min="item.start" :max="item.post_end" @focus="prepare_date_field">
</div>
<div class="col">
<input type="date" class="form-control form-control-sm" title="Teardown End"
v-model="item.post_end" disabled style="opacity: 1"
:min="item.end" @focus="prepare_date_field">
</div>
</div>
<div class="mt-3">
<label class="mr-3">Addresses: </label>
<div v-for="(address, a_id) in item.addresses" class="btn-group btn-group-sm mr-3"
@click.stop="deleteAddress(id, a_id)">
<button class="btn btn-secondary" disabled style="opacity: 1">
{{ address }}
</button>
<button class="btn btn-danger">
<font-awesome-icon icon="trash"/>
</button>
</div>
<div class="btn-group btn-group-sm">
<input type="text" v-model="new_address[id]">
<button class="btn btn-secondary" @click.stop="addAddress(id)" style="white-space: nowrap;">
<font-awesome-icon icon="envelope"/>&nbsp;add
</button>
</div>
</div>
</template>
</ExpandableTable>
</AsyncLoader>
</template> </template>
<script> <script>
import {mapActions, mapMutations, mapState} from 'vuex'; import {mapActions, mapMutations, mapState} from 'vuex';
import ExpandableTable from "@/components/ExpandableTable.vue"; import Table from '@/components/Table';
import AsyncLoader from "@/components/AsyncLoader.vue";
export default { export default {
name: 'Events', name: 'Events',
components: {AsyncLoader, ExpandableTable}, components: {Table},
computed: mapState(['events']), computed: mapState(['events']),
data() {
return {new_address: []}
},
methods: { methods: {
...mapActions(['changeEvent', 'deleteEvent', 'updateEvent']), ...mapActions(['changeEvent', 'deleteEvent']),
...mapMutations(['openAddEventModal']), ...mapMutations(['openAddEventModal']),
safeDeleteEvent(id) { safeDeleteEvent(id) {
if (confirm('do you want to completely delete this event and related data?')) { if (confirm('do you want to completely delete this event and related data?')) {
this.deleteEvent(id) this.deleteEvent(id)
} }
}, },
addAddress(id) {
const a = this.new_address[id];
if (!this.events[id].addresses.includes(a))
this.events[id].addresses.push(a)
this.new_address[id] = ""
this.updateEvent({id: this.events[id].eid, partial_event: {addresses: this.events[id].addresses}});
},
deleteAddress(id, a_id) {
this.events[id].addresses = this.events[id].addresses.filter((e, i) => i !== a_id);
this.updateEvent({id: this.events[id].eid, partial_event: {addresses: this.events[id].addresses}});
}
}, },
}; };
</script> </script>
<style scoped> <style scoped>
</style> </style>