class PersonRole(models.Model): """Terms held in office by Members of Congress, Presidents, and Vice Presidents. Each term corresponds with an election, meaning each term in the House covers two years (one 'Congress'), as President/Vice President four years, and in the Senate six years (three 'Congresses').""" person = models.ForeignKey('person.Person', related_name='roles') role_type = models.IntegerField(choices=RoleType, db_index=True, help_text="The type of this role: a U.S. senator, a U.S. congressperson, a U.S. president, or a U.S. vice president.") current = models.BooleanField(default=False, choices=[(False, "No"), (True, "Yes")], db_index=True, help_text="Whether the role is currently held, or if this is archival information.") startdate = models.DateField(db_index=True, help_text="The date the role began (when the person took office).") enddate = models.DateField(db_index=True, help_text="The date the role ended (when the person resigned, died, etc.)") # http://en.wikipedia.org/wiki/Classes_of_United_States_Senators senator_class = models.IntegerField(choices=SenatorClass, blank=True, null=True, db_index=True, help_text="For senators, their election class, which determines which years they are up for election. (It has nothing to do with seniority.)") # None for representatives senator_rank = models.IntegerField(choices=SenatorRank, blank=True, null=True, help_text="For senators, their state rank, i.e. junior or senior. For historical data, this is their last known rank.") # None for representatives # http://en.wikipedia.org/wiki/List_of_United_States_congressional_districts district = models.IntegerField(blank=True, null=True, db_index=True, help_text="For representatives, the number of their congressional district. 0 for at-large districts, -1 in historical data if the district is not known.") # None for senators/presidents state = models.CharField(choices=sorted(statenames.items()), max_length=2, blank=True, db_index=True, help_text="For senators and representatives, the two-letter USPS abbrevation for the state or territory they are serving. Values are the abbreviations for the 50 states (each of which have at least one representative and two senators, assuming no vacancies) plus DC, PR, and the island territories AS, GU, MP, and VI (all of which have a non-voting delegate), and for really old historical data you will also find PI (Philippines, 1907-1946), DK (Dakota Territory, 1861-1889), and OR (Orleans Territory, 1806-1811) for non-voting delegates.") party = models.CharField(max_length=255, blank=True, null=True, db_index=True, help_text="The political party of the person. If the person changes party, it is usually the most recent party during this role.") caucus = models.CharField(max_length=255, blank=True, null=True, help_text="For independents, the party that the legislator caucuses with. If changed during a term, the most recent.") website = models.CharField(max_length=255, blank=True, help_text="The URL to the official website of the person during this role, if known.") phone = models.CharField(max_length=64, blank=True, null=True, help_text="The last known phone number of the DC congressional office during this role, if known.") leadership_title = models.CharField(max_length=255, blank=True, null=True, help_text="The last known leadership role held during this role, if any.") extra = JSONField(blank=True, null=True, help_text="Additional schema-less information stored with this object.") # API api_recurse_on = ('person',) api_additional_fields = { "title": "get_title_abbreviated", "title_long": "get_title", "description": "get_description", "congress_numbers": "congress_numbers", } api_example_parameters = { "current": "true", "sort": "state" } class Meta: pass # ordering = ['startdate'] # causes prefetch_related to be slow def __unicode__(self): return '%s / %s to %s / %s' % (self.person.fullname, self.startdate, self.enddate, self.get_role_type_display()) def continues_from(self, prev): if self.startdate - prev.enddate > datetime.timedelta(days=120): return False if self.role_type != prev.role_type: return False if self.senator_class != prev.senator_class: return False if self.state != prev.state: return False if self.district != prev.district: return False return True def get_title(self): """The long form of the title used to prefix the names of people with this role: Representative, Senator, President, Delegate, or Resident Commissioner.""" return self.get_title_name(short=False) def get_title_abbreviated(self): """The title used to prefix the names of people with this role: Rep., Sen., President, Del. (delegate), or Commish. (resident commissioner).""" return self.get_title_name(short=True) def get_title_name(self, short): if self.role_type == RoleType.president: return 'President' if short else 'President of the United States' if self.role_type == RoleType.vicepresident: return 'Vice President' if short else 'Vice President of the United States (and President of the Senate)' if self.role_type == RoleType.senator: return 'Sen.' if short else 'Senator' if self.role_type == RoleType.representative: if self.state not in stateapportionment: # All of the former 'states' were territories that sent delegates. return 'Rep.' if short else 'Delegate' if self.state == 'PR': return 'Commish.' if short else 'Resident Commissioner' if stateapportionment[self.state] == 'T': return 'Rep.' if short else 'Delegate' return 'Rep.' if short else 'Representative' def state_name(self): if not self.state: return "the United States" return statenames[self.state] def state_name_article(self): if not self.state: return "the United States" ret = statenames[self.state] if self.state in ("DC", "MP", "VI", "PI", "OL"): ret = "the " + ret return ret def get_description(self): """A description of this role, e.g. Delegate for District of Columbia At Large.""" from django.contrib.humanize.templatetags.humanize import ordinal if self.role_type in (RoleType.president, RoleType.vicepresident): return self.get_title_name(False) if self.role_type == RoleType.senator: js = "" if self.current and self.senator_rank: js = self.get_senator_rank_display() + " " return js + self.get_title_name(False) + " from " + statenames[self.state] if self.role_type == RoleType.representative: if self.district == -1 or stateapportionment.get(self.state) in ("T", None): # unknown district / current territories and former state-things, all of which send/sent delegates return self.get_title_name(False) + " for " + self.state_name_article() elif self.district == 0: return self.get_title_name(False) + " for " + statenames[self.state] + " At Large" else: return self.get_title_name(False) + " for " + statenames[self.state] + "'s " + ordinal(self.district) + " congressional district" def get_description_natural(self): """A description of this role in sentence form, e.g. the delegate for the District of Columbia's at-large district.""" from website.templatetags.govtrack_utils import ordinalhtml if self.role_type in (RoleType.president, RoleType.vicepresident): return self.get_title_name(False) if self.role_type == RoleType.senator: js = "a " if self.current and self.senator_rank: js = "the " + self.get_senator_rank_display().lower() + " " return js + "senator from " + statenames[self.state] if self.role_type == RoleType.representative: if stateapportionment.get(self.state) in ("T", None): # current territories and former state-things, all of which send/sent delegates return "the %s from %s" % ( self.get_title_name(False).lower(), self.state_name_article() ) else: if self.district == -1: return "the representative for " + statenames[self.state] elif self.district == 0: return "the representative for " + statenames[self.state] + "'s at-large district" else: return "the representative for " + statenames[self.state] + "'s " + ordinalhtml(self.district) + " congressional district" def congress_numbers(self): """The Congressional sessions (Congress numbers) that this role spans, as a list from the starting Congress number through consecutive numbers to the ending Congress number.""" # Senators can span Congresses, so return a range. c1 = get_congress_from_date(self.startdate, range_type="start") c2 = get_congress_from_date(self.enddate, range_type="end") if not c1 or not c2: return None return range(c1, c2+1) # congress number only, not session def most_recent_congress_number(self): n = self.congress_numbers() if not n: return None n = n[-1] if n > settings.CURRENT_CONGRESS: n = settings.CURRENT_CONGRESS # we don't ever mean to ask for a future one (senators, PR res com) return n @property def leadership_title_full(self): if not self.leadership_title: return None if self.leadership_title == "Speaker": return "Speaker of the House" return RoleType.by_value(self.role_type).congress_chamber + " " + self.leadership_title def get_party_on_date(self, when): if self.extra and "party_affiliations" in self.extra: for pa in self.extra["party_affiliations"]: if pa['start'] <= when.date().isoformat() <= pa['end']: return pa['party'] return self.party @property def is_territory(self): # a current territory return stateapportionment.get(self.state) == "T" @property def is_historical_territory(self): # a historical territory # note: self.state is "" for presidents/vps return self.state and stateapportionment.get(self.state) is None def create_events(self, prev_role, next_role): now = datetime.datetime.now().date() from events.models import Feed, Event with Event.update(self) as E: f = self.person.get_feed() if not prev_role or not self.continues_from(prev_role): E.add("termstart", self.startdate, f) if not next_role or not next_role.continues_from(self): if self.enddate <= now: # because we're not sure of end date until it happens E.add("termend", self.enddate, f) def render_event(self, eventid, feeds): self.person.role = self # affects name generation return { "type": "Elections and Offices", "date_has_no_time": True, "date": self.startdate if eventid == "termstart" else self.enddate, "title": self.person.name + (" takes office as " if eventid == "termstart" else " leaves office as ") + self.get_description(), "url": self.person.get_absolute_url(), "body_text_template": "{{name}} {{verb}} {{term}}.", "body_html_template": "<p>{{name}} {{verb}} {{term}}.</p>", "context": { "name": self.person.name, "verb": ("takes office as" if eventid == "termstart" else "leaves office as"), "term": self.get_description(), } } def logical_dates(self, round_end=False): startdate = None enddate = None prev_role = None found_me = False for role in self.person.roles.filter(role_type=self.role_type, senator_class=self.senator_class, state=self.state, district=self.district).order_by('startdate'): if found_me and not role.continues_from(prev_role): break if prev_role == None or not role.continues_from(prev_role): startdate = role.startdate enddate = role.logical_enddate(round_end=round_end) prev_role = role if role.id == self.id: found_me = True if not found_me: raise Exception("Didn't find myself?!") return (startdate, enddate) def logical_enddate(self, round_end=False): if round_end and self.enddate.month == 1 and self.enddate.day < 10: return datetime.date(self.enddate.year-1, 12, 31) return self.enddate def next_election_year(self): # For current terms, roles end at the end of a Congress on Jan 3. # The election occurs in the year before. if not self.current: raise ValueError() return self.enddate.year-1 def get_most_recent_session_stats(self): # Which Congress and session's end date is the most recently covered by this role? errs = [] congresses = self.congress_numbers() for congress, session, sd, ed in reversed(get_all_sessions()): if congress not in congresses: continue if self.startdate < ed <= self.enddate: try: return self.person.get_session_stats(session) except ValueError as e: errs.append(unicode(e)) raise ValueError("No statistics are available for this role: %s" % "; ".join(errs)) def opposing_party(self): if self.party == "Democrat": return "Republican" if self.party == "Republican": return "Democrat" return None def get_sort_key(self): # As it happens, our enums define a good sort order between senators and representatives. return (self.role_type, self.senator_rank)
class PersonRole(models.Model): """Terms held in office by Members of Congress, Presidents, and Vice Presidents. Each term corresponds with an election, meaning each term in the House covers two years (one 'Congress'), as President/Vice President four years, and in the Senate six years (three 'Congresses').""" person = models.ForeignKey('person.Person', related_name='roles', on_delete=models.CASCADE) role_type = models.IntegerField( choices=RoleType, db_index=True, help_text= "The type of this role: a U.S. senator, a U.S. congressperson, a U.S. president, or a U.S. vice president." ) current = models.BooleanField( default=False, choices=[(False, "No"), (True, "Yes")], db_index=True, help_text= "Whether the role is currently held, or if this is archival information." ) startdate = models.DateField( db_index=True, help_text="The date the role began (when the person took office).") enddate = models.DateField( db_index=True, help_text= "The date the role ended (when the person resigned, died, etc.)") # http://en.wikipedia.org/wiki/Classes_of_United_States_Senators senator_class = models.IntegerField( choices=SenatorClass, blank=True, null=True, db_index=True, help_text= "For senators, their election class, which determines which years they are up for election. (It has nothing to do with seniority.)" ) # None for representatives senator_rank = models.IntegerField( choices=SenatorRank, blank=True, null=True, help_text= "For senators, their state rank, i.e. junior or senior. For historical data, this is their last known rank." ) # None for representatives # http://en.wikipedia.org/wiki/List_of_United_States_congressional_districts district = models.IntegerField( blank=True, null=True, db_index=True, help_text= "For representatives, the number of their congressional district. 0 for at-large districts, -1 in historical data if the district is not known." ) # None for senators/presidents state = models.CharField( choices=sorted(statenames.items()), max_length=2, blank=True, db_index=True, help_text= "For senators and representatives, the two-letter USPS abbrevation for the state or territory they are serving. Values are the abbreviations for the 50 states (each of which have at least one representative and two senators, assuming no vacancies) plus DC, PR, and the island territories AS, GU, MP, and VI (all of which have a non-voting delegate), and for really old historical data you will also find PI (Philippines, 1907-1946), DK (Dakota Territory, 1861-1889), and OR (Orleans Territory, 1806-1811) for non-voting delegates." ) party = models.CharField( max_length=255, blank=True, null=True, db_index=True, help_text= "The political party of the person. If the person changes party, it is usually the most recent party during this role." ) caucus = models.CharField( max_length=255, blank=True, null=True, help_text= "For independents, the party that the legislator caucuses with. If changed during a term, the most recent." ) website = models.CharField( max_length=255, blank=True, help_text= "The URL to the official website of the person during this role, if known." ) phone = models.CharField( max_length=64, blank=True, null=True, help_text= "The last known phone number of the DC congressional office during this role, if known." ) leadership_title = models.CharField( max_length=255, blank=True, null=True, help_text= "The last known leadership role held during this role, if any.") extra = JSONField( blank=True, null=True, help_text="Additional schema-less information stored with this object." ) # API api_recurse_on = ('person', ) api_additional_fields = { "title": "get_title_abbreviated", "title_long": "get_title", "description": "get_description", "congress_numbers": "congress_numbers", } api_example_parameters = {"current": "true", "sort": "state"} class Meta: pass # ordering = ['startdate'] # causes prefetch_related to be slow def __str__(self): return '[%s] %s / %s to %s / %s / %s' % ( self.id, self.person.fullname, self.startdate, self.enddate, self.get_role_type_display(), ",".join( str(c) for c in (self.congress_numbers() or []))) def get_office_id(self): if self.role_type in (RoleType.president, RoleType.vicepresident): return RoleType.by_value(self.role_type).key if self.role_type == RoleType.senator: return ("sen", self.state, self.senator_class) if self.role_type == RoleType.representative: return ("rep", self.state, self.district) raise ValueError() def continues_from(self, prev): if self.startdate - prev.enddate > datetime.timedelta(days=120): return False if self.role_type != prev.role_type: return False if self.senator_class != prev.senator_class: return False if self.state != prev.state: return False if self.district != prev.district: return False return True def get_title(self): """The long form of the title used to prefix the names of people with this role: Representative, Senator, President, or Resident Commissioner.""" return self.get_title_name(short=False) def get_title_abbreviated(self): """The title used to prefix the names of people with this role: Rep., Sen., President, Del. (delegate), or Commish. (resident commissioner).""" return self.get_title_name(short=True) def get_title_name(self, short): if self.role_type == RoleType.president: return 'President' if short else 'President of the United States' if self.role_type == RoleType.vicepresident: return 'Vice President' if short else 'Vice President of the United States' if self.role_type == RoleType.senator: return 'Sen.' if short else 'Senator' if self.role_type == RoleType.representative: if self.state not in stateapportionment: # All of the former 'states' were territories that sent delegates. return 'Rep.' if short else 'Representative' if self.state == 'PR': return 'Commish.' if short else 'Resident Commissioner' if stateapportionment[self.state] == 'T': # These folks are also commonly called delegates, but out of respect # for their disenfranchised constituents we refer to them as representatives. return 'Rep.' if short else 'Representative' return 'Rep.' if short else 'Representative' def state_name(self): if not self.state: return "the United States" return statenames[self.state] def state_name_article(self): if not self.state: return "the United States" ret = statenames[self.state] if self.state in ("DC", "MP", "VI", "PI", "OL"): ret = "the " + ret return ret def get_description(self): """A description of this role, e.g. Delegate for District of Columbia At Large.""" from django.contrib.humanize.templatetags.humanize import ordinal if self.role_type in (RoleType.president, RoleType.vicepresident): return self.get_title_name(False) if self.role_type == RoleType.senator: js = "" if self.current and self.senator_rank: js = self.get_senator_rank_display() + " " return js + self.get_title_name(False) + " for " + statenames[ self.state] if self.role_type == RoleType.representative: if self.district == -1 or stateapportionment.get(self.state) in ( "T", None ): # unknown district / current territories and former state-things, all of which send/sent delegates return self.get_title_name( False) + " for " + self.state_name_article() elif self.district == 0: return self.get_title_name(False) + " for " + statenames[ self.state] + " At Large" else: return self.get_title_name(False) + " for " + statenames[ self.state] + "'s " + ordinal( self.district) + " congressional district" def get_description_natural(self): """A description in HTML of this role in sentence form, e.g. the delegate for the District of Columbia's at-large district.""" from website.templatetags.govtrack_utils import ordinalhtml (statename, statename_article) = (self.state_name_article(), "") if statename.startswith("the "): (statename, statename_article) = (statename[4:], "the ") statename = '%s<a href="/congress/members/%s">%s</a>' % ( statename_article, self.state, statename) if self.role_type in (RoleType.president, RoleType.vicepresident): return self.get_title_name(False) if self.role_type == RoleType.senator: js = "a " if self.current and self.senator_rank: js = "the " + self.get_senator_rank_display().lower() + " " return js + "senator from " + statename if self.role_type == RoleType.representative: if stateapportionment.get(self.state) in ( "T", None ): # current territories and former state-things, all of which send/sent delegates return "the %s from %s" % (self.get_title_name(False).lower(), statename) else: if self.district == -1: return "the representative for " + statename elif self.district == 0: return "the representative for " + statename + "\u2019s at-large district" else: return "the representative for " + statename + "\u2019s " + ordinalhtml( self.district) + " congressional district" def congress_numbers(self): """The Congressional sessions (Congress numbers) that this role spans, as a list from the starting Congress number through consecutive numbers to the ending Congress number.""" # Senators can span Congresses, so return a range. c1 = get_congress_from_date(self.startdate, range_type="start") c2 = get_congress_from_date(self.enddate, range_type="end") if not c1 or not c2: return None return list(range(c1, c2 + 1)) # congress number only, not session def most_recent_congress_number(self): n = self.congress_numbers() if not n: return None n = n[-1] if n > settings.CURRENT_CONGRESS: n = settings.CURRENT_CONGRESS # we don't ever mean to ask for a future one (senators, PR res com) return n def get_party(self): # If the person didn't change parties, just return the party. # Otherwise return "most recently a PARTY1 (year1-year2) and before that (year3-4), and ..." if self.party is None: return "(unknown party)" from parser.processor import Processor parties = (self.extra or {}).get("party_affiliations", []) def a_an(word): return "a" if word[0].lower() not in "aeiou" else "an" if len(parties) <= 1: return a_an(self.party) + " " + self.party + ( "" if not self.caucus else " caucusing with the " + self.caucus + "s") parties = [ "%s %s%s (%d-%s)" % ( a_an(entry["party"]), entry["party"], "" if not entry.get("caucus") else " caucusing with the " + entry["caucus"] + "s", Processor.parse_datetime(entry["start"]).year, "" if self.current and i == len(parties) - 1 else PersonRole.round_down_enddate( Processor.parse_datetime(entry["end"])).year, ) for i, entry in enumerate(parties) ] if self.current: most_recent_descr1 = "" most_recent_descr2 = ", " else: most_recent_descr1 = "most recently " most_recent_descr2 = " and " most_recent = parties.pop(-1) if len(parties) > 1: parties[-1] = "and " + parties[-1] return most_recent_descr1 + most_recent + most_recent_descr2 + "previously " + ( ", " if len(parties) > 2 else " ").join(parties) def get_party_on_date(self, when): if self.extra and "party_affiliations" in self.extra: for pa in self.extra["party_affiliations"]: if pa['start'] <= when.date().isoformat() <= pa['end']: return pa['party'] return self.party @property def is_territory(self): # a current territory return stateapportionment.get(self.state) == "T" @property def is_historical_territory(self): # a historical territory # note: self.state is "" for presidents/vps return self.state and stateapportionment.get(self.state) is None def create_events(self, prev_role, next_role): now = datetime.datetime.now().date() from events.models import Feed, Event with Event.update(self) as E: f = self.person.get_feed() if not prev_role or not self.continues_from(prev_role): E.add("termstart", self.startdate, f) if not next_role or not next_role.continues_from(self): if self.enddate <= now: # because we're not sure of end date until it happens E.add("termend", self.enddate, f) def render_event(self, eventid, feeds): self.person.role = self # affects name generation return { "type": "Elections and Offices", "date_has_no_time": True, "date": self.startdate if eventid == "termstart" else self.enddate, "title": self.person.name + (" takes office as " if eventid == "termstart" else " leaves office as ") + self.get_description(), "url": self.person.get_absolute_url(), "body_text_template": "{{name}} {{verb}} {{term}}.", "body_html_template": "<p>{{name}} {{verb}} {{term}}.</p>", "context": { "name": self.person.name, "verb": ("takes office as" if eventid == "termstart" else "leaves office as"), "term": self.get_description(), } } def logical_dates(self, round_end=False): startdate = None enddate = None prev_role = None found_me = False for role in self.person.roles.filter( role_type=self.role_type, senator_class=self.senator_class, state=self.state, district=self.district).order_by('startdate'): if found_me and not role.continues_from(prev_role): break if prev_role == None or not role.continues_from(prev_role): startdate = role.startdate enddate = PersonRole.round_down_enddate(role.enddate, do_rounding=round_end) prev_role = role if role.id == self.id: found_me = True if not found_me: raise Exception("Didn't find myself?!") return (startdate, enddate) @staticmethod def round_down_enddate(d, do_rounding=True): # If a date ends in the first three days of January, round it down to # December 31 of the previou year, so that we can show a nice year for # term end dates (2011-2012 rather than 2011-2013 which misleadingly # implies it was more than a few days in 2013 that probably weren't # in session anyway). if do_rounding: if d.month == 1 and d.day <= 3: return datetime.date(d.year - 1, 12, 31) return d def next_election_year(self): # For current terms, roles end at the end of a Congress on Jan 3. # The election occurs in the year before. # # EXCEPT: Senators appointed to fill a term may be up for re-election # by special election sooner than the term end date stored in our # data. The end date is thus not known because it will be when the # special election winner is certified. if not self.current: raise ValueError() if (self.extra or {}).get("end-type") == "special-election": return self.enddate.year return self.enddate.year - 1 def is_up_for_election(self): if not self.current: return False if settings.CURRENT_ELECTION_DATE is None: return False # no election cycle is current if settings.CURRENT_ELECTION_DATE < datetime.datetime.now().date(): return False # election is over return self.next_election_year( ) == settings.CURRENT_ELECTION_DATE.year # is up this cycle def did_election_just_happen(self): if not self.current: return False if settings.CURRENT_ELECTION_DATE is None: return False # no election cycle is current if settings.CURRENT_ELECTION_DATE > datetime.datetime.now().date(): return False # election hasn't happened yet return self.next_election_year( ) == settings.CURRENT_ELECTION_DATE.year # is up this cycle def get_most_recent_session_stats(self): # Which Congress and session's end date is the most recently covered by this role? errs = [] congresses = self.congress_numbers() for congress, session, sd, ed in reversed(get_all_sessions()): if congress not in congresses: continue if self.startdate < ed <= self.enddate: try: return self.person.get_session_stats(session) except ValueError as e: errs.append(str(e)) raise ValueError("No statistics are available for this role: %s" % "; ".join(errs)) def opposing_party(self): if self.party == "Democrat": return "Republican" if self.party == "Republican": return "Democrat" return None def get_sort_key(self): # As it happens, our enums define a good sort order between senators and representatives. return (self.role_type, self.senator_rank)