class FOIARequest(models.Model): """A Freedom of Information Act request""" # pylint: disable=too-many-public-methods # pylint: disable=too-many-instance-attributes user = models.ForeignKey(User) title = models.CharField(max_length=255, db_index=True) slug = models.SlugField(max_length=255) status = models.CharField(max_length=10, choices=STATUS, db_index=True) jurisdiction = models.ForeignKey('jurisdiction.Jurisdiction') agency = models.ForeignKey('agency.Agency', blank=True, null=True) date_submitted = models.DateField(blank=True, null=True, db_index=True) date_updated = models.DateField(blank=True, null=True, db_index=True) date_done = models.DateField(blank=True, null=True, verbose_name='Date response received') date_due = models.DateField(blank=True, null=True, db_index=True) days_until_due = models.IntegerField(blank=True, null=True) date_followup = models.DateField(blank=True, null=True) date_estimate = models.DateField(blank=True, null=True, verbose_name='Estimated Date Completed') date_processing = models.DateField(blank=True, null=True) embargo = models.BooleanField(default=False) permanent_embargo = models.BooleanField(default=False) date_embargo = models.DateField(blank=True, null=True) price = models.DecimalField(max_digits=14, decimal_places=2, default='0.00') requested_docs = models.TextField(blank=True) description = models.TextField(blank=True) featured = models.BooleanField(default=False) tracker = models.BooleanField(default=False) sidebar_html = models.TextField(blank=True) tracking_id = models.CharField(blank=True, max_length=255) mail_id = models.CharField(blank=True, max_length=255, editable=False) updated = models.BooleanField(default=False) email = models.CharField(blank=True, max_length=254) other_emails = fields.EmailsListField(blank=True, max_length=255) times_viewed = models.IntegerField(default=0) disable_autofollowups = models.BooleanField(default=False) missing_proxy = models.BooleanField( default=False, help_text='This request requires a proxy to file, but no such ' 'proxy was avilable upon draft creation.') parent = models.ForeignKey('self', blank=True, null=True, on_delete=models.SET_NULL) block_incoming = models.BooleanField( default=False, help_text=('Block emails incoming to this request from ' 'automatically being posted on the site')) crowdfund = models.OneToOneField('crowdfund.Crowdfund', related_name='foia', blank=True, null=True) read_collaborators = models.ManyToManyField( User, related_name='read_access', blank=True, ) edit_collaborators = models.ManyToManyField( User, related_name='edit_access', blank=True, ) access_key = models.CharField(blank=True, max_length=255) objects = FOIARequestQuerySet.as_manager() tags = TaggableManager(through=TaggedItemBase, blank=True) foia_type = 'foia' def __unicode__(self): return self.title def get_absolute_url(self): """The url for this object""" return reverse('foia-detail', kwargs={ 'jurisdiction': self.jurisdiction.slug, 'jidx': self.jurisdiction.pk, 'slug': self.slug, 'idx': self.pk, }) def save(self, *args, **kwargs): """Normalize fields before saving and set the embargo expiration if necessary""" self.slug = slugify(self.slug) self.title = self.title.strip() if self.embargo: if self.status in END_STATUS: default_date = date.today() + timedelta(30) existing_date = self.date_embargo self.date_embargo = default_date if not existing_date else existing_date else: self.date_embargo = None if self.status == 'submitted' and self.date_processing is None: self.date_processing = date.today() # add a reversion comment if possible if 'comment' in kwargs: comment = kwargs.pop('comment') if reversion.revision_context_manager.is_active(): reversion.set_comment(comment) super(FOIARequest, self).save(*args, **kwargs) def is_editable(self): """Can this request be updated?""" return self.status == 'started' def has_crowdfund(self): """Does this request have crowdfunding enabled?""" return bool(self.crowdfund) def is_payable(self): """Can this request be payed for by the user?""" has_open_crowdfund = self.has_crowdfund( ) and not self.crowdfund.expired() has_payment_status = self.status == 'payment' return has_payment_status and not has_open_crowdfund def get_stripe_amount(self): """Output a Stripe Checkout formatted price""" return int(self.price * 100) def is_public(self): """Is this document viewable to everyone""" return self.has_perm(AnonymousUser(), 'view') # Request Sharing and Permissions def has_perm(self, user, perm): """Short cut for checking a FOIA permission""" return user.has_perm('foia.%s_foiarequest' % perm, self) ## Creator def created_by(self, user): """Did this user create this request?""" return self.user == user ## Editors def has_editor(self, user): """Checks whether the given user is an editor.""" user_is_editor = False if self.edit_collaborators.filter(pk=user.pk).exists(): user_is_editor = True return user_is_editor def add_editor(self, user): """Grants the user permission to edit this request.""" if not self.has_viewer(user) and not self.has_editor( user) and not self.created_by(user): self.edit_collaborators.add(user) self.save() logger.info('%s granted edit access to %s', user, self) return def remove_editor(self, user): """Revokes the user's permission to edit this request.""" if self.has_editor(user): self.edit_collaborators.remove(user) self.save() logger.info('%s revoked edit access from %s', user, self) return def demote_editor(self, user): """Reduces the editor's access to that of a viewer.""" self.remove_editor(user) self.add_viewer(user) return ## Viewers def has_viewer(self, user): """Checks whether the given user is a viewer.""" user_is_viewer = False if self.read_collaborators.filter(pk=user.pk).exists(): user_is_viewer = True return user_is_viewer def add_viewer(self, user): """Grants the user permission to view this request.""" if not self.has_viewer(user) and not self.has_editor( user) and not self.created_by(user): self.read_collaborators.add(user) self.save() logger.info('%s granted view access to %s', user, self) return def remove_viewer(self, user): """Revokes the user's permission to view this request.""" if self.has_viewer(user): self.read_collaborators.remove(user) logger.info('%s revoked view access from %s', user, self) self.save() return def promote_viewer(self, user): """Enhances the viewer's access to that of an editor.""" self.remove_viewer(user) self.add_editor(user) return ## Access key def generate_access_key(self): """Generates a random key for accessing the request when it is private.""" key = utils.generate_key(24) self.access_key = key self.save() logger.info('New access key generated for %s', self) return key def public_documents(self): """Get a list of public documents attached to this request""" return self.files.filter(access='public') def first_request(self): """Return the first request text""" try: return self.communications.all()[0].communication except IndexError: return '' def last_comm(self): """Return the last communication""" return self.communications.last() def last_response(self): """Return the most recent response""" return self.communications.filter( response=True).order_by('-date').first() def set_mail_id(self): """Set the mail id, which is the unique identifier for the auto mailer system""" # use raw sql here in order to avoid race conditions uid = int( md5(self.title.encode('utf8') + datetime.now().isoformat()).hexdigest(), 16) % 10**8 mail_id = '%s-%08d' % (self.pk, uid) cursor = connection.cursor() cursor.execute( "UPDATE foia_foiarequest " "SET mail_id = CASE WHEN mail_id='' THEN %s ELSE mail_id END " "WHERE id = %s", [mail_id, self.pk]) # set object's mail id to what is in the database self.mail_id = FOIARequest.objects.get(pk=self.pk).mail_id def get_mail_id(self): """Get the mail id - generate it if it doesn't exist""" if not self.mail_id: self.set_mail_id() return self.mail_id def get_other_emails(self): """Get the other emails for this request as a list""" # Adding blank emails here breaks mailgun backend return [ e for e in fields.email_separator_re.split(self.other_emails) if e ] def get_to_who(self): """Who communications are to""" if self.agency: return self.agency.name else: return '' def get_saved(self): """Get the old model that is saved in the db""" try: return FOIARequest.objects.get(pk=self.pk) except FOIARequest.DoesNotExist: return None def latest_response(self): """How many days since the last response""" response = self.last_response() if response: return (date.today() - response.date.date()).days def processing_length(self): """How many days since the request was set as processing""" days_since = 0 if self.date_processing: days_since = (date.today() - self.date_processing).days return days_since def update(self, anchor=None): """Various actions whenever the request has been updated""" # pylint: disable=unused-argument # Do something with anchor self.updated = True self.save() self.update_dates() def notify(self, action): """ Notify the owner of the request. Notify followers if the request is not under embargo. Mark any existing notifications with the same message as read, to avoid notifying users with duplicated information. """ identical_notifications = ( Notification.objects.for_object(self).get_unread().filter( action__actor_object_id=action.actor_object_id, action__verb=action.verb)) for notification in identical_notifications: notification.mark_read() utils.notify(self.user, action) if self.is_public(): utils.notify(followers(self), action) def submit(self, appeal=False, snail=False, thanks=False): """ The request has been submitted. Notify admin and try to auto submit. There is functionally no difference between appeals and other submissions besides the receiving agency. The only difference between a thanks andother submissions is that we do not set the request status, unless the request requires a proxy. """ # can email appeal if the agency has an appeal agency which has an email address # and can accept emailed appeals can_email_appeal = (appeal and self.agency and self.agency.appeal_agency and self.agency.appeal_agency.email and self.agency.appeal_agency.can_email_appeals) # update email addresses for the request if can_email_appeal: self.email = self.agency.appeal_agency.get_email() self.other_emails = self.agency.appeal_agency.other_emails elif not self.email and self.agency: self.email = self.agency.get_email() self.other_emails = self.agency.other_emails # if agency isnt approved, do not email or snail mail # it will be handled after agency is approved approved_agency = self.agency and self.agency.status == 'approved' can_email = self.email and not appeal and not self.missing_proxy comm = self.last_comm() # if the request can be emailed, email it, otherwise send a notice to the admin # if this is a thanks, send it as normal but do not change the status if not snail and approved_agency and (can_email or can_email_appeal): if appeal and not thanks: self.status = 'appealing' elif self.has_ack() and not thanks: self.status = 'processed' elif not thanks: self.status = 'ack' self._send_msg() self.update_dates() elif self.missing_proxy: # flag for proxy re-submitting self.status = 'submitted' self.date_processing = date.today() task.models.FlaggedTask.objects.create( foia=self, text='This request was filed for an agency requiring a ' 'proxy, but no proxy was available. Please add a suitable ' 'proxy for the state and refile it with a note that the ' 'request is being filed by a state citizen. Make sure the ' 'new request is associated with the original user\'s ' 'account. To add someone as a proxy, change their user type ' 'to "Proxy" and make sure they properly have their state ' 'set on the backend. This message should only appear when ' 'a suitable proxy does not exist.') elif approved_agency: # snail mail it if not thanks: self.status = 'submitted' self.date_processing = date.today() notice = 'n' if self.communications.count() == 1 else 'u' notice = 'a' if appeal else notice comm.delivered = 'mail' comm.save() task.models.SnailMailTask.objects.create(category=notice, communication=comm) elif not thanks: # there should never be a thanks to an unapproved agency # not an approved agency, all we do is mark as submitted self.status = 'submitted' self.date_processing = date.today() self.save() def followup(self, automatic=False, show_all_comms=True): """Send a follow up email for this request""" from muckrock.foia.models.communication import FOIACommunication if self.date_estimate and date.today() < self.date_estimate: estimate = 'future' elif self.date_estimate: estimate = 'past' else: estimate = 'none' comm = FOIACommunication.objects.create(foia=self, from_who='MuckRock.com', to_who=self.get_to_who(), date=datetime.now(), response=False, full_html=False, autogenerated=automatic, communication=render_to_string( 'text/foia/followup.txt', { 'request': self, 'estimate': estimate })) if not self.email and self.agency: self.email = self.agency.get_email() self.other_emails = self.agency.other_emails self.save() if self.email: self._send_msg(show_all_comms) else: self.status = 'submitted' self.date_processing = date.today() self.save() comm.delivered = 'mail' comm.save() task.models.SnailMailTask.objects.create(category='f', communication=comm) # Do not self.update() here for now to avoid excessive emails self.update_dates() def appeal(self, appeal_message): """Send a followup to the agency or its appeal agency.""" from muckrock.foia.models.communication import FOIACommunication communication = FOIACommunication.objects.create( foia=self, from_who=self.user.get_full_name(), to_who=self.get_to_who(), date=datetime.now(), communication=appeal_message, response=False, full_html=False, autogenerated=False) self.submit(appeal=True) return communication def pay(self, user, amount): """ Users can make payments for request fees. Upon payment, we create a snail mail task and we set the request to a processing status. Payments are always snail mail, because we need to mail the check to the agency. Since collaborators may make payments, we do not assume the user is the request creator. Returns the communication that was generated. """ from muckrock.foia.models.communication import FOIACommunication from muckrock.task.models import SnailMailTask # We mark the request as processing self.status = 'submitted' self.date_processing = date.today() self.save() # We create the payment communication and a snail mail task for it. payable_to = self.agency.payable_to if self.agency else None comm = FOIACommunication.objects.create( foia=self, from_who='MuckRock.com', to_who=self.get_to_who(), date=datetime.now(), delivered='mail', response=False, full_html=False, autogenerated=False, communication=render_to_string('message/communication/payment.txt', { 'amount': amount, 'payable_to': payable_to })) SnailMailTask.objects.create(communication=comm, category='p', user=user, amount=amount) # We perform some logging and activity generation logger.info('%s has paid %0.2f for request %s', user.username, amount, self.title) utils.new_action(user, 'paid fees', target=self) # We return the communication we generated, in case the caller wants to do anything with it return comm def _send_msg(self, show_all_comms=True): """Send a message for this request as an email or fax""" # self.email should be set before calling this method # get last comm to set delivered and raw_email comm = self.communications.last() subject = comm.subject or self.default_subject() subject = subject[:255] # pylint:disable=attribute-defined-outside-init self.reverse_communications = self.communications.reverse() body = render_to_string('text/foia/request_email.txt', { 'request': self, 'show_all_comms': show_all_comms }) # send the msg if all(c.isdigit() for c in self.email): self._send_fax(subject, body, comm) else: self._send_email(subject, body, comm) comm.subject = subject comm.save() # unblock incoming messages if we send one out self.block_incoming = False self.save() def _send_email(self, subject, body, comm): """Send the message as an email""" from_addr = self.get_mail_id() cc_addrs = self.get_other_emails() from_email = '%s@%s' % (from_addr, settings.MAILGUN_SERVER_NAME) msg = EmailMultiAlternatives(subject=subject, body=body, from_email=from_email, to=[self.email], bcc=cc_addrs + ['*****@*****.**'], headers={ 'Cc': ','.join(cc_addrs), 'X-Mailgun-Variables': { 'comm_id': comm.pk } }) msg.attach_alternative(linebreaks(escape(body)), 'text/html') # atach all files from the latest communication for file_ in comm.files.all(): msg.attach(file_.name(), file_.ffile.read()) msg.send(fail_silently=False) # update communication comm.set_raw_email(msg.message()) comm.delivered = 'email' def _send_fax(self, subject, body, comm): """Send the message as a fax""" api = PhaxioApi( settings.PHAXIO_KEY, settings.PHAXIO_SECRET, raise_errors=True, ) files = [f.ffile for f in comm.files.all()] callback_url = 'https://%s%s' % ( settings.MUCKROCK_URL, reverse('phaxio-callback'), ) api.send(to=self.email, header_text=subject[:50], string_data=body, string_data_type='text', files=files, batch=True, batch_delay=settings.PHAXIO_BATCH_DELAY, batch_collision_avoidance=True, callback_url=callback_url, **{'tag[comm_id]': comm.pk}) comm.delivered = 'fax' def update_dates(self): """Set the due date, follow up date and days until due attributes""" cal = self.jurisdiction.get_calendar() # first submit if not self.date_submitted: self.date_submitted = date.today() days = self.jurisdiction.get_days() if days: self.date_due = cal.business_days_from(date.today(), days) # updated from mailgun without setting status or submitted if self.status in ['ack', 'processed']: # unpause the count down if self.days_until_due is not None: self.date_due = cal.business_days_from(date.today(), self.days_until_due) self.days_until_due = None self._update_followup_date() # if we are no longer waiting on the agency, do not follow up if self.status not in ['ack', 'processed'] and self.date_followup: self.date_followup = None # if we need to respond, pause the count down until we do if self.status in ['fix', 'payment'] and self.date_due: last_datetime = self.last_comm().date if not last_datetime: last_datetime = datetime.now() self.days_until_due = cal.business_days_between( last_datetime.date(), self.date_due) self.date_due = None self.save() def _update_followup_date(self): """Update the follow up date""" try: new_date = self.last_comm().date.date() + timedelta( self._followup_days()) if self.date_due and self.date_due > new_date: new_date = self.date_due if not self.date_followup or self.date_followup < new_date: self.date_followup = new_date except IndexError: # This request has no communications at the moment, cannot asign a follow up date pass def _followup_days(self): """How many days do we wait until we follow up?""" if self.status == 'ack' and self.jurisdiction: # if we have not at least been acknowledged yet, set the days # to the period required by law jurisdiction_days = self.jurisdiction.get_days() if jurisdiction_days is not None: return jurisdiction_days if self.date_estimate and date.today() < self.date_estimate: # return the days until the estimated date date_difference = self.date_estimate - date.today() return date_difference.days if self.jurisdiction and self.jurisdiction.level == 'f': return 30 else: return 15 def update_tags(self, tags): """Update the requests tags""" tag_set = set() for tag in parse_tags(tags): new_tag, _ = Tag.objects.get_or_create(name=tag) tag_set.add(new_tag) self.tags.set(*tag_set) def user_actions(self, user): '''Provides action interfaces for users''' is_owner = self.created_by(user) can_follow = user.is_authenticated() and not is_owner is_following = user in followers(self) is_admin = user.is_staff kwargs = { 'jurisdiction': self.jurisdiction.slug, 'jidx': self.jurisdiction.pk, 'idx': self.pk, 'slug': self.slug } return [ Action(test=True, link=reverse('foia-clone', kwargs=kwargs), title='Clone', desc='Start a new request using this one as a base', class_name='primary'), Action(test=can_follow, link=reverse('foia-follow', kwargs=kwargs), title=('Unfollow' if is_following else 'Follow'), class_name=('default' if is_following else 'primary')), Action( test=self.has_perm(user, 'flag'), title='Get Help', action='flag', desc= u'Something broken, buggy, or off? Let us know and we’ll fix it', class_name='failure modal'), Action(test=is_admin, title='Contact User', action='contact_user', desc=u'Send this request\'s owner an email', class_name='modal'), ] def contextual_request_actions(self, user, can_edit): '''Provides context-sensitive action interfaces for requests''' can_follow_up = can_edit and self.status != 'started' can_appeal = self.has_perm(user, 'appeal') kwargs = { 'jurisdiction': self.jurisdiction.slug, 'jidx': self.jurisdiction.pk, 'idx': self.pk, 'slug': self.slug } return [ Action(test=user.is_staff, link=reverse('foia-admin-fix', kwargs=kwargs), title='Admin Fix', desc='Open the admin fix form', class_name='default'), Action( test=can_edit, title='Get Advice', action='question', desc= u'Get your questions answered by Muckrock’s community of FOIA experts', class_name='modal'), Action(test=can_follow_up, title='Follow Up', action='follow_up', desc='Send a message directly to the agency', class_name='reply'), Action(test=can_appeal, title='Appeal', action='appeal', desc=u'Appeal an agency’s decision', class_name='reply'), ] def total_pages(self): """Get the total number of pages for this request""" pages = self.files.aggregate(Sum('pages'))['pages__sum'] if pages is None: return 0 return pages def has_ack(self): """Has this request been acknowledged?""" return self.communications.filter(response=True).exists() def proxy_reject(self): """Mark this request as being rejected due to a proxy being required""" from muckrock.task.models import FlaggedTask # mark the agency as requiring a proxy going forward self.agency.requires_proxy = True self.agency.save() # mark to re-file with a proxy FlaggedTask.objects.create( foia=self, text='This request was rejected as requiring a proxy; please refile' ' it with one of our volunteers names and a note that the request is' ' being filed by a state citizen. Make sure the new request is' ' associated with the original user\'s account. To add someone as' ' a proxy, change their user type to "Proxy" and make sure they' ' properly have their state set on the backend. This message should' ' only appear the first time an agency rejects a request for being' ' from an out-of-state resident.') self.notes.create( author=User.objects.get(username='******'), note='The request has been rejected with the agency stating that ' 'you must be a resident of the state. MuckRock is working with our ' 'in-state volunteers to refile this request, and it should appear ' 'in your account within a few days.', ) def default_subject(self): """Make a subject line for a communication for this request""" law_name = self.jurisdiction.get_law_name() if self.tracking_id: return 'RE: %s Request #%s' % (law_name, self.tracking_id) elif self.communications.count() > 1: return 'RE: %s Request: %s' % (law_name, self.title) else: return '%s Request: %s' % (law_name, self.title) class Meta: # pylint: disable=too-few-public-methods ordering = ['title'] verbose_name = 'FOIA Request' app_label = 'foia' permissions = ( ('view_foiarequest', 'Can view this request'), ('embargo_foiarequest', 'Can embargo request to make it private'), ('embargo_perm_foiarequest', 'Can embargo a request permananently'), ('crowdfund_foiarequest', 'Can start a crowdfund campaign for the request'), ('appeal_foiarequest', 'Can appeal the requests decision'), ('thank_foiarequest', 'Can thank the FOI officer for their help'), ('flag_foiarequest', 'Can flag the request for staff attention'), ('followup_foiarequest', 'Can send a manual follow up'), )
class Agency(models.Model, RequestHelper): """An agency for a particular jurisdiction that has at least one agency type""" name = models.CharField(max_length=255) slug = models.SlugField(max_length=255) jurisdiction = models.ForeignKey(Jurisdiction, related_name='agencies') types = models.ManyToManyField(AgencyType, blank=True) status = models.CharField(choices=( ('pending', 'Pending'), ('approved', 'Approved'), ('rejected', 'Rejected'), ), max_length=8, default='pending') user = models.ForeignKey( User, null=True, blank=True, help_text='The user who submitted this agency', ) appeal_agency = models.ForeignKey('self', null=True, blank=True) payable_to = models.ForeignKey('self', related_name='receivable', null=True, blank=True) image = ThumbnailerImageField(upload_to='agency_images', blank=True, null=True, resize_source={ 'size': (900, 600), 'crop': 'smart' }) image_attr_line = models.CharField(blank=True, max_length=255, help_text='May use html') public_notes = models.TextField(blank=True, help_text='May use html') stale = models.BooleanField(default=False) manual_stale = models.BooleanField( default=False, help_text='For marking an agency stale by hand.') location = PointField(blank=True) addresses = models.ManyToManyField( 'communication.Address', through='AgencyAddress', related_name='agencies', ) emails = models.ManyToManyField( 'communication.EmailAddress', through='AgencyEmail', related_name='agencies', ) phones = models.ManyToManyField( 'communication.PhoneNumber', through='AgencyPhone', related_name='agencies', ) contact_salutation = models.CharField(blank=True, max_length=30) contact_first_name = models.CharField(blank=True, max_length=100) contact_last_name = models.CharField(blank=True, max_length=100) contact_title = models.CharField(blank=True, max_length=255) url = models.URLField(blank=True, verbose_name='FOIA Web Page', help_text='Begin with http://') notes = models.TextField(blank=True) aliases = models.TextField(blank=True) parent = models.ForeignKey('self', null=True, blank=True, related_name='children') website = models.CharField(max_length=255, blank=True) twitter = models.CharField(max_length=255, blank=True) twitter_handles = models.TextField(blank=True) foia_logs = models.URLField(blank=True, verbose_name='FOIA Logs', help_text='Begin with http://') foia_guide = models.URLField(blank=True, verbose_name='FOIA Processing Guide', help_text='Begin with http://') exempt = models.BooleanField(default=False) requires_proxy = models.BooleanField(default=False) # Depreacted fields can_email_appeals = models.BooleanField(default=False) address = models.TextField(blank=True) email = models.EmailField(blank=True) other_emails = fields.EmailsListField(blank=True, max_length=255) phone = models.CharField(blank=True, max_length=30) fax = models.CharField(blank=True, max_length=30) objects = AgencyQuerySet.as_manager() def __unicode__(self): return self.name def get_absolute_url(self): """The url for this object""" return reverse('agency-detail', kwargs={ 'jurisdiction': self.jurisdiction.slug, 'jidx': self.jurisdiction.pk, 'slug': self.slug, 'idx': self.pk, }) def save(self, *args, **kwargs): """Save the agency""" self.slug = slugify(self.slug) self.name = self.name.strip() super(Agency, self).save(*args, **kwargs) def link_display(self): """Returns link if approved""" if self.status == 'approved': return mark_safe('<a href="%s">%s</a>' % (self.get_absolute_url(), self.name)) else: return self.name def is_stale(self): """Should this agency be marked as stale? If the latest response to any open request is greater than STALE_DURATION days ago, or if no responses to any open request, if the oldest open request was sent greater than STALE_DURATION days ago. If no open requests, do not mark as stale.""" # check if agency is manually marked as stale if self.manual_stale: return True # find any open requests, if none, not stale foias = self.foiarequest_set.get_open().order_by('date_submitted') if not foias: return False # find the latest response to an open request latest_responses = [] for foia in foias: response = foia.latest_response() if response: latest_responses.append(response) if latest_responses: return min(latest_responses) >= STALE_DURATION # no response to open requests, use oldest open request submit date return (date.today() - foias[0].date_submitted).days >= STALE_DURATION def mark_stale(self, manual=False): """Mark this agency as stale and create a StaleAgencyTask if one doesn't already exist.""" self.stale = True self.manual_stale = manual self.save() try: task, created = StaleAgencyTask.objects.get_or_create( resolved=False, agency=self) if created: logger.info('Created new StaleAgencyTask <%d> for Agency <%d>', task.pk, self.pk) except MultipleObjectsReturned as exception: # If there are multiple StaleAgencyTasks returned, just return the first one. # We only want this method to return a single task. # Also, log the exception as a warning. task = StaleAgencyTask.objects.filter(resolved=False, agency=self).first() logger.warning(exception) return task def unmark_stale(self): """Unmark this agency as stale and resolve all of its StaleAgencyTasks.""" self.stale = False self.manual_stale = False self.save() (StaleAgencyTask.objects.filter(resolved=False, agency=self).update(resolved=True)) def count_thanks(self): """Count how many thanks this agency has received""" return (self.foiarequest_set.filter( communications__thanks=True).distinct().count()) def get_requests(self): """Just returns the foiareqest_set value. Used for compatability with RequestHeper mixin""" return self.foiarequest_set def get_user(self): """Get the agency user for this agency""" try: return self.profile.user except Profile.DoesNotExist: user = User.objects.create_user(unique_username(self.name)) Profile.objects.create( user=user, acct_type='agency', date_update=date.today(), agency=self, ) return user def get_emails(self, request_type='primary', email_type='to'): """Get the specified type of email addresses for this agency""" return self.emails.filter( agencyemail__request_type=request_type, agencyemail__email_type=email_type, ) def get_faxes(self, request_type='primary'): """Get the contact fax numbers""" return self.phones.filter( type='fax', agencyphone__request_type=request_type, ) def get_phones(self, request_type='none'): """Get the phone numbers""" return self.phones.filter( type='phone', agencyphone__request_type=request_type, ) def get_addresses(self, request_type='primary'): """Get the contact addresses""" return self.addresses.filter( agencyaddress__request_type=request_type, ) class Meta: # pylint: disable=too-few-public-methods verbose_name_plural = 'agencies'