<commit_before>from __future__ import division

import calendar
import datetime
from functools import partial
import re
import itertools
import random
from collections import defaultdict

from django.core import exceptions
from django.core.urlresolvers import reverse
from django.core.validators import MinValueValidator

from django.db.models import Q
from django.db import transaction
from django.db.models.signals import post_init

from django.utils.dateformat import DateFormat

from django.contrib.contenttypes.fields import (
    GenericRelation, GenericForeignKey
)
from django.contrib.contenttypes.models import ContentType
from django.contrib.gis.db import models

from django.db.models.fields import DateField

from markitup.fields import MarkupField

from django_date_extensions.fields import ApproximateDateField, ApproximateDate

from slug_helpers.models import validate_slug_not_redirecting
from images.models import HasImageMixin, Image

from pombola.tasks.models import Task

from pombola.scorecards.models import ScorecardMixin
from pombola.budgets.models import BudgetsMixin

from mapit import models as mapit_models

from pombola.country import significant_positions_filter

date_help_text = "Format: '2011-12-31', '31 Jan 2011', 'Jan 2011' or '2011' or 'future'"

def validate_person_slug(slug):
    return validate_slug_not_redirecting('core', 'Person', slug)

def validate_organisation_slug(slug):
    return validate_slug_not_redirecting('core', 'Organisation', slug)

def validate_place_slug(slug):
    return validate_slug_not_redirecting('core', 'Place', slug)


# FIXME: Ideally this should be in django_date_extensions; we should
# make a pull request to include it.
def approximate_date_to_date(approx_date, assume):
    """Convert an approximate date to the earliest or latest date it could mean

    'assume' should be either 'earliest' or 'latest' to indicate
    whether this function should return the earliest or latest
    possible date that might be indicated by the approximate date.

    For example:

    >>> approximate_date_to_date(ApproximateDate(year=2003), assume='earliest')
    datetime.date(2003, 1, 1)
    >>> approximate_date_to_date(ApproximateDate(year=2003), assume='latest')
    datetime.date(2003, 12, 31)

    >>> approximate_date_to_date(ApproximateDate(year=2004, month=2), assume='earliest')
    datetime.date(2004, 2, 1)
    >>> approximate_date_to_date(ApproximateDate(year=2004, month=2), assume='latest')
    datetime.date(2004, 2, 29)

    >>> approximate_date_to_date(None, assume='earliest')
    datetime.date(1, 1, 1)
    >>> approximate_date_to_date(None, assume='latest')
    datetime.date(9999, 12, 31)

    >>> approximate_date_to_date(ApproximateDate(past=True), assume='earliest')
    datetime.date(1, 1, 1)
    >>> approximate_date_to_date(ApproximateDate(past=True), assume='latest')
    datetime.date(1, 1, 1)

    >>> approximate_date_to_date(ApproximateDate(future=True), assume='earliest')
    datetime.date(9999, 12, 31)
    >>> approximate_date_to_date(ApproximateDate(future=True), assume='latest')
    datetime.date(9999, 12, 31)

    """
    if assume not in ('earliest', 'latest'):
        raise ValueError("approx_end must be either 'earliest' or 'latest'")
    earliest = (assume == 'earliest')
    if (earliest and not approx_date) or (approx_date and approx_date.past):
        return datetime.date.min
    if (not earliest and not approx_date) or (approx_date and approx_date.future):
        return datetime.date.max
    year, month, day = approx_date.year, approx_date.month, approx_date.day
    # If we just have a year:
    if month == 0 and day == 0:
        if earliest:
            return datetime.date(year, 1, 1)
        else:
            return datetime.date(year, 12, 31)
    # If we just have a month and a year:
    if day == 0:
        if earliest:
            return datetime.date(year, month, 1)
        else:
            last_day = calendar.monthrange(year, month)[1]
            return datetime.date(year, month, last_day)
    # Otherwise we must have a complete date:
    return datetime.date(year, month, day)

def get_latest_day_of_year(year):
    """
    Return a date object for the last day of the year, or today if it's the current year.
    Will fail if a future year is passed.
    """
    year_end_date = datetime.date(year, 12, 31)
    today = datetime.date.today()
    if year > today.year:
        raise ValueError("year cannot be later than the current year.")
    return min(today, year_end_date)


class ModelBase(models.Model):
    created = models.DateTimeField( auto_now_add=True )
    updated = models.DateTimeField( auto_now=True )

    fields_to_whitespace_normalize = []

    def save(self, *args, **kwargs):
        self.normalize_whitespace()
        super(ModelBase, self).save(*args, **kwargs)

    def normalize_whitespace(self):
        for field in self.fields_to_whitespace_normalize:
            previous = getattr(self, field)
            normalized = re.sub(r'(?ms)\s+', ' ', previous).strip()
            setattr(self, field, normalized)

    def css_class(self):
        return self._meta.model_name

    def get_admin_url(self):
        url = reverse(
            'admin:%s_%s_change' % ( self._meta.app_label, self._meta.model_name),
            args=[self.id]
        )
        return url

    def get_disqus_thread_data(self, request):
        return {
            'disqus_canonical_url':
            request.build_absolute_uri(self.get_absolute_url()),
            'disqus_identifier':
            '{0}-{1}'.format(self.css_class(), self.id)
        }

    def get_popolo_id(self, id_scheme):
        table_name = self._meta.db_table
        return '{1}:{2}'.format(id_scheme, table_name, self.id)

    @property
    def show_active(self):
        """Used to indicate whether results in search should be greyed out"""
        return True

    class Meta:
       abstract = True


class ManagerBase(models.GeoManager):
    def update_or_create(self, filter_attrs, attrs):
        """Given unique look-up attributes, and extra data attributes, either
        updates the entry referred to if it exists, or creates it if it doesn't.

        Returns the object updated or created, having saved the changes.
        """
        try:
            obj = self.get(**filter_attrs)
            changed = False
            for k, v in attrs.items():
                if obj.__dict__[k] != v:
                    changed = True
                    obj.__dict__[k] = v
            if changed:
                obj.save()
        except exceptions.ObjectDoesNotExist:
            attrs.update(filter_attrs)
            obj = self.create(**attrs)
            obj.save()

        return obj


class Identifier(ModelBase):
    """This model represents alternative identifiers for objects"""

    scheme = models.CharField(max_length=200)
    identifier = models.CharField(max_length=500)

    content_type = models.ForeignKey(
        ContentType, related_name='pombola_identifier_set'
    )
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

    objects = ManagerBase()

    def __unicode__(self):
        return '"%s%s"' % (self.scheme, self.identifier)

    class Meta:
        unique_together = ('scheme', 'identifier')


class IdentifierMixin(object):

    """Useful methods for objects that may be referred to by an Indentifier"""

    def get_identifier(self, scheme):
        """Returns the identifier in a particular scheme for this object

        If there is no identifier for this object in that scheme, then
        None is returned. If there is more than one identifier found,
        then an exception is thrown."""
        try:
            identifier = Identifier.objects.get(
                content_type = ContentType.objects.get_for_model(self),
                scheme=scheme,
                object_id=self.id)
            return identifier.identifier
        except exceptions.ObjectDoesNotExist:
            return None

    def get_identifiers(self, scheme):
        """Returns all identifiers in a particular scheme for this object"""

        return Identifier.objects.filter(
            content_type=ContentType.objects.get_for_model(self),
            scheme=scheme,
            object_id=self.id).values_list('identifier', flat=True)

    def get_all_identifiers(self):
        """Return all identifiers for this object in any scheme

        This returns a dict which maps a scheme to a set of
        identifiers."""
        # get_for_model caches results, so there should only be one
        # database hit per model
        results = defaultdict(set)
        for identifier in self.identifiers.all():
            results[identifier.scheme].add(identifier.identifier)
        return results


class ContactKind(ModelBase):
    name = models.CharField(max_length=200, unique=True)
    slug = models.SlugField(max_length=200, unique=True, help_text="created from name")

    fields_to_whitespace_normalize = ['name']

    objects = ManagerBase()

    def __unicode__(self):
        return self.name

    class Meta:
       ordering = ["slug"]


class Contact(ModelBase):
    kind = models.ForeignKey('ContactKind')
    value = models.TextField()
    note = models.TextField(blank=True, help_text="publicly visible, use to clarify contact detail")
    source = models.CharField(max_length=500, blank=True, default='', help_text="where did this contact detail come from")

    preferred = models.BooleanField(help_text="Should this contact detail be listed before others of the same type?")

    # link to other objects using the ContentType system
    content_type = models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

    fields_to_whitespace_normalize = ['value']

    objects = ManagerBase()

    def __unicode__(self):
        return "%s (%s for %s)" % (self.value, self.kind, self.content_object)

    def generate_tasks(self):
        """generate tasks for ourselves, and for the foreign object"""
        Task.call_generate_tasks_on_if_possible(self.content_object)
        return []

    class Meta:
       ordering = ["content_type", "-preferred", "object_id", "kind"]


class InformationSource(ModelBase):
    source = models.CharField(max_length=500)
    note = models.TextField(blank=True)
    entered = models.BooleanField(default=False, help_text="has the information in this source been entered into this system?")

    # link to other objects using the ContentType system
    content_type = models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

    fields_to_whitespace_normalize = ['source']

    objects = ManagerBase()

    def __unicode__(self):
        return "%s: %s" % (self.source, self.content_object)

    class Meta:
       ordering = ["content_type", "object_id", "source"]


class PersonQuerySet(models.query.GeoQuerySet):
    def is_politician(self, when=None):
        # FIXME - Don't like the look of this, rather a big subquery.
        return self.filter(position__in=Position.objects.all().current_politician_positions(when))

class PersonManager(ManagerBase):
    def get_queryset(self):
        return PersonQuerySet(self.model)

    def loose_match_name(self, name):
        """Search for a loose match on a name. May not be too reliable"""

        # import here to avoid creating an import loop
        from haystack.query import SearchQuerySet

        # Try matching all the bits
        results = SearchQuerySet().filter_and(content=name).models(self.model)

        # if that fails try matching all the bits in any order
        if not len(results):
            results = SearchQuerySet().models(Person)
            for bit in re.split(r'\s+', name):
                results = results.filter_and(content=bit)

        # If we have exactly one result return that
        if len(results) == 1:
            return results[0].object
        else:
            return None

    def get_by_slug_or_id(self, identifier):
        try:
            return self.get(slug=identifier)
        except self.model.DoesNotExist:
            try:
                person_id = int(identifier)
            except ValueError:
                raise self.model.DoesNotExist, "Person matching query does not exist."
            return self.get(pk=person_id)

    def get_featured(self):
        # select all the presidential aspirants
        return self.filter(can_be_featured=True)

    def get_next_featured(self, current_slug, want_previous=False):
        """ Returns the next featured person, in slug order: using slug order because it's unique and easy to
            exclude the current person.

            If no slug is provided, returns a random person.
            If the slug is purely numeric (n), this consistently returns a person (actually the nth wrapping around
            where necessary): this allows js to generate random calls that can nonetheless be served from the cache.\
        """

        # original code that selects based on the can_be_featured flag
        # all_results = self.filter(can_be_featured=True)

        # select all the presidential aspirants
        all_results = self.get_featured()

        if not all_results.exists():
            return None
        sort_order = 'slug'
        if not current_slug:
            return random.choice(all_results)
        elif current_slug.isdigit():
            all_results = all_results.order_by(sort_order)
            return all_results[int(current_slug) % len(all_results)] # ignore direction: just provide a person
        else:
            all_results = all_results.exclude(slug=current_slug)
        if len(all_results) == 0: # special case: return the excluded person if they are the only one or nothing
            all_results = self.filter(can_be_featured=True)
            if all_results.exists():
                return all_results[0]
            else:
                return None
        if want_previous:
            sort_order = '-slug'
            results = all_results.order_by(sort_order).filter(slug__lt=current_slug)[:1]
        else:
            results = all_results.order_by(sort_order).filter(slug__gt=current_slug)[:1]
        if len(results) == 1:
            return results[0]
        else: # we're at the start/end of the list, wrap round to the other end
            results = all_results.order_by(sort_order)[:1]
            if len(results) == 1:
                return results[0]
            else:
                return None


class Person(ModelBase, HasImageMixin, ScorecardMixin, IdentifierMixin):
    title = models.CharField(max_length=100, blank=True)
    legal_name = models.CharField(max_length=300)
    slug = models.SlugField(
        max_length=200,
        unique=True,
        help_text="auto-created from first name and last name",
        validators=[validate_person_slug],
    )
    gender = models.CharField(max_length=20, blank=True, help_text="this is typically, but not restricted to, 'male' or 'female'")
    date_of_birth = ApproximateDateField(blank=True, help_text=date_help_text)
    date_of_death = ApproximateDateField(blank=True, help_text=date_help_text)
    # religion
    # tribe
    summary = MarkupField(blank=True, default='')

    # The "primary" email address in some sense - there may be others
    # (or this one) in contacts.
    email = models.EmailField(blank=True)

    hidden = models.BooleanField(
        default=False,
        help_text="hide this person's pages from normal users")

    contacts = GenericRelation(Contact)
    identifiers = GenericRelation(Identifier)
    images = GenericRelation(Image)
    objects = PersonManager()

    can_be_featured = models.BooleanField(default=False, help_text="can this person be featured on the home page (e.g., is their data appropriate and extant)?")

    # Additional fields added largely for Popolo spec compliance:
    biography = MarkupField(blank=True, default='')
    national_identity = models.CharField(max_length=100, blank=True)
    family_name = models.CharField(max_length=300, blank=True)
    given_name = models.CharField(max_length=300, blank=True)
    additional_name = models.CharField(max_length=300, blank=True)
    honorific_prefix = models.CharField(max_length=300, blank=True)
    honorific_suffix = models.CharField(max_length=300, blank=True)
    sort_name = models.CharField(max_length=300, blank=True)

    fields_to_whitespace_normalize = ['title',
                                      'legal_name',
                                      'gender',
                                      'email',
                                      'national_identity',
                                      'family_name',
                                      'given_name',
                                      'additional_name',
                                      'honorific_prefix',
                                      'honorific_suffix',
                                      'sort_name']

    @property
    def name(self):
        # n.b. we're deliberately not using
        # self.alternative_names.filter(name_to_use=True) here, since
        # that would do a new query per person, even if the
        # alternative names had been loaded by prefetch_related.  See:
        #   http://stackoverflow.com/a/12974801/223092
        alternative_names_to_use = [an for an in self.alternative_names.all()
                                    if an.name_to_use]
        if alternative_names_to_use:
            return alternative_names_to_use[0].alternative_name
        else:
            return self.legal_name

    @property
    def everypolitician_uuid(self):
        return self.get_identifier('everypolitician')

    def additional_names(self, include_name_to_use=False):
        filter_args = {}
        if not include_name_to_use:
            filter_args['name_to_use'] = False
        return [an.alternative_name
                for an in
                self.alternative_names.filter(**filter_args)]

    def all_names_set(self):
        """Return a set of all known names for this Person"""
        result = set(self.additional_names(include_name_to_use=True))
        result.add(self.legal_name)
        return result

    @transaction.atomic
    def add_alternative_name(self, alternative_name, name_to_use=False, note=''):
        if name_to_use:
            # Make sure that no other alternative names are set as
            # the name to use:
            for an in self.alternative_names.all():
                an.name_to_use = False
                an.save()
        alternative_name = re.sub(r'\s+', ' ', alternative_name).strip()
        AlternativePersonName.objects.update_or_create({'person': self,
                                                        'alternative_name': alternative_name,
                                                        'note': note},
                                                       {'name_to_use': name_to_use})
        apn = AlternativePersonName(person=self,
                                    alternative_name=alternative_name,
                                    name_to_use=name_to_use)<commit_msg>Add writeinpublic_identifier method to Person<commit_after>from __future__ import division

import calendar
import datetime
from functools import partial
import re
import itertools
import random
from collections import defaultdict

from django.core import exceptions
from django.core.urlresolvers import reverse
from django.core.validators import MinValueValidator

from django.db.models import Q
from django.db import transaction
from django.db.models.signals import post_init

from django.utils.dateformat import DateFormat

from django.contrib.contenttypes.fields import (
    GenericRelation, GenericForeignKey
)
from django.contrib.contenttypes.models import ContentType
from django.contrib.gis.db import models

from django.db.models.fields import DateField

from markitup.fields import MarkupField

from django_date_extensions.fields import ApproximateDateField, ApproximateDate

from slug_helpers.models import validate_slug_not_redirecting
from images.models import HasImageMixin, Image

from pombola.tasks.models import Task

from pombola.scorecards.models import ScorecardMixin
from pombola.budgets.models import BudgetsMixin

from mapit import models as mapit_models

from pombola.country import significant_positions_filter

date_help_text = "Format: '2011-12-31', '31 Jan 2011', 'Jan 2011' or '2011' or 'future'"

def validate_person_slug(slug):
    return validate_slug_not_redirecting('core', 'Person', slug)

def validate_organisation_slug(slug):
    return validate_slug_not_redirecting('core', 'Organisation', slug)

def validate_place_slug(slug):
    return validate_slug_not_redirecting('core', 'Place', slug)


# FIXME: Ideally this should be in django_date_extensions; we should
# make a pull request to include it.
def approximate_date_to_date(approx_date, assume):
    """Convert an approximate date to the earliest or latest date it could mean

    'assume' should be either 'earliest' or 'latest' to indicate
    whether this function should return the earliest or latest
    possible date that might be indicated by the approximate date.

    For example:

    >>> approximate_date_to_date(ApproximateDate(year=2003), assume='earliest')
    datetime.date(2003, 1, 1)
    >>> approximate_date_to_date(ApproximateDate(year=2003), assume='latest')
    datetime.date(2003, 12, 31)

    >>> approximate_date_to_date(ApproximateDate(year=2004, month=2), assume='earliest')
    datetime.date(2004, 2, 1)
    >>> approximate_date_to_date(ApproximateDate(year=2004, month=2), assume='latest')
    datetime.date(2004, 2, 29)

    >>> approximate_date_to_date(None, assume='earliest')
    datetime.date(1, 1, 1)
    >>> approximate_date_to_date(None, assume='latest')
    datetime.date(9999, 12, 31)

    >>> approximate_date_to_date(ApproximateDate(past=True), assume='earliest')
    datetime.date(1, 1, 1)
    >>> approximate_date_to_date(ApproximateDate(past=True), assume='latest')
    datetime.date(1, 1, 1)

    >>> approximate_date_to_date(ApproximateDate(future=True), assume='earliest')
    datetime.date(9999, 12, 31)
    >>> approximate_date_to_date(ApproximateDate(future=True), assume='latest')
    datetime.date(9999, 12, 31)

    """
    if assume not in ('earliest', 'latest'):
        raise ValueError("approx_end must be either 'earliest' or 'latest'")
    earliest = (assume == 'earliest')
    if (earliest and not approx_date) or (approx_date and approx_date.past):
        return datetime.date.min
    if (not earliest and not approx_date) or (approx_date and approx_date.future):
        return datetime.date.max
    year, month, day = approx_date.year, approx_date.month, approx_date.day
    # If we just have a year:
    if month == 0 and day == 0:
        if earliest:
            return datetime.date(year, 1, 1)
        else:
            return datetime.date(year, 12, 31)
    # If we just have a month and a year:
    if day == 0:
        if earliest:
            return datetime.date(year, month, 1)
        else:
            last_day = calendar.monthrange(year, month)[1]
            return datetime.date(year, month, last_day)
    # Otherwise we must have a complete date:
    return datetime.date(year, month, day)

def get_latest_day_of_year(year):
    """
    Return a date object for the last day of the year, or today if it's the current year.
    Will fail if a future year is passed.
    """
    year_end_date = datetime.date(year, 12, 31)
    today = datetime.date.today()
    if year > today.year:
        raise ValueError("year cannot be later than the current year.")
    return min(today, year_end_date)


class ModelBase(models.Model):
    created = models.DateTimeField( auto_now_add=True )
    updated = models.DateTimeField( auto_now=True )

    fields_to_whitespace_normalize = []

    def save(self, *args, **kwargs):
        self.normalize_whitespace()
        super(ModelBase, self).save(*args, **kwargs)

    def normalize_whitespace(self):
        for field in self.fields_to_whitespace_normalize:
            previous = getattr(self, field)
            normalized = re.sub(r'(?ms)\s+', ' ', previous).strip()
            setattr(self, field, normalized)

    def css_class(self):
        return self._meta.model_name

    def get_admin_url(self):
        url = reverse(
            'admin:%s_%s_change' % ( self._meta.app_label, self._meta.model_name),
            args=[self.id]
        )
        return url

    def get_disqus_thread_data(self, request):
        return {
            'disqus_canonical_url':
            request.build_absolute_uri(self.get_absolute_url()),
            'disqus_identifier':
            '{0}-{1}'.format(self.css_class(), self.id)
        }

    def get_popolo_id(self, id_scheme):
        table_name = self._meta.db_table
        return '{1}:{2}'.format(id_scheme, table_name, self.id)

    @property
    def show_active(self):
        """Used to indicate whether results in search should be greyed out"""
        return True

    class Meta:
       abstract = True


class ManagerBase(models.GeoManager):
    def update_or_create(self, filter_attrs, attrs):
        """Given unique look-up attributes, and extra data attributes, either
        updates the entry referred to if it exists, or creates it if it doesn't.

        Returns the object updated or created, having saved the changes.
        """
        try:
            obj = self.get(**filter_attrs)
            changed = False
            for k, v in attrs.items():
                if obj.__dict__[k] != v:
                    changed = True
                    obj.__dict__[k] = v
            if changed:
                obj.save()
        except exceptions.ObjectDoesNotExist:
            attrs.update(filter_attrs)
            obj = self.create(**attrs)
            obj.save()

        return obj


class Identifier(ModelBase):
    """This model represents alternative identifiers for objects"""

    scheme = models.CharField(max_length=200)
    identifier = models.CharField(max_length=500)

    content_type = models.ForeignKey(
        ContentType, related_name='pombola_identifier_set'
    )
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

    objects = ManagerBase()

    def __unicode__(self):
        return '"%s%s"' % (self.scheme, self.identifier)

    class Meta:
        unique_together = ('scheme', 'identifier')


class IdentifierMixin(object):

    """Useful methods for objects that may be referred to by an Indentifier"""

    def get_identifier(self, scheme):
        """Returns the identifier in a particular scheme for this object

        If there is no identifier for this object in that scheme, then
        None is returned. If there is more than one identifier found,
        then an exception is thrown."""
        try:
            identifier = Identifier.objects.get(
                content_type = ContentType.objects.get_for_model(self),
                scheme=scheme,
                object_id=self.id)
            return identifier.identifier
        except exceptions.ObjectDoesNotExist:
            return None

    def get_identifiers(self, scheme):
        """Returns all identifiers in a particular scheme for this object"""

        return Identifier.objects.filter(
            content_type=ContentType.objects.get_for_model(self),
            scheme=scheme,
            object_id=self.id).values_list('identifier', flat=True)

    def get_all_identifiers(self):
        """Return all identifiers for this object in any scheme

        This returns a dict which maps a scheme to a set of
        identifiers."""
        # get_for_model caches results, so there should only be one
        # database hit per model
        results = defaultdict(set)
        for identifier in self.identifiers.all():
            results[identifier.scheme].add(identifier.identifier)
        return results


class ContactKind(ModelBase):
    name = models.CharField(max_length=200, unique=True)
    slug = models.SlugField(max_length=200, unique=True, help_text="created from name")

    fields_to_whitespace_normalize = ['name']

    objects = ManagerBase()

    def __unicode__(self):
        return self.name

    class Meta:
       ordering = ["slug"]


class Contact(ModelBase):
    kind = models.ForeignKey('ContactKind')
    value = models.TextField()
    note = models.TextField(blank=True, help_text="publicly visible, use to clarify contact detail")
    source = models.CharField(max_length=500, blank=True, default='', help_text="where did this contact detail come from")

    preferred = models.BooleanField(help_text="Should this contact detail be listed before others of the same type?")

    # link to other objects using the ContentType system
    content_type = models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

    fields_to_whitespace_normalize = ['value']

    objects = ManagerBase()

    def __unicode__(self):
        return "%s (%s for %s)" % (self.value, self.kind, self.content_object)

    def generate_tasks(self):
        """generate tasks for ourselves, and for the foreign object"""
        Task.call_generate_tasks_on_if_possible(self.content_object)
        return []

    class Meta:
       ordering = ["content_type", "-preferred", "object_id", "kind"]


class InformationSource(ModelBase):
    source = models.CharField(max_length=500)
    note = models.TextField(blank=True)
    entered = models.BooleanField(default=False, help_text="has the information in this source been entered into this system?")

    # link to other objects using the ContentType system
    content_type = models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

    fields_to_whitespace_normalize = ['source']

    objects = ManagerBase()

    def __unicode__(self):
        return "%s: %s" % (self.source, self.content_object)

    class Meta:
       ordering = ["content_type", "object_id", "source"]


class PersonQuerySet(models.query.GeoQuerySet):
    def is_politician(self, when=None):
        # FIXME - Don't like the look of this, rather a big subquery.
        return self.filter(position__in=Position.objects.all().current_politician_positions(when))

class PersonManager(ManagerBase):
    def get_queryset(self):
        return PersonQuerySet(self.model)

    def loose_match_name(self, name):
        """Search for a loose match on a name. May not be too reliable"""

        # import here to avoid creating an import loop
        from haystack.query import SearchQuerySet

        # Try matching all the bits
        results = SearchQuerySet().filter_and(content=name).models(self.model)

        # if that fails try matching all the bits in any order
        if not len(results):
            results = SearchQuerySet().models(Person)
            for bit in re.split(r'\s+', name):
                results = results.filter_and(content=bit)

        # If we have exactly one result return that
        if len(results) == 1:
            return results[0].object
        else:
            return None

    def get_by_slug_or_id(self, identifier):
        try:
            return self.get(slug=identifier)
        except self.model.DoesNotExist:
            try:
                person_id = int(identifier)
            except ValueError:
                raise self.model.DoesNotExist, "Person matching query does not exist."
            return self.get(pk=person_id)

    def get_featured(self):
        # select all the presidential aspirants
        return self.filter(can_be_featured=True)

    def get_next_featured(self, current_slug, want_previous=False):
        """ Returns the next featured person, in slug order: using slug order because it's unique and easy to
            exclude the current person.

            If no slug is provided, returns a random person.
            If the slug is purely numeric (n), this consistently returns a person (actually the nth wrapping around
            where necessary): this allows js to generate random calls that can nonetheless be served from the cache.\
        """

        # original code that selects based on the can_be_featured flag
        # all_results = self.filter(can_be_featured=True)

        # select all the presidential aspirants
        all_results = self.get_featured()

        if not all_results.exists():
            return None
        sort_order = 'slug'
        if not current_slug:
            return random.choice(all_results)
        elif current_slug.isdigit():
            all_results = all_results.order_by(sort_order)
            return all_results[int(current_slug) % len(all_results)] # ignore direction: just provide a person
        else:
            all_results = all_results.exclude(slug=current_slug)
        if len(all_results) == 0: # special case: return the excluded person if they are the only one or nothing
            all_results = self.filter(can_be_featured=True)
            if all_results.exists():
                return all_results[0]
            else:
                return None
        if want_previous:
            sort_order = '-slug'
            results = all_results.order_by(sort_order).filter(slug__lt=current_slug)[:1]
        else:
            results = all_results.order_by(sort_order).filter(slug__gt=current_slug)[:1]
        if len(results) == 1:
            return results[0]
        else: # we're at the start/end of the list, wrap round to the other end
            results = all_results.order_by(sort_order)[:1]
            if len(results) == 1:
                return results[0]
            else:
                return None


class Person(ModelBase, HasImageMixin, ScorecardMixin, IdentifierMixin):
    title = models.CharField(max_length=100, blank=True)
    legal_name = models.CharField(max_length=300)
    slug = models.SlugField(
        max_length=200,
        unique=True,
        help_text="auto-created from first name and last name",
        validators=[validate_person_slug],
    )
    gender = models.CharField(max_length=20, blank=True, help_text="this is typically, but not restricted to, 'male' or 'female'")
    date_of_birth = ApproximateDateField(blank=True, help_text=date_help_text)
    date_of_death = ApproximateDateField(blank=True, help_text=date_help_text)
    # religion
    # tribe
    summary = MarkupField(blank=True, default='')

    # The "primary" email address in some sense - there may be others
    # (or this one) in contacts.
    email = models.EmailField(blank=True)

    hidden = models.BooleanField(
        default=False,
        help_text="hide this person's pages from normal users")

    contacts = GenericRelation(Contact)
    identifiers = GenericRelation(Identifier)
    images = GenericRelation(Image)
    objects = PersonManager()

    can_be_featured = models.BooleanField(default=False, help_text="can this person be featured on the home page (e.g., is their data appropriate and extant)?")

    # Additional fields added largely for Popolo spec compliance:
    biography = MarkupField(blank=True, default='')
    national_identity = models.CharField(max_length=100, blank=True)
    family_name = models.CharField(max_length=300, blank=True)
    given_name = models.CharField(max_length=300, blank=True)
    additional_name = models.CharField(max_length=300, blank=True)
    honorific_prefix = models.CharField(max_length=300, blank=True)
    honorific_suffix = models.CharField(max_length=300, blank=True)
    sort_name = models.CharField(max_length=300, blank=True)

    fields_to_whitespace_normalize = ['title',
                                      'legal_name',
                                      'gender',
                                      'email',
                                      'national_identity',
                                      'family_name',
                                      'given_name',
                                      'additional_name',
                                      'honorific_prefix',
                                      'honorific_suffix',
                                      'sort_name']

    @property
    def name(self):
        # n.b. we're deliberately not using
        # self.alternative_names.filter(name_to_use=True) here, since
        # that would do a new query per person, even if the
        # alternative names had been loaded by prefetch_related.  See:
        #   http://stackoverflow.com/a/12974801/223092
        alternative_names_to_use = [an for an in self.alternative_names.all()
                                    if an.name_to_use]
        if alternative_names_to_use:
            return alternative_names_to_use[0].alternative_name
        else:
            return self.legal_name

    @property
    def everypolitician_uuid(self):
        return self.get_identifier('everypolitician')

    # We previously used EveryPolitician popolo to feed the WriteInPublic
    # install for South Africa. However when we switched away from using
    # EveryPolitician we needed a replacement identifier. This method
    # returns the EveryPolitician UUID if available, otherwise it returns
    # a custom identifier.
    @property
    def writeinpublic_identifier(self):
        if self.everypolitician_uuid:
            return self.everypolitician_uuid
        else:
            return u"peoples-assembly-person-{}".format(self.id)

    def additional_names(self, include_name_to_use=False):
        filter_args = {}
        if not include_name_to_use:
            filter_args['name_to_use'] = False
        return [an.alternative_name
                for an in
                self.alternative_names.filter(**filter_args)]

    def all_names_set(self):
        """Return a set of all known names for this Person"""
        result = set(self.additional_names(include_name_to_use=True))
        result.add(self.legal_name)
        return result

    @transaction.atomic
    def add_alternative_name(self, alternative_name, name_to_use=False, note=''):
        if name_to_use:
            # Make sure that no other alternative names are set as
            # the name to use:
            for an in self.alternative_names.all():
                an.name_to_use = False
                an.save()
        alternative_name = re.sub(r'\s+', ' ', alternative_name).strip()
        AlternativePersonName.objects.update_or_create({'person': self,
                                                        'alternative_name': alternative_name,
                                                        'note': note},
                                                       {'name_to_use': name_to_use})
        apn = AlternativePersonName(person=self,
                                    alternative_name=alternative_name,
                                    name_to_use=name_to_use)
{'fraction_non_alphanumeric': 0.08781732363821916, 'fraction_numerical': 0.009950248756218905, 'mean_word_length': 2.7015771083199547, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 65, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 38, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3588362', 'n_tokens_mistral': 11204, 'n_tokens_neox': 10664, 'n_words': 2987}