# Copyright The IETF Trust 2007-2019, All Rights Reserved # -*- coding: utf-8 -*- from __future__ import absolute_import, print_function, unicode_literals from django.conf import settings from django.urls import reverse as urlreverse from django.db import models from django.utils.encoding import python_2_unicode_compatible from django.utils.text import slugify from ietf.person.models import Email, Person from ietf.name.models import (LiaisonStatementPurposeName, LiaisonStatementState, LiaisonStatementEventTypeName, LiaisonStatementTagName, DocRelationshipName) from ietf.doc.models import Document from ietf.group.models import Group from ietf.utils.models import ForeignKey # maps (previous state id, new state id) to event type id STATE_EVENT_MAPPING = { ('pending','approved'):'approved', ('pending','dead'):'killed', ('pending','posted'):'posted', ('approved','posted'):'posted', ('dead','pending'):'resurrected', ('pending','pending'):'submitted' } @python_2_unicode_compatible class LiaisonStatement(models.Model): title = models.CharField(max_length=255) from_groups = models.ManyToManyField(Group, blank=True, related_name='liaisonstatement_from_set') from_contact = ForeignKey(Email, blank=True, null=True) to_groups = models.ManyToManyField(Group, blank=True, related_name='liaisonstatement_to_set') to_contacts = models.CharField(max_length=2000, help_text="Contacts at recipient group") response_contacts = models.CharField(blank=True, max_length=255, help_text="Where to send a response") # RFC4053 technical_contacts = models.CharField(blank=True, max_length=255, help_text="Who to contact for clarification") # RFC4053 action_holder_contacts = models.CharField(blank=True, max_length=255, help_text="Who makes sure action is completed") # incoming only? cc_contacts = models.TextField(blank=True) purpose = ForeignKey(LiaisonStatementPurposeName) deadline = models.DateField(null=True, blank=True) other_identifiers = models.TextField(blank=True, null=True) # Identifiers from other bodies body = models.TextField(blank=True) tags = models.ManyToManyField(LiaisonStatementTagName, blank=True) attachments = models.ManyToManyField(Document, through='LiaisonStatementAttachment', blank=True) state = ForeignKey(LiaisonStatementState, default='pending') class Meta: ordering = ['id'] def __str__(self): return self.title or "" def change_state(self,state_id=None,person=None): '''Helper function to change state of liaison statement and create appropriate event''' previous_state_id = self.state_id self.set_state(state_id) event_type_id = STATE_EVENT_MAPPING[(previous_state_id,state_id)] LiaisonStatementEvent.objects.create( type_id=event_type_id, by=person, statement=self, desc='Statement {}'.format(event_type_id.capitalize()) ) def get_absolute_url(self): return settings.IDTRACKER_BASE_URL + urlreverse('ietf.liaisons.views.liaison_detail',kwargs={'object_id':self.id}) def is_outgoing(self): return self.to_groups.first().type_id == 'sdo' def latest_event(self, *args, **filter_args): """Get latest event of optional Python type and with filter arguments, e.g. d.latest_event(type="xyz") returns an LiaisonStatementEvent while d.latest_event(WriteupDocEvent, type="xyz") returns a WriteupDocEvent event.""" model = args[0] if args else LiaisonStatementEvent e = model.objects.filter(statement=self).filter(**filter_args).order_by('-time', '-id')[:1] return e[0] if e else None def name(self): if self.from_groups.count(): frm = ', '.join([i.acronym or i.name for i in self.from_groups.all()]) else: frm = self.from_contact.person.name if self.to_groups.count(): to = ', '.join([i.acronym or i.name for i in self.to_groups.all()]) else: to = self.to_contacts return slugify("liaison" + " " + self.submitted.strftime("%Y-%m-%d") + " " + frm[:50] + " " + to[:50] + " " + self.title[:115]) @property def posted(self): if hasattr(self,'prefetched_posted_events') and self.prefetched_posted_events: return self.prefetched_posted_events[0].time else: event = self.latest_event(type='posted') if event: return event.time return None @property def submitted(self): event = self.latest_event(type='submitted') if event: return event.time return None @property def sort_date(self): """Returns the date to use for sorting, for posted statements this is post date, for pending statements this is submitted date""" if self.state_id == 'posted': return self.posted else: return self.submitted @property def modified(self): event = self.liaisonstatementevent_set.all().order_by('-time').first() if event: return event.time return None @property def approved(self): return self.state_id in ('approved','posted') @property def action_taken(self): if hasattr(self,'prefetched_tags'): return bool(self.prefetched_tags) else: return self.tags.filter(slug='taken').exists() def active_attachments(self): '''Returns attachments with removed ones filtered out''' return self.attachments.exclude(liaisonstatementattachment__removed=True) @property def awaiting_action(self): if getattr(self, '_awaiting_action', None) != None: return bool(self._awaiting_action) return self.tags.filter(slug='awaiting').exists() def _get_group_display(self, groups): '''Returns comma separated string of group acronyms, non-wg are uppercase''' acronyms = [] for group in groups: if group.type.slug == 'wg': acronyms.append(group.acronym) else: acronyms.append(group.acronym.upper()) return ', '.join(acronyms) @property def from_groups_display(self): '''Returns comma separated list of from_group names''' if hasattr(self, 'prefetched_from_groups'): return self._get_group_display(self.prefetched_from_groups) else: return self._get_group_display(self.from_groups.order_by('acronym')) @property def to_groups_display(self): '''Returns comma separated list of to_group names''' if hasattr(self, 'prefetched_to_groups'): return self._get_group_display(self.prefetched_to_groups) else: return self._get_group_display(self.to_groups.order_by('acronym')) def from_groups_short_display(self): '''Returns comma separated list of from_group acronyms. For use in admin interface''' groups = self.to_groups.order_by('acronym').values_list('acronym',flat=True) return ', '.join(groups) from_groups_short_display.short_description = 'From Groups' def set_state(self,slug): try: state = LiaisonStatementState.objects.get(slug=slug) except LiaisonStatementState.DoesNotExist: return self.state = state self.save() def approver_emails(self): '''Send mail requesting approval of pending liaison statement. Send mail to the intersection of approvers for all from_groups ''' approval_set = set() if self.from_groups.first(): approval_set.update(self.from_groups.first().liaison_approvers()) if self.from_groups.count() > 1: for group in self.from_groups.all(): approval_set.intersection_update(group.liaison_approvers()) return list(set([ r.email.address for r in approval_set ])) @python_2_unicode_compatible class LiaisonStatementAttachment(models.Model): statement = ForeignKey(LiaisonStatement) document = ForeignKey(Document) removed = models.BooleanField(default=False) def __str__(self): return self.document.name @python_2_unicode_compatible class RelatedLiaisonStatement(models.Model): source = ForeignKey(LiaisonStatement, related_name='source_of_set') target = ForeignKey(LiaisonStatement, related_name='target_of_set') relationship = ForeignKey(DocRelationshipName) def __str__(self): return "%s %s %s" % (self.source.title, self.relationship.name.lower(), self.target.title) @python_2_unicode_compatible class LiaisonStatementGroupContacts(models.Model): group = ForeignKey(Group, unique=True, null=True) contacts = models.CharField(max_length=255,blank=True) cc_contacts = models.CharField(max_length=255,blank=True) def __str__(self): return "%s" % self.group.name @python_2_unicode_compatible class LiaisonStatementEvent(models.Model): time = models.DateTimeField(auto_now_add=True) type = ForeignKey(LiaisonStatementEventTypeName) by = ForeignKey(Person) statement = ForeignKey(LiaisonStatement) desc = models.TextField() def __str__(self): return "%s %s by %s at %s" % (self.statement.title, self.type.slug, self.by.plain_name(), self.time) class Meta: ordering = ['-time', '-id']