# w.c.s. - web application for online forms
# Copyright (C) 2005-2010  Entr'ouvert
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.

from django.utils import six
from django.utils.encoding import force_text

from quixote import get_publisher, get_response, get_request, get_session, redirect
from quixote.directory import Directory
from quixote.html import TemplateIO, htmltext

from wcs.qommon import _, force_str
import wcs.qommon.storage as st
from wcs.qommon import errors
from wcs.qommon import misc, get_cfg
from wcs.qommon.backoffice.listing import pagination_links
from wcs.roles import Role

from wcs.qommon import ident
from wcs.qommon.ident.idp import is_idp_managing_user_attributes, is_idp_managing_user_roles
from wcs.qommon.form import *
from wcs.qommon.admin.emails import EmailsDirectory
from wcs.qommon.backoffice.menu import html_top
from wcs.qommon.admin.menu import error_page

class UserUI(object):
    def __init__(self, user):
        self.user = user

    def form(self):
        ident_methods = get_cfg('identification', {}).get('methods', [])
        users_cfg = get_cfg('users', {})

        form = Form(enctype='multipart/form-data')
        # do not display user attribute fields if the site has been set to get
        # them filled by SAML requests
        if not is_idp_managing_user_attributes():
            formdef = get_publisher().user_class.get_formdef()
            if not formdef or not users_cfg.get('field_name'):
                form.add(StringWidget, 'name', title = _('Name'), required = True, size=30,
                        value = self.user.name)
            if not formdef or not users_cfg.get('field_email'):
                form.add(EmailWidget, 'email', title = _('Email'), required = False, size=30,
                        value = self.user.email)
            if formdef:
                formdef.add_fields_to_form(form, form_data = self.user.form_data)
            form.add(CheckboxWidget, 'is_admin', title = _('Administrator Account'),
                    value = self.user.is_admin)

        roles = list(Role.select(order_by='name'))
        if len(roles) and not is_idp_managing_user_roles():
            form.add(WidgetList, 'roles', title = _('Roles'), element_type = SingleSelectWidget,
                    value = self.user.roles,
                    add_element_label = _('Add Role'),
                    element_kwargs = {
                        'render_br': False,
                        'options': [(None, '---', None)] + [(x.id, x.name, x.id) for x in roles if not x.is_internal()]})

        for klass in [x for x in ident.get_method_classes() if x.key in ident_methods]:
            if klass.method_admin_widget:
                value = klass().get_value(self.user)
                form.add(klass.method_admin_widget, 'method_%s' % klass.key, required = False,
                        value = value)

        form.add_submit('submit', _('Submit'))
        form.add_submit('cancel', _('Cancel'))
        return form

    def submit_form(self, form):
        if not self.user:
            self.user = get_publisher().user_class()
        for f in ('name', 'email', 'is_admin', 'roles'):
            widget = form.get_widget(f)
            if widget:
                setattr(self.user, f, widget.parse())
        if not is_idp_managing_user_attributes():
            formdef = get_publisher().user_class.get_formdef()
            if formdef:
                data = formdef.get_data(form)
                self.user.set_attributes_from_formdata(data)
                self.user.form_data = data

        # user is stored first so it get an id; necessary for some ident
        # methods
        self.user.store()

        ident_methods = get_cfg('identification', {}).get('methods', [])
        for klass in [x for x in ident.get_method_classes() if x.key in ident_methods]:
            widget = form.get_widget('method_%s' % klass.key)
            if widget:
                klass().submit(self.user, widget)

            # XXX: and store!
            # XXX 2: but pay attention to errors set on widget (think
            # "duplicate username") (and the calling method will also
            # have to check this)


class UserPage(Directory):
    _q_exports = ['', 'edit', 'delete', 'token']

    def __init__(self, component):
        self.user = get_publisher().user_class.get(component)
        self.user_ui = UserUI(self.user)
        get_response().breadcrumb.append((component + '/', self.user.display_name))

    def _q_index(self):
        html_top('users', '%s - %s' % (_('User'), self.user.display_name))
        r = TemplateIO(html=True)
        r += htmltext('<div id="appbar">')
        r += htmltext('<h2>%s</h2>') % self.user.display_name
        r += htmltext('<span class="actions">')
        r += self.get_actions()
        r += htmltext('</span>')
        r += htmltext('</div>')
        users_cfg = get_cfg('users', {})

        r += htmltext('<div class="splitcontent-left">')

        r += htmltext('<div class="bo-block">')
        r += htmltext('<h3>%s</h3>') % _('Profile')

        r += htmltext('<div class="form">')

        if not users_cfg.get('field_name'):
            r += htmltext('<div class="title">%s</div>') % _('Name')
            r += htmltext('<div class="StringWidget content">%s</div>') % self.user.name

        if not users_cfg.get('field_email') and self.user.email:
            r += htmltext('<div class="title">%s</div>') % _('Email')
            r += htmltext('<div class="StringWidget content">%s</div>') % self.user.email

        formdef = self.user.get_formdef()
        if formdef:
            if self.user.form_data:
                for field in formdef.fields:
                    if not hasattr(field, str('get_view_value')):
                        continue
                    value = self.user.form_data.get(field.id)
                    r += htmltext('<div class="title">')
                    r += field.label
                    r += htmltext('</div>')
                    r += htmltext('<div class="StringWidget content">')
                    if value is None:
                        r += htmltext('<i>%s</i>') % _('Not set')
                    else:
                        r += field.get_view_value(value)
                    r += htmltext('</div>')

        r += htmltext('</div>')

        r += htmltext('</div>') # bo-block
        r += htmltext('</div>') # splitcontent-left

        r += htmltext('<div class="splitcontent-right">')
        r += htmltext('<div class="bo-block">')
        r += htmltext('<h3>%s</h3>') % _('Roles')

        if self.user.roles or self.user.is_admin:
            r += htmltext('<div class="form">')
            r += htmltext('<div class="StringWidget content"><ul>')
            if self.user.is_admin:
                r += htmltext('<li><strong>%s</strong></li>') % _('Site Administrator')
            for k in self.user.roles or []:
                try:
                    r += htmltext('<li>%s</li>') % Role.get(k).name
                except KeyError:
                    # removed role ?
                    r += htmltext('<li><em>')
                    r += _('Unknown role (%s)') % k
                    r += htmltext('</em></li>')
            r += htmltext('</ul></div>')
            r += htmltext('</div>')

        r += htmltext('</div>') # bo-block

        if self.user.lasso_dump:
            import lasso
            identity = lasso.Identity.newFromDump(self.user.lasso_dump)
            server = misc.get_lasso_server()
            if len(identity.providerIds) and server:
                r += htmltext('<div class="bo-block" id="saml-details">')
                r += htmltext('<h3>%s</h3>') % _('SAML Details')
                r += htmltext('<div class="StringWidget content"><ul>')
                for pid in identity.providerIds:
                    provider = server.getProvider(pid)
                    label = misc.get_provider_label(provider)
                    if label:
                        label = '%s (%s)' % (label, pid)
                    else:
                        label = pid
                    federation = identity.getFederation(pid)
                    r += htmltext('<li>')
                    r += _('Account federated with %s') % label
                    r += htmltext('<br />')
                    if federation.localNameIdentifier:
                        r += _("local: ") + federation.localNameIdentifier.content
                    if federation.remoteNameIdentifier:
                        r += _("remote: ") + federation.remoteNameIdentifier.content
                    r += htmltext('</li>')
                r += htmltext('</ul></div>')

                if get_cfg('debug', {}).get('debug_mode', False):
                    r += htmltext('<h4>%s</h4>') % _('Lasso Identity Dump')
                    r += htmltext('<pre>%s</pre>') % self.user.lasso_dump
                r += htmltext('</div>') # bo-block

        r += htmltext('</div>') # splitcontent-right
        return r.getvalue()

    def get_actions(self):
        ident_methods = get_cfg('identification', {}).get('methods', [])
        r = TemplateIO(html=True)

        if is_idp_managing_user_attributes() and not is_idp_managing_user_roles():
            r += htmltext('<a href="edit">%s</a>') % _('Manage Roles')
        elif not (is_idp_managing_user_attributes() and is_idp_managing_user_roles()):
            r += htmltext('<a href="edit">%s</a>') % _('Edit')
        r += htmltext('<a href="delete" rel="popup">%s</a>') % _('Delete')

        for method in ident_methods:
            try:
                actions = ident.get_method_user_directory(
                                method, self.user).get_actions()
            except AttributeError:
                continue
            for action_url, action_label in actions:
                r += htmltext('<a href="%s/%s">%s</a>') % (method, action_url, action_label)

        debug_cfg = get_cfg('debug', {})
        if debug_cfg.get('logger', True):
            r += htmltext('<a href="../../settings/logs/by_user/%s/">') % self.user.id
            r += htmltext('%s</a>') % _('Logs')

        return r.getvalue()

    def edit(self):
        form = self.user_ui.form()
        if form.get_widget('cancel').parse():
            return redirect('.')

        if form.get_widget('roles') and form.get_widget('roles').get_widget(
                'add_element').parse():
            form.clear_errors()
            display_form = True
        else:
            display_form = (not form.is_submitted() or form.has_errors())

        if display_form:
            get_response().breadcrumb.append( ('edit', _('Edit')) )
            html_top('users', title = _('Edit User'))
            r = TemplateIO(html=True)
            r += htmltext('<h2>%s</h2>') % _('Edit User')
            r += form.render()
            return r.getvalue()
        else:
            self.user_ui.submit_form(form)
            return redirect('.')

    def delete(self):
        form = Form(enctype='multipart/form-data')
        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
                        "You are about to irrevocably delete this user.")))
        form.add_submit('delete', _('Delete'))
        form.add_submit("cancel", _("Cancel"))
        if form.get_widget('cancel').parse():
            return redirect('.')
        if not form.is_submitted() or form.has_errors():
            get_response().breadcrumb.append(('delete', _('Delete')))
            html_top('users', title = _('Delete User'))
            r = TemplateIO(html=True)
            r += htmltext('<h2>%s %s</h2>') % (_('Deleting User:'), self.user.name)
            r += form.render()
            return r.getvalue()
        else:
            ident_methods = get_cfg('identification', {}).get('methods', [])
            for klass in [x for x in ident.get_method_classes() if x.key in ident_methods]:
                if hasattr(klass, str('delete')):
                    klass().delete(self.user)
            self.user.remove_self()
            return redirect('..')

    def _q_lookup(self, component):
        ident_methods = get_cfg('identification', {}).get('methods', [])
        if component in ident_methods:
            get_response().breadcrumb.append((component + '/', None))
            return ident.get_method_user_directory(component, self.user)

class UsersDirectory(Directory):
    _q_exports = ['', 'new']

    def _q_index(self):
        get_response().breadcrumb.append( ('users/', _('Users')) )
        html_top('users', title=_('Users'))
        r = TemplateIO(html=True)

        limit = int(get_request().form.get('limit',
            get_publisher().get_site_option('default-page-size') or 20))
        offset = int(get_request().form.get('offset', 0))

        checked_roles = None
        if get_request().form.get('filter'):
            checked_roles = get_request().form.get('role', [])
            if isinstance(checked_roles, six.string_types):
                checked_roles = [checked_roles]

        if checked_roles:
            # optimize query by removing the roles criterias if they are all
            # checked
            possible_roles = ['admin', 'none']
            possible_roles.extend(Role.keys())
            if set(possible_roles) == set(checked_roles):
                checked_roles = None

        total_count = get_publisher().user_class.count()

        # declarative criteria to only get checked roles
        criterias = []
        if checked_roles:
            if 'admin' in checked_roles:
                criterias.append(st.Equal('is_admin', True))
            if 'none' in checked_roles:
                criterias.append(st.And([
                    st.Equal('is_admin', False), st.Equal('roles', [])]))
            other_roles = [x for x in checked_roles if x not in ('admin', 'none')]
            if other_roles:
                criterias.append(st.Intersects('roles', other_roles))
            criterias = [st.Or(criterias)]

        query = get_request().form.get('q')
        if query:
            criterias.append(st.Or([st.ILike('name', query), st.ILike('email', query)]))

        if criterias:
            filtered_count = get_publisher().user_class.count(criterias)
            if filtered_count < offset:
                # reset offset if we are past the number of elements
                offset = 0
                get_request().form['offset'] = 0
        else:
            filtered_count = total_count

        users = get_publisher().user_class.select(order_by='name',
                clause=criterias, offset=offset, limit=limit)

        r += htmltext('<div id="listing">')
        r += htmltext('<ul>')
        r += htmltext('<li>%s %s</li>') % (_('Total number of users:'), total_count)

        if criterias:
            r += htmltext('<li>%s %s</li>') % (_('Number of filtered users:'),
                    filtered_count)

        r += htmltext('</ul>')

        r += htmltext('<ul class="biglist">')
        for user in users:
            if user.is_admin:
                r += htmltext('<li class="user-is-admin">')
            elif user.roles:
                r += htmltext('<li class="user-has-roles">')
            else:
                r += htmltext('<li class="simple-user">')
            r += htmltext('<strong class="label"><a href="%s/">%s</a></strong>') % (user.id, user.display_name)
            if user.email and False:
                r += htmltext('<p class="details">')
                r += user.email
                r += htmltext('</p>')
            r += htmltext('</li>')
        r += htmltext('</ul>')

        r += pagination_links(offset, limit, filtered_count)
        r += htmltext('</div>')

        if get_request().form.get('ajax') == 'true':
            get_response().filter = {'raw': True}
            return r.getvalue()

        ident_methods = get_cfg('identification', {}).get('methods', [])

        r2 = TemplateIO(html=True)
        r2 += htmltext('<div id="appbar">')
        r2 += htmltext('<h2>%s</h2>') % _('Users')

        info_notice = None
        if not ident_methods:
            info_notice = _('An authentification system must be configured before creating users.')
        elif ident_methods == ['idp'] and len(get_cfg('idp', {}).items()) == 0:
            info_notice = _('SAML support must be setup before creating users.')
        else:
            # if attributes are managed by the identity provider, do not expose
            # the possibility to create users, as only the roles field would
            # be shown, and the creation would fail on missing fields.
            if not is_idp_managing_user_attributes():
                r2 += htmltext('<span class="actions">')
                r2 += htmltext('<a class="new-item" href="new">%s</a>') % _('New User')
                r2 += htmltext('</span>')
            get_response().filter['sidebar'] = self.get_sidebar(offset, limit)

        r2 += htmltext('</div>')

        if info_notice:
            r2 += htmltext('<div class="infonotice"><p>%s</p></div>') % info_notice

        r2 += r.getvalue()

        return r2.getvalue()

    def get_sidebar(self, offset=None, limit=None):
        r = TemplateIO(html=True)

        get_response().add_javascript(['jquery.js', 'wcs.listing.js'])
        r += htmltext('<form id="listing-settings">')

        if offset or limit:
            if not offset:
                offset = 0
            r += htmltext('<input type="hidden" name="offset" value="%s"/>') % offset

        if limit:
            r += htmltext('<input type="hidden" name="limit" value="%s"/>') % limit

        r += htmltext('<h3>%s</h3>') % _('Search')
        if get_request().form.get('q'):
            q = get_request().form.get('q')
            r += htmltext('<input name="q" value="%s">') % force_str(q)
        else:
            r += htmltext('<input name="q">')
        r += htmltext('<input type="submit" value="%s"/>') % _('Search')

        r += htmltext('<h3>%s</h3>') % _('Filter on Roles')
        r += htmltext('<input type="hidden" name="filter" value="true"/>')
        r += htmltext('<ul>')
        roles = [('admin', _('Site Administrator'))]
        for role in Role.select():
            roles.append((role.id, role.name))
        roles.append(('none', _('None')))

        checked_roles = get_request().form.get('role', [])
        if not checked_roles and not get_request().form.get('filter'):
            # take everything as default
            checked_roles = [str(x[0]) for x in roles]

        for role_id, role_title in roles:
            checked = ''
            if str(role_id) in checked_roles:
                checked = 'checked'
            r += htmltext('<li><label><input type="checkbox" name="role" value="%s"%s/>%s</label></li>') % (
                    role_id, checked, role_title)
        r += htmltext('</ul>')
        r += htmltext('<input type="submit" value="%s"/>') % _('Filter')
        r += htmltext('</form>')
        return r.getvalue()

    def new(self):
        get_response().breadcrumb.append( ('users/', _('Users')) )
        get_response().breadcrumb.append( ('new', _('New')) )

        ident_methods = get_cfg('identification', {}).get('methods', [])
        if not ident_methods:
            return error_page('users',
                _('An authentification system must be configured before creating users.'))
        if ident_methods == ['idp'] and len(get_cfg('idp', {}).items()) == 0:
            return error_page('users',
                    _('SAML support must be setup before creating users.'))
        if is_idp_managing_user_attributes():
            raise errors.TraversalError()

        # XXX: user must be logged in to get here
        user = get_publisher().user_class()
        user_ui = UserUI(user)
        first_user = get_publisher().user_class.count() == 0
        if first_user:
            user.is_admin = first_user
        form = user_ui.form()
        if form.get_widget('cancel').parse():
            return redirect('.')

        if not form.is_submitted() or form.has_errors():
            html_top('users', title = _('New User'))
            r = TemplateIO(html=True)
            r += htmltext('<h2>%s</h2>') % _('New User')
            r += form.render()
            return r.getvalue()
        else:
            user_ui.submit_form(form)
            if first_user:
                req = get_request()
                if req.user:
                    user_ui.user.name_identifiers = req.user.name_identifiers
                    user_ui.user.lasso_dump = req.user.lasso_dump
                    user_ui.user.store()
                get_session().set_user(user_ui.user.id)
            return redirect('.')

    def _q_lookup(self, component):
        get_response().breadcrumb.append( ('users/', _('Users')) )
        try:
            return UserPage(component)
        except KeyError:
            raise errors.TraversalError()


EmailsDirectory.register('email_with_token', N_('Identification token'),
        N_('Available variables: token, token_url, sitename'),
        category = N_('Identification'),
        default_subject = N_('Access to [sitename]'),
        default_body = N_('''\
Hello,

An administrator delivered you access to [sitename].

Please visit [token_url] to enable it.

'''))

