"""All classes relative to users and their online identities."""
from future import standard_library
standard_library.install_aliases()
from builtins import str
from builtins import object
from datetime import datetime, timedelta
from itertools import chain, permutations
from functools import total_ordering
import urllib.request, urllib.parse, urllib.error
import hashlib
import simplejson as json
from collections import defaultdict
from enum import IntEnum
import logging
from abc import abstractmethod
from future.utils import as_native_str
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy import (
    Boolean,
    Column,
    String,
    ForeignKey,
    Integer,
    UnicodeText,
    DateTime,
    Time,
    Binary,
    inspect,
    event,
    Index,
    func,
    UniqueConstraint
)
from pyramid.httpexceptions import HTTPUnauthorized
from sqlalchemy.orm import (
    relationship, backref, deferred)
from sqlalchemy.orm.attributes import NO_VALUE
from sqlalchemy.sql.functions import count
from pyramid.security import Everyone
from sqla_rdfbridge.mapping import PatternIriClass
from ..lib import config
from ..lib.utils import get_global_base_url
from ..lib.locale import locale_compatible, locale_ancestry
from ..lib.model_watcher import get_model_watcher
from ..lib.sqla import CrudOperation
from ..lib.sqla_types import (
    URLString, EmailString, EmailUnicode, CaseInsensitiveWord, CoerceUnicode)
from . import Base, DiscussionBoundBase, NamedClassMixin, OriginMixin
from ..auth import *
from assembl.lib.raven_client import capture_exception
from ..semantic.namespaces import (
    SIOC, ASSEMBL, QUADNAMES, FOAF, DCTERMS, RDF)
from ..semantic.virtuoso_mapping import (
    QuadMapPatternS, USER_SECTION, PRIVATE_USER_SECTION,
    AppQuadStorageManager)
log = logging.getLogger(__name__)
# None-tolerant min, max
def minN(a, b):
    if a is None:
        return b
    if b is None:
        return a
    return min(a, b)
def maxN(a, b):
    if a is None:
        return b
    if b is None:
        return a
    return max(a, b)
[docs]class AgentProfile(Base):
    """An agent identified on the platform.
    Agents can be :py:class:`User` or simply the author of an
    imported message; they could also be a group, bot or computer.
    Agents have at least one :py:class:`AbstractAgentAccount`.
    """
    __tablename__ = "agent_profile"
    __external_typename = "Agent"
    __table_args__ = (
        Index("agent_profile_name_vidx",
              func.to_tsvector('simple', 'agent_profile.name'),
              postgresql_using='gin'),
    )
    rdf_class = FOAF.Agent
    rdf_sections = (USER_SECTION,)
    # This is very hackish. We need Posts to point to accounts vs users,
    # but right now they do not know the accounts.
    agent_as_account_iri = PatternIriClass(
            QUADNAMES.agent_as_account_iri,
            get_global_base_url() + '/data/Agent/%d', None,
            ('id', Integer, False))
    id = Column(Integer, primary_key=True)
    name = Column(CoerceUnicode(1024),
        info={'rdf': QuadMapPatternS(
            None, FOAF.name, sections = (PRIVATE_USER_SECTION,))})
    description = Column(UnicodeText,
        info={'rdf': QuadMapPatternS(
            None, DCTERMS.description, sections = (PRIVATE_USER_SECTION,))})
    type = Column(String(60))
    __mapper_args__ = {
        'polymorphic_identity': 'agent_profile',
        'polymorphic_on': type,
        'with_polymorphic': '*'
    }
    @as_native_str()
    def __repr__(self):
        r = super(AgentProfile, self).__repr__()
        name = self.name or ""
        return r[:-1] + name + ">"
    def get_preferred_email_account(self):
        if inspect(self).attrs.accounts.loaded_value is NO_VALUE:
            account = self.db.query(AbstractAgentAccount).filter(
                (AbstractAgentAccount.profile_id == self.id)
                & (AbstractAgentAccount.email != None)
                & (AbstractAgentAccount.email != '')).order_by(
                AbstractAgentAccount.verified.desc(),
                AbstractAgentAccount.preferred.desc()).first()
            if account:
                return account
        elif self.accounts:
            accounts = [a for a in self.accounts if a.email]
            accounts.sort(key=lambda e: (not e.verified, not e.preferred))
            if accounts:
                return accounts[0]
    def get_preferred_email(self):
        preferred_account = self.get_preferred_email_account()
        if preferred_account is not None:
            return preferred_account.email
    def real_name(self):
        if not self.name:
            for acc in self.identity_accounts:
                name = acc.real_name()
                if name:
                    self.name = name
                    break
        return self.name
    def display_name(self):
        # TODO: Prefer types?
        if self.name:
            return self.name
        for acc in self.identity_accounts:
            if acc.username:
                return acc.display_name()
        for acc in self.accounts:
            name = acc.display_name()
            if name:
                return name
[docs]    def merge(self, other_profile):
        """Merge another profile on this profile, because they are the same entity.
        This identity is usually found out after an email account is verified,
        or a social account is added to another account.
        All foreign keys that refer to the other agent profile must now refer
        to this one."""
        from .social_auth import SocialAuthAccount
        log.warn("Merging AgentProfiles: %d <= %d" % (self.id, other_profile.id))
        session = self.db
        assert self.id
        assert not (
            isinstance(other_profile, User) and not isinstance(self, User))
        my_accounts = {a.signature(): a for a in self.accounts}
        my_social_emails = {s.email.lower() for s in self.accounts
                            if isinstance(s, SocialAuthAccount) and s.email}
        for other_account in other_profile.accounts[:]:
            my_account = my_accounts.get(other_account.signature())
            if my_account:
                # if chrono order of accounts corresponds to merge priority
                if my_account.prefer_newest_info_on_merge == (
                        my_account.id > other_account.id):
                    # prefer info from my_account
                    my_account.merge(other_account)
                    session.delete(other_account)
                else:
                    other_account.merge(my_account)
                    other_account.profile = self
                    session.delete(my_account)
            elif (isinstance(other_account, EmailAccount) and
                  other_account.email.lower() in my_social_emails):
                pass
            else:
                other_account.profile = self
        if other_profile.name and not self.name:
            self.name = other_profile.name
        for post in other_profile.posts_created[:]:
            post.creator = self
            post.creator_id = self.id
        for extract in other_profile.extracts_attributed[:]:
            extract.attributed_to = self
        from .action import Action
        for action in session.query(Action).filter_by(
            actor_id=other_profile.id).all():
                action.actor = self
                action.actor_id = self.id
        my_status_by_discussion = {
            s.discussion_id: s for s in self.agent_status_in_discussion
        }
        with self.db.no_autoflush:
            for status in other_profile.agent_status_in_discussion[:]:
                if status.discussion_id in my_status_by_discussion:
                    my_status = my_status_by_discussion[status.discussion_id]
                    my_status.user_created_on_this_discussion |= status.\
                        
user_created_on_this_discussion
                    my_status.first_visit = minN(my_status.first_visit,
                                                 status.first_visit)
                    my_status.last_visit = maxN(my_status.last_visit,
                                                status.last_visit)
                    my_status.first_subscribed = minN(
                        my_status.first_subscribed, status.first_subscribed)
                    my_status.last_unsubscribed = minN(
                        my_status.last_unsubscribed, status.last_unsubscribed)
                    status.delete()
                else:
                    status.agent_profile = self 
    def has_permission(self, verb, subject):
        if self is subject.owner:
            return True
        from .permissions import Permission
        return self.db.query(Permission).filter_by(
            actor_id=self.id,
            subject_id=subject.id,
            verb=verb,
            allow=True
        ).one()
    def avatar_url(self, size=32, app_url=None, email=None):
        default = config.get('avatar.default_image_url') or \
            
(app_url and app_url+'/static/img/icon/user.png')
        offline_mode = config.get('offline_mode')
        if offline_mode == "true":
            return default
        acc = self.get_preferred_email_account()
        if acc:
            url = acc.avatar_url(size)
            if url:
                return url
        for acc in self.identity_accounts:
            url = acc.avatar_url(size)
            if url:
                return url
        # Otherwise: Use the gravatar URL
        email = email or self.get_preferred_email()
        if not email:
            return default
        default = config.get('avatar.gravatar_default') or default
        return EmailAccount.avatar_url_for(email, size, default)
    def external_avatar_url(self):
        return "/user/id/%d/avatar/" % (self.id,)
    def get_agent_preload(self, view_def='default'):
        result = self.generic_json(view_def, user_id=self.id)
        return json.dumps(result)
    @classmethod
    def count_posts_in_discussion_all_profiles(cls, discussion):
        from .post import Post
        return dict(discussion.db.query(
            Post.creator_id, count(Post.id)).filter_by(
            discussion_id=discussion.id, hidden=False).group_by(
            Post.creator_id))
    def count_posts_in_discussion(self, discussion_id):
        from .post import Post
        return self.db.query(Post).filter_by(
            creator_id=self.id, discussion_id=discussion_id).count()
[docs]    def count_posts_in_current_discussion(self):
        "CAN ONLY BE CALLED FROM API V2"
        from ..auth.util import get_current_discussion
        discussion = get_current_discussion()
        if discussion is None:
            return None
        return self.count_posts_in_discussion(discussion.id) 
    def get_status_in_discussion(self, discussion_id):
        return self.db.query(AgentStatusInDiscussion).filter_by(
            discussion_id=discussion_id, profile_id=self.id).first()
    @property
    def status_in_current_discussion(self):
        # Use from api v2
        from ..auth.util import get_current_discussion
        discussion = get_current_discussion()
        if discussion:
            return self.get_status_in_discussion(discussion.id)
    def is_visiting_discussion(self, discussion_id):
        from assembl.models.discussion import Discussion
        d = Discussion.get(discussion_id)
        self.update_agent_status_last_visit(d)
[docs]    @classmethod
    def special_quad_patterns(cls, alias_maker, discussion_id):
        return [
            QuadMapPatternS(cls.agent_as_account_iri.apply(cls.id),
                SIOC.account_of, cls.iri_class().apply(cls.id),
                name=QUADNAMES.account_of_self),
            QuadMapPatternS(cls.agent_as_account_iri.apply(cls.id),
                RDF.type, SIOC.UserAccount, name=QUADNAMES.pseudo_account_type)] 
    # True iff the user visits current discussion for the first time
    @property
    def is_first_visit(self):
        status = self.status_in_current_discussion
        if status:
            return status.last_visit == status.first_visit
        return True
    @property
    def last_visit(self):
        status = self.status_in_current_discussion
        if status:
            return status.last_visit
    @property
    def first_visit(self):
        status = self.status_in_current_discussion
        if status:
            return status.first_visit
    @property
    def accepted_tos_version(self):
        status = self.status_in_current_discussion
        if status:
            return status.accepted_tos_version
    @accepted_tos_version.setter
    def accepted_tos_version(self, value):
        status = self.status_in_current_discussion
        assert status
        status.accepted_tos_version = int(value) if value else None
    @property
    def was_created_on_current_discussion(self):
        # Use from api v2
        status = self.status_in_current_discussion
        if status:
            return status.user_created_on_this_discussion
        return False
[docs]    def is_owner(self, user_id):
        return user_id == self.id 
    def get_preferred_locale(self):
        # TODO: per-user preferred locale
        # Want a 2-letter locale string
        # Currently expecting only a scalar value, not a list. Might change
        # In the near future.
        prefs = self.language_preference
        prefs.sort()  # natural order defined on class
        if prefs is None or len(prefs) is 0:
            # Correct way is to get the default from the app global config
            prefs = config.get_config().\
                
get('available_languages', 'fr_CA en_CA').split()[0]
            assert prefs[0]
            return prefs[0]
        return prefs[0].locale
    def successful_social_login(self):
        self.successful_login(True)
[docs]    def successful_login(self, social=False):
        "A successful email login"
        self.last_login = datetime.utcnow()
        if not social:
            self.last_idealoom_login = self.last_login 
    def assembl_login_expiry(self):
        duration = config.get('login_expiry_email', None)
        if duration is None:
            # default to no expiry
            duration = config.get('login_expiry_default', 0)
        if not duration:
            return None
        last_login = self.last_idealoom_login
        if not last_login:
            # Return a date saying it's just expired.
            return datetime.utcnow() - timedelta(1)
        return last_login + timedelta(float(duration))
[docs]    def login_expiry_req(self):
        """Get login expiry date. May be None."""
        from assembl.auth.util import get_current_discussion
        discussion = None
        try:
            # If called from within request
            discussion = get_current_discussion()
        except Exception as e:
            # This is actually called from changes.json, so the request
            # and discussion are inaccessible in that case.
            pass
        return self.login_expiry(discussion) 
[docs]    def login_expiry(self, discussion=None):
        """When will this account's login expire, maybe in the context
        of a specific discussion."""
        accounts = [a for a in self.social_accounts if a.verified]
        autologin = None
        if discussion:
            autologin = discussion.preferences['authorization_server_backend']
        from ..auth.util import user_has_permission
        if autologin and not user_has_permission(
                discussion.id, self.id, P_OVERRIDE_SOCIAL_AUTOLOGIN):
            # the discussion restricts access to this specific
            # social identity provider. The override permission
            # bypasses that, mostly for external moderators.
            autologin_accs_expiry = [
                a.login_expiry() for a in accounts
                if a.provider_with_idp == autologin]
            if len(autologin_accs_expiry):
                if None in autologin_accs_expiry:
                    return None
                return max(autologin_accs_expiry)
            # No social login, treat as already expired
            return datetime.utcnow() - timedelta(1)
        expiries = [a.login_expiry() for a in accounts]
        expiries.append(self.assembl_login_expiry())
        if None in expiries:
            return None
        return max(expiries) 
    def login_expired(self, discussion):
        expiry = self.login_expiry(discussion)
        if expiry is None:
            return False
        return expiry < datetime.utcnow() 
[docs]class AbstractAgentAccount(Base):
    """An abstract class for online accounts that identify AgentsProfiles
    The main subclasses are :py:class:`EmailAccount` and
    :py:class:`.social_auth.SocialAuthAccount`."""
    __tablename__ = "abstract_agent_account"
    __external_typename = "UserAccount"
    rdf_class = SIOC.UserAccount
    rdf_sections = (PRIVATE_USER_SECTION,)
    prefer_newest_info_on_merge = True
    id = Column(Integer, primary_key=True)
    type = Column(String(60))
    profile_id = Column(
        Integer,
        ForeignKey('agent_profile.id', ondelete='CASCADE', onupdate='CASCADE'),
        nullable=False, index=True,
        info={'rdf': QuadMapPatternS(None, SIOC.account_of, sections=(USER_SECTION,))})
    profile = relationship('AgentProfile', backref=backref(
        'accounts', cascade="all, delete-orphan"))
    preferred = Column(Boolean(), default=False, server_default='0')
    verified = Column(Boolean(), default=False, server_default='0')
    # Note some social accounts don't disclose email (eg twitter), so nullable
    # Virtuoso + nullable -> no unique index (sigh)
    # Also, unverified emails are allowed to collide.
    # IMPORTANT: Use email_ci below when appropriate.
    email = Column(EmailString(100))
    # Access to email as a case-insensitive object,
    # for comparison and search purposes.
    @hybrid_property
    def email_ci(self):
        return CaseInsensitiveWord(self.email)
    __table_args__ = (
        Index("ix_public_abstract_agent_account_email_ci", func.lower(email)),)
    # info={'rdf': QuadMapPatternS(None, SIOC.email)}
    # Note: we could also have a FOAF.mbox, but we'd have to make
    # them into URLs with mailto:
    full_name = Column(CoerceUnicode(512))
    def get_default_parent_context(self, request=None, user_id=None):
        return self.profile.get_collection_context(
            'accounts', request=request, user_id=user_id)
[docs]    def container_url(self):
        return "/data/AgentProfile/%d/accounts" % (self.profile_id) 
[docs]    def signature(self):
        "Identity of signature implies identity of underlying account"
        return ('abstract_agent_account', self.id) 
    def merge(self, other):
        pass
[docs]    def is_owner(self, user_id):
        return self.profile_id == user_id 
[docs]    @classmethod
    def restrict_to_owners_condition(cls, query, user_id, alias=None, alias_maker=None):
        if not alias:
            if alias_maker:
                alias = alias_maker.alias_from_class(cls)
            else:
                alias = cls
        return (query, alias.profile_id == user_id) 
    __mapper_args__ = {
        'polymorphic_identity': 'abstract_agent_account',
        'polymorphic_on': type,
        'with_polymorphic': '*'
    }
    crud_permissions = CrudPermissions(
        P_READ, P_SYSADMIN, P_SYSADMIN, P_SYSADMIN,
        P_READ, P_READ, P_READ)
[docs]    @classmethod
    def user_can_cls(cls, user_id, operation, permissions):
        s = super(AbstractAgentAccount, cls).user_can_cls(
            user_id, operation, permissions)
        return MAYBE if s is False else s 
[docs]    def user_can(self, user_id, operation, permissions):
        # bypass for permission-less new users
        if user_id == self.profile_id:
            return True
        return super(AbstractAgentAccount, self).user_can(
            user_id, operation, permissions) 
[docs]    def update_from_json(
            self, json, user_id=None, context=None, object_importer=None,
            permissions=None, parse_def_name='default_reverse'):
        # DO NOT update email... but we still want
        # to allow to set it on create.
        if 'email' in json:
            del json['email']
        return super(AbstractAgentAccount, self).update_from_json(
            json, user_id, context, object_importer, permissions, parse_def_name)  
[docs]class EmailAccount(AbstractAgentAccount):
    """An email account"""
    __mapper_args__ = {
        'polymorphic_identity': 'agent_email_account',
    }
    profile_e = relationship(AgentProfile, backref=backref('email_accounts'))
    def display_name(self):
        if self.verified:
            return self.email
[docs]    def signature(self):
        return ('agent_email_account',
                self.email.lower() if self.email else None) 
[docs]    def merge(self, other):
        """Merge another EmailAccount on this one, because they are the same email."""
        log.warn("Merging EmailAccounts: %d, %d" % (self.id, other.id))
        if other.verified:
            self.verified = True 
    def other_account(self):
        if not self.verified:
            return self.db.query(self.__class__).filter_by(
                email_ci=self.email_ci, verified=True).first()
    def avatar_url(self, size=32, default=None):
        return self.avatar_url_for(self.email, size, default)
[docs]    def unique_query(self):
        query, _ = super(EmailAccount, self).unique_query()
        return query.filter_by(
            type=self.type, email_ci=self.email_ci, verified=True), self.verified 
    @staticmethod
    def avatar_url_for(email, size=32, default=None):
        args = {'s': str(size)}
        if default:
            args['d'] = default
        return "//www.gravatar.com/avatar/%s?%s" % (
            hashlib.md5(email.lower().encode('utf-8')).hexdigest(), urllib.parse.urlencode(args))
    @staticmethod
    def get_or_make_profile(session, email, name=None):
        emails = list(session.query(EmailAccount).filter_by(
            email_ci=email).all())
        # We do not want unverified user emails
        # This is costly. I should have proper boolean markers
        emails = [e for e in emails if e.verified or not isinstance(e.profile, User)]
        user_emails = [e for e in emails if isinstance(e.profile, User)]
        if user_emails:
            assert len(user_emails) == 1
            return user_emails[0]
        elif emails:
            # should also be 1 but less confident.
            return emails[0]
        else:
            profile = AgentProfile(name=name)
            emailAccount = EmailAccount(email=email, profile=profile)
            session.add(emailAccount)
            return emailAccount 
[docs]class IdentityProvider(Base):
    """An identity provider (or sometimes a category of identity providers.)
    This is a service that provides online identities, expressed as
    :py:class:`.social_auth.SocialAuthAccount`."""
    __tablename__ = "identity_provider"
    __external_typename = "Usergroup"
    rdf_class = SIOC.Usergroup
    rdf_sections = (PRIVATE_USER_SECTION,)
    id = Column(Integer, primary_key=True)
    provider_type = Column(String(32), nullable=False)
    name = Column(String(60), nullable=False,
        info={'rdf': QuadMapPatternS(None, SIOC.name)})
    # TODO: More complicated model, where trust also depends on realm.
    trust_emails = Column(Boolean, default=True)
    @classmethod
    def get_by_type(cls, provider_type, create=True):
        db = cls.default_db()
        provider = db.query(cls).filter_by(
            provider_type=provider_type).first()
        if create and not provider:
            # TODO: Better heuristic for name
            name = provider_type.split("-")[0]
            provider = cls(
                provider_type=provider_type, name=name)
            db.add(provider)
            db.flush()
        return provider
    @classmethod
    def populate_db(cls, db=None):
        db = db or cls.default_db()
        providers = config.get("login_providers") or []
        trusted_providers = config.get("trusted_login_providers") or []
        if not isinstance(providers, list):
            providers = providers.split()
        if not isinstance(trusted_providers, list):
            trusted_providers = trusted_providers.split()
        db.execute("lock table %s in exclusive mode" % cls.__table__.name)
        db_providers = db.query(cls).all()
        db_providers_by_type = {
            p.provider_type: p for p in db_providers}
        for provider in providers:
            db_provider = db_providers_by_type.get(provider, None)
            if db_provider is None:
                db.add(cls(
                    name=provider, provider_type=provider,
                    trust_emails=(provider in trusted_providers)))
            else:
                db_provider.trust_emails = (provider in trusted_providers) 
[docs]class AgentStatusInDiscussion(DiscussionBoundBase):
    """Information about a user's activity in a discussion
    Whether the user has logged in and is subscribed to notifications."""
    __tablename__ = 'agent_status_in_discussion'
    __table_args__ = (
        UniqueConstraint('discussion_id', 'profile_id'), )
    id = Column(Integer, primary_key=True)
    discussion_id = Column(Integer, ForeignKey(
            "discussion.id", ondelete='CASCADE', onupdate='CASCADE'),
        nullable=False, index=True)
    discussion = relationship(
        "Discussion", backref=backref(
            "agent_status_in_discussion", cascade="all, delete-orphan"))
    profile_id = Column(Integer, ForeignKey(
            "agent_profile.id", ondelete='CASCADE', onupdate='CASCADE'),
        nullable=False, index=True)
    agent_profile = relationship(
        AgentProfile, backref=backref(
            "agent_status_in_discussion", cascade="all, delete-orphan"))
    first_visit = Column(DateTime)
    last_visit = Column(DateTime)
    first_subscribed = Column(DateTime)
    last_unsubscribed = Column(DateTime)
    user_created_on_this_discussion = Column(Boolean, server_default='0')
    last_connected = Column(DateTime)
    last_disconnected = Column(DateTime)
    accepted_tos_version = Column(Integer)
[docs]    def get_discussion_id(self):
        return self.discussion_id or self.discussion.id 
[docs]    @classmethod
    def get_discussion_conditions(cls, discussion_id, alias_maker=None):
        return (cls.discussion_id == discussion_id,) 
[docs]    def is_owner(self, user_id):
        return user_id == self.profile_id 
    crud_permissions = CrudPermissions(
        P_READ, P_ADMIN_DISC, P_ADMIN_DISC, P_ADMIN_DISC,
        P_READ, P_READ, P_READ) 
@event.listens_for(AgentStatusInDiscussion, 'after_insert', propagate=True)
def send_user_to_socket_for_asid(mapper, connection, target):
    agent_profile = target.agent_profile
    if not target.agent_profile:
        agent_profile = AgentProfile.get(target.profile_id)
    agent_profile.send_to_changes(
        connection, CrudOperation.UPDATE, target.discussion_id)
[docs]class User(NamedClassMixin, OriginMixin, AgentProfile):
    """
    A user of the platform.
    """
    __tablename__ = "user"
    __mapper_args__ = {
        'polymorphic_identity': 'user'
    }
    id = Column(
        Integer,
        ForeignKey('agent_profile.id', ondelete='CASCADE', onupdate='CASCADE'),
        primary_key=True
    )
    preferred_email = Column(EmailUnicode(100))
    #    info={'rdf': QuadMapPatternS(None, FOAF.mbox)})
    verified = Column(Boolean(), default=False)
    password = deferred(Column(Binary(115)))
    timezone = Column(Time(True))
    last_login = Column(DateTime)
    last_idealoom_login = Column(DateTime)
    login_failures = Column(Integer, default=0)
    username = Column(CoerceUnicode(20), unique=True)
    social_accounts = relationship('SocialAuthAccount')
    def __init__(self, **kwargs):
        if kwargs.get('password', None) is not None:
            from ..auth.password import hash_password
            kwargs['password'] = hash_password(kwargs['password'])
        super(User, self).__init__(**kwargs)
    def get_default_parent_context(self, request=None, user_id=None):
        from pyramid.threadlocal import get_current_request
        from ..auth.util import discussion_from_request
        if not request:
            request = get_current_request()
        if request:
            d = discussion_from_request(request)
            if d:
                return d.get_collection_context(
                    'all_users', request=request, user_id=user_id)
        return super(User, self).get_default_parent_context(
            request, user_id=user_id)
    @property
    def real_name_p(self):
        return self.real_name()
    @classmethod
    def get_naming_column_name(cls):
        return "username"
    @classmethod
    def getByName(cls, name, session=None, query=None, parent_object=None):
        if name == 'current':
            from ..auth.util import get_current_user_id
            user_id = get_current_user_id()
            if not user_id:
                return None
            return User.get_instance(user_id)
    @real_name_p.setter
    def real_name_p(self, name):
        if name:
            name = name.strip()
        if not name:
            return
        elif len(name) < 3:
            if not self.name or len(self.name) < len(name):
                self.name = name
        else:
            self.name = name
    @property
    def password_p(self):
        return ""
    @password_p.setter
    def password_p(self, password):
        from ..auth.password import hash_password
        if password:
            self.password = hash_password(password)
    def check_password(self, password):
        if self.password:
            from ..auth.password import verify_password
            return verify_password(password, self.password)
        return False
    def get_preferred_email(self):
        if self.preferred_email:
            return self.preferred_email
        return super(User, self).get_preferred_email()
[docs]    def merge(self, other_user):
        """Merge another user on this one, because they are the same entity.
        This identity is usually found out after an email account is verified,
        or a social account is added to another account.
        All foreign keys that refer to the other user must now refer
        to this one."""
        log.warn("Merging Users: %d <= %d" % (self.id, other_user.id))
        super(User, self).merge(other_user)
        if isinstance(other_user, User):
            session = self.db
            if other_user.preferred_email and not self.preferred_email:
                self.preferred_email = other_user.preferred_email
            if other_user.last_login:
                if self.last_login:
                    self.last_login = max(
                        self.last_login, other_user.last_login)
                else:
                    self.last_login = other_user.last_login
            self.creation_date = min(
                self.creation_date, other_user.creation_date)
            if other_user.password and not self.password:
                # NOTE: The user may be confused by the implicit change of
                # password when we destroy the second account.
                # Use most recent login
                if other_user.last_login and (
                        (not self.last_login)
                        or (other_user.last_login > self.last_login)):
                    self.password = other_user.password
            for extract in other_user.extracts_created[:]:
                extract.creator = self
            for idea_content_link in other_user.idealinks_created[:]:
                idea_content_link.creator = self
            for attachment in other_user.attachments[:]:
                attachment.creator = self
            for role in other_user.roles[:]:
                role.user = self
            for role in other_user.local_roles[:]:
                role.user = self
            for post in other_user.posts_moderated[:]:
                post.moderator = self
                post.moderator_id = self.id
            for announcement in other_user.announcements_created[:]:
                announcement.creator = self
            for announcement in other_user.announcements_updated[:]:
                announcement.last_updated_by = self
            if other_user.username and not self.username:
                self.username = other_user.username
                other_user.username = None
            my_lang_pref_signatures = {
                (lp.locale, lp.source_of_evidence)
                for lp in self.language_preference
            }
            for lang_pref in other_user.language_preference:
                # TODO: there's been a case here resulting in
                # two cookie instances
                if ((lang_pref.locale, lang_pref.source_of_evidence) in
                        my_lang_pref_signatures):
                    # First rough implementation: One has priority.
                    # There is no internal merging that makes sense,
                    # except maybe reordering (punted)
                    lang_pref.delete()
                else:
                    lang_pref.user_id = self.id
                    # TODO: Ensure consistent order value.
            old_autoflush = session.autoflush
            session.autoflush = False
            for notification_subscription in \
                    
other_user.notification_subscriptions[:]:
                notification_subscription.user = self
                notification_subscription.user_id = self.id
                if notification_subscription.find_duplicate(False) is not None:
                    self.db.delete(notification_subscription)
            session.autoflush = old_autoflush 
    def send_email(self, **kwargs):
        subject = kwargs.get('subject', '')
        body = kwargs.get('body', '')
        # Send email.
    def avatar_url(self, size=32, app_url=None, email=None):
        return super(User, self).avatar_url(
            size, app_url, email or self.preferred_email)
    def display_name(self):
        if self.name:
            return self.name
        if self.username:
            return self.username
        return super(User, self).display_name()
    @property
    def permissions_for_current_discussion(self):
        from .discussion import Discussion
        from pyramid.threadlocal import get_current_request
        request = get_current_request()
        discussion_id = request.discussion_id
        if discussion_id:
            return {Discussion.uri_generic(discussion_id):
                    request.permissions}
        return self.get_all_permissions()
    def get_permissions(self, discussion_id):
        from ..auth.util import get_permissions
        return get_permissions(self.id, discussion_id)
    def get_all_permissions(self):
        from ..auth.util import get_permissions
        from .discussion import Discussion
        permissions = {
            Discussion.uri_generic(d_id): get_permissions(self.id, d_id)
            for (d_id,) in self.db.query(Discussion.id)}
        return permissions
[docs]    def send_to_changes(self, connection=None, operation=CrudOperation.UPDATE,
                        discussion_id=None, view_def="changes"):
        """invoke the modelWatcher on creation/modification"""
        super(User, self).send_to_changes(
            connection, operation, discussion_id, view_def)
        watcher = get_model_watcher()
        if operation == CrudOperation.UPDATE:
            watcher.processAccountModified(self.id)
        elif operation == CrudOperation.CREATE:
            watcher.processAccountCreated(self.id) 
    def has_role_in(self, discussion_id, role):
        from .permissions import Role, LocalUserRole
        return self.db.query(LocalUserRole).join(Role).filter(
            LocalUserRole.profile_id == self.id,
            Role.name == role,
            LocalUserRole.requested == False,  # noqa: E712
            LocalUserRole.discussion_id == discussion_id).first()
    def is_participant(self, discussion_id):
        return self.has_role_in(discussion_id, R_PARTICIPANT)
    def create_agent_status_in_discussion(self, discussion):
        s = self.get_status_in_discussion(discussion.id)
        if s:
            return s
        s = AgentStatusInDiscussion(
            agent_profile=self,
            discussion=discussion)
        self.db.add(s)
        return s
    def update_agent_status_last_visit(self, discussion, status=None):
        agent_status = status or self.create_agent_status_in_discussion(discussion)
        _now = datetime.utcnow()
        agent_status.last_visit = _now
        if not agent_status.first_visit:
            agent_status.first_visit = _now
    def update_agent_status_subscribe(self, discussion):
        # Set the AgentStatusInDiscussion
        agent_status = self.create_agent_status_in_discussion(discussion)
        if not agent_status.first_subscribed:
            _now = datetime.utcnow()
            agent_status.first_subscribed = _now
    def update_agent_status_unsubscribe(self, discussion):
        agent_status = self.create_agent_status_in_discussion(discussion)
        _now = datetime.utcnow()
        agent_status.last_unsubscribed = _now
    def subscribe(self, discussion, role=R_PARTICIPANT):
        from .permissions import Role, LocalUserRole
        if not self.has_role_in(discussion.id, role):
            role = self.db.query(Role).filter_by(name=role).one()
            self.db.add(LocalUserRole(
                user=self, role=role, discussion=discussion))
        # Set the AgentStatusInDiscussion
        self.update_agent_status_subscribe(discussion)
    def unsubscribe(self, discussion, role=R_PARTICIPANT):
        lur = self.has_role_in(discussion.id, role)
        if lur:
            self.db.delete(lur)
        # Set the AgentStatusInDiscussion
        self.update_agent_status_unsubscribe(discussion)
    @classmethod
    def extra_collections(cls):
        from assembl.views.traversal import (
            RelationCollectionDefinition, AbstractCollectionDefinition,
            UserNSBoundDictContext)
        from .discussion import Discussion
        from .user_key_values import UserPreferenceCollection
        from .permissions import UserTemplate
        class NotificationSubscriptionCollection(RelationCollectionDefinition):
            def __init__(self, cls):
                super(NotificationSubscriptionCollection, self).__init__(
                    cls, User.notification_subscriptions.property)
            def decorate_query(self, query, owner_alias, last_alias, parent_instance, ctx):
                query = super(
                    NotificationSubscriptionCollection, self).decorate_query(
                    query, owner_alias, last_alias, parent_instance, ctx)
                discussion = ctx.get_instance_of_class(Discussion)
                if discussion is not None:
                    # Materialize active subscriptions... TODO: Make this batch,
                    # also dematerialize
                    if isinstance(parent_instance, UserTemplate):
                        parent_instance.get_notification_subscriptions()
                    else:
                        parent_instance.get_notification_subscriptions(
                            discussion.id, request=ctx.get_request())
                    query = query.filter(last_alias.discussion_id == discussion.id)
                return query
            def contains(self, parent_instance, instance):
                if not super(NotificationSubscriptionCollection, self).contains(
                        parent_instance, instance):
                    return False
                # Don't I need the context to get the discussion? Rats!
                return True
            def get_default_view(self):
                return "extended"
        class LocalRoleCollection(RelationCollectionDefinition):
            def __init__(self, cls):
                super(LocalRoleCollection, self).__init__(
                    cls, User.local_roles.property)
            def decorate_query(self, query, owner_alias, last_alias, parent_instance, ctx):
                query = super(
                    LocalRoleCollection, self).decorate_query(
                    query, owner_alias, last_alias, parent_instance, ctx)
                discussion = ctx.get_instance_of_class(Discussion)
                if discussion is not None:
                    query = query.filter(last_alias.discussion_id == discussion.id)
                return query
            def contains(self, parent_instance, instance):
                if not super(LocalRoleCollection, self).contains(
                        parent_instance, instance):
                    return False
                # Don't I need the context to get the discussion? Rats!
                return True
            def get_default_view(self):
                return "default"
        class PreferencePseudoCollection(AbstractCollectionDefinition):
            def __init__(self):
                super(PreferencePseudoCollection, self).__init__(
                    cls, 'preferences', UserPreferenceCollection)
            def decorate_query(
                    self, query, owner_alias, coll_alias, parent_instance,
                    ctx):
                log.error("This should not happen")
            def contains(self, parent_instance, instance):
                log.error("This should not happen")
            def make_context(self, parent_ctx):
                from ..auth.util import (
                    get_current_user_id, user_has_permission)
                user_id = parent_ctx._instance.id
                discussion = None
                discussion_id = parent_ctx.get_discussion_id()
                current_user_id = get_current_user_id()
                if user_id != current_user_id and not user_has_permission(
                        discussion_id, current_user_id, P_SYSADMIN):
                    raise HTTPUnauthorized()
                if discussion_id:
                    discussion = Discussion.get(discussion_id)
                coll = UserPreferenceCollection(user_id, discussion)
                return UserNSBoundDictContext(coll, parent_ctx)
        return (NotificationSubscriptionCollection(cls),
                LocalRoleCollection(cls),
                PreferencePseudoCollection())
[docs]    def get_notification_subscriptions_for_current_discussion(self):
        "CAN ONLY BE CALLED WITH A CURRENT REQUEST"
        from pyramid.threadlocal import get_current_request
        request = get_current_request()
        discussion = request.discussion
        if discussion is None:
            return []
        return self.get_notification_subscriptions(discussion.id, request=request) 
    def get_preferences_for_discussion(self, discussion):
        from .user_key_values import UserPreferenceCollection
        return UserPreferenceCollection(self.id, discussion)
    def get_preferences_for_current_discussion(self):
        from ..auth.util import get_current_discussion
        discussion = get_current_discussion()
        if discussion:
            return self.get_preferences_for_discussion(discussion)
[docs]    def get_notification_subscriptions(
            self, discussion_id, reset_defaults=False, request=None, on_thread=True):
        """the notification subscriptions for this user and discussion.
        Includes materialized subscriptions from the template."""
        from .notification import (
            NotificationSubscription, NotificationSubscriptionStatus, NotificationCreationOrigin,
            NotificationSubscriptionGlobal)
        from .discussion import Discussion
        from ..auth.util import get_roles
        from .permissions import UserTemplate
        my_subscriptions = self.db.query(NotificationSubscription).filter_by(
            discussion_id=discussion_id, user_id=self.id).all()
        by_class = defaultdict(list)
        for sub in my_subscriptions:
            by_class[sub.__class__].append(sub)
        my_subscriptions_classes = set(by_class.keys())
        needed_classes = UserTemplate.get_applicable_notification_subscriptions_classes()
        missing = set(needed_classes) - my_subscriptions_classes
        changed = False
        for cl, subs in by_class.items():
            if issubclass(cl, NotificationSubscriptionGlobal):
                if len(subs) > 1:
                    # This may not actually be an error, in the case of non-global.
                    log.error("There were many subscriptions of class %s" % (cl))
                    subs.sort(key=lambda sub: sub.id)
                    first_sub = subs[0]
                    for sub in subs[1:]:
                        first_sub.merge(sub)
                        sub.delete()
                        changed = True
            else:
                # Is this needed? Looking for mergeable subscriptions in non-global
                # This code will not be active for some time anyway.
                local_changed = True
                while local_changed:
                    local_changed = False
                    for a, b in permutations(subs, 2):
                        if a.id > b.id:
                            continue  # break symmetry, merge newer on older
                        if a.can_merge(b):
                            a.merge(b)
                            b.delete()
                            local_changed = True
                            changed = True
                            subs.remove(b)
                            break  # inner, re-permute w/o b
        if changed:
            self.db.flush()
            my_subscriptions = list(chain(*list(by_class.items())))
        if (not missing) and not reset_defaults:
            return my_subscriptions
        discussion = Discussion.get(discussion_id)
        assert discussion
        if request is None:
            my_roles = get_roles(self.id, discussion_id)
        else:
            my_roles = request.roles
        subscribed = defaultdict(bool)
        for role in my_roles:
            template, changed = discussion.get_user_template(
                role, role == R_PARTICIPANT, on_thread)
            if template is None:
                continue
            template_subscriptions = template.get_notification_subscriptions()
            for subscription in template_subscriptions:
                subscribed[subscription.__class__] |= subscription.status == NotificationSubscriptionStatus.ACTIVE
        if reset_defaults:
            for sub in my_subscriptions[:]:
                if (sub.creation_origin ==
                        NotificationCreationOrigin.DISCUSSION_DEFAULT
                        # only actual defaults
                        and sub.__class__ in subscribed):
                    if (sub.status == NotificationSubscriptionStatus.ACTIVE
                            and not subscribed[sub.__class__]):
                        sub.status = NotificationSubscriptionStatus.INACTIVE_DFT
                    elif (sub.status == NotificationSubscriptionStatus.INACTIVE_DFT
                            and subscribed[sub.__class__]):
                        sub.status = NotificationSubscriptionStatus.ACTIVE
        def create_missing(include_inactive=False):
            my_sub_types = self.db.query(NotificationSubscription.type).filter_by(
                discussion_id=discussion_id, user_id=self.id).distinct().all()
            my_sub_types = {x for (x,) in my_sub_types}
            discussion_kwarg = (dict(discussion_id=discussion.id)
                if discussion.id else dict(discussion=discussion))
            return [
                cls(
                    user_id=self.id,
                    creation_origin=NotificationCreationOrigin.DISCUSSION_DEFAULT,
                    status=(NotificationSubscriptionStatus.ACTIVE if subscribed[cls]
                            else NotificationSubscriptionStatus.INACTIVE_DFT),
                    **discussion_kwarg
                )
                for cls in needed_classes
                if (include_inactive or subscribed[cls]) and
                cls.__mapper_args__['polymorphic_identity'] not in my_sub_types]
        if on_thread:
            if self.locked_object_creation(create_missing, NotificationSubscription, 10):
                # if changes, recalculate my_subscriptions
                my_subscriptions = self.db.query(NotificationSubscription).filter_by(
                    discussion_id=discussion_id, user_id=self.id).all()
        else:
            for ob in create_missing():
                self.db.add(ob)
                my_subscriptions.append(ob)
        # Now calculate the dematerialized ones (always out-of-thread)
        defaults = create_missing(True)
        return chain(my_subscriptions, defaults) 
[docs]    def user_can(self, user_id, operation, permissions):
        # bypass for permission-less new users
        if user_id == self.id:
            return True
        return super(User, self).user_can(user_id, operation, permissions)  
User.creation_date.info['rdf'] = QuadMapPatternS(
    None, DCTERMS.created, sections=(PRIVATE_USER_SECTION,))
[docs]class AnonymousUser(DiscussionBoundBase, User):
    "A fake anonymous user bound to a source."
    __tablename__ = "anonymous_user"
    __mapper_args__ = {
        'polymorphic_identity': 'anonymous_user'
    }
    id = Column(
        Integer,
        ForeignKey('user.id', ondelete='CASCADE', onupdate='CASCADE'),
        primary_key=True
    )
    source_id = Column(Integer, ForeignKey(
        "content_source.id",
        ondelete='CASCADE', onupdate='CASCADE'),
        nullable=False, unique=True)
    source = relationship(
        "ContentSource", backref=backref(
            "anonymous_user", cascade="all, delete-orphan",
            uselist=False))
    def __init__(self, **kwargs):
        kwargs['verified'] = True
        kwargs['name'] = "anonymous"
        super(AnonymousUser, self).__init__(**kwargs)
    # Create an index for (discussion, role)?
[docs]    def get_discussion_id(self):
        source = self.source or ContentSource.get(self.source_id)
        return source.discussion_id 
[docs]    @classmethod
    def get_discussion_conditions(cls, discussion_id, alias_maker=None):
        from .generic import ContentSource
        if alias_maker is None:
            anonymous_user = cls
            source = ContentSource
        else:
            anonymous_user = alias_maker.alias_from_class(cls)
            source = alias_maker.alias_from_relns(anonymous_user.source)
        return (anonymous_user.source_id == source.id,
                source.discussion_id == discussion_id)  
[docs]class PartnerOrganization(DiscussionBoundBase):
    """A corporate entity that we want to display in the discussion's page"""
    __tablename__ = "partner_organization"
    id = Column(Integer, primary_key=True,
        info={'rdf': QuadMapPatternS(None, ASSEMBL.db_id)})
    discussion_id = Column(Integer, ForeignKey(
        "discussion.id", ondelete='CASCADE'), nullable=False, index=True,
        info={'rdf': QuadMapPatternS(None, DCTERMS.contributor)})
    discussion = relationship(
        'Discussion', backref=backref(
            'partner_organizations', cascade="all, delete-orphan"),
        info={'rdf': QuadMapPatternS(None, ASSEMBL.in_conversation)})
    name = Column(CoerceUnicode(256),
        info={'rdf': QuadMapPatternS(None, FOAF.name)})
    description = Column(UnicodeText,
        info={'rdf': QuadMapPatternS(None, DCTERMS.description)})
    logo = Column(URLString(),
        info={'rdf': QuadMapPatternS(None, FOAF.logo)})
    homepage = Column(URLString(),
        info={'rdf': QuadMapPatternS(None, FOAF.homepage)})
    is_initiator = Column(Boolean)
[docs]    def populate_from_context(self, context):
        if not(self.discussion or self.discussion_id):
            from .discussion import Discussion
            self.discussion = context.get_instance_of_class(Discussion)
        super(PartnerOrganization, self).populate_from_context(context) 
[docs]    def unique_query(self):
        query, _ = super(PartnerOrganization, self).unique_query()
        return query.filter_by(name=self.name), True 
[docs]    def get_discussion_id(self):
        return self.discussion_id or self.discussion.id 
[docs]    @classmethod
    def get_discussion_conditions(cls, discussion_id, alias_maker=None):
        return (cls.discussion_id == discussion_id,) 
    crud_permissions = CrudPermissions(P_ADMIN_DISC) 
[docs]class LanguagePreferenceOrder(IntEnum):
    Explicit = 0
    Cookie = 1
    Parameter = 2
    DeducedFromTranslation = 3
    OS_Default = 4
    Discussion = 5 
LanguagePreferenceOrder.unique_prefs = (
    LanguagePreferenceOrder.Cookie,
    LanguagePreferenceOrder.Parameter,
    LanguagePreferenceOrder.OS_Default)
[docs]class LanguagePreferenceCollection(object):
    """A collection of :py:class:`UserLanguagePreference`,
    allowing to decide on which languages to display."""
    @abstractmethod
    def find_locale(self, locale):
        pass
    @classmethod
    def getCurrent(cls, req=None):
        from pyramid.threadlocal import get_current_request
        # Very very hackish, but this call is costly and frequent.
        # Let's cache it in the request. Useful for view_def use.
        if req is None:
            req = get_current_request()
        assert req
        if getattr(req, "lang_prefs", 0) is 0:
            user_id = req.authenticated_userid
            if user_id and user_id != Everyone:
                try:
                    req.lang_prefs = UserLanguagePreferenceCollection(user_id)
                    return req.lang_prefs
                except Exception:
                    capture_exception()
            # use my locale negotiator
            locale = req.locale_name
            req.lang_prefs = LanguagePreferenceCollectionWithDefault(locale)
        return req.lang_prefs
    @abstractmethod
    def default_locale_code(self):
        pass
    @abstractmethod
    def known_languages(self):
        return [] 
[docs]class LanguagePreferenceCollectionWithDefault(LanguagePreferenceCollection):
    """A LanguagePreferenceCollection with a fallback language."""
    def __init__(self, locale_code):
        self.default_locale = locale_code
    def default_locale_code(self):
        return self.default_locale
    def find_locale(self, locale):
        if locale_compatible(locale, self.default_locale):
            return UserLanguagePreference(
                locale=self.default_locale,
                source_of_evidence=LanguagePreferenceOrder.Cookie.value)
        else:
            return UserLanguagePreference(
                locale=locale,
                translate=self.default_locale,
                source_of_evidence=LanguagePreferenceOrder.Cookie.value)
    def known_languages(self):
        return [self.default_locale] 
[docs]class UserLanguagePreferenceCollection(LanguagePreferenceCollection):
    """A LanguagePreferenceCollection that represent one user's preferences."""
    def __init__(self, user_id):
        user = User.get(user_id)
        user_prefs = user.language_preference
        assert user_prefs
        user_prefs.sort(reverse=True)
        prefs_by_locale = {
            user_pref.locale: user_pref
            for user_pref in user_prefs
        }
        user_prefs.reverse()
        prefs_with_trans = [up for up in user_prefs if up.translate]
        prefs_without_trans = [
            up for up in user_prefs if not up.translate]
        prefs_without_trans_by_loc = {
            up.locale: up for up in prefs_without_trans}
        # First look for translation targets
        for (loc, pref) in list(prefs_by_locale.items()):
            for n, l in enumerate(locale_ancestry(loc)):
                if n == 0:
                    continue
                if l in prefs_by_locale:
                    break
                prefs_by_locale[l] = pref
        for pref in prefs_with_trans:
            for l in locale_ancestry(pref.translate):
                if l in prefs_without_trans_by_loc:
                    break
                locale = l
                new_pref = UserLanguagePreference(
                    locale=locale,
                    source_of_evidence=
                        LanguagePreferenceOrder.DeducedFromTranslation.value,
                    preferred_order=pref.preferred_order)
                prefs_without_trans.append(new_pref)
                prefs_without_trans_by_loc[l] = new_pref
                if l not in prefs_by_locale:
                    prefs_by_locale[l] = new_pref
        default_pref = None
        if prefs_with_trans:
            prefs_with_trans.sort()
            target_lang_code = prefs_with_trans[0].translate
            default_pref = prefs_without_trans_by_loc.get(
                target_lang_code, None)
        if not default_pref:
            # using the untranslated locales, if any.
            prefs_without_trans.sort()
            # TODO: Or use discussion locales otherwise?
            # As it stands, the cookie is the fallback.
            default_pref = (
                prefs_without_trans[0] if prefs_without_trans else None)
        self.user_prefs = prefs_by_locale
        self.default_pref = default_pref
    def default_locale_code(self):
        return self.default_pref.locale
    def find_locale(self, locale, db=None):
        # This code needs to mirror
        # LanguagePreferenceCollection.getPreferenceForLocale
        for locale in locale_ancestry(locale):
            if locale in self.user_prefs:
                return self.user_prefs[locale]
        if self.default_pref is None:
            # this should never actually happen
            return None
        return UserLanguagePreference(
            locale=locale,
            translate=self.default_pref.locale,
            source_of_evidence=self.default_pref.source_of_evidence,
            user=None)  # Do not give the user or this gets added to session
    def known_languages(self):
        return list({pref.translate or pref.locale
                     for pref in self.user_prefs.values()}) 
[docs]@total_ordering
class UserLanguagePreference(Base):
    """Does this user wants data in this language to be displayed or translated?"""
    __tablename__ = 'user_language_preference'
    __table_args__ = (UniqueConstraint(
        'user_id', 'locale', 'source_of_evidence'), )
    id = Column(Integer, primary_key=True)
    user_id = Column(
        Integer, ForeignKey(
            User.id, ondelete='CASCADE', onupdate='CASCADE'),
        nullable=False, index=True)
    locale = Column(String(11), index=True)
    translate = Column(String(11))
    # Sort the preferences within a source_of_evidence
    # Descending order preference, 0 - is the highest
    preferred_order = Column(Integer, nullable=False, default=0)
    # This is the actual evidence source, whose contract is defined in
    # LanguagePreferenceOrder. They have priority over preferred_order
    source_of_evidence = Column(Integer, nullable=False)
    user = relationship('User', backref=backref(
                        'language_preference',
                        cascade='all, delete-orphan',
                        order_by=source_of_evidence))
    crud_permissions = CrudPermissions(
            P_READ, P_SYSADMIN, P_SYSADMIN, P_SYSADMIN,
            P_READ, P_READ, P_READ)
[docs]    def is_owner(self, user_id):
        return user_id == self.user_id 
    def __eq__(self, other):
        return self is other
    def __ne__(self, other):
        return self is not other
    def __lt__(self, other):
        if not isinstance(other, self.__class__):
            return True
        if self.source_of_evidence != other.source_of_evidence:
            return self.source_of_evidence < other.source_of_evidence
        if (self.preferred_order or 0) != (other.preferred_order or 0):
            return (self.preferred_order or 0) < (other.preferred_order or 0)
        if (self.id or 0) != (other.id or 0):
            return (self.id or 0) < (other.id or 0)
        return id(self) < id(other)
    def __hash__(self):
        if self.id:
            return hash(self.id)
        return hash(self.user_id) ^ hash(self.source_of_evidence) ^ hash(self.locale)
    # def set_priority_order(self, code):
    #     # code can be ignored. This value should be updated for each user
    #     # as each preferred language is committed
    #     current_languages = self.db.query(UserLanguagePreference).\
    #                         filter_by(user=self.user).\
    #                         order_by(self.preferred_order).all()
    #     if self.source_of_evidence == 0:
    #         pass
[docs]    def unique_query(self):
        query, _ = super(UserLanguagePreference, self).unique_query()
        query = query.filter_by(
            user_id=self.user_id or self.user.id,
            locale=self.locale,
            source_of_evidence=self.source_of_evidence)
        return query, True 
    @as_native_str()
    def __repr__(self):
        return \
            
"{user_id: %d, locale: %s, translated_to: %s "\
            
"source_of_evidence: %s, preferred_order: %d}" % (
                self.user_id or -1,
                self.locale, self.translate,
                LanguagePreferenceOrder(self.source_of_evidence).name,
                self.preferred_order or 0
            )