diff --git a/core/files/migrations/0002_alter_file_file.py b/core/files/migrations/0002_alter_file_file.py new file mode 100644 index 0000000..6a0c33e --- /dev/null +++ b/core/files/migrations/0002_alter_file_file.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.7 on 2024-01-10 19:04 + +from django.db import migrations, models +import files.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('files', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='file', + name='file', + field=models.FileField(upload_to=files.models.hash_upload), + ), + ] diff --git a/core/mail/migrations/0004_alter_emailattachment_file.py b/core/mail/migrations/0004_alter_emailattachment_file.py new file mode 100644 index 0000000..4342c8e --- /dev/null +++ b/core/mail/migrations/0004_alter_emailattachment_file.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.7 on 2024-01-10 19:04 + +from django.db import migrations, models +import files.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mail', '0003_emailattachment'), + ] + + operations = [ + migrations.AlterField( + model_name='emailattachment', + name='file', + field=models.FileField(upload_to=files.models.hash_upload), + ), + ] diff --git a/core/mail/protocol.py b/core/mail/protocol.py index 9a515fb..b199f36 100644 --- a/core/mail/protocol.py +++ b/core/mail/protocol.py @@ -158,8 +158,8 @@ def receive_email(envelope, log=None): log.warning("Header to does not match envelope to") log.info(f"Header to: {header_to}, envelope to: {envelope.rcpt_tos[0]}") - recipient = envelope.rcpt_tos[0].lower() - sender = envelope.mail_from + recipient = envelope.rcpt_tos[0].lower() if envelope.rcpt_tos else header_to.lower() + sender = envelope.mail_from if envelope.mail_from else header_from subject = parsed.get('Subject') subject = unescape_and_decode_quoted_printable(subject) subject = unescape_and_decode_base64(subject) @@ -188,7 +188,7 @@ def receive_email(envelope, log=None): in_reply_to=header_message_id, event=target_event, issue_thread=active_issue_thread) reply = make_reply(reply_email, references) else: - #change state if not new + # change state if not new if active_issue_thread.state != 'pending_new': active_issue_thread.state = 'pending_open' active_issue_thread.save() diff --git a/core/tickets/api_v2.py b/core/tickets/api_v2.py index ac958e6..a4beaaa 100644 --- a/core/tickets/api_v2.py +++ b/core/tickets/api_v2.py @@ -13,11 +13,12 @@ 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, StateChange +from tickets.models import IssueThread, Comment, STATE_CHOICES class IssueSerializer(serializers.ModelSerializer): timeline = serializers.SerializerMethodField() + last_activity = serializers.SerializerMethodField() class Meta: model = IssueThread @@ -36,6 +37,18 @@ class IssueSerializer(serializers.ModelSerializer): raise serializers.ValidationError('invalid state') return attrs + @staticmethod + def get_last_activity(self): + try: + last_state_change = self.state_changes.order_by('-timestamp').first().timestamp \ + if self.state_changes.count() > 0 else None + last_comment = self.comments.order_by('-timestamp').first().timestamp if self.comments.count() > 0 else None + last_mail = self.emails.order_by('-timestamp').first().timestamp if self.emails.count() > 0 else None + args = [x for x in [last_state_change, last_comment, last_mail] if x is not None] + return max(args) + except AttributeError: + return None + @staticmethod def get_timeline(obj): timeline = [] @@ -65,6 +78,9 @@ class IssueSerializer(serializers.ModelSerializer): }) return sorted(timeline, key=lambda x: x['timestamp']) + def get_queryset(self): + return IssueThread.objects.all().order_by('-last_activity') + class IssueViewSet(viewsets.ModelViewSet): serializer_class = IssueSerializer diff --git a/core/tickets/migrations/0005_remove_issuethread_last_activity.py b/core/tickets/migrations/0005_remove_issuethread_last_activity.py new file mode 100644 index 0000000..37a03ef --- /dev/null +++ b/core/tickets/migrations/0005_remove_issuethread_last_activity.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.7 on 2024-01-10 19:04 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tickets', '0004_remove_issuethread_state_alter_statechange_state'), + ] + + operations = [ + migrations.RemoveField( + model_name='issuethread', + name='last_activity', + ), + ] diff --git a/core/tickets/models.py b/core/tickets/models.py index 4e388de..a721385 100644 --- a/core/tickets/models.py +++ b/core/tickets/models.py @@ -27,7 +27,6 @@ class IssueThread(SoftDeleteModel): id = models.AutoField(primary_key=True) name = models.CharField(max_length=255) assigned_to = models.CharField(max_length=255, null=True) - last_activity = models.DateTimeField(auto_now=True) manually_created = models.BooleanField(default=False) @property diff --git a/core/tickets/tests/v2/test_tickets.py b/core/tickets/tests/v2/test_tickets.py index e15d813..01b598f8 100644 --- a/core/tickets/tests/v2/test_tickets.py +++ b/core/tickets/tests/v2/test_tickets.py @@ -59,7 +59,7 @@ class IssueApiTest(TestCase): self.assertEqual(response.json()[0]['name'], "test issue") self.assertEqual(response.json()[0]['state'], "pending_new") self.assertEqual(response.json()[0]['assigned_to'], None) - self.assertEqual(response.json()[0]['last_activity'], issue.last_activity.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + self.assertEqual(response.json()[0]['last_activity'], comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) self.assertEqual(len(response.json()[0]['timeline']), 4) self.assertEqual(response.json()[0]['timeline'][0]['type'], 'state') self.assertEqual(response.json()[0]['timeline'][1]['type'], 'mail') @@ -85,6 +85,65 @@ class IssueApiTest(TestCase): self.assertEqual(response.json()[0]['timeline'][3]['timestamp'], comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + def test_issues_incomplete_timeline(self): + now = datetime.now() + issue1 = IssueThread.objects.create( + name="test issue", + ) + issue2 = IssueThread.objects.create( + name="test issue", + ) + issue3 = IssueThread.objects.create( + name="test issue", + ) + mail1 = Email.objects.create( + subject='test', + body='test', + sender='test', + recipient='test', + issue_thread=issue2, + timestamp=now + timedelta(seconds=2), + ) + comment = Comment.objects.create( + issue_thread=issue3, + comment="test", + timestamp=now + timedelta(seconds=3), + ) + + response = self.client.get('/api/2/tickets/') + self.assertEqual(200, response.status_code) + self.assertEqual(3, len(response.json())) + self.assertEqual(issue1.id, response.json()[0]['id']) + self.assertEqual(issue2.id, response.json()[1]['id']) + self.assertEqual(issue3.id, response.json()[2]['id']) + self.assertEqual(issue1.state_changes.first().timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), + response.json()[0]['last_activity']) + self.assertEqual(mail1.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), + response.json()[1]['last_activity']) + self.assertEqual(comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), + response.json()[2]['last_activity']) + self.assertEqual(1, len(response.json()[0]['timeline'])) + self.assertEqual(2, len(response.json()[1]['timeline'])) + self.assertEqual(2, len(response.json()[2]['timeline'])) + self.assertEqual('state', response.json()[0]['timeline'][0]['type']) + self.assertEqual('state', response.json()[1]['timeline'][0]['type']) + self.assertEqual('mail', response.json()[1]['timeline'][1]['type']) + self.assertEqual('state', response.json()[2]['timeline'][0]['type']) + self.assertEqual('comment', response.json()[2]['timeline'][1]['type']) + self.assertEqual('pending_new', response.json()[0]['timeline'][0]['state']) + self.assertEqual('pending_new', response.json()[1]['timeline'][0]['state']) + self.assertEqual('test', response.json()[1]['timeline'][1]['sender']) + self.assertEqual('test', response.json()[1]['timeline'][1]['recipient']) + self.assertEqual('test', response.json()[1]['timeline'][1]['subject']) + self.assertEqual('test', response.json()[1]['timeline'][1]['body']) + self.assertEqual(mail1.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), + response.json()[1]['timeline'][1]['timestamp']) + self.assertEqual('pending_new', response.json()[2]['timeline'][0]['state']) + self.assertEqual('test', response.json()[2]['timeline'][1]['comment']) + self.assertEqual(comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), + response.json()[2]['timeline'][1]['timestamp']) + + def test_manual_creation(self): response = self.client.post('/api/2/tickets/manual/', {'name': 'test issue', 'sender': 'test', 'recipient': 'test', 'body': 'test'},