from itertools import groupby from django.utils import timezone from django.db import models from django_softdelete.models import SoftDeleteModel from authentication.models import ExtendedUser from inventory.models import Event, Item from django.db.models.signals import post_save, pre_save from django.dispatch import receiver STATE_CHOICES = ( ('pending_new', 'New'), ('pending_open', 'Open'), ('pending_shipping', 'Needs to be shipped'), ('pending_physical_confirmation', 'Needs to be confirmed physically'), ('pending_return', 'Needs to be returned'), ('pending_postponed', 'Postponed'), ('waiting_details', 'Waiting for details'), ('waiting_pre_shipping', 'Waiting for Address/Shipping Info'), ('closed_returned', 'Closed: Returned'), ('closed_shipped', 'Closed: Shipped'), ('closed_not_found', 'Closed: Not found'), ('closed_not_our_problem', 'Closed: Not our problem'), ('closed_duplicate', 'Closed: Duplicate'), ('closed_timeout', 'Closed: Timeout'), ('closed_spam', 'Closed: Spam'), ('closed_nothing_missing', 'Closed: Nothing missing'), ('closed_wtf', 'Closed: WTF'), ('found_open', 'Item Found and stored externally'), ('found_closed', 'Item Found and stored externally and closed'), ) RELATION_STATUS_CHOICES = ( ('possible', 'Possible'), ('confirmed', 'Confirmed'), ('discarded', 'Discarded'), ('provided', 'Provided'), ) class IssueThread(SoftDeleteModel): id = models.AutoField(primary_key=True) uuid = models.CharField(max_length=255, unique=True, null=False, blank=False) name = models.CharField(max_length=255) event = models.ForeignKey(Event, null=True, on_delete=models.SET_NULL, related_name='issue_threads') manually_created = models.BooleanField(default=False) def short_uuid(self): return self.uuid[:8] @property def state(self): try: state_changes = sorted(self.state_changes.all(), key=lambda x: x.timestamp, reverse=True) if state_changes: return state_changes[0].state else: return None except AttributeError: return 'none' @state.setter def state(self, value): if self.state == value: return self.state_changes.create(state=value) if value == 'closed_spam' and self.emails.exists(): self.emails.first().train_spam() @property def assigned_to(self): try: assignments = sorted(self.assignments.all(), key=lambda x: x.timestamp, reverse=True) if assignments: return assignments[0].assigned_to else: return None except AttributeError: return None @assigned_to.setter def assigned_to(self, value): if self.assigned_to == value: return self.assignments.create(assigned_to=value) @property def related_items(self): groups = groupby(self.item_relation_changes.all(), lambda rel: rel.item.id) return [sorted(v, key=lambda r: r.timestamp)[0].item for k, v in groups] def __str__(self): return '[' + str(self.id) + '][' + self.short_uuid() + '] ' + self.name class Meta: permissions = [ ('send_mail', 'Can send mail'), ('add_issuethread_manual', 'Can add issue thread manually'), ('assign_issuethread', 'Can assign issue thread'), ] @receiver(pre_save, sender=IssueThread) def set_uuid(sender, instance, **kwargs): import uuid if instance.uuid is None or instance.uuid == '': instance.uuid = str(uuid.uuid4()) @receiver(post_save, sender=IssueThread) def create_issue_thread(sender, instance, created, **kwargs): if created: StateChange.objects.create(issue_thread=instance, state='pending_new') class Comment(models.Model): id = models.AutoField(primary_key=True) issue_thread = models.ForeignKey(IssueThread, on_delete=models.CASCADE, related_name='comments') comment = models.TextField() timestamp = models.DateTimeField(auto_now_add=True) def __str__(self): return str(self.issue_thread) + ' comment #' + str(self.id) class StateChange(models.Model): id = models.AutoField(primary_key=True) issue_thread = models.ForeignKey(IssueThread, on_delete=models.CASCADE, related_name='state_changes') state = models.CharField(max_length=255, choices=STATE_CHOICES, default='pending_new') timestamp = models.DateTimeField(auto_now_add=True) def __str__(self): return str(self.issue_thread) + ' state change to ' + self.state class Assignment(models.Model): id = models.AutoField(primary_key=True) issue_thread = models.ForeignKey(IssueThread, on_delete=models.CASCADE, related_name='assignments') assigned_to = models.ForeignKey(ExtendedUser, on_delete=models.CASCADE, related_name='assigned_tickets') timestamp = models.DateTimeField(auto_now_add=True) def __str__(self): return str(self.issue_thread) + ' assigned to ' + self.assigned_to.username class ItemRelation(models.Model): id = models.AutoField(primary_key=True) issue_thread = models.ForeignKey(IssueThread, on_delete=models.CASCADE, related_name='item_relation_changes') item = models.ForeignKey(Item, on_delete=models.CASCADE, related_name='issue_relation_changes') timestamp = models.DateTimeField(auto_now_add=True) status = models.CharField(max_length=255, choices=RELATION_STATUS_CHOICES, default='possible') def __str__(self): return str(self.issue_thread) + ' related to ' + str(self.item) class ShippingVoucher(models.Model): id = models.AutoField(primary_key=True) issue_thread = models.ForeignKey(IssueThread, on_delete=models.CASCADE, related_name='shipping_vouchers', null=True) voucher = models.CharField(max_length=255) type = models.CharField(max_length=255) timestamp = models.DateTimeField(auto_now_add=True) used_at = models.DateTimeField(null=True) def __str__(self): return self.voucher + ' (' + self.type + ')' def save(self, *args, **kwargs): if self.used_at is None and self.issue_thread is not None: self.used_at = timezone.now() super().save(*args, **kwargs)