<commit_before># coding: utf-8

import datetime
from decimal import Decimal
import gzip
import os
import re
import urllib, urllib2

from django.conf import settings
from django.core.cache import cache
from django.core import urlresolvers
from django.db import models
from django.template.defaultfilters import slugify

from BeautifulSoup import BeautifulSoup

from parliament.core import parsetools, text_utils
from parliament.core.utils import memoize_property, ActiveManager

import logging
logger = logging.getLogger(__name__)

POL_LOOKUP_URL = 'http://www.parl.gc.ca/MembersOfParliament/ProfileMP.aspx?Key=%d&Language=E'

class InternalXref(models.Model):
    """A general-purpose table for quickly storing internal links."""
    text_value = models.CharField(max_length=50, blank=True, db_index=True)
    int_value = models.IntegerField(blank=True, null=True, db_index=True)
    target_id = models.IntegerField(db_index=True)
    
    # CURRENT SCHEMAS
    # party_names
    # bill_callbackid
    # session_legisin -- LEGISinfo ID for a session
    # edid_postcode -- the EDID -- which points to a riding, but is NOT the primary  key -- for a postcode
    schema = models.CharField(max_length=15, db_index=True)
    
    def __unicode__(self):
        return u"%s: %s %s for %s" % (self.schema, self.text_value, self.int_value, self.target_id)

class PartyManager(models.Manager):
    
    def get_by_name(self, name):
        x = InternalXref.objects.filter(schema='party_names', text_value=name.strip().lower())
        if len(x) == 0:
            raise Party.DoesNotExist()
        elif len(x) > 1:
            raise Exception("More than one party matched %s" % name.strip().lower())
        else:
            return self.get_query_set().get(pk=x[0].target_id)
            
class Party(models.Model):
    """A federal political party."""
    name = models.CharField(max_length=100)
    slug = models.CharField(max_length=10, blank=True)
    short_name = models.CharField(max_length=100, blank=True)
    
    objects = PartyManager()
    
    class Meta:
        verbose_name_plural = 'Parties'
        ordering = ('name',)
        
    def __init__(self, *args, **kwargs):
        # If we're creating a new object, set a flag to save the name to the alternate-names table.
        super(Party, self).__init__(*args, **kwargs)
        self._saveAlternate = True

    def save(self):
        if not getattr(self, 'short_name', None):
            self.short_name = self.name
        super(Party, self).save()
        if getattr(self, '_saveAlternate', False):
            self.add_alternate_name(self.name)

    def delete(self):
        InternalXref.objects.filter(schema='party_names', target_id=self.id).delete()
        super(Party, self).delete()

    def add_alternate_name(self, name):
        name = name.strip().lower()
        # check if exists
        x = InternalXref.objects.filter(schema='party_names', text_value=name)
        if len(x) == 0:
            InternalXref(schema='party_names', target_id=self.id, text_value=name).save()
        else:
            if x[0].target_id != self.id:
                raise Exception("Name %s already points to a different party" % name.strip().lower())
                
    def __unicode__(self):
        return self.name

class Person(models.Model):
    """Abstract base class for models representing a person."""
    
    name = models.CharField(max_length=100)
    name_given = models.CharField("Given name", max_length=50, blank=True)
    name_family = models.CharField("Family name", max_length=50, blank=True)

    def __unicode__(self):
        return self.name
    
    class Meta:
        abstract = True
        ordering = ('name',)

class PoliticianManager(models.Manager):
    
    def elected(self):
        """Returns a QuerySet of all politicians that were once elected to office."""
        return self.get_query_set().annotate(
            electedcount=models.Count('electedmember')).filter(electedcount__gte=1)
            
    def never_elected(self):
        """Returns a QuerySet of all politicians that were never elected as MPs.
        
        (at least during the time period covered by our database)"""
        return self.get_query_set().filter(electedmember__isnull=True)
        
    def current(self):
        """Returns a QuerySet of all current MPs."""
        return self.get_query_set().filter(electedmember__end_date__isnull=True,
            electedmember__start_date__isnull=False).distinct()
        
    def elected_but_not_current(self):
        """Returns a QuerySet of former MPs."""
        return self.get_query_set().exclude(electedmember__end_date__isnull=True)
    
    def filter_by_name(self, name):
        """Returns a list of politicians matching a given name."""
        return [i.politician for i in 
            PoliticianInfo.sr_objects.filter(schema='alternate_name', value=parsetools.normalizeName(name))]
    
    def get_by_name(self, name, session=None, riding=None, election=None, party=None, saveAlternate=True, strictMatch=False):
        """ Return a Politician by name. Uses a bunch of methods to try and deal with variations in names.
        If given any of a session, riding, election, or party, returns only politicians who match.
        If given session and optinally riding, tries to match the name more laxly.
        
        saveAlternate: If we have <NAME> and we match, via session/riding, to <NAME>, save Tom
            in the alternate names table
        strictMatch: Even if given a session, don't try last-name-only matching.
        
        """
        
        # Alternate names for a pol are in the InternalXref table. Assemble a list of possibilities
        poss = PoliticianInfo.sr_objects.filter(schema='alternate_name', value=parsetools.normalizeName(name))
        if len(poss) >= 1:
            # We have one or more results
            if session or riding or party:
                # We've been given extra criteria -- see if they match
                result = None
                for p in poss:
                    # For each possibility, assemble a list of matching Members
                    members = ElectedMember.objects.filter(politician=p.politician)
                    if riding: members = members.filter(riding=riding)
                    if session: members = members.filter(sessions=session)
                    if party: members = members.filter(party=party)
                    if len(members) >= 1:
                        if result: # we found another match on a previous journey through the loop
                            # can't disambiguate, raise exception
                            raise Politician.MultipleObjectsReturned(name)
                        # We match! Save the result.
                        result = members[0].politician
                if result:
                    return result
            elif election:
                raise Exception("Election not implemented yet in Politician get_by_name")
            else:
                # No extra criteria -- return what we got (or die if we can't disambiguate)
                if len(poss) > 1:
                    raise Politician.MultipleObjectsReturned(name)
                else:
                    return poss[0].politician
        if session and not strictMatch:
            # We couldn't find the pol, but we have the session and riding, so let's give this one more shot
            # We'll try matching only on last name
            match = re.search(r'\s([A-Z][\w-]+)$', name.strip()) # very naive lastname matching
            if match:
                lastname = match.group(1)
                pols = self.get_query_set().filter(name_family=lastname, electedmember__sessions=session).distinct()
                if riding:
                    pols = pols.filter(electedmember__riding=riding)
                if len(pols) > 1:
                    if riding:
                        raise Exception("DATA ERROR: There appear to be two politicians with the same last name elected to the same riding from the same session... %s %s %s" % (lastname, session, riding))
                elif len(pols) == 1:
                    # yes!
                    pol = pols[0]
                    if saveAlternate:
                        pol.add_alternate_name(name) # save the name we were given as an alternate
                    return pol
        raise Politician.DoesNotExist("Could not find politician named %s" % name)
        
    def get_by_parlinfo_id(self, parlinfoid, session=None):
        PARLINFO_LOOKUP_URL = 'http://www2.parl.gc.ca/parlinfo/Files/Parliamentarian.aspx?Item=%s&Language=E'
        try:
            info = PoliticianInfo.sr_objects.get(schema='parlinfo_id', value=parlinfoid.lower())
            return info.politician
        except PoliticianInfo.DoesNotExist:
            print "Looking up parlinfo ID %s" % parlinfoid 
            parlinfourl = PARLINFO_LOOKUP_URL % parlinfoid
            parlinfopage = urllib2.urlopen(parlinfourl).read()
            match = re.search(
              r'href="http://webinfo\.parl\.gc\.ca/MembersOfParliament/ProfileMP\.aspx\?Key=(\d+)&amp;Language=E">MP profile',
              parlinfopage)
            if not match:
                raise Politician.DoesNotExist
            pol = self.get_by_parl_id(match.group(1), session=session)
            pol.save_parlinfo_id(parlinfoid)
            return pol

    def get_by_slug_or_id(self, slug_or_id):
        if slug_or_id.isdigit():
            return self.get(id=slug_or_id)
        return self.get(slug=slug_or_id)
    
    def get_by_parl_id(self, parlid, session=None, election=None, lookOnline=True):
        try:
            info = PoliticianInfo.sr_objects.get(schema='parl_id', value=unicode(parlid))
            return info.politician
        except PoliticianInfo.DoesNotExist:
            invalid_ID_cache_key = 'invalid-pol-parl-id-%s' % parlid
            if cache.get(invalid_ID_cache_key):
                raise Politician.DoesNotExist("ID %s cached as invalid" % parlid)
            if not lookOnline:
                return None # FIXME inconsistent behaviour: when should we return None vs. exception?
            #print "Unknown parlid %d... " % parlid,
            try:
                soup = BeautifulSoup(urllib2.urlopen(POL_LOOKUP_URL % parlid))
            except urllib2.HTTPError:
                cache.set(invalid_ID_cache_key, True, 300)
                raise Politician.DoesNotExist("Couldn't open " + (POL_LOOKUP_URL % parlid))
            if soup.find('table', id='MasterPage_BodyContent_PageContent_PageContent_pnlError'):
                cache.set(invalid_ID_cache_key, True, 300)
                raise Politician.DoesNotExist("Invalid page for parlid %s" % parlid)
            polname = soup.find('span', id='MasterPage_MasterPage_BodyContent_PageContent_Content_TombstoneContent_TombstoneContent_ucHeaderMP_lblMPNameData').string
            polriding = soup.find('a', id='MasterPage_MasterPage_BodyContent_PageContent_Content_TombstoneContent_TombstoneContent_ucHeaderMP_hlConstituencyProfile').string
            parlinfolink = soup.find('a', id='MasterPage_MasterPage_BodyContent_PageContent_Content_TombstoneContent_TombstoneContent_ucHeaderMP_hlFederalExperience')
                        
            try:
                riding = Riding.objects.get_by_name(polriding)
            except Riding.DoesNotExist:
                raise Politician.DoesNotExist("Couldn't find riding %s" % polriding)
            if session:
                pol = self.get_by_name(name=polname, session=session, riding=riding)
            else:
                pol = self.get_by_name(name=polname, riding=riding)
            #print "found %s." % pol
            pol.save_parl_id(parlid)
            polid = pol.id
            if parlinfolink:
                match = re.search(r'Item=(.+?)&', parlinfolink['href'])
                pol.save_parlinfo_id(match.group(1))
            return self.get_query_set().get(pk=polid)
    getByParlID = get_by_parl_id

class Politician(Person):
    """Someone who has run for federal office."""
    GENDER_CHOICES = (
        ('M', 'Male'),
        ('F', 'Female'),
    )

    WORDCLOUD_PATH = 'autoimg/wordcloud-pol'

    dob = models.DateField(blank=True, null=True)
    gender = models.CharField(max_length=1, blank=True, choices=GENDER_CHOICES)
    headshot = models.ImageField(upload_to='polpics', blank=True, null=True)
    slug = models.CharField(max_length=30, blank=True, db_index=True)
    
    objects = PoliticianManager()

    def to_api_dict(self, representation):
        d = dict(
            name=self.name
        )
        if representation == 'detail':
            info = self.info_multivalued()
            d.update(
                given_name=self.name_given,
                family_name=self.name_family,
                gender=self.get_gender_display().lower(),
                image=self.headshot.url if self.headshot else None,
                other_info=info,
                links=[]
            )
            if 'email' in info:
                d['email'] = info.pop('email')[0]
            if self.parlpage:
                d['links'].append({
                    'url': self.parlpage,
                    'note': 'Page on parl.gc.ca'
                })
            if 'web_site' in info:
                d['links'].append({
                    'url': info.pop('web_site')[0],
                    'note': 'Official site'
                })
            if 'phone' in info:
                d['voice'] = info.pop('phone')[0]
            if 'fax' in info:
                d['fax'] = info.pop('fax')[0]
            d['memberships'] = [
                member.to_api_dict('detail', include_politician=False)
                for member in self.electedmember_set.all().select_related(depth=1).order_by('-end_date')
            ]
        return d

    def add_alternate_name(self, name):
        normname = parsetools.normalizeName(name)
        if normname not in self.alternate_names():
            self.set_info_multivalued('alternate_name', normname)

    def alternate_names(self):
        """Returns a list of ways of writing this politician's name."""
        return self.politicianinfo_set.filter(schema='alternate_name').values_list('value', flat=True)
        
    def add_slug(self):
        """Assigns a slug to this politician, unless there's a conflict."""<commit_msg>Add RSS link to politicians API<commit_after># coding: utf-8

import datetime
from decimal import Decimal
import gzip
import os
import re
import urllib, urllib2

from django.conf import settings
from django.core.cache import cache
from django.core import urlresolvers
from django.db import models
from django.template.defaultfilters import slugify

from BeautifulSoup import BeautifulSoup

from parliament.core import parsetools, text_utils
from parliament.core.utils import memoize_property, ActiveManager

import logging
logger = logging.getLogger(__name__)

POL_LOOKUP_URL = 'http://www.parl.gc.ca/MembersOfParliament/ProfileMP.aspx?Key=%d&Language=E'

class InternalXref(models.Model):
    """A general-purpose table for quickly storing internal links."""
    text_value = models.CharField(max_length=50, blank=True, db_index=True)
    int_value = models.IntegerField(blank=True, null=True, db_index=True)
    target_id = models.IntegerField(db_index=True)
    
    # CURRENT SCHEMAS
    # party_names
    # bill_callbackid
    # session_legisin -- LEGISinfo ID for a session
    # edid_postcode -- the EDID -- which points to a riding, but is NOT the primary  key -- for a postcode
    schema = models.CharField(max_length=15, db_index=True)
    
    def __unicode__(self):
        return u"%s: %s %s for %s" % (self.schema, self.text_value, self.int_value, self.target_id)

class PartyManager(models.Manager):
    
    def get_by_name(self, name):
        x = InternalXref.objects.filter(schema='party_names', text_value=name.strip().lower())
        if len(x) == 0:
            raise Party.DoesNotExist()
        elif len(x) > 1:
            raise Exception("More than one party matched %s" % name.strip().lower())
        else:
            return self.get_query_set().get(pk=x[0].target_id)
            
class Party(models.Model):
    """A federal political party."""
    name = models.CharField(max_length=100)
    slug = models.CharField(max_length=10, blank=True)
    short_name = models.CharField(max_length=100, blank=True)
    
    objects = PartyManager()
    
    class Meta:
        verbose_name_plural = 'Parties'
        ordering = ('name',)
        
    def __init__(self, *args, **kwargs):
        # If we're creating a new object, set a flag to save the name to the alternate-names table.
        super(Party, self).__init__(*args, **kwargs)
        self._saveAlternate = True

    def save(self):
        if not getattr(self, 'short_name', None):
            self.short_name = self.name
        super(Party, self).save()
        if getattr(self, '_saveAlternate', False):
            self.add_alternate_name(self.name)

    def delete(self):
        InternalXref.objects.filter(schema='party_names', target_id=self.id).delete()
        super(Party, self).delete()

    def add_alternate_name(self, name):
        name = name.strip().lower()
        # check if exists
        x = InternalXref.objects.filter(schema='party_names', text_value=name)
        if len(x) == 0:
            InternalXref(schema='party_names', target_id=self.id, text_value=name).save()
        else:
            if x[0].target_id != self.id:
                raise Exception("Name %s already points to a different party" % name.strip().lower())
                
    def __unicode__(self):
        return self.name

class Person(models.Model):
    """Abstract base class for models representing a person."""
    
    name = models.CharField(max_length=100)
    name_given = models.CharField("Given name", max_length=50, blank=True)
    name_family = models.CharField("Family name", max_length=50, blank=True)

    def __unicode__(self):
        return self.name
    
    class Meta:
        abstract = True
        ordering = ('name',)

class PoliticianManager(models.Manager):
    
    def elected(self):
        """Returns a QuerySet of all politicians that were once elected to office."""
        return self.get_query_set().annotate(
            electedcount=models.Count('electedmember')).filter(electedcount__gte=1)
            
    def never_elected(self):
        """Returns a QuerySet of all politicians that were never elected as MPs.
        
        (at least during the time period covered by our database)"""
        return self.get_query_set().filter(electedmember__isnull=True)
        
    def current(self):
        """Returns a QuerySet of all current MPs."""
        return self.get_query_set().filter(electedmember__end_date__isnull=True,
            electedmember__start_date__isnull=False).distinct()
        
    def elected_but_not_current(self):
        """Returns a QuerySet of former MPs."""
        return self.get_query_set().exclude(electedmember__end_date__isnull=True)
    
    def filter_by_name(self, name):
        """Returns a list of politicians matching a given name."""
        return [i.politician for i in 
            PoliticianInfo.sr_objects.filter(schema='alternate_name', value=parsetools.normalizeName(name))]
    
    def get_by_name(self, name, session=None, riding=None, election=None, party=None, saveAlternate=True, strictMatch=False):
        """ Return a Politician by name. Uses a bunch of methods to try and deal with variations in names.
        If given any of a session, riding, election, or party, returns only politicians who match.
        If given session and optinally riding, tries to match the name more laxly.
        
        saveAlternate: If we have <NAME> and we match, via session/riding, to <NAME>, save Tom
            in the alternate names table
        strictMatch: Even if given a session, don't try last-name-only matching.
        
        """
        
        # Alternate names for a pol are in the InternalXref table. Assemble a list of possibilities
        poss = PoliticianInfo.sr_objects.filter(schema='alternate_name', value=parsetools.normalizeName(name))
        if len(poss) >= 1:
            # We have one or more results
            if session or riding or party:
                # We've been given extra criteria -- see if they match
                result = None
                for p in poss:
                    # For each possibility, assemble a list of matching Members
                    members = ElectedMember.objects.filter(politician=p.politician)
                    if riding: members = members.filter(riding=riding)
                    if session: members = members.filter(sessions=session)
                    if party: members = members.filter(party=party)
                    if len(members) >= 1:
                        if result: # we found another match on a previous journey through the loop
                            # can't disambiguate, raise exception
                            raise Politician.MultipleObjectsReturned(name)
                        # We match! Save the result.
                        result = members[0].politician
                if result:
                    return result
            elif election:
                raise Exception("Election not implemented yet in Politician get_by_name")
            else:
                # No extra criteria -- return what we got (or die if we can't disambiguate)
                if len(poss) > 1:
                    raise Politician.MultipleObjectsReturned(name)
                else:
                    return poss[0].politician
        if session and not strictMatch:
            # We couldn't find the pol, but we have the session and riding, so let's give this one more shot
            # We'll try matching only on last name
            match = re.search(r'\s([A-Z][\w-]+)$', name.strip()) # very naive lastname matching
            if match:
                lastname = match.group(1)
                pols = self.get_query_set().filter(name_family=lastname, electedmember__sessions=session).distinct()
                if riding:
                    pols = pols.filter(electedmember__riding=riding)
                if len(pols) > 1:
                    if riding:
                        raise Exception("DATA ERROR: There appear to be two politicians with the same last name elected to the same riding from the same session... %s %s %s" % (lastname, session, riding))
                elif len(pols) == 1:
                    # yes!
                    pol = pols[0]
                    if saveAlternate:
                        pol.add_alternate_name(name) # save the name we were given as an alternate
                    return pol
        raise Politician.DoesNotExist("Could not find politician named %s" % name)
        
    def get_by_parlinfo_id(self, parlinfoid, session=None):
        PARLINFO_LOOKUP_URL = 'http://www2.parl.gc.ca/parlinfo/Files/Parliamentarian.aspx?Item=%s&Language=E'
        try:
            info = PoliticianInfo.sr_objects.get(schema='parlinfo_id', value=parlinfoid.lower())
            return info.politician
        except PoliticianInfo.DoesNotExist:
            print "Looking up parlinfo ID %s" % parlinfoid 
            parlinfourl = PARLINFO_LOOKUP_URL % parlinfoid
            parlinfopage = urllib2.urlopen(parlinfourl).read()
            match = re.search(
              r'href="http://webinfo\.parl\.gc\.ca/MembersOfParliament/ProfileMP\.aspx\?Key=(\d+)&amp;Language=E">MP profile',
              parlinfopage)
            if not match:
                raise Politician.DoesNotExist
            pol = self.get_by_parl_id(match.group(1), session=session)
            pol.save_parlinfo_id(parlinfoid)
            return pol

    def get_by_slug_or_id(self, slug_or_id):
        if slug_or_id.isdigit():
            return self.get(id=slug_or_id)
        return self.get(slug=slug_or_id)
    
    def get_by_parl_id(self, parlid, session=None, election=None, lookOnline=True):
        try:
            info = PoliticianInfo.sr_objects.get(schema='parl_id', value=unicode(parlid))
            return info.politician
        except PoliticianInfo.DoesNotExist:
            invalid_ID_cache_key = 'invalid-pol-parl-id-%s' % parlid
            if cache.get(invalid_ID_cache_key):
                raise Politician.DoesNotExist("ID %s cached as invalid" % parlid)
            if not lookOnline:
                return None # FIXME inconsistent behaviour: when should we return None vs. exception?
            #print "Unknown parlid %d... " % parlid,
            try:
                soup = BeautifulSoup(urllib2.urlopen(POL_LOOKUP_URL % parlid))
            except urllib2.HTTPError:
                cache.set(invalid_ID_cache_key, True, 300)
                raise Politician.DoesNotExist("Couldn't open " + (POL_LOOKUP_URL % parlid))
            if soup.find('table', id='MasterPage_BodyContent_PageContent_PageContent_pnlError'):
                cache.set(invalid_ID_cache_key, True, 300)
                raise Politician.DoesNotExist("Invalid page for parlid %s" % parlid)
            polname = soup.find('span', id='MasterPage_MasterPage_BodyContent_PageContent_Content_TombstoneContent_TombstoneContent_ucHeaderMP_lblMPNameData').string
            polriding = soup.find('a', id='MasterPage_MasterPage_BodyContent_PageContent_Content_TombstoneContent_TombstoneContent_ucHeaderMP_hlConstituencyProfile').string
            parlinfolink = soup.find('a', id='MasterPage_MasterPage_BodyContent_PageContent_Content_TombstoneContent_TombstoneContent_ucHeaderMP_hlFederalExperience')
                        
            try:
                riding = Riding.objects.get_by_name(polriding)
            except Riding.DoesNotExist:
                raise Politician.DoesNotExist("Couldn't find riding %s" % polriding)
            if session:
                pol = self.get_by_name(name=polname, session=session, riding=riding)
            else:
                pol = self.get_by_name(name=polname, riding=riding)
            #print "found %s." % pol
            pol.save_parl_id(parlid)
            polid = pol.id
            if parlinfolink:
                match = re.search(r'Item=(.+?)&', parlinfolink['href'])
                pol.save_parlinfo_id(match.group(1))
            return self.get_query_set().get(pk=polid)
    getByParlID = get_by_parl_id

class Politician(Person):
    """Someone who has run for federal office."""
    GENDER_CHOICES = (
        ('M', 'Male'),
        ('F', 'Female'),
    )

    WORDCLOUD_PATH = 'autoimg/wordcloud-pol'

    dob = models.DateField(blank=True, null=True)
    gender = models.CharField(max_length=1, blank=True, choices=GENDER_CHOICES)
    headshot = models.ImageField(upload_to='polpics', blank=True, null=True)
    slug = models.CharField(max_length=30, blank=True, db_index=True)
    
    objects = PoliticianManager()

    def to_api_dict(self, representation):
        d = dict(
            name=self.name
        )
        if representation == 'detail':
            info = self.info_multivalued()
            members = list(self.electedmember_set.all().select_related(depth=1).order_by('-end_date'))
            current = not members[0].end_date
            d.update(
                given_name=self.name_given,
                family_name=self.name_family,
                gender=self.get_gender_display().lower(),
                image=self.headshot.url if self.headshot else None,
                other_info=info,
                links=[]
            )
            if 'email' in info:
                d['email'] = info.pop('email')[0]
            if self.parlpage:
                d['links'].append({
                    'url': self.parlpage,
                    'note': 'Page on parl.gc.ca'
                })
            if current:
                d['links'].append({
                    'url': urlresolvers.reverse('politician_activity_feed', kwargs={'pol_id': self.id}),
                    'note': 'openparliament.ca RSS activity feed'
                })
            if 'web_site' in info:
                d['links'].append({
                    'url': info.pop('web_site')[0],
                    'note': 'Official site'
                })
            if 'phone' in info:
                d['voice'] = info.pop('phone')[0]
            if 'fax' in info:
                d['fax'] = info.pop('fax')[0]
            d['memberships'] = [
                member.to_api_dict('detail', include_politician=False)
                for member in members
            ]
        return d

    def add_alternate_name(self, name):
        normname = parsetools.normalizeName(name)
        if normname not in self.alternate_names():
            self.set_info_multivalued('alternate_name', normname)

    def alternate_names(self):
        """Returns a list of ways of writing this politician's name."""
        return self.politicianinfo_set.filter(schema='alternate_name').values_list('value', flat=True)
        
    def add_slug(self):
        """Assigns a slug to this politician, unless there's a conflict."""
{'fraction_non_alphanumeric': 0.09701288024116196, 'fraction_numerical': 0.0038709235406960813, 'mean_word_length': 2.2709243697478994, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 19, 'https://': 0, 'lorem ipsum': 0, 'www.': 4, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3711629', 'n_tokens_mistral': 8521, 'n_tokens_neox': 8176, 'n_words': 2045}