Compare commits

...

2 commits

Author SHA1 Message Date
02747dd107 link item to ticket frontend
All checks were successful
/ test (push) Successful in 2m50s
/ deploy (push) Successful in 9m16s
2025-01-26 18:39:02 +01:00
140370739f stash
All checks were successful
/ test (push) Successful in 2m30s
/ deploy (push) Successful in 3m12s
2025-01-23 21:57:14 +01:00
8 changed files with 138 additions and 12 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

@ -34,6 +34,7 @@ drf-yasg==1.21.7
frozenlist==1.4.1 frozenlist==1.4.1
h11==0.14.0 h11==0.14.0
hyperlink==21.0.0 hyperlink==21.0.0
hypothesis==6.124.2
idna==3.4 idna==3.4
incremental==22.10.0 incremental==22.10.0
inflection==0.5.1 inflection==0.5.1

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

@ -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

@ -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"
@click="linkTicketItem({ticket_id: ticket.id, item_id: 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;