# -*- coding: utf-8 -*-
"""This module contains the data model of the application."""

#if $identity != 'none'
# symbols which are imported by "from ${package}.model import *"
__all__ = ['Group', 'Permission', 'User', 'Visit', 'VisitIdentity']

from datetime import datetime
#end if

import pkg_resources
#if $sqlobject == 'True'
pkg_resources.require("$sqlobjectversion")
#elif $sqlalchemy == 'True'
pkg_resources.require("$sqlalchemyversion")
#if $elixir == 'True'
pkg_resources.require("$elixirversion")
#end if
#end if

#if $sqlobject == 'True'
from turbogears.database import PackageHub
# import some basic SQLObject classes for declaring the data model
# (see http://www.sqlobject.org/SQLObject.html#declaring-the-class)
from sqlobject import SQLObject, SQLObjectNotFound, RelatedJoin
from sqlobject.inheritance import InheritableSQLObject
# import some datatypes for table columns from SQLObject
# (see http://www.sqlobject.org/SQLObject.html#column-types for more)
from sqlobject import StringCol, UnicodeCol, IntCol, DateTimeCol
#end if
#if $sqlalchemy == 'True'
#if $elixir == 'True'
# import the basic Elixir classes and functions for declaring the data model
# (see http://elixir.ematia.de/trac/wiki/TutorialDivingIn)
from elixir import Entity, Field, OneToMany, ManyToOne, ManyToMany
from elixir import options_defaults, using_options, setup_all
# import some datatypes for table columns from Elixir
# (see http://www.sqlalchemy.org/docs/04/types.html for more)
from elixir import String, Unicode, Integer, DateTime
from turbogears.database import get_engine, metadata, session
#else
from turbogears.database import get_engine, metadata, session
# import the standard SQLAlchemy mapper
from sqlalchemy.orm import mapper
# To use the session-aware mapper use this import instead
# from turbogears.database import session_mapper as mapper
# import some basic SQLAlchemy classes for declaring the data model
# (see http://www.sqlalchemy.org/docs/05/ormtutorial.html)
from sqlalchemy import Table, Column, ForeignKey
from sqlalchemy.orm import relation
# import some datatypes for table columns from SQLAlchemy
# (see http://www.sqlalchemy.org/docs/05/reference/sqlalchemy/types.html for more)
from sqlalchemy import String, Unicode, Integer, DateTime
#end if
#end if
#if $identity != 'none'
from turbogears import identity
#end if

#if $sqlobject == 'True'
__connection__ = hub = PackageHub('${package}')


# your data model


# class YourDataClass(SQLObject):
#     pass
#end if
#if $sqlalchemy == 'True'
#if $elixir == 'True'
options_defaults['autosetup'] = False


# your data model

# class YourDataClass(Entity):
#     pass
#else

# your data tables
# http://www.sqlalchemy.org/docs/05/metadata.html

# your_table = Table('yourtable', metadata,
#     Column('my_id', Integer, primary_key=True)
# )


# your model classes
# http://www.sqlalchemy.org/docs/05/ormtutorial.html#define-a-python-class-to-be-mapped

# class YourDataClass(object):
#     pass


# set up mappers between your data tables and classes
# http://www.sqlalchemy.org/docs/05/mappers.html

# mapper(YourDataClass, your_table)
#end if
#end if


#if $identity == 'sqlobject'
# the identity model


class Visit(SQLObject):
    """A visit to your site."""

    visit_key = StringCol(length=40, alternateID=True,
                          alternateMethodName='by_visit_key')
    created = DateTimeCol(default=datetime.now)
    expiry = DateTimeCol()

    @classmethod
    def lookup_visit(cls, visit_key):
        try:
            return cls.by_visit_key(visit_key)
        except SQLObjectNotFound:
            return None


class VisitIdentity(SQLObject):
    """A Visit that is linked to a User object."""

    visit_key = StringCol(length=40, alternateID=True,
                          alternateMethodName='by_visit_key')
    user_id = IntCol()


class Group(SQLObject):
    """An ultra-simple group definition."""

    # names like "user" and "group" are reserved words in SQL
    # so we set the name to something safe for SQL
    class sqlmeta:
        table = 'tg_group'

    def __repr__(self):
        return '<Group: name="%s", display_name="%s">' % (
            self.group_name, self.display_name)

    def __unicode__(self):
        return self.display_name or self.group_name

    group_name = UnicodeCol(length=16, alternateID=True,
                            alternateMethodName='by_group_name')
    display_name = UnicodeCol(length=255)
    created = DateTimeCol(default=datetime.now)

    # collection of all users belonging to this group
    users = RelatedJoin('User', intermediateTable='user_group',
                        joinColumn='group_id', otherColumn='user_id')

    # collection of all permissions for this group
    permissions = RelatedJoin('Permission', joinColumn='group_id',
                              intermediateTable='group_permission',
                              otherColumn='permission_id')


class User(SQLObject):
    """Reasonably basic User definition.

    Probably would want additional attributes.

    """

    # names like "user" and "group" are reserved words in SQL
    # so we set the name to something safe for SQL
    class sqlmeta:
        table = 'tg_user'

    user_name = UnicodeCol(length=16, alternateID=True,
                           alternateMethodName='by_user_name')
    email_address = UnicodeCol(length=255, alternateID=True,
                               alternateMethodName='by_email_address')
    display_name = UnicodeCol(length=255)
    password = UnicodeCol(length=40)
    created = DateTimeCol(default=datetime.now)

    # groups this user belongs to
    groups = RelatedJoin('Group', intermediateTable='user_group',
                         joinColumn='user_id', otherColumn='group_id')

    def __repr__(self):
        return '<User: name="%s", email="%s", display name="%s">' % (
            self.user_name, self.email_address, self.display_name)

    def __unicode__(self):
        return self.display_name or self.user_name

    def _get_permissions(self):
        perms = set()
        for g in self.groups:
            perms |= set(g.permissions)
        return perms

    def _set_password(self, cleartext_password):
        """Run cleartext_password through the hash algorithm before saving."""
        password_hash = identity.encrypt_password(cleartext_password)
        self._SO_set_password(password_hash)

    def set_password_raw(self, password):
        """Saves the password as-is to the database."""
        self._SO_set_password(password)


class Permission(SQLObject):
    """A relationship that determines what each Group can do."""

    permission_name = UnicodeCol(length=16, alternateID=True,
                                 alternateMethodName='by_permission_name')
    description = UnicodeCol(length=255)

    groups = RelatedJoin('Group',
                         intermediateTable='group_permission',
                         joinColumn='permission_id',
                         otherColumn='group_id')

    def __repr__(self):
        return '<Permission: name="%s">' % self.permission_name

    def __unicode__(self):
        return self.permission_name
#end if
#if $identity == 'sqlalchemy'
#if $elixir == 'True'
# the identity model

class Visit(Entity):
    """A visit to your site."""

    using_options(tablename='visit')

    visit_key = Field(String(40), primary_key=True)
    created = Field(DateTime, nullable=False, default=datetime.now)
    expiry = Field(DateTime)

    @classmethod
    def lookup_visit(cls, visit_key):
        return Visit.get(visit_key)
    by_visit_key = lookup_visit


class VisitIdentity(Entity):
    """A Visit that is linked to a User object."""

    using_options(tablename='visit_identity')

    visit_key = Field(String(40), primary_key=True)
    user = ManyToOne('User', colname='user_id', use_alter=True)

    @classmethod
    def by_visit_key(cls, visit_key):
        """Look up VisitIdentity by given visit key."""
        return cls.get(visit_key)


class Group(Entity):
    """An ultra-simple group definition."""

    using_options(tablename='tg_group')

    group_id = Field(Integer, primary_key=True)
    group_name = Field(Unicode(16), unique=True, nullable=False)
    display_name = Field(Unicode(255))
    created = Field(DateTime, default=datetime.now)
    users = ManyToMany('User', tablename='user_group')
    permissions = ManyToMany('Permission', tablename='group_permission')

    def __repr__(self):
        return '<Group: name="%s", display_name="%s">' % (
            self.group_name, self.display_name)

    def __unicode__(self):
        return self.display_name or self.group_name

    @classmethod
    def by_group_name(cls, group_name):
        """Look up Group by given group name."""
        return cls.get_by(group_name=group_name)
    by_name = by_group_name


class User(Entity):
    """Reasonably basic User definition.

    Probably would want additional attributes.

    """

    using_options(tablename='tg_user')

    user_id = Field(Integer, primary_key=True)
    user_name = Field(Unicode(16), unique=True, nullable=False)
    email_address = Field(Unicode(255), unique=True)
    display_name = Field(Unicode(255))
    _password = Field(Unicode(40), colname='password')
    created = Field(DateTime, default=datetime.now)
    groups = ManyToMany('Group', tablename='user_group')

    def __repr__(self):
        return '<User: name="%s", email="%s", display name="%s">' % (
            self.user_name, self.email_address, self.display_name)

    def __unicode__(self):
        return self.display_name or self.user_name

    @property
    def permissions(self):
        p = set()
        for g in self.groups:
            p |= set(g.permissions)
        return p

    @classmethod
    def by_email_address(cls, email_address):
        """Look up User by given email address.

        This class method that can be used to search users based on their email
        addresses since it is unique.

        """
        return cls.get_by(email_address=email_address)

    @classmethod
    def by_user_name(cls, user_name):
        """Look up User by given user name.

        This class method that permits to search users based on their
        user_name attribute.

        """
        return cls.get_by(user_name=user_name)
    by_name = by_user_name

    def _set_password(self, password):
        """Run cleartext password through the hash algorithm before saving."""
        self._password = identity.encrypt_password(password)

    def _get_password(self):
        """Returns password."""
        return self._password

    password = property(_get_password, _set_password)


class Permission(Entity):
    """A relationship that determines what each Group can do."""

    using_options(tablename='permission')

    permission_id = Field(Integer, primary_key=True)
    permission_name = Field(Unicode(16), unique=True, nullable=False)
    description = Field(Unicode(255))
    groups = ManyToMany('Group', tablename='group_permission')

    def __repr__(self):
        return '<Permission: name="%s">' % self.permission_name

    def __unicode__(self):
        return self.permission_name

    @classmethod
    def by_permission_name(cls, permission_name):
        """Look up Permission by given permission name."""
        return cls.get_by(permission_name=permission_name)
    by_name = by_permission_name


# Set up all Elixir entities declared above

setup_all()
#else
# the identity schema

visits_table = Table('visit', metadata,
    Column('visit_key', String(40), primary_key=True),
    Column('created', DateTime, nullable=False, default=datetime.now),
    Column('expiry', DateTime)
)

visit_identity_table = Table('visit_identity', metadata,
    Column('visit_key', String(40), primary_key=True),
    Column('user_id', Integer, ForeignKey('tg_user.user_id'), index=True)
)

groups_table = Table('tg_group', metadata,
    Column('group_id', Integer, primary_key=True),
    Column('group_name', Unicode(16), unique=True, nullable=False),
    Column('display_name', Unicode(255)),
    Column('created', DateTime, default=datetime.now)
)

users_table = Table('tg_user', metadata,
    Column('user_id', Integer, primary_key=True),
    Column('user_name', Unicode(16), unique=True, nullable=False),
    Column('email_address', Unicode(255), unique=True),
    Column('display_name', Unicode(255)),
    Column('password', Unicode(40)),
    Column('created', DateTime, default=datetime.now)
)

permissions_table = Table('permission', metadata,
    Column('permission_id', Integer, primary_key=True),
    Column('permission_name', Unicode(16), unique=True, nullable=False),
    Column('description', Unicode(255))
)

user_group_table = Table('user_group', metadata,
    Column('user_id', Integer, ForeignKey('tg_user.user_id',
        onupdate='CASCADE', ondelete='CASCADE'), primary_key=True),
    Column('group_id', Integer, ForeignKey('tg_group.group_id',
        onupdate='CASCADE', ondelete='CASCADE'), primary_key=True)
)

group_permission_table = Table('group_permission', metadata,
    Column('group_id', Integer, ForeignKey('tg_group.group_id',
        onupdate='CASCADE', ondelete='CASCADE'), primary_key=True),
    Column('permission_id', Integer, ForeignKey('permission.permission_id',
        onupdate='CASCADE', ondelete='CASCADE'), primary_key=True)
)


# the identity model

class Visit(object):
    """A visit to your site."""

    @classmethod
    def lookup_visit(cls, visit_key):
        """Look up Visit by given visit key."""
        return session.query(cls).get(visit_key)


class VisitIdentity(object):
    """A Visit that is linked to a User object."""

    @classmethod
    def by_visit_key(cls, visit_key):
        """Look up VisitIdentity by given visit key."""
        return session.query(cls).get(visit_key)


class Group(object):
    """An ultra-simple group definition."""

    def __repr__(self):
        return '<Group: name="%s", display_name="%s">' % (
            self.group_name, self.display_name)

    def __unicode__(self):
        return self.display_name or self.group_name

    @classmethod
    def by_group_name(cls, group_name):
        """Look up Group by given group name."""
        return session.query(cls).filter_by(group_name=group_name).first()
    by_name = by_group_name


class User(object):
    """Reasonably basic User definition.

    Probably would want additional attributes.

    """

    def __repr__(self):
        return '<User: name="%s", email="%s", display name="%s">' % (
            self.user_name, self.email_address, self.display_name)

    def __unicode__(self):
        return self.display_name or self.user_name

    @property
    def permissions(self):
        """Return all permissions of all groups the user belongs to."""
        p = set()
        for g in self.groups:
            p |= set(g.permissions)
        return p

    @classmethod
    def by_email_address(cls, email_address):
        """Look up User by given email address.

        This class method that can be used to search users based on their email
        addresses since it is unique.

        """
        return session.query(cls).filter_by(email_address=email_address).first()

    @classmethod
    def by_user_name(cls, user_name):
        """Look up User by given user name.

        This class method that permits to search users based on their
        user_name attribute.

        """
        return session.query(cls).filter_by(user_name=user_name).first()
    by_name = by_user_name

    def _set_password(self, password):
        """Run cleartext password through the hash algorithm before saving."""
        self._password = identity.encrypt_password(password)

    def _get_password(self):
        """Returns password."""
        return self._password

    password = property(_get_password, _set_password)


class Permission(object):
    """A relationship that determines what each Group can do."""

    def __repr__(self):
        return '<Permission: name="%s">' % self.permission_name

    def __unicode__(self):
        return self.permission_name

    @classmethod
    def by_permission_name(cls, permission_name):
        """Look up Permission by given permission name."""
        return session.query(cls).filter_by(permission_name=permission_name).first()
    by_name = by_permission_name


# set up mappers between identity tables and classes

mapper(Visit, visits_table)

mapper(VisitIdentity, visit_identity_table,
        properties=dict(user=relation(User, backref='visit_identity')))

mapper(User, users_table,
        properties=dict(_password=users_table.c.password))

mapper(Group, groups_table,
        properties=dict(users=relation(User,
                secondary=user_group_table, backref='groups')))

mapper(Permission, permissions_table,
        properties=dict(groups=relation(Group,
                secondary=group_permission_table, backref='permissions')))
#end if
#end if

# functions for populating the database

#if $identity == 'none'
def bootstrap_model(clean=False):
    """Create all database tables and fill them with default data.

    This function is run by the 'bootstrap' function from the command module.
    By default it creates all database tables for your model.

    You can add more functions as you like to add more boostrap data to the
    database or enhance the function below.

    If 'clean' is True, all tables defined by your model will be dropped before
    creating them again.

    """
    create_tables(clean)
#else
def bootstrap_model(clean=False, user=None):
    """Create all database tables and fill them with default data.

    This function is run by the 'bootstrap' function from the command module.
    By default it calls two functions to create all database tables for your
    model and optionally create a user.

    You can add more functions as you like to add more boostrap data to the
    database or enhance the functions below.

    If 'clean' is True, all tables defined by your model will be dropped before
    creating them again. If 'user' is not None, 'create_user' will be called
    with the given username.

    """
    create_tables(clean)
    if user:
        create_default_user(user)
#end if

def create_tables(drop_all=False):
    """Create all tables defined in the model in the database.

    Optionally drop existing tables before creating them.

    """
#if $sqlobject == 'True'
    ## XXX: This is duplicated from testutil.DBTestSO and should be factored
    ## out to a reusable function (maybe into turbogears.database?).
    from turbogears.util import get_model
    from inspect import isclass

    model = get_model()
    if not model:
        from ${package}.command import ConfigurationError
        raise ConfigurationError(
            "Unable to create database tables without a model")

    try:
        so_classes = [model.__dict__[x] for x in model.soClasses]
    except AttributeError:
        so_classes = model.__dict__.values()

    if drop_all:
        print "Dropping all database tables defined in model."
        for item in reversed(so_classes):
            if isclass(item) and issubclass(item, SQLObject) and \
                    item is not SQLObject and item is not InheritableSQLObject:
                item.dropTable(ifExists=True, cascade=True)

    # list of constraints we will collect
    constraints = list()

    for item in so_classes:
        if isclass(item) and issubclass(item, SQLObject) and \
                item is not SQLObject and item is not InheritableSQLObject:
            # create table without applying constraints, collect
            # all the constaints for later creation.
            # see http://sqlobject.org/FAQ.html#mutually-referencing-tables
            # for more info
            collected_constraints = item.createTable(ifNotExists=True,
                applyConstraints=False)

            if collected_constraints:
                constraints.extend(collected_constraints)

    # now that all tables are created, add the constaints we collected
    for postponed_constraint in constraints:
        # item is the last processed item and we borrow its connection
        item._connection.query(postponed_constraint)
#elif $sqlalchemy == 'True'
    get_engine()
    if drop_all:
        print "Dropping all database tables defined in model."
        metadata.drop_all()
    metadata.create_all()
#end if

    print "All database tables defined in model created."

#if $identity != 'none'
def create_default_user(user_name, password=None):
    """Create a default user."""
    try:
        u = User.by_user_name(user_name)
    except:
        u = None
    if u:
        print "User '%s' already exists in database." % user_name
        return
    from getpass import getpass
    from sys import stdin
    while password is None:
        try:
            password = getpass("Enter password for user '%s': "
                % user_name.encode(stdin.encoding)).strip()
            password2 = getpass("Confirm password: ").strip()
            if password != password2:
                print "Passwords do not match."
            else:
                password = password.decode(stdin.encoding)
                break
        except (EOFError, KeyboardInterrupt):
            print "User creation cancelled."
            return
#if $identity == 'sqlobject'
    hub.begin()
    u = User(user_name=user_name, display_name=u"Default User",
        email_address=u"%s@nowhere.xyz" % user_name, password=password)
    hub.commit()
#end if
#if $identity == 'sqlalchemy'
    u = User()
    u.user_name = user_name
    u.display_name = u"Default User"
    u.email_address = u"%s@nowhere.xyz" % user_name
    u.password = password
    session.add(u)
    session.flush()
#end if
    print "User '%s' created." % user_name
#end if
