Exemplo n.º 1
0
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)
Exemplo n.º 2
0
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)