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):
query_tokens = query.split(' ')
matches = []
for item in items:
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:
if token in item.description:
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:
yield {'search_score': value, 'item': item}
yield {'search_score': value, 'item': item, 'search_matches': matches}
@api_view(['GET'])

View file

@ -137,10 +137,12 @@ class ItemSerializer(BasicItemSerializer):
class SearchResultSerializer(serializers.Serializer):
search_score = serializers.IntegerField()
search_matches = serializers.ListField(child=serializers.DictField())
item = ItemSerializer()
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:
model = Item

View file

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

View file

@ -139,10 +139,12 @@ class IssueSerializer(BasicIssueSerializer):
class SearchResultSerializer(serializers.Serializer):
search_score = serializers.IntegerField()
search_matches = serializers.ListField(child=serializers.DictField())
issue = IssueSerializer()
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:
model = IssueThread

View file

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

View file

@ -4,6 +4,7 @@ from django.test import TestCase, Client
from authentication.models import ExtendedUser
from inventory.models import Event, Container, Item
from inventory.models import Comment as ItemComment
from mail.models import Email, EmailAttachment
from tickets.models import IssueThread, StateChange, Comment, ItemRelation, Assignment
from django.contrib.auth.models import Permission
@ -407,16 +408,16 @@ class IssueSearchTest(TestCase):
mail1 = Email.objects.create(
subject='test',
body='test aBc',
sender='test',
recipient='test',
sender='bar@test',
recipient='2@test',
issue_thread=issue,
timestamp=now,
)
mail2 = Email.objects.create(
subject='test',
subject='Re: test',
body='test',
sender='test',
recipient='test',
sender='2@test',
recipient='1@test',
issue_thread=issue,
in_reply_to=mail1.reference,
timestamp=now + timedelta(seconds=2),
@ -436,6 +437,11 @@ class IssueSearchTest(TestCase):
item=self.item,
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')
response = self.client.get(f'/api/2/{self.event.slug}/tickets/{search_query}/')
self.assertEqual(200, response.status_code)
@ -465,3 +471,21 @@ class IssueSearchTest(TestCase):
self.assertGreater(score3, score2)
self.assertGreater(score2, score1)
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)',
'5kg-de': '5kg Paket (DE)',
'10kg-de': '10kg Paket (DE)',
'2kg-eu': '2kg Paket (EU)',
'5kg-eu': '5kg Paket (EU)',
'10kg-eu': '10kg Paket (EU)',
}
@ -564,6 +563,14 @@ const store = createStore({
state.fetchedData.tickets = 0;
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

View file

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