예제 #1
0
class Pollbook(ModelBase):
    """Pollbook model, stores one election census."""

    __versioned__ = {}
    __tablename__ = 'pollbook_meta'

    id = db.Column(UuidType, primary_key=True, default=uuid.uuid4)

    name = db.Column(NestedMutableJson)

    weight = db.Column(db.Integer, nullable=False, default=1)

    priority = db.Column(db.Integer, nullable=False, default=0)

    election_id = db.Column(UuidType,
                            db.ForeignKey('election.id'),
                            nullable=False)

    election = db.relationship('Election',
                               back_populates='pollbooks',
                               lazy='joined')

    voters = db.relationship('Voter', cascade='all, delete-orphan')
    voter_objects = db.relationship('Voter', lazy='dynamic')
    census_file_imports = db.relationship('CensusFileImport')

    @property
    def has_votes(self):
        """True if there are already cast votes in this pollbook."""
        return bool(
            self.voter_objects.filter(Voter.votes.__ne__(None)).count())

    @property
    def self_added_voters(self):
        """List of all selv added voters."""
        return [x for x in self.voters if x.self_added]

    @property
    def valid_voters(self):
        """List of all valid voters."""
        return [x for x in self.voters if x.is_valid_voter()]

    @property
    def voters_admin_added(self):
        """List of all voters added by the admins."""
        return [x for x in self.voters if not x.self_added]

    @property
    def valid_voters_with_vote(self):
        """List of all valid voters with a vote."""
        voters = [
            x for x in self.voters if x.is_valid_voter() and len(x.votes) > 0
        ]
        return voters

    @property
    def valid_voters_without_vote(self):
        """List of all valid voters without a vote."""
        voters = [x for x in self.voters if x.is_valid_voter() and not x.votes]
        return voters
예제 #2
0
class ElectionGroupCount(ModelBase):

    __versioned__ = {}

    id = db.Column(evalg.database.types.UuidType,
                   default=uuid.uuid4,
                   primary_key=True)

    group_id = db.Column(evalg.database.types.UuidType,
                         db.ForeignKey('election_group.id'))

    election_group = db.relationship('ElectionGroup',
                                     back_populates='election_group_counts',
                                     lazy='joined')

    election_results = db.relationship('ElectionResult')

    initiated_at = db.Column(evalg.database.types.UtcDateTime)

    finished_at = db.Column(evalg.database.types.UtcDateTime)

    audit = db.Column(evalg.database.types.MutableJson)

    @hybrid_property
    def status(self):
        if self.finished_at:
            return 'finished'
        return 'ongoing'

    @status.expression
    def status(cls):
        return case([(cls.finished_at.isnot(None), 'finished')],
                    else_='ongoing')
예제 #3
0
class PersonExternalId(ModelBase):
    """ Person external ID. """

    __versioned__ = {}
    __tablename__ = 'person_external_id'

    person_id = db.Column(evalg.database.types.UuidType,
                          db.ForeignKey('person.id'),
                          nullable=False)

    id_type = db.Column(db.UnicodeText, primary_key=True)

    id_value = db.Column(db.UnicodeText, primary_key=True)

    person = db.relationship('Person', back_populates='identifiers')

    @validates('id_type')
    def validate_id_type(self, key, id_type):
        return PersonIdType(id_type).value

    @classmethod
    def find_ids(cls, *where):
        def ensure_iterable(obj):
            if iterable_but_not_str(obj):
                return obj
            return (obj, )

        or_clauses = or_(
            and_(
                cls.id_type == id_type,
                cls.id_value.in_(ensure_iterable(values)),
            ) for id_type, values in where)
        return cls.query.filter(or_clauses)
예제 #4
0
class ElectionList(ModelBase):
    """ List of electable candidates in an election. """

    __versioned__ = {}

    id = db.Column(
        UuidType,
        primary_key=True,
        default=uuid.uuid4)

    name = db.Column(NestedMutableJson)

    description = db.Column(NestedMutableJson)

    information_url = db.Column(UrlType)

    election_id = db.Column(
        UuidType,
        db.ForeignKey('election.id'),
        nullable=False)

    election = db.relationship(
        'Election',
        back_populates='lists',
        lazy='joined')

    candidates = db.relationship('Candidate', cascade='all, delete-orphan')

    @property
    def election_status(self):
        return self.election.status
예제 #5
0
class GroupExternalIDType(ModelBase):
    """ Group external ID type. """

    __versioned__ = {}

    code = db.Column(db.UnicodeText, primary_key=True)

    description = db.Column(db.UnicodeText)
예제 #6
0
class Person(ModelBase):
    """ Person. """

    __versioned__ = {}

    # Prioritized order of external ids to use when eg creating a voter object
    PREFERRED_IDS = ('feide_id', 'nin')

    id = db.Column(evalg.database.types.UuidType,
                   primary_key=True,
                   default=uuid.uuid4)

    email = db.Column(db.UnicodeText, index=True)

    display_name = db.Column(db.UnicodeText)

    last_update = db.Column(evalg.database.types.UtcDateTime, default=utcnow)

    last_update_from_feide = db.Column(evalg.database.types.UtcDateTime,
                                       default=utcnow)

    principal = db.relationship('PersonPrincipal', uselist=False)

    identifiers = db.relationship(
        'PersonExternalId',
        back_populates='person',
        cascade='all, delete-orphan',
    )

    def get_preferred_id(self, *preference):
        """
        Get the first available *preferred* identifier

        :param preference:
            ``PersonExternalId.id_type``s to consider. The first id_type will
            be the *most* preferred.

        :rtype: PersonExternalId
        :return:
            Returns the most preferred ``PersonExternalId`` object, or ``None``
            if the person does not have any of the given id types.
        """
        preferred_ids = preference or self.PREFERRED_IDS

        for obj in sorted(
            (obj for obj in self.identifiers if obj.id_type in preferred_ids),
                key=lambda o: preferred_ids.index(o.id_type)):
            return obj
        return None
예제 #7
0
class AbstractElection(ModelBase):
    """Base model for elections and election groups."""

    __abstract__ = True

    id = db.Column(evalg.database.types.UuidType,
                   default=uuid.uuid4,
                   primary_key=True)

    # Translated name
    name = db.Column(evalg.database.types.MutableJson)

    # Translated text
    description = db.Column(evalg.database.types.MutableJson)

    # Template metadata
    meta = db.Column(evalg.database.types.NestedMutableJson)
예제 #8
0
class GroupMembership(ModelBase):
    """ Group memberships. """

    __versioned__ = {}

    id = db.Column(evalg.database.types.UuidType,
                   primary_key=True,
                   default=uuid.uuid4)

    group_id = db.Column(evalg.database.types.UuidType,
                         db.ForeignKey('group.id'),
                         nullable=False)

    person_id = db.Column(evalg.database.types.UuidType,
                          db.ForeignKey('person.id'),
                          nullable=False)

    __table_args__ = (schema.UniqueConstraint('group_id', 'person_id'), )
예제 #9
0
class Group(ModelBase):
    """ Group of persons. """

    __versioned__ = {}

    id = db.Column(evalg.database.types.UuidType,
                   primary_key=True,
                   default=uuid.uuid4)

    dp_group_id = db.Column(db.UnicodeText, index=True)

    name = db.Column(db.UnicodeText, unique=True, nullable=False)

    last_update = db.Column(evalg.database.types.UtcDateTime, default=utcnow)

    principals = db.relationship('GroupPrincipal')

    external_ids = db.relationship('GroupExternalID', back_populates='group')
예제 #10
0
class GroupExternalID(ModelBase):
    """ Group external ID. """

    __versioned__ = {}
    __tablename__ = 'group_external_id'

    group_id = db.Column(evalg.database.types.UuidType,
                         db.ForeignKey('group.id'),
                         nullable=False)

    external_id = db.Column(db.UnicodeText, primary_key=True)

    type_code = db.Column(db.UnicodeText,
                          db.ForeignKey('group_external_id_type.code'),
                          primary_key=True)

    group = db.relationship('Group', back_populates='external_ids')

    id_type = db.relationship('GroupExternalIDType')  # no b.ref needed
예제 #11
0
class Candidate(ModelBase):

    __versioned__ = {}

    id = db.Column(
        UuidType,
        primary_key=True,
        default=uuid.uuid4)

    list_id = db.Column(
        UuidType,
        db.ForeignKey('election_list.id'),
        nullable=False)

    list = db.relationship(
        'ElectionList',
        back_populates='candidates',
        lazy='joined')

    name = db.Column(
        db.UnicodeText,
        nullable=False)

    meta = db.Column(NestedMutableJson)

    information_url = db.Column(UrlType)

    priority = db.Column(
        db.Integer,
        default=0)

    pre_cumulated = db.Column(
        db.Boolean,
        default=False)

    user_cumulated = db.Column(
        db.Boolean,
        default=False)

    def __str__(self):
        return self.name

    @property
    def election_status(self):
        return self.list.election.status
예제 #12
0
class ElectionResult(ModelBase):
    """The ElectionResult class"""
    __versioned__ = {}

    id = db.Column(evalg.database.types.UuidType,
                   default=uuid.uuid4,
                   primary_key=True)

    election_id = db.Column(evalg.database.types.UuidType,
                            db.ForeignKey('election.id'))

    election = db.relationship('Election',
                               back_populates='election_results',
                               lazy='joined')

    election_group_count_id = db.Column(
        evalg.database.types.UuidType,
        db.ForeignKey('election_group_count.id'))

    election_group_count = db.relationship('ElectionGroupCount',
                                           back_populates='election_results',
                                           lazy='joined')
    """ election group count that the result belongs to """

    election_protocol = deferred(db.Column(evalg.database.types.MutableJson))

    ballots = deferred(db.Column(evalg.database.types.NestedMutableJson))
    """ These are deferred to avoid loading too much data """

    result = db.Column(evalg.database.types.MutableJson)

    pollbook_stats = db.Column(evalg.database.types.MutableJson)

    @property
    def election_protocol_text(self):
        """election_protocol_text-property"""
        try:
            protcol_cls = count.PROTOCOL_MAPPINGS[self.election.type_str]
            return protcol_cls.from_dict(self.election_protocol).render()
        except KeyError:
            raise Exception('Unsupported counting method for protocol')
        return self.value
예제 #13
0
class ElectionGroup(AbstractElection):

    __versioned__ = {}

    ou_id = db.Column(evalg.database.types.UuidType,
                      db.ForeignKey('organizational_unit.id'),
                      nullable=True)

    ou = db.relationship('OrganizationalUnit')

    # Organizational unit
    elections = db.relationship('Election')

    election_group_counts = db.relationship('ElectionGroupCount')

    # Public election key
    public_key = db.Column(db.Text)

    # Announced if set
    announced_at = db.Column(evalg.database.types.UtcDateTime)

    # Published if set
    published_at = db.Column(evalg.database.types.UtcDateTime)

    # Cancelled if set
    cancelled_at = db.Column(evalg.database.types.UtcDateTime)

    # Deleted if set
    deleted_at = db.Column(evalg.database.types.UtcDateTime)

    # Name of the template used to create the election group
    template_name = db.Column(db.UnicodeText)

    # Internal use
    type = db.Column(db.UnicodeText)

    def announce(self):
        """Mark as announced."""
        self.announced_at = utcnow()

    def unannounce(self):
        """Mark as unannounced."""
        self.announced_at = None

    @hybrid_property
    def announced(self):
        return self.announced_at is not None

    @announced.expression
    def announced(cls):
        return cls.announced_at.isnot(None)

    def publish(self):
        """Mark as published."""
        self.published_at = utcnow()

    def unpublish(self):
        """Mark as unpublished."""
        self.published_at = None

    @hybrid_property
    def published(self):
        return self.published_at is not None

    @published.expression
    def published(cls):
        return cls.published_at.isnot(None)

    def cancel(self):
        """ Mark as cancelled. """
        self.cancelled_at = utcnow()

    @hybrid_property
    def cancelled(self):
        return self.cancelled_at is not None

    def delete(self):
        """ Mark as deleted. """
        self.deleted_at = utcnow()

    @hybrid_property
    def deleted(self):
        return self.deleted_at is not None

    @deleted.expression
    def deleted(cls):
        return cls.deleted_at.isnot(None)

    @hybrid_property
    def status(self):
        statuses = set(list(map(lambda x: x.status, self.elections)))
        if not statuses:
            return 'draft'
        if len(statuses) == 1:
            return statuses.pop()
        if len(statuses) == 2 and 'inactive' in statuses:
            statuses.discard('inactive')
            return statuses.pop()
        return 'multipleStatuses'

    @property
    def announcement_blockers(self):
        """Check whether the election group can be announced."""
        blockers = []
        if self.announced:
            blockers.append('already-announced')
        for election in self.elections:
            if election.active:
                if election.missing_start_or_end:
                    blockers.append('missing-start-or-end')
                if election.start_after_end:
                    blockers.append('start-must-be-before-end')
        return blockers

    @property
    def publication_blockers(self):
        """Check whether the election group can be published."""
        blockers = []
        if self.published:
            blockers.append('already-published')
        if not self.public_key:
            blockers.append('missing-key')
        no_active_elections = True
        for election in self.elections:
            if election.active:
                if election.missing_start_or_end:
                    blockers.append('missing-start-or-end')
                if election.start_after_end:
                    blockers.append('start-must-be-before-end')
                no_active_elections = False
        if no_active_elections:
            blockers.append('no-active-election')
        return blockers
예제 #14
0
class Election(AbstractElection):
    """Election."""

    __versioned__ = {}

    # Some ID for the UI
    sequence = db.Column(db.Text)

    start = db.Column(evalg.database.types.UtcDateTime)

    end = db.Column(evalg.database.types.UtcDateTime)

    information_url = db.Column(evalg.database.types.UrlType)

    contact = db.Column(db.Text)

    mandate_period_start = db.Column(db.Date)

    mandate_period_end = db.Column(db.Date)

    group_id = db.Column(evalg.database.types.UuidType,
                         db.ForeignKey('election_group.id'))

    election_group = db.relationship('ElectionGroup',
                                     back_populates='elections',
                                     lazy='joined')

    election_results = db.relationship('ElectionResult',
                                       cascade='all, delete-orphan')

    lists = db.relationship('ElectionList', cascade='all, delete-orphan')

    pollbooks = db.relationship('Pollbook', cascade='all, delete-orphan')

    # Whether election is active.
    # We usually create more elections than needed to make templates consistent
    # But not all elections should be used. This can improve voter UI, by
    # telling voter that their group does not have an active election.
    active = db.Column(db.Boolean, default=False)

    @hybrid_property
    def announced_at(self):
        return self.election_group.announced_at

    @announced_at.expression
    def announced_at(cls):
        return select([ElectionGroup.announced_at
                       ]).where(cls.group_id == ElectionGroup.id).as_scalar()

    @hybrid_property
    def published_at(self):
        return self.election_group.published_at

    @published_at.expression
    def published_at(cls):
        return select([ElectionGroup.published_at
                       ]).where(cls.group_id == ElectionGroup.id).as_scalar()

    @hybrid_property
    def cancelled_at(self):
        return self.election_group.cancelled_at

    @cancelled_at.expression
    def cancelled_at(cls):
        return select([ElectionGroup.cancelled_at
                       ]).where(cls.group_id == ElectionGroup.id).as_scalar()

    @hybrid_property
    def status(self):
        """
        inactive → draft → announced → published → ongoing/closed/cancelled
        """
        if not self.active:
            return 'inactive'
        if self.election_group.cancelled_at:
            return 'cancelled'
        if self.election_group.published_at:
            if self.end <= utcnow():
                return 'closed'
            if self.start <= utcnow():
                return 'ongoing'
            return 'published'
        if self.election_group.announced_at:
            return 'announced'
        return 'draft'

    @status.expression
    def status(cls):
        return case([(cls.active is False, 'inactive'),
                     (cls.cancelled_at.isnot(None), 'cancelled'),
                     (and_(cls.published_at.isnot(None),
                           cls.end <= func.now()), 'closed'),
                     (and_(cls.published_at.isnot(None),
                           cls.start <= func.now()), 'ongoing'),
                     (cls.published_at.isnot(None), 'published'),
                     (cls.announced_at.isnot(None), 'announced')],
                    else_='draft')

    @hybrid_property
    def has_started(self):
        """Check if an election is past its start time."""
        return bool(self.start and self.start <= utcnow())

    @has_started.expression
    def has_started(cls):
        return case(
            [(and_(cls.start.isnot(None), cls.start <= func.now()), True)],
            else_=False)

    @property
    def missing_start_or_end(self):
        return not self.start or not self.end

    @property
    def start_after_end(self):
        if self.missing_start_or_end:
            return False
        return self.start > self.end

    @property
    def has_votes(self):
        """True if there are already cast votes in this election"""
        for pollbook in self.pollbooks:
            if pollbook.has_votes:
                return True
        return False

    @property
    def is_locked(self):
        """
        A wrapper property for several existing checks

        True if self.has_votes OR self.is_ongoing
        """
        return self.is_ongoing or self.has_votes

    @hybrid_property
    def is_ongoing(self):
        """Check if an election is currently ongoing."""
        return bool(self.election_group.published_at
                    and self.start <= utcnow() and self.end >= utcnow())

    @is_ongoing.expression
    def is_ongoing(cls):
        return case([(and_(cls.published_at.isnot(None),
                           cls.start.isnot(None), cls.start <= func.now(),
                           cls.end.isnot(None), cls.end >= func.now()), True)],
                    else_=False)

    @property
    def ou_id(self):
        return self.election_group.ou_id

    @property
    def ou(self):
        return self.election_group.ou

    @property
    def list_ids(self):
        return [l.id for l in self.lists if not l.deleted]

    @property
    def num_choosable(self):
        return self.meta['candidate_rules']['seats']

    @property
    def num_substitutes(self):
        return self.meta['candidate_rules'].get('substitutes', 0)

    @property
    def candidates(self):
        if len(self.lists) > 1:
            raise Exception('Not intended for use on election with '
                            'with multiple candidate lists')
        return self.lists[0].candidates

    @property
    def quotas(self):
        quotas = []
        if self.election_group.meta['candidate_rules'].get('candidate_gender'):
            quota_names = self.meta['counting_rules']['affirmative_action']
            for quota_name in quota_names:
                if quota_name == 'gender_40':  # the only one supported so far
                    males = []
                    females = []
                    min_value = 0
                    min_value_substitutes = 0  # for uiostv .. etc
                    for candidate in self.candidates:
                        if candidate.meta['gender'] == 'male':
                            males.append(candidate)
                        elif candidate.meta['gender'] == 'female':
                            females.append(candidate)
                    if self.type_str == 'uio_stv':
                        # no other elections implemented yet...
                        if self.num_choosable <= 1:
                            min_value = 0
                        elif self.num_choosable <= 3:
                            min_value = 1
                        elif self.num_choosable:
                            min_value = math.ceil(0.4 * self.num_choosable)
                        if self.num_substitutes <= 1:
                            min_value_substitutes = 0
                        elif self.num_substitutes <= 3:
                            min_value_substitutes = 1
                        elif self.num_substitutes:
                            min_value_substitutes = math.ceil(
                                0.4 * self.num_substitutes)
                    # handle universal cases when members < min_value
                    min_value_males = min([min_value, len(males)])
                    min_value_females = min([min_value, len(females)])
                    quotas.append(
                        QuotaGroup({
                            'en': 'Males',
                            'nn': 'Menn',
                            'nb': 'Menn'
                        }, males, min_value_males,
                                   min([
                                       min_value_substitutes,
                                       len(males) - min_value_males
                                   ])))
                    quotas.append(
                        QuotaGroup(
                            {
                                'en': 'Females',
                                'nn': 'Kvinner',
                                'nb': 'Kvinner'
                            }, females, min_value_females,
                            min([
                                min_value_substitutes,
                                len(females) - min_value_females
                            ])))

        return quotas

    @property
    def type_str(self):
        """type_str-property"""
        return self.meta['counting_rules']['method']
예제 #15
0
class OrganizationalUnit(ModelBase):
    """ Organizational unit. """

    __versioned__ = {}

    id = db.Column(
        UuidType,
        default=uuid.uuid4,
        primary_key=True)

    name = db.Column(
        JsonType,
        nullable=False)

    external_id = db.Column(
        db.Text,
        nullable=False,
        unique=True)

    deleted = db.Column(
        db.Boolean,
        default=False)

    tag = db.Column(db.String)

    parent = db.relationship(
        'OrganizationalUnit',
        backref='children',
        remote_side=id)

    parent_id = db.Column(
        UuidType(),
        db.ForeignKey('organizational_unit.id'))

    def isunder(self, other, acceptsame=True):
        """ Checks if self is a sub ou of other. """
        if isinstance(other, OrganizationalUnit):
            other = other.id
        if self.id == other:
            return True
        if self.parent_id is None:
            return False
        return self.parent.isunder(other)

    def __lt__(self, other):
        """ Checks if self is a sub ou of other. """
        return self.isunder(other, acceptsame=False)

    def __le__(self, other):
        """ Checks if self is a sub ou of other. """
        return self.isunder(other, acceptsame=True)

    def isover(self, other, acceptsame=True):
        """ Checks if other is a sub ou of self. """
        if isinstance(other, OrganizationalUnit):
            return other.isunder(self.id)
        return other in self.subous(acceptsame)

    def __gt__(self, other):
        return self.isover(other, acceptsame=False)

    def __ge__(self, other):
        return self.isover(other, acceptsame=True)

    def __eq__(self, other):
        """ Checks for equality between ous. """
        return isinstance(other, OrganizationalUnit) and self.id == other.id

    def subous(self, includeself=True):
        """ Generates all children, grandchildren and so on of self. """
        queue = [self] if includeself else self.children[:]
        while queue:
            front = queue.pop()
            yield front
            queue.extend(front.children)
예제 #16
0
class Voter(ModelBase):
    """ Voter / census member model."""

    __versioned__ = {}
    __tablename__ = 'pollbook_voters'

    id = sqlalchemy.schema.Column(UuidType,
                                  primary_key=True,
                                  default=uuid.uuid4)

    id_type = sqlalchemy.schema.Column(
        sqlalchemy.types.UnicodeText,
        doc='person identifier type',
        nullable=False,
    )

    id_value = sqlalchemy.schema.Column(
        sqlalchemy.types.UnicodeText,
        doc='person identifier value',
        nullable=False,
    )

    pollbook_id = sqlalchemy.schema.Column(UuidType,
                                           db.ForeignKey('pollbook_meta.id'),
                                           nullable=False)

    pollbook = db.relationship('Pollbook', back_populates='voters')

    self_added = db.Column(sqlalchemy.types.Boolean,
                           doc='voter was added to the poll book by himself',
                           nullable=False)

    reviewed = db.Column(sqlalchemy.types.Boolean,
                         doc='voter has been reviewed by admin',
                         nullable=False)

    verified = sqlalchemy.schema.Column(
        sqlalchemy.types.Boolean,
        doc='voter is verified, and any vote should be counted',
        nullable=False)

    votes = db.relationship('Vote', cascade='all, delete-orphan')

    reason = sqlalchemy.schema.Column(
        sqlalchemy.types.UnicodeText,
        doc='reason why this voter should be included in the pollbook',
        nullable=True)

    def is_valid_voter(self):
        """
        Checks if the voter is a valid one.

        Valid voters have the status ADMIN_ADDED_AUTO_VERIFIED or
        SELF_ADDED_VERIFIED
        """
        if (self.verified_status == VerifiedStatus.ADMIN_ADDED_AUTO_VERIFIED
                or self.verified_status == VerifiedStatus.SELF_ADDED_VERIFIED):
            return True
        return False

    def ensure_rereview(self):
        """Ensure that the admin need to make a new review of the voter."""
        if self.verified_status is VerifiedStatus.SELF_ADDED_REJECTED:
            self.reviewed = False

    def undo_review(self):
        if self.verified_status in (VerifiedStatus.SELF_ADDED_VERIFIED,
                                    VerifiedStatus.SELF_ADDED_REJECTED):
            self.reviewed = False
            self.verified = False
        elif self.verified_status is VerifiedStatus.ADMIN_ADDED_REJECTED:
            self.reviewed = False
            self.verified = True

    @hybrid_property
    def has_voted(self):
        """Has the voter voted."""
        return len(self.votes) > 0

    @has_voted.expression
    def has_voted(cls):
        """has_voted sqlalchemy expression."""
        return (select([
            case(
                [(exists().where(and_(Vote.voter_id
                                      == cls.id, )).correlate(cls), True)],
                else_=False,
            ).label("has_votes")
        ]).label("number_has_votes"))

    @hybrid_property
    def verified_status(self):
        return VERIFIED_STATUS_MAP[(self.self_added, self.reviewed,
                                    self.verified)]

    verified_status_check_constraint = not_(
        or_(
            and_(self_added.is_(True), reviewed.is_(False),
                 verified.is_(True)),
            and_(self_added.is_(False), reviewed.is_(True),
                 verified.is_(True)),
            and_(self_added.is_(False), reviewed.is_(False),
                 verified.is_(False))))

    #
    # TODO: Get ID_TYPE_CHOICES from PersonExternalId, or implement a separate
    # set of id types? We may not want to support dp_user_id or uid here?
    #
    @validates('id_type')
    def validate_id_type(self, key, id_type):
        return PersonIdType(id_type).value

    __table_args__ = (UniqueConstraint('pollbook_id',
                                       'id_type',
                                       'id_value',
                                       name='_pollbook_voter_uc'),
                      CheckConstraint(
                          verified_status_check_constraint,
                          name='_pollbook_voter_cc_verified_status'))