示例#1
0
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'),
        )
示例#2
0
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'