This commit is contained in:
j3d1 2024-06-18 16:43:23 +02:00
parent 5af3e72218
commit 804c47a3b8
9 changed files with 66 additions and 182 deletions

View file

@ -1,6 +1,6 @@
from django.contrib import admin
from tickets.models import IssueThread, Comment, StateChange, Assignment, ItemRelation, ShippingCode
from tickets.models import IssueThread, Comment, StateChange, Assignment, ItemRelation, ShippingVoucher
class IssueThreadAdmin(admin.ModelAdmin):
@ -23,7 +23,7 @@ class ItemRelationAdmin(admin.ModelAdmin):
pass
class ShippingCodesAdmin(admin.ModelAdmin):
class ShippingVouchersAdmin(admin.ModelAdmin):
pass
@ -32,4 +32,4 @@ admin.site.register(Comment, CommentAdmin)
admin.site.register(StateChange, StateChangeAdmin)
admin.site.register(Assignment, AssignmentAdmin)
admin.site.register(ItemRelation, ItemRelationAdmin)
admin.site.register(ShippingCode, ShippingCodesAdmin)
admin.site.register(ShippingVoucher, ShippingVouchersAdmin)

View file

@ -13,8 +13,8 @@ from core.settings import MAIL_DOMAIN
from mail.models import Email
from mail.protocol import send_smtp, make_reply, collect_references
from notify_sessions.models import SystemEvent
from tickets.models import IssueThread, Comment, STATE_CHOICES, ShippingCode
from tickets.serializers import IssueSerializer, CommentSerializer, ShippingCodeSerializer
from tickets.models import IssueThread, Comment, STATE_CHOICES, ShippingVoucher
from tickets.serializers import IssueSerializer, CommentSerializer, ShippingVoucherSerializer
class IssueViewSet(viewsets.ModelViewSet):
@ -118,7 +118,7 @@ def add_comment(request, pk):
router = routers.SimpleRouter()
router.register(r'tickets', IssueViewSet, basename='issues')
router.register(r'shipping_codes', ShippingCodeViewSet, basename='shipping_codes')
router.register(r'shipping_vouchers', ShippingVoucherViewSet, basename='shipping_vouchers')
urlpatterns = ([
re_path(r'^tickets/(?P<pk>\d+)/reply/$', reply, name='reply'),

View file

@ -1,25 +0,0 @@
# Generated by Django 4.2.7 on 2024-06-15 17:37
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('tickets', '0011_issuethread_related_items'),
]
operations = [
migrations.CreateModel(
name='ShippingCode',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('code', models.CharField(max_length=255)),
('type', models.CharField(max_length=255)),
('timestamp', models.DateTimeField(auto_now_add=True)),
('used_at', models.DateTimeField(null=True)),
('issue_thread', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='shipping_codes', to='tickets.issuethread')),
],
),
]

View file

@ -138,9 +138,9 @@ class ItemRelation(models.Model):
return str(self.issue_thread) + ' related to ' + str(self.item)
class ShippingCode(models.Model):
class ShippingVoucher(models.Model):
id = models.AutoField(primary_key=True)
issue_thread = models.ForeignKey(IssueThread, on_delete=models.CASCADE, related_name='shipping_codes', null=True)
issue_thread = models.ForeignKey(IssueThread, on_delete=models.CASCADE, related_name='shipping_vouchers', null=True)
code = models.CharField(max_length=255)
type = models.CharField(max_length=255)
timestamp = models.DateTimeField(auto_now_add=True)

View file

@ -2,7 +2,7 @@ from rest_framework import serializers
from authentication.models import ExtendedUser
from mail.api_v2 import AttachmentSerializer
from tickets.models import IssueThread, Comment, STATE_CHOICES, ShippingCode
from tickets.models import IssueThread, Comment, STATE_CHOICES, ShippingVoucher
from inventory.serializers import ItemSerializer
@ -29,9 +29,9 @@ class StateSerializer(serializers.Serializer):
return obj['value']
class ShippingCodeSerializer(serializers.ModelSerializer):
class ShippingVoucherSerializer(serializers.ModelSerializer):
class Meta:
model = ShippingCode
model = ShippingVoucher
fields = ('id', 'code', 'type', 'timestamp', 'issue_thread', 'used_at')
read_only_fields = ('id', 'timestamp', 'used_at')
@ -122,13 +122,13 @@ class IssueSerializer(serializers.ModelSerializer):
'timestamp': relation.timestamp,
'item': ItemSerializer(relation.item).data,
})
for shipping_code in obj.shipping_codes.all():
for shipping_voucher in obj.shipping_vouchers.all():
timeline.append({
'type': 'shipping_code',
'id': shipping_code.id,
'timestamp': shipping_code.used_at,
'code': shipping_code.code,
'code_type': shipping_code.type,
'type': 'shipping_voucher',
'id': shipping_voucher.id,
'timestamp': shipping_voucher.used_at,
'code': shipping_voucher.code,
'code_type': shipping_voucher.type,
})
return sorted(timeline, key=lambda x: x['timestamp'])

View file

@ -4,12 +4,12 @@ from django.test import TestCase, Client
from authentication.models import ExtendedUser
from mail.models import Email, EmailAttachment
from tickets.models import IssueThread, StateChange, Comment, ShippingCode
from tickets.models import IssueThread, StateChange, Comment, ShippingVoucher
from django.contrib.auth.models import Permission
from knox.models import AuthToken
class ShippingCodeApiTest(TestCase):
class ShippingVoucherApiTest(TestCase):
def setUp(self):
super().setUp()
@ -20,13 +20,13 @@ class ShippingCodeApiTest(TestCase):
self.client = Client(headers={'Authorization': 'Token ' + self.token[1]})
def test_issues_empty(self):
response = self.client.get('/api/2/shipping_codes/')
response = self.client.get('/api/2/shipping_vouchers/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), [])
def test_issues_list(self):
ShippingCode.objects.create(code='1234', type='2kg-eu')
response = self.client.get('/api/2/shipping_codes/')
ShippingVoucher.objects.create(code='1234', type='2kg-eu')
response = self.client.get('/api/2/shipping_vouchers/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()[0]['code'], '1234')
self.assertEqual(response.json()[0]['used_at'], None)
@ -34,7 +34,7 @@ class ShippingCodeApiTest(TestCase):
self.assertEqual(response.json()[0]['type'], '2kg-eu')
def test_issues_create(self):
response = self.client.post('/api/2/shipping_codes/', {'code': '1234', 'type': '2kg-eu'})
response = self.client.post('/api/2/shipping_vouchers/', {'code': '1234', 'type': '2kg-eu'})
self.assertEqual(response.status_code, 201)
self.assertEqual(response.json()['code'], '1234')
self.assertEqual(response.json()['used_at'], None)

View file

@ -1,91 +0,0 @@
<template>
<div class="timeline-item-description">
<i class="avatar | small">
<font-awesome-icon icon="user"/>
</i>
<span><a href="#">$USER</a> has claimed sipping code
<ClipboardButton class="btn btn-primary badge badge-pill" title="Copy shipping code to clipboard" :payload="item.code">{{ item.code }}
<font-awesome-icon icon="clipboard"/>
</ClipboardButton> of type <span class="badge badge-pill badge-secondary">{{ item.code_type }}</span> for this ticket at <time
:datetime="timestamp">{{ timestamp }}</time>
</span>
</div>
</template>
<script>
import {mapState} from "vuex";
import ClipboardButton from "@/components/inputs/ClipboardButton.vue";
export default {
name: 'TimelineShippingCode',
components: {ClipboardButton},
props: {
'item': {
type: Object,
required: true
}
},
computed: {
...mapState(['state_options']),
'timestamp': function () {
return new Date(this.item.timestamp).toLocaleString();
},
}
};
</script>
<style scoped>
a {
color: inherit;
}
.timeline-item-description {
display: flex;
padding-top: 6px;
gap: 8px;
color: var(--gray);
img {
flex-shrink: 0;
}
a {
/*color: var(--c-grey-500);*/
font-weight: 500;
text-decoration: none;
&:hover,
&:focus {
outline: 0; /* Don't actually do this */
color: var(--info);
}
}
}
.avatar {
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
overflow: hidden;
aspect-ratio: 1 / 1;
flex-shrink: 0;
width: 40px;
height: 40px;
&.small {
width: 28px;
height: 28px;
}
img {
object-fit: cover;
}
}
</style>

View file

@ -21,7 +21,7 @@ const store = createStore({
state_options: [],
messageTemplates: [],
messageTemplateVariables: [],
shippingCodes: [],
shippingVouchers: [],
lastEvent: '37C3',
lastUsed: {},
@ -51,7 +51,7 @@ const store = createStore({
showAddBoxModal: false,
test: ['foo', 'bar', 'baz'],
shippingCodeTypes: {
shippingVoucherTypes: {
'2kg-de': '2kg Paket (DE)',
'5kg-de': '5kg Paket (DE)',
'10kg-de': '10kg Paket (DE)',
@ -219,8 +219,8 @@ const store = createStore({
setMessageTemplateVariables(state, variables) {
state.messageTemplateVariables = variables;
},
setShippingCodes(state, codes) {
state.shippingCodes = codes;
setShippingVouchers(state, codes) {
state.shippingVouchers = codes;
},
},
actions: {
@ -465,16 +465,16 @@ const store = createStore({
dispatch('fetchMessageTemplates');
}
},
async fetchShippingCodes({commit, state}) {
const {data, success} = await http.get('/2/shipping_codes/', state.user.token);
async fetchShippingVouchers({commit, state}) {
const {data, success} = await http.get('/2/shipping_vouchers/', state.user.token);
if (data && success) {
commit('setShippingCodes', data);
commit('setShippingVouchers', data);
}
},
async createShippingCode({dispatch, state}, code) {
const {data, success} = await http.post('/2/shipping_codes/', code, state.user.token);
async createShippingVoucher({dispatch, state}, code) {
const {data, success} = await http.post('/2/shipping_vouchers/', code, state.user.token);
if (data && success) {
dispatch('fetchShippingCodes');
dispatch('fetchShippingVouchers');
}
}
},
@ -506,7 +506,7 @@ const store = createStore({
"loadedItems",
"messageTemplates",
"messageTemplatesVariables",
"shippingCodes",
"shippingVouchers",
],
watch: [
"test",
@ -519,7 +519,7 @@ const store = createStore({
"loadedItems",
"messageTemplates",
"messageTemplatesVariables",
"shippingCodes",
"shippingVouchers",
],
mutations: [
//"replaceTickets",

View file

@ -1,9 +1,9 @@
<template>
<div>
<h3>Shipping Codes</h3>
<h3>Shipping Vouchers</h3>
<div class="mt-3">
<h5>Shipping Code Types</h5>
<span v-for="(type, key) in availableShippingCodeTypes()" :key="key" class="mr-2">
<h5>Shipping Voucher Types</h5>
<span v-for="(type, key) in availableShippingVoucherTypes()" :key="key" class="mr-2">
<span v-if="type.count > 2" class="badge badge-success">{{ type.name }} - {{ type.count }}</span>
<span v-else-if="type.count > 0" class="badge badge-warning" v-if="type.count > 0">
{{ type.name }} - {{ type.count }}
@ -12,23 +12,23 @@
</span>
</div>
<div class="mt-3">
<h5>Available Shipping Codes</h5>
<h5>Available Shipping Vouchers</h5>
<ul>
<li v-for="code in shippingCodes" :key="code.code">
<span v-if="code.issue_thread == null">{{ code.type }} - {{ code.code }}</span>
<span v-else><s style="color:var(--danger)">{{ code.type }} - {{ code.code }}</s> use in <a
:href="'/'+ getEventSlug + '/ticket/' + code.issue_thread">#{{ code.issue_thread }}</a></span>
<li v-for="voucher in shippingVouchers" :key="voucher.voucher">
<span v-if="voucher.issue_thread == null">{{ voucher.type }} - {{ voucher.voucher }}</span>
<span v-else><s style="color:var(--danger)">{{ voucher.type }} - {{ voucher.voucher }}</s> used in <a
:href="'/'+ getEventSlug + '/ticket/' + voucher.issue_thread">#{{ voucher.issue_thread }}</a></span>
</li>
</ul>
</div>
<div class="mt-3">
<textarea class="form-control mb-3" rows="5" placeholder="Shipping Code List" v-model="bulk_codes"
<textarea class="form-control mb-3" rows="5" placeholder="Shipping Voucher List" v-model="bulk_vouchers"
v-if="bulk"></textarea>
<div class="input-group">
<input type="text" class="form-control" placeholder="Shipping Code" v-model="code" v-if="!bulk">
<input type="text" class="form-control" placeholder="Shipping Voucher" v-model="voucher" v-if="!bulk">
<select class="form-control" v-model="type">
<option v-for="it in Object.keys(shippingCodeTypes)" :value="it">{{
shippingCodeTypes[it]
<option v-for="it in Object.keys(shippingVoucherTypes)" :value="it">{{
shippingVoucherTypes[it]
}}
</option>
</select>
@ -38,9 +38,9 @@
<label for="bulk" style="margin: 0;">Bulk</label>
</div>
</div>
<button class="btn btn-primary form-control" @click="createSingleOrBulkShippingCode">
<button class="btn btn-primary form-control" @click="createSingleOrBulkShippingVoucher">
<font-awesome-icon icon="plus"/>
{{ (bulk ? "Add Shipping Codes" : "Add Shipping Code") }}
{{ (bulk ? "Add Shipping Vouchers" : "Add Shipping Voucher") }}
</button>
</div>
</div>
@ -56,44 +56,44 @@ export default {
components: {Table},
data() {
return {
code: '',
bulk_codes: '',
voucher: '',
bulk_vouchers: '',
type: '2kg-eu',
bulk: false,
};
},
computed: {
...mapState(['shippingCodes', 'shippingCodeTypes']),
...mapState(['shippingVouchers', 'shippingVoucherTypes']),
...mapGetters(['getEventSlug']),
},
methods: {
...mapActions(['fetchShippingCodes', 'createShippingCode']),
createSingleOrBulkShippingCode() {
...mapActions(['fetchShippingVouchers', 'createShippingVoucher']),
createSingleOrBulkShippingVoucher() {
if (this.bulk) {
const list = this.bulk_codes.split('\n');
if (confirm('Are you sure you want to add ' + list.length + ' shipping codes as ' + this.type + '?')) {
const jobs = list.map(code => {
return this.createShippingCode({code: code.trim(), type: this.type});
const list = this.bulk_vouchers.split('\n');
if (confirm('Are you sure you want to add ' + list.length + ' shipping vouchers as ' + this.type + '?')) {
const jobs = list.map(voucher => {
return this.createShippingVoucher({voucher: voucher.trim(), type: this.type});
});
Promise.all(jobs).then(() => {
this.bulk_codes = '';
this.bulk_vouchers = '';
});
}
} else {
this.createShippingCode({code: this.code, type: this.type}).then(() => {
this.code = '';
this.createShippingVoucher({voucher: this.voucher, type: this.type}).then(() => {
this.voucher = '';
});
}
},
availableShippingCodeTypes() {
return Object.keys(this.shippingCodeTypes).map(key => {
var count = this.shippingCodes.filter(code => code.type === key && code.issue_thread === null).length;
return {id: key, count: count, name: this.shippingCodeTypes[key]};
availableShippingVoucherTypes() {
return Object.keys(this.shippingVoucherTypes).map(key => {
var count = this.shippingVouchers.filter(voucher => voucher.type === key && voucher.issue_thread === null).length;
return {id: key, count: count, name: this.shippingVoucherTypes[key]};
});
},
},
mounted() {
this.fetchShippingCodes();
this.fetchShippingVouchers();
},
};
</script>