Compare commits

...

4 commits

Author SHA1 Message Date
9395226c5f disable automatic ticket state change
All checks were successful
/ test (push) Successful in 2m27s
/ deploy (push) Successful in 3m13s
2025-01-27 20:01:31 +01:00
c26152d3c5 fix frontend bug in ticket view
All checks were successful
/ test (push) Successful in 2m36s
2025-01-27 19:57:22 +01:00
70516db074 ~ change "the algorithm" ~
All checks were successful
/ test (push) Successful in 2m31s
2025-01-26 20:03:06 +01:00
2677f4b8b6 link item to ticket frontend
All checks were successful
/ test (push) Successful in 2m44s
2025-01-26 19:56:25 +01:00
8 changed files with 144 additions and 18 deletions

View file

@ -39,13 +39,61 @@ class ItemViewSet(viewsets.ModelViewSet):
def filter_items(items, query): def filter_items(items, query):
query_tokens = query.split(' ') query_tokens = query.split(' ')
matches = []
for item in items: for item in items:
value = 0 value = 0
if "I#" + str(item.id) in query:
value += 12
matches.append(
{'type': 'item_id', 'text': f'is exactly {item.id} and matched "I#{item.id}"'})
elif "#" + str(item.id) in query:
value += 11
matches.append(
{'type': 'item_id', 'text': f'is exactly {item.id} and matched "#{item.id}"'})
elif str(item.id) in query:
value += 10
matches.append({'type': 'item_id', 'text': f'is exactly {item.id}'})
for issue in item.related_issues:
if "T#" + issue.short_uuid() in query:
value += 8
matches.append({'type': 'ticket_uuid',
'text': f'is exactly {issue.short_uuid()} and matched "T#{issue.short_uuid()}"'})
elif "#" + issue.short_uuid() in query:
value += 5
matches.append({'type': 'ticket_uuid',
'text': f'is exactly {issue.short_uuid()} and matched "#{issue.short_uuid()}"'})
elif issue.short_uuid() in query:
value += 3
matches.append({'type': 'ticket_uuid', 'text': f'is exactly {issue.short_uuid()}'})
if "T#" + str(issue.id) in query:
value += 8
matches.append({'type': 'ticket_id', 'text': f'is exactly {issue.id} and matched "T#{issue.id}"'})
elif "#" + str(issue.id) in query:
value += 5
matches.append({'type': 'ticket_id', 'text': f'is exactly {issue.id} and matched "#{issue.id}"'})
elif str(issue.id) in query:
value += 3
matches.append({'type': 'ticket_id', 'text': f'is exactly {issue.id}'})
for comment in issue.comments.all():
for token in query_tokens:
if token in comment.comment:
value += 1
matches.append({'type': 'ticket_comment', 'text': f'contains {token}'})
for token in query_tokens:
if token in issue.name:
value += 1
matches.append({'type': 'ticket_name', 'text': f'contains {token}'})
for token in query_tokens: for token in query_tokens:
if token in item.description: if token in item.description:
value += 1 value += 1
matches.append({'type': 'item_description', 'text': f'contains {token}'})
for comment in item.comments.all():
for token in query_tokens:
if token in comment.comment:
value += 1
matches.append({'type': 'comment', 'text': f'contains {token}'})
if value > 0: if value > 0:
yield {'search_score': value, 'item': item} yield {'search_score': value, 'item': item, 'search_matches': matches}
@api_view(['GET']) @api_view(['GET'])

View file

@ -137,10 +137,12 @@ class ItemSerializer(BasicItemSerializer):
class SearchResultSerializer(serializers.Serializer): class SearchResultSerializer(serializers.Serializer):
search_score = serializers.IntegerField() search_score = serializers.IntegerField()
search_matches = serializers.ListField(child=serializers.DictField())
item = ItemSerializer() item = ItemSerializer()
def to_representation(self, instance): def to_representation(self, instance):
return {**ItemSerializer(instance['item']).data, 'search_score': instance['search_score']} return {**ItemSerializer(instance['item']).data, 'search_score': instance['search_score'],
'search_matches': instance['search_matches']}
class Meta: class Meta:
model = Item model = Item

View file

@ -144,37 +144,70 @@ def add_comment(request, pk):
def filter_issues(issues, query): def filter_issues(issues, query):
query_tokens = query.lower().split(' ') query_tokens = query.lower().split(' ')
matches = []
for issue in issues: for issue in issues:
value = 0 value = 0
if issue.short_uuid() in query: if "T#" + issue.short_uuid() in query:
value += 12
matches.append(
{'type': 'ticket_uuid', 'text': f'is exactly {issue.short_uuid()} and matched "T#{issue.short_uuid()}"'})
elif "#" + issue.short_uuid() in query:
value += 11
matches.append(
{'type': 'ticket_uuid', 'text': f'is exactly {issue.short_uuid()} and matched "#{issue.short_uuid()}"'})
elif issue.short_uuid() in query:
value += 10 value += 10
matches.append({'type': 'ticket_uuid', 'text': f'is exactly {issue.short_uuid()}'})
if "T#" + str(issue.id) in query: if "T#" + str(issue.id) in query:
value += 10 value += 10
matches.append({'type': 'ticket_id', 'text': f'is exactly {issue.id} and matched "T#{issue.id}"'})
elif "#" + str(issue.id) in query: elif "#" + str(issue.id) in query:
value += 9 value += 7
matches.append({'type': 'ticket_id', 'text': f'is exactly {issue.id} and matched "#{issue.id}"'})
elif str(issue.id) in query:
value += 4
matches.append({'type': 'ticket_id', 'text': f'is exactly {issue.id}'})
for item in issue.related_items: for item in issue.related_items:
if "I#" + str(item.id) in query: if "I#" + str(item.id) in query:
value += 8 value += 8
matches.append({'type': 'item_id', 'text': f'is exactly {item.id} and matched "I#{item.id}"'})
elif "#" + str(item.id) in query: elif "#" + str(item.id) in query:
value += 5 value += 5
matches.append({'type': 'item_id', 'text': f'is exactly {item.id} and matched "#{item.id}"'})
elif str(item.id) in query:
value += 3
matches.append({'type': 'item_id', 'text': f'is exactly {item.id}'})
for token in query_tokens: for token in query_tokens:
if token in item.description.lower(): if token in item.description.lower():
value += 1 value += 1
matches.append({'type': 'item_description', 'text': f'contains {token}'})
for comment in item.comments.all():
for token in query_tokens:
if token in comment.comment.lower():
value += 1
matches.append({'type': 'item_comment', 'text': f'contains {token}'})
for token in query_tokens: for token in query_tokens:
if token in issue.name.lower(): if token in issue.name.lower():
value += 1 value += 1
matches.append({'type': 'ticket_name', 'text': f'contains {token}'})
for comment in issue.comments.all(): for comment in issue.comments.all():
for token in query_tokens: for token in query_tokens:
if token in comment.comment.lower(): if token in comment.comment.lower():
value += 1 value += 1
matches.append({'type': 'ticket_comment', 'text': f'contains {token}'})
for email in issue.emails.all(): for email in issue.emails.all():
for token in query_tokens: for token in query_tokens:
if token in email.subject.lower(): if token in email.subject.lower():
value += 1 value += 1
matches.append({'type': 'email_subject', 'text': f'contains {token}'})
if token in email.body.lower(): if token in email.body.lower():
value += 1 value += 1
matches.append({'type': 'email_body', 'text': f'contains {token}'})
if token in email.sender.lower():
value += 1
matches.append({'type': 'email_sender', 'text': f'contains {token}'})
if value > 0: if value > 0:
yield {'search_score': value, 'issue': issue} yield {'search_score': value, 'issue': issue, 'search_matches': matches}
@api_view(['GET']) @api_view(['GET'])

View file

@ -139,10 +139,12 @@ class IssueSerializer(BasicIssueSerializer):
class SearchResultSerializer(serializers.Serializer): class SearchResultSerializer(serializers.Serializer):
search_score = serializers.IntegerField() search_score = serializers.IntegerField()
search_matches = serializers.ListField(child=serializers.DictField())
issue = IssueSerializer() issue = IssueSerializer()
def to_representation(self, instance): def to_representation(self, instance):
return {**IssueSerializer(instance['issue']).data, 'search_score': instance['search_score']} return {**IssueSerializer(instance['issue']).data, 'search_score': instance['search_score'],
'search_matches': instance['search_matches']}
class Meta: class Meta:
model = IssueThread model = IssueThread

View file

@ -9,6 +9,7 @@ class RelationSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = ItemRelation model = ItemRelation
fields = ('id', 'status', 'timestamp', 'item', 'issue_thread') fields = ('id', 'status', 'timestamp', 'item', 'issue_thread')
read_only_fields = ('id', 'timestamp')
class BasicIssueSerializer(serializers.ModelSerializer): class BasicIssueSerializer(serializers.ModelSerializer):

View file

@ -4,6 +4,7 @@ from django.test import TestCase, Client
from authentication.models import ExtendedUser from authentication.models import ExtendedUser
from inventory.models import Event, Container, Item from inventory.models import Event, Container, Item
from inventory.models import Comment as ItemComment
from mail.models import Email, EmailAttachment from mail.models import Email, EmailAttachment
from tickets.models import IssueThread, StateChange, Comment, ItemRelation, Assignment from tickets.models import IssueThread, StateChange, Comment, ItemRelation, Assignment
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
@ -407,16 +408,16 @@ class IssueSearchTest(TestCase):
mail1 = Email.objects.create( mail1 = Email.objects.create(
subject='test', subject='test',
body='test aBc', body='test aBc',
sender='test', sender='bar@test',
recipient='test', recipient='2@test',
issue_thread=issue, issue_thread=issue,
timestamp=now, timestamp=now,
) )
mail2 = Email.objects.create( mail2 = Email.objects.create(
subject='test', subject='Re: test',
body='test', body='test',
sender='test', sender='2@test',
recipient='test', recipient='1@test',
issue_thread=issue, issue_thread=issue,
in_reply_to=mail1.reference, in_reply_to=mail1.reference,
timestamp=now + timedelta(seconds=2), timestamp=now + timedelta(seconds=2),
@ -436,6 +437,11 @@ class IssueSearchTest(TestCase):
item=self.item, item=self.item,
timestamp=now + timedelta(seconds=5), timestamp=now + timedelta(seconds=5),
) )
item_comment = ItemComment.objects.create(
item=self.item,
comment="baz",
timestamp=now + timedelta(seconds=6),
)
search_query = b64encode(b'abC').decode('utf-8') search_query = b64encode(b'abC').decode('utf-8')
response = self.client.get(f'/api/2/{self.event.slug}/tickets/{search_query}/') response = self.client.get(f'/api/2/{self.event.slug}/tickets/{search_query}/')
self.assertEqual(200, response.status_code) self.assertEqual(200, response.status_code)
@ -465,3 +471,21 @@ class IssueSearchTest(TestCase):
self.assertGreater(score3, score2) self.assertGreater(score3, score2)
self.assertGreater(score2, score1) self.assertGreater(score2, score1)
self.assertGreater(score1, 0) self.assertGreater(score1, 0)
search_query = b64encode(b'foo').decode('utf-8')
response = self.client.get(f'/api/2/{self.event.slug}/tickets/{search_query}/')
self.assertEqual(200, response.status_code)
self.assertEqual(1, len(response.json()))
self.assertEqual(issue.id, response.json()[0]['id'])
search_query = b64encode(b'bar').decode('utf-8')
response = self.client.get(f'/api/2/{self.event.slug}/tickets/{search_query}/')
self.assertEqual(200, response.status_code)
self.assertEqual(1, len(response.json()))
self.assertEqual(issue.id, response.json()[0]['id'])
search_query = b64encode(b'baz').decode('utf-8')
response = self.client.get(f'/api/2/{self.event.slug}/tickets/{search_query}/')
self.assertEqual(200, response.status_code)
self.assertEqual(1, len(response.json()))
self.assertEqual(issue.id, response.json()[0]['id'])

View file

@ -61,7 +61,6 @@ const store = createStore({
'2kg-de': '2kg Paket (DE)', '2kg-de': '2kg Paket (DE)',
'5kg-de': '5kg Paket (DE)', '5kg-de': '5kg Paket (DE)',
'10kg-de': '10kg Paket (DE)', '10kg-de': '10kg Paket (DE)',
'2kg-eu': '2kg Paket (EU)',
'5kg-eu': '5kg Paket (EU)', '5kg-eu': '5kg Paket (EU)',
'10kg-eu': '10kg Paket (EU)', '10kg-eu': '10kg Paket (EU)',
} }
@ -564,6 +563,14 @@ const store = createStore({
state.fetchedData.tickets = 0; state.fetchedData.tickets = 0;
await Promise.all([dispatch('loadTickets'), dispatch('fetchShippingVouchers')]); await Promise.all([dispatch('loadTickets'), dispatch('fetchShippingVouchers')]);
} }
},
async linkTicketItem({dispatch, state, getters}, {ticket_id, item_id}) {
const {data, success} = await getters.session.post(`/2/matches/`, {issue_thread: ticket_id, item: item_id});
if (data && success) {
state.fetchedData.tickets = 0;
state.fetchedData.items = 0;
await Promise.all([dispatch('loadTickets'), dispatch('loadEventItems')]);
}
} }
}, },
plugins: [persistentStatePlugin({ // TODO change remember to some kind of enable field plugins: [persistentStatePlugin({ // TODO change remember to some kind of enable field

View file

@ -17,7 +17,7 @@
<textarea placeholder="add comment..." v-model="newComment" <textarea placeholder="add comment..." v-model="newComment"
class="form-control"> class="form-control">
</textarea> </textarea>
<AsyncButton class="btn btn-primary float-right" :task="addCommentAndClear"> <AsyncButton class="btn btn-secondary float-right" :task="addCommentAndClear">
<font-awesome-icon icon="comment"/> <font-awesome-icon icon="comment"/>
Save Comment Save Comment
</AsyncButton> </AsyncButton>
@ -25,7 +25,7 @@
</div> </div>
</template> </template>
<template v-slot:timeline_action2> <template v-slot:timeline_action2>
<span class="timeline-item-icon | faded-icon"> <span class="timeline-item-icon | filled-icon">
<font-awesome-icon icon="envelope"/> <font-awesome-icon icon="envelope"/>
</span> </span>
<div class="new-mail card bg-dark"> <div class="new-mail card bg-dark">
@ -81,6 +81,13 @@
<font-awesome-icon icon="clipboard"/> <font-awesome-icon icon="clipboard"/>
Copy&nbsp;DHL&nbsp;contact&nbsp;to&nbsp;clipboard Copy&nbsp;DHL&nbsp;contact&nbsp;to&nbsp;clipboard
</ClipboardButton> </ClipboardButton>
<div class="btn-group">
<input type="text" class="form-control" v-model="item_id">
<button class="form-control btn btn-success" :disabled="!item_id"
@click="linkTicketItem({ticket_id: ticket.id, item_id: parseInt(item_id)}).then(()=>item_id='')">
Link&nbsp;Item
</button>
</div>
<div class="btn-group"> <div class="btn-group">
<select class="form-control" v-model="shipping_voucher_type"> <select class="form-control" v-model="shipping_voucher_type">
<option v-for="type in availableShippingVoucherTypes.filter(t=>t.count>0)" <option v-for="type in availableShippingVoucherTypes.filter(t=>t.count>0)"
@ -141,6 +148,7 @@ export default {
selected_state: null, selected_state: null,
selected_assignee: null, selected_assignee: null,
shipping_voucher_type: null, shipping_voucher_type: null,
item_id: "",
newMail: "", newMail: "",
newComment: "" newComment: ""
} }
@ -166,6 +174,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']),
...mapActions(['linkTicketItem']),
...mapMutations(['openLightboxModalWith']), ...mapMutations(['openLightboxModalWith']),
changeTicketStatus() { changeTicketStatus() {
this.ticket.state = this.selected_state; this.ticket.state = this.selected_state;
@ -198,10 +207,10 @@ export default {
}, },
mounted() { mounted() {
this.scheduleAfterInit(() => [Promise.all([this.fetchTicketStates(), this.loadTickets(), this.loadUsers(), this.fetchShippingVouchers()]).then(() => { this.scheduleAfterInit(() => [Promise.all([this.fetchTicketStates(), this.loadTickets(), this.loadUsers(), this.fetchShippingVouchers()]).then(() => {
if (this.ticket.state === "pending_new") { //if (this.ticket.state === "pending_new") {
this.selected_state = "pending_open"; // this.selected_state = "pending_open";
this.changeTicketStatus() // this.changeTicketStatus()
} //}
this.selected_state = this.ticket.state; this.selected_state = this.ticket.state;
this.selected_assignee = this.ticket.assigned_to this.selected_assignee = this.ticket.assigned_to
})]); })]);