class BaseModel(object):
    """
    This is the base class for all the Open Civic objects. This contains
    common methods and abstractions for OCD objects.
    """

    # to be overridden by children. Something like "person" or "organization".
    # Used in :func:`validate`.
    _type = None
    _schema = None

    def __init__(self):
        super(BaseModel, self).__init__()
        self._id = str(uuid.uuid1())
        self._related = []
        self.extras = {}

    # validation

    def validate(self, schema=None):
        """
        Validate that we have a valid object.

        On error, this will raise a `ScrapeValueError`

        This also expects that the schemas assume that omitting required
        in the schema asserts the field is optional, not required. This is
        due to upstream schemas being in JSON Schema v3, and not validictory's
        modified syntax.
        ^ TODO: FIXME
        """
        if schema is None:
            schema = self._schema

        type_checker = Draft3Validator.TYPE_CHECKER.redefine(
            "datetime", lambda c, d: isinstance(d, (datetime.date, datetime.datetime))
        )
        type_checker = type_checker.redefine(
            "date", lambda c, d: (isinstance(d, datetime.date)
                                  and not isinstance(d, datetime.datetime))
        )

        ValidatorCls = jsonschema.validators.extend(Draft3Validator, type_checker=type_checker)
        validator = ValidatorCls(schema, format_checker=FormatChecker())

        errors = [str(error) for error in validator.iter_errors(self.as_dict())]
        if errors:
            raise ScrapeValueError('validation of {} {} failed: {}'.format(
                self.__class__.__name__, self._id, '\n\t'+'\n\t'.join(errors)
            ))

    def pre_save(self, jurisdiction_id):
        pass

    def as_dict(self):
        d = {}
        for attr in self._schema['properties'].keys():
            if hasattr(self, attr):
                d[attr] = getattr(self, attr)
        d['_id'] = self._id
        return d

    # operators

    def __setattr__(self, key, val):
        if key[0] != '_' and key not in self._schema['properties'].keys():
            raise ScrapeValueError('property "{}" not in {} schema'.format(key, self._type))
        super(BaseModel, self).__setattr__(key, val) class SourceMixin(object):
    def __init__(self):
        super(SourceMixin, self).__init__()
        self.sources = []

    def add_source(self, url, *, note=''):
        """ Add a source URL from which data was collected """
        new = {'url': url, 'note': note}
        self.sources.append(new) class LinkMixin(object):
    def __init__(self):
        super(LinkMixin, self).__init__()
        self.links = []

    def add_link(self, url, *, note=''):
        self.links.append({"note": note, "url": url}) class ContactDetailMixin(object):
    def __init__(self):
        super(ContactDetailMixin, self).__init__()
        self.contact_details = []

    def add_contact_detail(self, *, type, value, note=''):
        self.contact_details.append({"type": type, "value": value, "note": note}) class OtherNameMixin(object):
    def __init__(self):
        super(OtherNameMixin, self).__init__()
        self.other_names = []

    def add_name(self, name, *, start_date='', end_date='', note=''):
        other_name = {'name': name}
        if start_date:
            other_name['start_date'] = start_date
        if end_date:
            other_name['end_date'] = end_date
        if note:
            other_name['note'] = note
        self.other_names.append(other_name) class IdentifierMixin(object):
    def __init__(self):
        super(IdentifierMixin, self).__init__()
        self.identifiers = []

    def add_identifier(self, identifier, *, scheme=''):
        self.identifiers.append({"identifier": identifier, "scheme": scheme}) def _make_pseudo_id(**kwargs):
    """ pseudo ids are just JSON """
    # ensure keys are sorted so that these are deterministic
    return '~' + json.dumps(kwargs, sort_keys=True) class ScrapeValueError(PupaError, ValueError):
    """ An invalid value was passed to a pupa scrape object. """
import copy
from .base import (BaseModel, SourceMixin, LinkMixin, ContactDetailMixin, OtherNameMixin,
                   IdentifierMixin)
from .schemas.post import schema as post_schema
from .schemas.person import schema as person_schema
from .schemas.membership import schema as membership_schema
from .schemas.organization import schema as org_schema
from ..utils import _make_pseudo_id
from pupa.exceptions import ScrapeValueError

# a copy of the org schema without sources
org_schema_no_sources = copy.deepcopy(org_schema)
org_schema_no_sources['properties'].pop('sources')


class Post(BaseModel, LinkMixin, ContactDetailMixin):
    """
    A popolo-style Post
    """

    _type = 'post'
    _schema = post_schema

    def __init__(self, *, label, role, organization_id=None, chamber=None,
                 division_id=None, start_date='', end_date='',
                 maximum_memberships=1):
        super(Post, self).__init__()
        self.label = label
        self.role = role
        self.organization_id = pseudo_organization(organization_id, chamber)
        self.division_id = division_id
        self.start_date = start_date
        self.end_date = end_date
        self.maximum_memberships = maximum_memberships

    def __str__(self):
        return self.label


class Membership(BaseModel, ContactDetailMixin, LinkMixin):
    """
    A popolo-style Membership.
    """

    _type = 'membership'
    _schema = membership_schema

    def __init__(self, *, person_id, organization_id, post_id=None, role='', label='',
                 start_date='', end_date='', on_behalf_of_id=None,
                 person_name=''
                 ):
        """
        Constructor for the Membership object.

        We require a person ID and organization ID, as required by the
        popolo spec. Additional arguments may be given, which match those
        defined by popolo.
        """
        super(Membership, self).__init__()
        self.person_id = person_id
        self.person_name = person_name
        self.organization_id = organization_id
        self.post_id = post_id
        self.start_date = start_date
        self.end_date = end_date
        self.role = role
        self.label = label
        self.on_behalf_of_id = on_behalf_of_id

    def __str__(self):
        return self.person_id + ' membership in ' + self.organization_id


class Person(BaseModel, SourceMixin, ContactDetailMixin, LinkMixin, IdentifierMixin,
             OtherNameMixin):
    """
    Details for a Person in Popolo format.
    """

    _type = 'person'

    _schema = person_schema