diff --git a/invenio/ext/login/legacy_user.py b/invenio/ext/login/legacy_user.py index 2f368654f..f27122fc9 100644 --- a/invenio/ext/login/legacy_user.py +++ b/invenio/ext/login/legacy_user.py @@ -1,301 +1,291 @@ # -*- coding: utf-8 -*- ## ## This file is part of Invenio. -## Copyright (C) 2012, 2013 CERN. +## Copyright (C) 2012, 2013, 2014 CERN. ## ## Invenio 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. ## ## Invenio 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 Invenio; if not, write to the Free Software Foundation, Inc., ## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. -""" - Legacy UserInfo object. -""" +"""Provide support for legacy UserInfo object.""" from flask import session, request, has_request_context, current_app from flask.ext.login import UserMixin from werkzeug.datastructures import CallbackDict, CombinedMultiDict from invenio.ext.cache import cache __all__ = ['UserInfo'] CFG_USER_DEFAULT_INFO = { 'remote_ip': '', 'remote_host': '', 'referer': '', 'uri': '', 'agent': '', 'uid': -1, 'nickname': '', 'email': '', 'group': [], 'guest': '1', 'session': None, 'precached_permitted_restricted_collections': [], 'precached_usebaskets': False, 'precached_useloans': False, 'precached_usegroups': False, 'precached_usealerts': False, 'precached_usemessages': False, 'precached_viewsubmissions': False, 'precached_useapprove': False, 'precached_useadmin': False, 'precached_usestats': False, 'precached_viewclaimlink': False, 'precached_usepaperclaim': False, 'precached_usepaperattribution': False, 'precached_canseehiddenmarctags': False, 'precached_sendcomments': False, } class UserInfo(CombinedMultiDict, UserMixin): """ This provides legacy implementations for the methods that Flask-Login and Invenio 1.x expects user objects to have. """ def __init__(self, uid=None, force=False): - """ - Keeps information about user. - """ + """Retreave information about user.""" def on_update(self): """ Changes own status when the user info is modified. """ self.modified = True self.modified = False self.uid = uid self.req = self._get_request_info() acc = {} if uid is not None and uid > 0: data = self._login(uid, force) acc = self._precache(data, force) else: data = self._create_guest() self.info = CallbackDict(data, on_update) - #FIXME remove req after everybody start using flask request. + # FIXME remove req after everybody start using flask request. CombinedMultiDict.__init__(self, [self.req, self.info, acc, dict(CFG_USER_DEFAULT_INFO)]) self.save() def get_key(self): - """ Generates key for caching user information. """ + """Generates key for caching user information.""" key = 'current_user::' + str(self.uid) return key def get_acc_key(self): - """ Generates key for caching autorizations. """ + """Generates key for caching autorizations.""" remote_ip = str(request.remote_addr) if has_request_context() else '0' return 'current_user::' + str(self.uid) + '::' + remote_ip def save(self): - """ - Saves modified data pernamently for logged users. - """ + """Save modified data pernamently for logged users.""" if not self.is_guest and self.modified: timeout = current_app.config.get( 'CFG_WEBSESSION_EXPIRY_LIMIT_DEFAULT', 0)*3600 cache.set(self.get_key(), dict(self.info), timeout=timeout) def reload(self): - """ - Reloads user login information and saves them. - """ + """Reload user login information and saves them.""" data = self._login(self.uid, force=True) acc = self._precache(data, force=True) self.info.update(data) CombinedMultiDict.__init__(self, [self.req, self.info, acc, dict(CFG_USER_DEFAULT_INFO)]) self.save() def update_request_info(self): self.req = self._get_request_info() def _get_request_info(self): - """ - Get request information. - """ - - #FIXME: we should support IPV6 too. (hint for FireRole) + """Get request information.""" + # FIXME: we should support IPV6 too. (hint for FireRole) data = {} if has_request_context(): data['remote_ip'] = request.remote_addr or '' data['remote_host'] = request.environ.get('REMOTE_HOST', '') data['referer'] = request.referrer data['uri'] = request.environ['PATH_INFO'] or '' data['agent'] = request.headers.get('User-Agent', 'N/A') - #data['session'] = session.sid return data def _create_guest(self): data = {'settings': {}} - if current_app.config.get('CFG_WEBSESSION_DIFFERENTIATE_BETWEEN_GUESTS', False): + if current_app.config.get( + 'CFG_WEBSESSION_DIFFERENTIATE_BETWEEN_GUESTS', False): from invenio.ext.sqlalchemy import db from invenio.modules.accounts.models import User - note = '1' if current_app.config.get('CFG_ACCESS_CONTROL_LEVEL_GUESTS', 0) == 0 else '0' + note = '1' if current_app.config.get( + 'CFG_ACCESS_CONTROL_LEVEL_GUESTS', 0) == 0 else '0' u = User(email='', note=note, password='guest') db.session.add(u) db.session.commit() data.update(u.__dict__) else: # Minimal information about user. data['id'] = data['uid'] = 0 return data def _login(self, uid, force=False): - """ - Get account information about currently logged user from database. + """Get account information about currently logged user from database. Should raise an exception when session.uid is not valid User.id. """ data = cache.get(self.get_key()) if not force and data is not None: return data from invenio.modules.accounts.models import User data = {} try: user = User.query.get(uid) data['id'] = data['uid'] = user.id or -1 data['nickname'] = user.nickname or '' data['email'] = user.email or '' data['note'] = user.note or '' - data['groups'] = map(lambda x: x.usergroup.name, - user.usergroups or []) + data['group'] = map(lambda x: x.usergroup.name, + user.usergroups or []) data.update(user.settings or {}) data['settings'] = user.settings or {} data['guest'] = str(int(user.guest)) # '1' or '0' self.modified = True except: data = self._create_guest() return data def _precache(self, info, force=False): - """ - Calculate prermitions for user actions. + """Calculate prermitions for user actions. FIXME: compatibility layer only !!! """ CFG_BIBAUTHORID_ENABLED = current_app.config.get( 'CFG_BIBAUTHORID_ENABLED', False) # get autorization key acc_key = self.get_acc_key() acc = cache.get(acc_key) if not force and acc_key is not None and acc is not None: return acc - #FIXME: acc_authorize_action should use flask request directly + # FIXME: acc_authorize_action should use flask request directly user_info = info user_info.update(self.req) from invenio.legacy.webuser import isUserSubmitter, isUserReferee, \ isUserAdmin, isUserSuperAdmin from invenio.modules.access.engine import acc_authorize_action from invenio.modules.access.control import acc_get_role_id, \ acc_is_user_in_role - from invenio.legacy.search_engine import get_permitted_restricted_collections + from invenio.legacy.search_engine import \ + get_permitted_restricted_collections data = {} data['precached_permitted_restricted_collections'] = \ get_permitted_restricted_collections(user_info) data['precached_usebaskets'] = acc_authorize_action( user_info, 'usebaskets')[0] == 0 data['precached_useloans'] = acc_authorize_action( user_info, 'useloans')[0] == 0 data['precached_usegroups'] = acc_authorize_action( user_info, 'usegroups')[0] == 0 data['precached_usealerts'] = acc_authorize_action( user_info, 'usealerts')[0] == 0 data['precached_usemessages'] = acc_authorize_action( user_info, 'usemessages')[0] == 0 data['precached_usestats'] = acc_authorize_action( user_info, 'runwebstatadmin')[0] == 0 try: data['precached_viewsubmissions'] = isUserSubmitter(user_info) except: data['precached_viewsubmissions'] = None data['precached_useapprove'] = isUserReferee(user_info) data['precached_useadmin'] = isUserAdmin(user_info) data['precached_usesuperadmin'] = isUserSuperAdmin(user_info) data['precached_canseehiddenmarctags'] = acc_authorize_action( user_info, 'runbibedit')[0] == 0 usepaperclaim = False usepaperattribution = False viewclaimlink = False if (CFG_BIBAUTHORID_ENABLED and acc_is_user_in_role( user_info, acc_get_role_id("paperclaimviewers"))): usepaperclaim = True if (CFG_BIBAUTHORID_ENABLED and acc_is_user_in_role( user_info, acc_get_role_id("paperattributionviewers"))): usepaperattribution = True viewlink = False try: viewlink = session['personinfo']['claim_in_process'] except (KeyError, TypeError): pass - if (current_app.config.get('CFG_BIBAUTHORID_ENABLED') and usepaperattribution and viewlink): + if (current_app.config.get('CFG_BIBAUTHORID_ENABLED') + and usepaperattribution and viewlink): viewclaimlink = True # if (CFG_BIBAUTHORID_ENABLED # and ((usepaperclaim or usepaperattribution) # and acc_is_user_in_role( # data, acc_get_role_id("paperattributionlinkviewers")))): # viewclaimlink = True data['precached_viewclaimlink'] = viewclaimlink data['precached_usepaperclaim'] = usepaperclaim data['precached_usepaperattribution'] = usepaperattribution timeout = current_app.config.get( 'CFG_WEBSESSION_EXPIRY_LIMIT_DEFAULT', 0)*3600 cache.set(acc_key, data, timeout=timeout) return data def is_authenticated(self): return not self.is_guest def is_authorized(self, name, **kwargs): from invenio.modules.access.engine import acc_authorize_action return acc_authorize_action(self, name)[0] == 0 def is_active(self): return not self.is_guest @property def is_guest(self): return True if self['email'] == '' else False @property def is_admin(self): return self.get('precached_useadmin', False) @property def is_super_admin(self): return self.get('precached_usesuperadmin', False) def get_id(self): return self.get('id', -1) diff --git a/invenio/legacy/websession/templates.py b/invenio/legacy/websession/templates.py index c88146262..c0e047fa9 100644 --- a/invenio/legacy/websession/templates.py +++ b/invenio/legacy/websession/templates.py @@ -1,2847 +1,2846 @@ ## This file is part of Invenio. ## Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012 CERN. ## ## Invenio 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. ## ## Invenio 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 Invenio; if not, write to the Free Software Foundation, Inc., ## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. __revision__ = "$Id$" import urllib import cgi from invenio.base.wrappers import lazy_import from invenio.config import \ CFG_CERN_SITE, \ CFG_SITE_LANG, \ CFG_SITE_NAME, \ CFG_SITE_NAME_INTL, \ CFG_SITE_SUPPORT_EMAIL, \ CFG_SITE_SECURE_URL, \ CFG_SITE_URL, \ CFG_WEBSESSION_RESET_PASSWORD_EXPIRE_IN_DAYS, \ CFG_WEBSESSION_ADDRESS_ACTIVATION_EXPIRE_IN_DAYS, \ CFG_WEBSESSION_DIFFERENTIATE_BETWEEN_GUESTS, \ CFG_WEBSEARCH_MAX_RECORDS_IN_GROUPS, \ CFG_ACCESS_CONTROL_LEVEL_ACCOUNTS, \ CFG_SITE_RECORD CFG_EXTERNAL_AUTH_USING_SSO = lazy_import('invenio.modules.access.local_config:CFG_EXTERNAL_AUTH_USING_SSO') CFG_EXTERNAL_AUTH_USING_SSO = lazy_import('invenio.modules.access.local_config:CFG_EXTERNAL_AUTH_LOGOUT_SSO') CFG_OPENID_PROVIDERS = lazy_import('invenio.modules.access.local_config:CFG_OPENID_PROVIDERS') CFG_OAUTH2_PROVIDERS = lazy_import('invenio.modules.access.local_config:CFG_OAUTH2_PROVIDERS') CFG_OAUTH1_PROVIDERS = lazy_import('invenio.modules.access.local_config:CFG_OAUTH1_PROVIDERS') CFG_OPENID_AUTHENTICATION = lazy_import('invenio.modules.access.local_config:CFG_OPENID_AUTHENTICATION') CFG_OAUTH2_AUTHENTICATION = lazy_import('invenio.modules.access.local_config:CFG_OAUTH2_AUTHENTICATION') CFG_OAUTH1_AUTHENTICATION = lazy_import('invenio.modules.access.local_config:CFG_OAUTH1_AUTHENTICATION') from invenio.utils.url import make_canonical_urlargd, create_url, create_html_link from invenio.utils.html import escape_html, nmtoken_from_string from invenio.base.i18n import gettext_set_language, language_list_long from invenio.modules.apikeys.models import WebAPIKey from invenio.legacy.websession.websession_config import CFG_WEBSESSION_GROUP_JOIN_POLICY class Template: def tmpl_back_form(self, ln, message, url, link): """ A standard one-message-go-back-link page. Parameters: - 'ln' *string* - The language to display the interface in - 'message' *string* - The message to display - 'url' *string* - The url to go back to - 'link' *string* - The link text """ out = """
%(message)s %(link)s
"""% { 'message' : message, 'url' : url, 'link' : link, 'ln' : ln } return out def tmpl_external_setting(self, ln, key, value): _ = gettext_set_language(ln) out = """ %s: %s """ % (key, value) return out def tmpl_external_user_settings(self, ln, html_settings): _ = gettext_set_language(ln) out = """

%(external_user_settings)s

%(html_settings)s

%(external_user_groups)s

%(consult_external_groups)s

""" % { 'external_user_settings' : _('External account settings'), 'html_settings' : html_settings, 'consult_external_groups' : _('You can consult the list of your external groups directly in the %(x_url_open)sgroups page%(x_url_close)s.', **{ 'x_url_open' : '' % ln, 'x_url_close' : '' }), 'external_user_groups' : _('External user groups'), } return out def tmpl_user_api_key(self, ln=CFG_SITE_LANG, keys_info=None): """ Displays all the API key that the user owns the user Parameters: - 'ln' *string* - The language to display the interface in - 'key_info' *tuples* - Contains the tuples with the key data (id, desciption, status) """ # load the right message language _ = gettext_set_language(ln) out = """

%(user_api_key)s

""" % { 'user_api_key' : _("API keys") } if keys_info and len(keys_info) != 0: out += "

%(user_keys)s

" % {'user_keys': _("These are your current API keys")} out += """ """ for key_info in keys_info: out += """ """ % { 'key_description': _("Description: " + cgi.escape(key_info[1])), 'key_status': _("Status: " + key_info[2]), 'key_id': _(key_info[0]), 'index': keys_info.index(key_info), 'key_label': _("API key"), 'remove_key' : _("Delete key"), 'sitesecureurl': CFG_SITE_SECURE_URL, 'input_type': ("submit", "hidden")[key_info[2] == WebAPIKey.CFG_WEB_API_KEY_STATUS['REVOKED']] } out += "
%(key_description)s %(key_status)s
%(key_label)s %(key_id)s
" out += """

%(create_new_key)s


(%(mandatory)s)

%(note)s: %(new_key_description_note)s
""" % { 'create_new_key' : _("If you want to create a new API key, please enter a description for it"), 'new_key_description_label' : _("Description for the new API key"), 'mandatory' : _("mandatory"), 'note' : _("Note"), 'new_key_description_note': _("The description should be something meaningful for you to recognize the API key"), 'create_new_key_button' : _("Create new key"), 'sitesecureurl': CFG_SITE_SECURE_URL } return out def tmpl_user_preferences(self, ln, email, email_disabled, password_disabled, nickname): """ Displays a form for the user to change his email/password. Parameters: - 'ln' *string* - The language to display the interface in - 'email' *string* - The email of the user - 'email_disabled' *boolean* - If the user has the right to edit his email - 'password_disabled' *boolean* - If the user has the right to edit his password - 'nickname' *string* - The nickname of the user (empty string if user does not have it) """ # load the right message language _ = gettext_set_language(ln) out = """

%(edit_params)s

%(change_user)s


(%(mandatory)s)
%(nickname_prefix)s%(nickname)s%(nickname_suffix)s
%(note)s: %(fixed_nickname_note)s

(%(mandatory)s)

%(example)s: john.doe@example.com
   
""" % { 'change_user' : _("If you want to change your email or set for the first time your nickname, please set new values in the form below."), 'edit_params' : _("Edit login credentials"), 'nickname_label' : _("Nickname"), 'nickname' : nickname, 'nickname_prefix' : nickname=='' and '
'+_("Example")+':johnd' or '', 'new_email' : _("New email address"), 'mandatory' : _("mandatory"), 'example' : _("Example"), 'note' : _("Note"), 'set_values' : _("Set new values"), 'email' : email, 'email_disabled' : email_disabled and "readonly" or "", 'sitesecureurl': CFG_SITE_SECURE_URL, 'fixed_nickname_note' : _('Since this is considered as a signature for comments and reviews, once set it can not be changed.') } if not password_disabled and not CFG_EXTERNAL_AUTH_USING_SSO: out += """

%(change_pass)s



%(note)s: %(old_password_note)s


%(note)s: %(password_note)s
   
""" % { 'change_pass' : _("If you want to change your password, please enter the old one and set the new value in the form below."), 'mandatory' : _("mandatory"), 'old_password' : _("Old password"), 'new_password' : _("New password"), 'optional' : _("optional"), 'note' : _("Note"), 'password_note' : _("The password phrase may contain punctuation, spaces, etc."), 'old_password_note' : _("You must fill the old password in order to set a new one."), 'retype_password' : _("Retype password"), 'set_values' : _("Set new password"), 'password_disabled' : password_disabled and "disabled" or "", 'sitesecureurl': CFG_SITE_SECURE_URL, } elif not CFG_EXTERNAL_AUTH_USING_SSO and CFG_CERN_SITE: out += "

" + _("""If you are using a lightweight CERN account you can %(x_url_open)sreset the password%(x_url_close)s.""", {'x_url_open' : '' % (make_canonical_urlargd({'email': email, 'returnurl': CFG_SITE_SECURE_URL + '/youraccount/edit' + make_canonical_urlargd({'lang' : ln}, {})}, {})), 'x_url_close' : ''}) + "

" elif CFG_EXTERNAL_AUTH_USING_SSO and CFG_CERN_SITE: out += "

" + _("""You can change or reset your CERN account password by means of the %(x_url_open)sCERN account system%(x_url_close)s.""") % \ {'x_url_open' : '', 'x_url_close' : ''} + "

" return out def tmpl_user_bibcatalog_auth(self, bibcatalog_username="", bibcatalog_password="", ln=CFG_SITE_LANG): """template for setting username and pw for bibcatalog backend""" _ = gettext_set_language(ln) out = """

%(edit_bibcatalog_settings)s

%(username)s: %(password)s:
""" % { 'sitesecureurl' : CFG_SITE_SECURE_URL, 'bibcatalog_username' : bibcatalog_username, 'bibcatalog_password' : bibcatalog_password, 'edit_bibcatalog_settings' : _("Edit cataloging interface settings"), 'username' : _("Username"), 'password' : _("Password"), 'update_settings' : _('Update settings') } return out def tmpl_user_lang_edit(self, ln, preferred_lang): _ = gettext_set_language(ln) out = """

%(edit_lang_settings)s

""" % { 'select_lang' : _('Select desired language of the web interface.'), 'update_settings' : _('Update settings') } return out def tmpl_user_websearch_edit(self, ln, current = 10, show_latestbox = True, show_helpbox = True): _ = gettext_set_language(ln) out = """

%(edit_websearch_settings)s

""" % { 'update_settings' : _("Update settings"), 'select_group_records' : _("Number of search results per page"), } return out def tmpl_user_external_auth(self, ln, methods, current, method_disabled): """ Displays a form for the user to change his authentication method. Parameters: - 'ln' *string* - The language to display the interface in - 'methods' *array* - The methods of authentication - 'method_disabled' *boolean* - If the user has the right to change this - 'current' *string* - The currently selected method """ # load the right message language _ = gettext_set_language(ln) out = """
%(edit_method)s

%(explain_method)s:

%(select_method)s: """ % { 'edit_method' : _("Edit login method"), 'explain_method' : _("Please select which login method you would like to use to authenticate yourself"), 'select_method' : _("Select method"), 'sitesecureurl': CFG_SITE_SECURE_URL, } for system in methods: out += """
""" % { 'system' : system, 'disabled' : method_disabled and 'disabled="disabled"' or "", 'selected' : current == system and 'checked="checked"' or "", 'id' : nmtoken_from_string(system), } out += """
 
""" % { 'select_method' : _("Select method"), } return out def tmpl_lost_password_form(self, ln): """ Displays a form for the user to ask for his password sent by email. Parameters: - 'ln' *string* - The language to display the interface in - 'msg' *string* - Explicative message on top of the form. """ # load the right message language _ = gettext_set_language(ln) out = "

" + _("If you have lost the password for your %(sitename)s %(x_fmt_open)sinternal account%(x_fmt_close)s, then please enter your email address in the following form in order to have a password reset link emailed to you.", **{'x_fmt_open' : '', 'x_fmt_close' : '', 'sitename' : CFG_SITE_NAME_INTL[ln]}) + "

" out += """
 
""" % { 'ln': ln, 'email' : _("Email address"), 'send' : _("Send password reset link"), } if CFG_CERN_SITE: out += "

" + _("If you have been using the %(x_fmt_open)sCERN login system%(x_fmt_close)s, then you can recover your password through the %(x_url_open)sCERN authentication system%(x_url_close)s.", **{'x_fmt_open' : '', 'x_fmt_close' : '', 'x_url_open' : '' % make_canonical_urlargd( {'lf': 'auth', 'returnURL': CFG_SITE_SECURE_URL + '/youraccount/login?ln='+ln}, {}), 'x_url_close' : ''}) + " " else: out += "

" + _("Note that if you have been using an external login system, then we cannot do anything and you have to ask there.") + " " out += _("Alternatively, you can ask %(x_name)s to change your login system from external to internal.", x_name=("""%(email)s""" % { 'email' : CFG_SITE_SUPPORT_EMAIL })) + "

" return out def tmpl_account_info(self, ln, uid, guest, CFG_CERN_SITE): """ Displays the account information Parameters: - 'ln' *string* - The language to display the interface in - 'uid' *string* - The user id - 'guest' *boolean* - If the user is guest - 'CFG_CERN_SITE' *boolean* - If the site is a CERN site """ # load the right message language _ = gettext_set_language(ln) out = """

%(account_offer)s

""" % { 'account_offer' : _("%(x_name)s offers you the possibility to personalize the interface, to set up your own personal library of documents, or to set up an automatic alert query that would run periodically and would notify you of search results by email.", x_name=CFG_SITE_NAME_INTL[ln]), } if not guest: out += """
%(your_settings)s
%(change_account)s
""" % { 'ln' : ln, 'your_settings' : _("Your Settings"), 'change_account' : _("Set or change your account email address or password. Specify your preferences about the look and feel of the interface.") } out += """
%(your_searches)s
%(search_explain)s
""" % { 'ln' : ln, 'your_searches' : _("Your Searches"), 'search_explain' : _("View all the searches you performed during the last 30 days."), } out += """
%(your_baskets)s
%(basket_explain)s""" % { 'ln' : ln, 'your_baskets' : _("Your Baskets"), 'basket_explain' : _("With baskets you can define specific collections of items, store interesting records you want to access later or share with others."), } if not guest: out += """
%(your_comments)s
%(comments_explain)s""" % { 'ln' : ln, 'your_comments' : _("Your Comments"), 'comments_explain' : _("Display all the comments you have submitted so far."), } if guest and CFG_WEBSESSION_DIFFERENTIATE_BETWEEN_GUESTS: out += self.tmpl_warning_guest_user(ln = ln, type = "baskets") out += """
%(your_alerts)s
%(explain_alerts)s""" % { 'ln' : ln, 'your_alerts' : _("Your Alerts"), 'explain_alerts' : _("Subscribe to a search which will be run periodically by our service. The result can be sent to you via Email or stored in one of your baskets."), } if guest and CFG_WEBSESSION_DIFFERENTIATE_BETWEEN_GUESTS: out += self.tmpl_warning_guest_user(type="alerts", ln = ln) out += "
" if CFG_CERN_SITE: out += """
%(your_loans)s
%(explain_loans)s
""" % { 'your_loans' : _("Your Loans"), 'explain_loans' : _("Check out book you have on loan, submit borrowing requests, etc. Requires CERN ID."), 'ln': ln, 'CFG_SITE_SECURE_URL': CFG_SITE_SECURE_URL } out += """
""" return out def tmpl_warning_guest_user(self, ln, type): """ Displays a warning message about the specified type Parameters: - 'ln' *string* - The language to display the interface in - 'type' *string* - The type of data that will get lost in case of guest account (for the moment: 'alerts' or 'baskets') """ # load the right message language _ = gettext_set_language(ln) if (type=='baskets'): msg = _("You are logged in as a guest user, so your baskets will disappear at the end of the current session.") + ' ' elif (type=='alerts'): msg = _("You are logged in as a guest user, so your alerts will disappear at the end of the current session.") + ' ' msg += _("If you wish you can %(x_url_open)slogin or register here%(x_url_close)s.", **{'x_url_open': '', 'x_url_close': ''}) return """
%s
""" % msg def tmpl_account_body(self, ln, user): """ Displays the body of the actions of the user Parameters: - 'ln' *string* - The language to display the interface in - 'user' *string* - The username (nickname or email) """ # load the right message language _ = gettext_set_language(ln) out = _("You are logged in as %(x_user)s. You may want to a) %(x_url1_open)slogout%(x_url1_close)s; b) edit your %(x_url2_open)saccount settings%(x_url2_close)s.") %\ {'x_user': user, 'x_url1_open': '', 'x_url1_close': '', 'x_url2_open': '', 'x_url2_close': '', } return out + "

" def tmpl_account_template(self, title, body, ln, url): """ Displays a block of the your account page Parameters: - 'ln' *string* - The language to display the interface in - 'title' *string* - The title of the block - 'body' *string* - The body of the block - 'url' *string* - The URL to go to the proper section """ out ="""
%s
%s
""" % (url, title, body) return out def tmpl_account_page(self, ln, warnings, warning_list, accBody, baskets, alerts, searches, messages, loans, groups, submissions, approvals, tickets, administrative, comments): """ Displays the your account page Parameters: - 'ln' *string* - The language to display the interface in - 'accBody' *string* - The body of the heading block - 'baskets' *string* - The body of the baskets block - 'alerts' *string* - The body of the alerts block - 'searches' *string* - The body of the searches block - 'messages' *string* - The body of the messages block - 'groups' *string* - The body of the groups block - 'submissions' *string* - The body of the submission block - 'approvals' *string* - The body of the approvals block - 'administrative' *string* - The body of the administrative block - 'comments' *string* - The body of the comments block """ # load the right message language _ = gettext_set_language(ln) out = "" if warnings == "1": out += self.tmpl_general_warnings(warning_list) out += self.tmpl_account_template(_("Your Account"), accBody, ln, '/youraccount/edit?ln=%s' % ln) if messages: out += self.tmpl_account_template(_("Your Messages"), messages, ln, '/yourmessages/display?ln=%s' % ln) if loans: out += self.tmpl_account_template(_("Your Loans"), loans, ln, '/yourloans/display?ln=%s' % ln) if baskets: out += self.tmpl_account_template(_("Your Baskets"), baskets, ln, '/yourbaskets/display?ln=%s' % ln) if comments: comments_description = _("You can consult the list of %(x_url_open)syour comments%(x_url_close)s submitted so far.") comments_description %= {'x_url_open': '', 'x_url_close': ''} out += self.tmpl_account_template(_("Your Comments"), comments_description, ln, '/yourcomments/?ln=%s' % ln) if alerts: out += self.tmpl_account_template(_("Your Alert Searches"), alerts, ln, '/youralerts/list?ln=%s' % ln) if searches: out += self.tmpl_account_template(_("Your Searches"), searches, ln, '/youralerts/display?ln=%s' % ln) if groups: groups_description = _("You can consult the list of %(x_url_open)syour groups%(x_url_close)s you are administering or are a member of.") groups_description %= {'x_url_open': '', 'x_url_close': ''} out += self.tmpl_account_template(_("Your Groups"), groups_description, ln, '/yourgroups/display?ln=%s' % ln) if submissions: submission_description = _("You can consult the list of %(x_url_open)syour submissions%(x_url_close)s and inquire about their status.") submission_description %= {'x_url_open': '', 'x_url_close': ''} out += self.tmpl_account_template(_("Your Submissions"), submission_description, ln, '/yoursubmissions.py?ln=%s' % ln) if approvals: approval_description = _("You can consult the list of %(x_url_open)syour approvals%(x_url_close)s with the documents you approved or refereed.") approval_description %= {'x_url_open': '', 'x_url_close': ''} out += self.tmpl_account_template(_("Your Approvals"), approval_description, ln, '/yourapprovals.py?ln=%s' % ln) #check if this user might have tickets if tickets: ticket_description = _("You can consult the list of %(x_url_open)syour tickets%(x_url_close)s.") ticket_description %= {'x_url_open': '', 'x_url_close': ''} out += self.tmpl_account_template(_("Your Tickets"), ticket_description, ln, '/yourtickets?ln=%s' % ln) if administrative: out += self.tmpl_account_template(_("Your Administrative Activities"), administrative, ln, '/admin') return out def tmpl_account_emailMessage(self, ln, msg): """ Displays a link to retrieve the lost password Parameters: - 'ln' *string* - The language to display the interface in - 'msg' *string* - Explicative message on top of the form. """ # load the right message language _ = gettext_set_language(ln) out ="" out +=""" %(msg)s %(try_again)s """ % { 'ln' : ln, 'msg' : msg, 'try_again' : _("Try again") } return out def tmpl_account_reset_password_email_body(self, email, reset_key, ip_address, ln=CFG_SITE_LANG): """ The body of the email that sends lost internal account passwords to users. """ _ = gettext_set_language(ln) out = """ %(intro)s %(intro2)s <%(link)s> %(outro)s %(outro2)s""" % { 'intro': _("Somebody (possibly you) coming from %(x_ip_address)s " "has asked\nfor a password reset at %(x_sitename)s\nfor " "the account \"%(x_email)s\"." % { 'x_sitename' :CFG_SITE_NAME_INTL.get(ln, CFG_SITE_NAME), 'x_email' : email, 'x_ip_address' : ip_address, } ), 'intro2' : _("If you want to reset the password for this account, please go to:"), 'link' : "%s/youraccount/resetpassword%s" % (CFG_SITE_SECURE_URL, make_canonical_urlargd({ 'ln' : ln, 'k' : reset_key }, {})), 'outro' : _("in order to confirm the validity of this request."), 'outro2' : _("Please note that this URL will remain valid for about %(days)s days only.", days=CFG_WEBSESSION_RESET_PASSWORD_EXPIRE_IN_DAYS), } return out def tmpl_account_address_activation_email_body(self, email, address_activation_key, ip_address, ln=CFG_SITE_LANG): """ The body of the email that sends email address activation cookie passwords to users. """ _ = gettext_set_language(ln) out = """ %(intro)s %(intro2)s <%(link)s> %(outro)s %(outro2)s""" % { 'intro': _("Somebody (possibly you) coming from %(x_ip_address)s " "has asked\nto register a new account at %(x_sitename)s\nfor the " "email address \"%(x_email)s\"." % { 'x_sitename' :CFG_SITE_NAME_INTL.get(ln, CFG_SITE_NAME), 'x_email' : email, 'x_ip_address' : ip_address, } ), 'intro2' : _("If you want to complete this account registration, please go to:"), 'link' : "%s/youraccount/access%s" % (CFG_SITE_SECURE_URL, make_canonical_urlargd({ 'ln' : ln, 'mailcookie' : address_activation_key }, {})), 'outro' : _("in order to confirm the validity of this request."), 'outro2' : _("Please note that this URL will remain valid for about %(days)s days only.", days=CFG_WEBSESSION_ADDRESS_ACTIVATION_EXPIRE_IN_DAYS), } return out def tmpl_account_emailSent(self, ln, email): """ Displays a confirmation message for an email sent Parameters: - 'ln' *string* - The language to display the interface in - 'email' *string* - The email to which the message has been sent """ # load the right message language _ = gettext_set_language(ln) out ="" out += _("Okay, a password reset link has been emailed to %(x_email)s.", x_email=email) return out def tmpl_account_delete(self, ln): """ Displays a confirmation message about deleting the account Parameters: - 'ln' *string* - The language to display the interface in """ # load the right message language _ = gettext_set_language(ln) out = "

" + _("""Deleting your account""") + '

' return out def tmpl_account_logout(self, ln): """ Displays a confirmation message about logging out Parameters: - 'ln' *string* - The language to display the interface in """ # load the right message language _ = gettext_set_language(ln) out = _("You are no longer recognized by our system.") + ' ' if CFG_EXTERNAL_AUTH_USING_SSO and CFG_EXTERNAL_AUTH_LOGOUT_SSO: out += _("""You are still recognized by the centralized %(x_fmt_open)sSSO%(x_fmt_close)s system. You can %(x_url_open)slogout from SSO%(x_url_close)s, too.""") % \ {'x_fmt_open' : '', 'x_fmt_close' : '', 'x_url_open' : '' % CFG_EXTERNAL_AUTH_LOGOUT_SSO, 'x_url_close' : ''} out += '
' out += _("If you wish you can %(x_url_open)slogin here%(x_url_close)s.") % \ {'x_url_open': '', 'x_url_close': ''} return out def tmpl_login_form(self, ln, referer, internal, register_available, methods, selected_method, msg=None): """ Displays a login form Parameters: - 'ln' *string* - The language to display the interface in - 'referer' *string* - The referer URL - will be redirected upon after login - 'internal' *boolean* - If we are producing an internal authentication - 'register_available' *boolean* - If users can register freely in the system - 'methods' *array* - The available authentication methods - 'selected_method' *string* - The default authentication method - 'msg' *string* - The message to print before the form, if needed """ # load the right message language _ = gettext_set_language(ln) out = "
" if msg is "": out += "

%(please_login)s

" % { 'please_login' : cgi.escape(_("If you already have an account, please login using the form below.")) } if CFG_CERN_SITE: out += "

" + _("If you don't own a CERN account yet, you can register a %(x_url_open)snew CERN lightweight account%(x_url_close)s.", **{'x_url_open' : '', 'x_url_close' : ''}) + "

" else: if register_available: out += "

"+_("If you don't own an account yet, please %(x_url_open)sregister%(x_url_close)s an internal account.") %\ {'x_url_open': '', 'x_url_close': ''} + "

" else: # users cannot register accounts, so advise them # how to get one, or be silent about register # facility if account level is more than 4: if CFG_ACCESS_CONTROL_LEVEL_ACCOUNTS < 5: out += "

" + _("If you don't own an account yet, please contact %(x_name)s.", x_name=('%s' % (cgi.escape(CFG_SITE_SUPPORT_EMAIL, True), cgi.escape(CFG_SITE_SUPPORT_EMAIL)))) + "

" else: out += "

%s

" % msg out += """
""" % {'CFG_SITE_SECURE_URL': CFG_SITE_SECURE_URL} if len(methods) - CFG_OPENID_AUTHENTICATION - CFG_OAUTH2_AUTHENTICATION - CFG_OAUTH1_AUTHENTICATION > 1: # more than one method, must make a select login_select = """" out += """ """ % { 'login_title' : cgi.escape(_("Login method:")), 'login_select' : login_select, } else: # only one login method available out += """""" % cgi.escape(methods[0], True) out += """
%(login_select)s
""" % { 'ln': cgi.escape(ln, True), 'referer' : cgi.escape(referer, True), 'username' : cgi.escape(_("Username")), 'password' : cgi.escape(_("Password")), 'remember_me' : cgi.escape(_("Remember login on this computer.")), 'login' : cgi.escape(_("login")), } if internal: out += """   (%(lost_pass)s)""" % { 'ln' : cgi.escape(ln, True), 'lost_pass' : cgi.escape(_("Lost your password?")) } out += """
""" out += """

%(note)s: %(note_text)s

""" % { 'note' : cgi.escape(_("Note")), 'note_text': cgi.escape(_("You can use your nickname or your email address to login."))} out += "
" if CFG_OPENID_AUTHENTICATION or \ CFG_OAUTH2_AUTHENTICATION or \ CFG_OAUTH1_AUTHENTICATION: # If OpenID or OAuth authentication is enabled, we put the login # forms of providers. out += self.tmpl_external_login_panel(ln, referer) return out def tmpl_lost_your_password_teaser(self, ln=CFG_SITE_LANG): """Displays a short sentence to attract user to the fact that maybe he lost his password. Used by the registration page. """ _ = gettext_set_language(ln) out = "" out += """%(maybe_lost_pass)s""" % { 'ln' : ln, 'maybe_lost_pass': ("Maybe you have lost your password?") } return out def tmpl_reset_password_form(self, ln, email, reset_key, msg=''): """Display a form to reset the password.""" _ = gettext_set_language(ln) out = "" out = "

%s

" % _("Your request is valid. Please set the new " "desired password in the following form.") if msg: out += """

%s

""" % msg out += """
%(set_password_for)s:%(email)s
""" % { 'ln' : ln, 'reset_key' : reset_key, 'email' : email, 'set_password_for' : _('Set a new password for'), 'type_new_password' : _('Type the new password'), 'type_it_again' : _('Type again the new password'), 'set_new_password' : _('Set the new password') } return out def tmpl_register_page(self, ln, referer, level): """ Displays a login form Parameters: - 'ln' *string* - The language to display the interface in - 'referer' *string* - The referer URL - will be redirected upon after login - 'level' *int* - Login level (0 - all access, 1 - accounts activated, 2+ - no self-registration) """ # load the right message language _ = gettext_set_language(ln) out = "" if level <= 1: out += _("Please enter your email address and desired nickname and password:") if level == 1: out += _("It will not be possible to use the account before it has been verified and activated.") out += """

(%(mandatory)s)

%(example)s: john.doe@example.com

(%(mandatory)s)

%(example)s: johnd

(%(optional)s)

%(note)s: %(password_contain)s

%(note)s: %(explain_acc)s""" % { 'referer' : cgi.escape(referer), 'ln' : cgi.escape(ln), 'email_address' : _("Email address"), 'nickname' : _("Nickname"), 'password' : _("Password"), 'mandatory' : _("mandatory"), 'optional' : _("optional"), 'example' : _("Example"), 'note' : _("Note"), 'password_contain' : _("The password phrase may contain punctuation, spaces, etc."), 'retype' : _("Retype Password"), 'register' : _("register"), 'explain_acc' : _("Please do not use valuable passwords such as your Unix, AFS or NICE passwords with this service. Your email address will stay strictly confidential and will not be disclosed to any third party. It will be used to identify you for personal services of %(x_name)s. For example, you may set up an automatic alert search that will look for new preprints and will notify you daily of new arrivals by email.", x_name=CFG_SITE_NAME), } else: # level >=2, so users cannot register accounts out += "

" + _("It is not possible to create an account yourself. Contact %(x_name)s if you want an account.", x_name=('%s' % (CFG_SITE_SUPPORT_EMAIL, CFG_SITE_SUPPORT_EMAIL))) + "

" return out def tmpl_account_adminactivities(self, ln, uid, guest, roles, activities): """ Displays the admin activities block for this user Parameters: - 'ln' *string* - The language to display the interface in - 'uid' *string* - The used id - 'guest' *boolean* - If the user is guest - 'roles' *array* - The current user roles - 'activities' *array* - The user allowed activities """ # load the right message language _ = gettext_set_language(ln) out = "" # guest condition if guest: return _("You seem to be a guest user. You have to %(x_url_open)slogin%(x_url_close)s first.", x_url_open='', x_url_close='') # no rights condition if not roles: return "

" + _("You are not authorized to access administrative functions.") + "

" # displaying form out += "

" + _("You are enabled to the following roles: %(x_role)s.", x_role=('' + ", ".join(roles) + "")) + '

' if activities: # print proposed links: activities.sort(lambda x, y: cmp(x.lower(), y.lower())) tmp_out = '' for action in activities: if action == "runbibedit": tmp_out += """
   
%s""" % (CFG_SITE_URL, CFG_SITE_RECORD, _("Run Record Editor")) if action == "runbibeditmulti": tmp_out += """
    %s""" % (CFG_SITE_URL, CFG_SITE_RECORD, _("Run Multi-Record Editor")) if action == "runbibcirculation": tmp_out += """
    %s""" % (CFG_SITE_URL, ln, _("Run BibCirculation")) if action == "runbibmerge": tmp_out += """
    %s""" % (CFG_SITE_URL, CFG_SITE_RECORD, _("Run Record Merger")) if action == "runbibswordclient": tmp_out += """
    %s""" % (CFG_SITE_URL, CFG_SITE_RECORD, _("Run BibSword Client")) if action == "runbatchuploader": tmp_out += """
    %s""" % (CFG_SITE_URL, ln, _("Run Batch Uploader")) if action == "cfgbibformat": tmp_out += """
    %s""" % (CFG_SITE_URL, ln, _("Configure BibFormat")) if action == "cfgbibknowledge": tmp_out += """
    %s""" % (CFG_SITE_URL, ln, _("Configure BibKnowledge")) if action == "cfgoaiharvest": tmp_out += """
    %s""" % (CFG_SITE_URL, ln, _("Configure OAI Harvest")) if action == "cfgoairepository": tmp_out += """
    %s""" % (CFG_SITE_URL, ln, _("Configure OAI Repository")) if action == "cfgbibindex": tmp_out += """
    %s""" % (CFG_SITE_URL, ln, _("Configure BibIndex")) if action == "cfgbibrank": tmp_out += """
    %s""" % (CFG_SITE_URL, ln, _("Configure BibRank")) if action == "cfgwebaccess": tmp_out += """
    %s""" % (CFG_SITE_URL, ln, _("Configure WebAccess")) if action == "cfgwebcomment": tmp_out += """
    %s""" % (CFG_SITE_URL, ln, _("Configure WebComment")) if action == "cfgweblinkback": tmp_out += """
    %s""" % (CFG_SITE_URL, ln, _("Configure WebLinkback")) if action == "cfgwebjournal": tmp_out += """
    %s""" % (CFG_SITE_URL, ln, _("Configure WebJournal")) if action == "cfgwebsearch": tmp_out += """
    %s""" % (CFG_SITE_URL, ln, _("Configure WebSearch")) if action == "cfgwebsubmit": tmp_out += """
    %s""" % (CFG_SITE_URL, ln, _("Configure WebSubmit")) if action == "runbibdocfile": tmp_out += """
    %s""" % (CFG_SITE_URL, CFG_SITE_RECORD, ln, _("Run Document File Manager")) if action == "cfgbibsort": tmp_out += """
    %s""" % (CFG_SITE_URL, ln, _("Configure BibSort")) if tmp_out: out += _("Here are some interesting web admin links for you:") + tmp_out out += "
" + _("For more admin-level activities, see the complete %(x_url_open)sAdmin Area%(x_url_close)s.", x_url_open='', x_url_close='') return out def tmpl_create_userinfobox(self, ln, url_referer, guest, username, submitter, referee, admin, usebaskets, usemessages, usealerts, usegroups, useloans, usestats): """ Displays the user block Parameters: - 'ln' *string* - The language to display the interface in - 'url_referer' *string* - URL of the page being displayed - 'guest' *boolean* - If the user is guest - 'username' *string* - The username (nickname or email) - 'submitter' *boolean* - If the user is submitter - 'referee' *boolean* - If the user is referee - 'admin' *boolean* - If the user is admin - 'usebaskets' *boolean* - If baskets are enabled for the user - 'usemessages' *boolean* - If messages are enabled for the user - 'usealerts' *boolean* - If alerts are enabled for the user - 'usegroups' *boolean* - If groups are enabled for the user - 'useloans' *boolean* - If loans are enabled for the user - 'usestats' *boolean* - If stats are enabled for the user @note: with the update of CSS classes (cds.cds -> invenio.css), the variables useloans etc are not used in this function, since they are in the menus. But we keep them in the function signature for backwards compatibility. """ # load the right message language _ = gettext_set_language(ln) out = """ """ % CFG_SITE_URL if guest: out += """%(guest_msg)s :: %(login)s""" % { 'sitesecureurl': CFG_SITE_SECURE_URL, 'ln' : ln, 'guest_msg' : _("guest"), 'referer' : url_referer and ('&referer=%s' % urllib.quote(url_referer)) or '', 'login' : _('login') } else: out += """ %(username)s :: """ % { 'sitesecureurl' : CFG_SITE_SECURE_URL, 'ln' : ln, 'username' : username } out += """%(logout)s""" % { 'sitesecureurl' : CFG_SITE_SECURE_URL, 'ln' : ln, 'logout' : _("logout"), } return out def tmpl_create_useractivities_menu(self, ln, selected, url_referer, guest, username, submitter, referee, admin, usebaskets, usemessages, usealerts, usegroups, useloans, usestats, usecomments): """ Returns the main navigation menu with actions based on user's priviledges @param ln: The language to display the interface in @type ln: string @param selected: If the menu is currently selected @type selected: boolean @param url_referer: URL of the page being displayed @type url_referer: string @param guest: If the user is guest @type guest: string @param username: The username (nickname or email) @type username: string @param submitter: If the user is submitter @type submitter: boolean @param referee: If the user is referee @type referee: boolean @param admin: If the user is admin @type admin: boolean @param usebaskets: If baskets are enabled for the user @type usebaskets: boolean @param usemessages: If messages are enabled for the user @type usemessages: boolean @param usealerts: If alerts are enabled for the user @type usealerts: boolean @param usegroups: If groups are enabled for the user @type usegroups: boolean @param useloans: If loans are enabled for the user @type useloans: boolean @param usestats: If stats are enabled for the user @type usestats: boolean @param usecomments: If comments are enabled for the user @type usecomments: boolean @return: html menu of the user activities @rtype: string """ # load the right message language _ = gettext_set_language(ln) out = '''
%(personalize)s
' return out def tmpl_create_adminactivities_menu(self, ln, selected, url_referer, guest, username, submitter, referee, admin, usebaskets, usemessages, usealerts, usegroups, useloans, usestats, activities): """ Returns the main navigation menu with actions based on user's priviledges @param ln: The language to display the interface in @type ln: string @param selected: If the menu is currently selected @type selected: boolean @param url_referer: URL of the page being displayed @type url_referer: string @param guest: If the user is guest @type guest: string @param username: The username (nickname or email) @type username: string @param submitter: If the user is submitter @type submitter: boolean @param referee: If the user is referee @type referee: boolean @param admin: If the user is admin @type admin: boolean @param usebaskets: If baskets are enabled for the user @type usebaskets: boolean @param usemessages: If messages are enabled for the user @type usemessages: boolean @param usealerts: If alerts are enabled for the user @type usealerts: boolean @param usegroups: If groups are enabled for the user @type usegroups: boolean @param useloans: If loans are enabled for the user @type useloans: boolean @param usestats: If stats are enabled for the user @type usestats: boolean @param activities: dictionary of admin activities @rtype activities: dict @return: html menu of the user activities @rtype: string """ # load the right message language _ = gettext_set_language(ln) out = '' if activities: out += '''
%(admin)s
' return out def tmpl_warning(self, warnings, ln=CFG_SITE_LANG): """ Display len(warnings) warning fields @param infos: list of strings @param ln=language @return: html output """ if not((type(warnings) is list) or (type(warnings) is tuple)): warnings = [warnings] warningbox = "" if warnings != []: warningbox = "
\n Warning:\n" for warning in warnings: lines = warning.split("\n") warningbox += "

" for line in lines[0:-1]: warningbox += line + "
\n" warningbox += lines[-1] + "

" warningbox += "

\n" return warningbox def tmpl_error(self, error, ln=CFG_SITE_LANG): """ Display error @param error: string @param ln=language @return: html output """ _ = gettext_set_language(ln) errorbox = "" if error != "": errorbox = "
\n Error:\n" errorbox += "

" errorbox += error + "

" errorbox += "

\n" return errorbox def tmpl_display_all_groups(self, infos, admin_group_html, member_group_html, external_group_html = None, warnings=[], ln=CFG_SITE_LANG): """ Displays the 3 tables of groups: admin, member and external Parameters: - 'ln' *string* - The language to display the interface in - 'admin_group_html' *string* - HTML code for displaying all the groups the user is the administrator of - 'member_group_html' *string* - HTML code for displaying all the groups the user is member of - 'external_group_html' *string* - HTML code for displaying all the external groups the user is member of """ _ = gettext_set_language(ln) group_text = self.tmpl_infobox(infos) group_text += self.tmpl_warning(warnings) if external_group_html: group_text += """
%s

%s

%s
""" %(admin_group_html, member_group_html, external_group_html) else: group_text += """
%s

%s
""" %(admin_group_html, member_group_html) return group_text def tmpl_display_admin_groups(self, groups, ln=CFG_SITE_LANG): """ Display the groups the user is admin of. Parameters: - 'ln' *string* - The language to display the interface in - 'groups' *list* - All the group the user is admin of - 'infos' *list* - Display infos on top of admin group table """ _ = gettext_set_language(ln) img_link = """ %(text)s
%(text)s
""" out = self.tmpl_group_table_title(img="/img/group_admin.png", text=_("You are an administrator of the following groups:") ) out += """ """ %(_("Group"), _("Description")) if len(groups) == 0: out += """ """ %(_("You are not an administrator of any groups."),) for group_data in groups: (grpID, name, description) = group_data edit_link = img_link % {'siteurl' : CFG_SITE_URL, 'grpID' : grpID, 'ln': ln, 'img':"webbasket_create_small.png", 'text':_("Edit group"), 'action':"edit" } members_link = img_link % {'siteurl' : CFG_SITE_URL, 'grpID' : grpID, 'ln': ln, 'img':"webbasket_usergroup.png", 'text':_("Edit %(x_num)s members", x_num=''), 'action':"members" } out += """ """ % (cgi.escape(name), cgi.escape(description), edit_link, members_link) out += """
%s %s    
%s
%s %s %s %s
     
""" % {'ln': ln, 'write_label': _("Create new group"), } return out def tmpl_display_member_groups(self, groups, ln=CFG_SITE_LANG): """ Display the groups the user is member of. Parameters: - 'ln' *string* - The language to display the interface in - 'groups' *list* - All the group the user is member of """ _ = gettext_set_language(ln) group_text = self.tmpl_group_table_title(img="/img/webbasket_us.png", text=_("You are a member of the following groups:")) group_text += """ """ % (_("Group"), _("Description")) if len(groups) == 0: group_text += """ """ %(_("You are not a member of any groups."),) for group_data in groups: (id, name, description) = group_data group_text += """ """ % (cgi.escape(name), cgi.escape(description)) group_text += """
%s %s
%s
%s %s
""" % {'ln': ln, 'join_label': _("Join new group"), 'leave_label':_("Leave group") } return group_text def tmpl_display_external_groups(self, groups, ln=CFG_SITE_LANG): """ Display the external groups the user is member of. Parameters: - 'ln' *string* - The language to display the interface in - 'groups' *list* - All the group the user is member of """ _ = gettext_set_language(ln) group_text = self.tmpl_group_table_title(img="/img/webbasket_us.png", text=_("You are a member of the following external groups:")) group_text += """ """ % (_("Group"), _("Description")) if len(groups) == 0: group_text += """ """ %(_("You are not a member of any external groups."),) for group_data in groups: (id, name, description) = group_data group_text += """ """ % (cgi.escape(name), cgi.escape(description)) group_text += """
%s %s
%s
%s %s
""" return group_text def tmpl_display_input_group_info(self, group_name, group_description, join_policy, act_type="create", grpID=None, warnings=[], ln=CFG_SITE_LANG): """ Display group data when creating or updating a group: Name, description, join_policy. Parameters: - 'ln' *string* - The language to display the interface in - 'group_name' *string* - name of the group - 'group_description' *string* - description of the group - 'join_policy' *string* - join policy - 'act_type' *string* - info about action : create or edit(update) - 'grpID' *int* - ID of the group(not None in case of group editing) - 'warnings' *list* - Display warning if values are not correct """ _ = gettext_set_language(ln) #default hidden_id ="" form_name = "create_group" action = CFG_SITE_URL + '/yourgroups/create' button_label = _("Create new group") button_name = "create_button" label = _("Create new group") delete_text = "" if act_type == "update": form_name = "update_group" action = CFG_SITE_URL + '/yourgroups/edit' button_label = _("Update group") button_name = "update" label = _('Edit group %(x_name)s', x_name=cgi.escape(group_name)) delete_text = """""" delete_text %= (_("Delete group"),"delete") if grpID is not None: hidden_id = """""" hidden_id %= grpID out = self.tmpl_warning(warnings) out += """
%(label)s %(label)s
%(join_policy_label)s %(join_policy)s
%(hidden_id)s
%(delete_text)s
""" out %= {'action' : action, 'logo': CFG_SITE_URL + '/img/webbasket_create.png', 'label': label, 'form_name' : form_name, 'name_label': _("Group name:"), 'delete_text': delete_text, 'description_label': _("Group description:"), 'join_policy_label': _("Group join policy:"), 'group_name': cgi.escape(group_name, 1), 'group_description': cgi.escape(group_description, 1), 'button_label': button_label, 'button_name':button_name, 'cancel_label':_("Cancel"), 'hidden_id':hidden_id, 'ln': ln, 'join_policy' :self.__create_join_policy_selection_menu("join_policy", join_policy, ln) } return out def tmpl_display_input_join_group(self, group_list, group_name, group_from_search, search, warnings=[], ln=CFG_SITE_LANG): """ Display the groups the user can join. He can use default select list or the search box Parameters: - 'ln' *string* - The language to display the interface in - 'group_list' *list* - All the group the user can join - 'group_name' *string* - Name of the group the user is looking for - 'group_from search' *list* - List of the group the user can join matching group_name - 'search' *int* - User is looking for group using group_name - 'warnings' *list* - Display warning if two group are selected """ _ = gettext_set_language(ln) out = self.tmpl_warning(warnings) search_content = "" if search: search_content = """ """ if group_from_search != []: search_content += self.__create_select_menu('grpID', group_from_search, _("Please select:")) else: search_content += _("No matching group") search_content += """ """ out += """
%(label)s %(label)s
%(search_content)s
%(list_label)s %(group_list)s  



""" out %= {'action' : CFG_SITE_URL + '/yourgroups/join', 'logo': CFG_SITE_URL + '/img/webbasket_create.png', 'label': _("Join group"), 'group_name': cgi.escape(group_name, 1), 'label2':_("or find it") + ': ', 'list_label':_("Choose group:"), 'ln': ln, 'find_label': _("Find group"), 'cancel_label':_("Cancel"), 'group_list' :self.__create_select_menu("grpID",group_list, _("Please select:")), 'search_content' : search_content } return out def tmpl_display_manage_member(self, grpID, group_name, members, pending_members, infos=[], warnings=[], ln=CFG_SITE_LANG): """Display current members and waiting members of a group. Parameters: - 'ln' *string* - The language to display the interface in - 'grpID *int* - ID of the group - 'group_name' *string* - Name of the group - 'members' *list* - List of the current members - 'pending_members' *list* - List of the waiting members - 'infos' *tuple of 2 lists* - Message to inform user about his last action - 'warnings' *list* - Display warning if two group are selected """ _ = gettext_set_language(ln) out = self.tmpl_warning(warnings) out += self.tmpl_infobox(infos) out += """

%(title)s

%(img_alt_header1)s %(header1)s
 
%(member_text)s
%(img_alt_header2)s %(header2)s
 
%(pending_text)s
%(img_alt_header3)s %(header3)s
 
%(invite_text)s
""" if members : member_list = self.__create_select_menu("member_id", members, _("Please select:")) member_text = """ %s """ % (member_list,_("Remove member")) else : member_text = """%s""" % _("No members.") if pending_members : pending_list = self.__create_select_menu("pending_member_id", pending_members, _("Please select:")) pending_text = """ %s """ % (pending_list,_("Accept member"), _("Reject member")) else : pending_text = """%s""" % _("No members awaiting approval.") header1 = self.tmpl_group_table_title(text=_("Current members")) header2 = self.tmpl_group_table_title(text=_("Members awaiting approval")) header3 = _("Invite new members") write_a_message_url = create_url( "%s/yourmessages/write" % CFG_SITE_URL, { 'ln' : ln, 'msg_subject' : _('Invitation to join "%(x_name)s" group', x_name=escape_html(group_name)), 'msg_body' : _("""\ Hello: I think you might be interested in joining the group "%(x_name)s". You can join by clicking here: %(x_url)s. Best regards. -""") % {'x_name': group_name, +""", **{'x_name': group_name, 'x_url': create_html_link("%s/yourgroups/join" % CFG_SITE_URL, { 'grpID' : grpID, 'join_button' : "1", }, - link_label=group_name, escape_urlargd=True, escape_linkattrd=True)}}) + link_label=group_name, escape_urlargd=True, escape_linkattrd=True)})}) link_open = '' % escape_html(write_a_message_url) - invite_text = _("If you want to invite new members to join your group, please use the %(x_url_open)sweb message%(x_url_close)s system.") % \ - {'x_url_open': link_open, - 'x_url_close': ''} + invite_text = _("If you want to invite new members to join your group, please use the %(x_url_open)sweb message%(x_url_close)s system.", + **{'x_url_open': link_open, 'x_url_close': ''}) action = CFG_SITE_URL + '/yourgroups/members?ln=' + ln out %= {'title':_('Group: %(x_name)s', x_name=escape_html(group_name)), 'member_text' : member_text, 'pending_text' :pending_text, 'action':action, 'grpID':grpID, 'header1': header1, 'header2': header2, 'header3': header3, 'img_alt_header1': _("Current members"), 'img_alt_header2': _("Members awaiting approval"), 'img_alt_header3': _("Invite new members"), 'invite_text': invite_text, 'imgurl': CFG_SITE_URL + '/img', 'cancel_label':_("Cancel"), 'ln':ln } return out def tmpl_display_input_leave_group(self, groups, warnings=[], ln=CFG_SITE_LANG): """Display groups the user can leave. Parameters: - 'ln' *string* - The language to display the interface in - 'groups' *list* - List of groups the user is currently member of - 'warnings' *list* - Display warning if no group is selected """ _ = gettext_set_language(ln) out = self.tmpl_warning(warnings) out += """
%(label)s %(label)s
%(list_label)s %(groups)s  
%(submit)s
""" if groups: groups = self.__create_select_menu("grpID", groups, _("Please select:")) list_label = _("Group list") submit = """""" % _("Leave group") else : groups = _("You are not member of any group.") list_label = "" submit = "" action = CFG_SITE_URL + '/yourgroups/leave?ln=%s' action %= (ln) out %= {'groups' : groups, 'list_label' : list_label, 'action':action, 'logo': CFG_SITE_URL + '/img/webbasket_create.png', 'label' : _("Leave group"), 'cancel_label':_("Cancel"), 'ln' :ln, 'submit' : submit } return out def tmpl_confirm_delete(self, grpID, ln=CFG_SITE_LANG): """ display a confirm message when deleting a group @param grpID *int* - ID of the group @param ln: language @return: html output """ _ = gettext_set_language(ln) action = CFG_SITE_URL + '/yourgroups/edit' out = """
%(message)s
"""% {'message': _("Are you sure you want to delete this group?"), 'ln':ln, 'yes_label': _("Yes"), 'no_label': _("No"), 'grpID':grpID, 'action': action } return out def tmpl_confirm_leave(self, uid, grpID, ln=CFG_SITE_LANG): """ display a confirm message @param grpID *int* - ID of the group @param ln: language @return: html output """ _ = gettext_set_language(ln) action = CFG_SITE_URL + '/yourgroups/leave' out = """
%(message)s
"""% {'message': _("Are you sure you want to leave this group?"), 'ln':ln, 'yes_label': _("Yes"), 'no_label': _("No"), 'grpID':grpID, 'action': action } return out def __create_join_policy_selection_menu(self, name, current_join_policy, ln=CFG_SITE_LANG): """Private function. create a drop down menu for selection of join policy @param current_join_policy: join policy as defined in CFG_WEBSESSION_GROUP_JOIN_POLICY @param ln: language """ _ = gettext_set_language(ln) elements = [(CFG_WEBSESSION_GROUP_JOIN_POLICY['VISIBLEOPEN'], _("Visible and open for new members")), (CFG_WEBSESSION_GROUP_JOIN_POLICY['VISIBLEMAIL'], _("Visible but new members need approval")) ] select_text = _("Please select:") return self.__create_select_menu(name, elements, select_text, selected_key=current_join_policy) def __create_select_menu(self, name, elements, select_text, multiple=0, selected_key=None): """ private function, returns a popup menu @param name: name of HTML control @param elements: list of (key, value) """ if multiple : out = """ """ % name out += '' % (select_text) for (key, label) in elements: selected = '' if key == selected_key: selected = ' selected="selected"' out += ''% (key, selected, label) out += '' return out def tmpl_infobox(self, infos, ln=CFG_SITE_LANG): """Display len(infos) information fields @param infos: list of strings @param ln=language @return: html output """ _ = gettext_set_language(ln) if not((type(infos) is list) or (type(infos) is tuple)): infos = [infos] infobox = "" for info in infos: infobox += '
' lines = info.split("\n") for line in lines[0:-1]: infobox += line + "
\n" infobox += lines[-1] + "
\n" return infobox def tmpl_navtrail(self, ln=CFG_SITE_LANG, title=""): """ display the navtrail, e.g.: Your account > Your group > title @param title: the last part of the navtrail. Is not a link @param ln: language return html formatted navtrail """ _ = gettext_set_language(ln) nav_h1 = '%s' nav_h2 = "" if (title != ""): nav_h2 = ' > %s' nav_h2 = nav_h2 % (CFG_SITE_URL, _("Your Groups")) return nav_h1 % (CFG_SITE_URL, _("Your Account")) + nav_h2 def tmpl_group_table_title(self, img="", text="", ln=CFG_SITE_LANG): """ display the title of a table: - 'img' *string* - img path - 'text' *string* - title - 'ln' *string* - The language to display the interface in """ out = "
" if img: out += """ """ % (CFG_SITE_URL + img) out += """ %s
""" % text return out def tmpl_admin_msg(self, group_name, grpID, ln=CFG_SITE_LANG): """ return message content for joining group - 'group_name' *string* - name of the group - 'grpID' *int* - ID of the group - 'ln' *string* - The language to display the interface in """ _ = gettext_set_language(ln) subject = _("Group %(x_name)s: New membership request", x_name=group_name) url = CFG_SITE_URL + "/yourgroups/members?grpID=%s&ln=%s" url %= (grpID, ln) # FIXME: which user? We should show his nickname. body = (_("A user wants to join the group %(x_name)s.", x_name=group_name)) + '
' body += _("Please %(x_url_open)saccept or reject%(x_url_close)s this user's request.", x_url_open='', x_url_close='') body += '
' return subject, body def tmpl_member_msg(self, group_name, accepted=0, ln=CFG_SITE_LANG): """ return message content when new member is accepted/rejected - 'group_name' *string* - name of the group - 'accepted' *int* - 1 if new membership has been accepted, 0 if it has been rejected - 'ln' *string* - The language to display the interface in """ _ = gettext_set_language(ln) if accepted: subject = _("Group %(x_name)s: Join request has been accepted", x_name=group_name) body = _("Your request for joining group %(x_name)s has been accepted.", x_name=group_name) else: subject = _("Group %(x_name)s: Join request has been rejected", x_name=group_name) body = _("Your request for joining group %(x_name)s has been rejected.", x_name=group_name) url = CFG_SITE_URL + "/yourgroups/display?ln=" + ln body += '
' body += _("You can consult the list of %(x_url_open)syour groups%(x_url_close)s.", x_url_open='', x_url_close='') body += '
' return subject, body def tmpl_delete_msg(self, group_name, ln=CFG_SITE_LANG): """ return message content when new member is accepted/rejected - 'group_name' *string* - name of the group - 'ln' *string* - The language to display the interface in """ _ = gettext_set_language(ln) subject = _("Group %(x_name)s has been deleted", x_name=group_name) url = CFG_SITE_URL + "/yourgroups/display?ln=" + ln body = _("Group %(x_name)s has been deleted by its administrator.", x_name=group_name) body += '
' body += _("You can consult the list of %(x_url_open)syour groups%(x_url_close)s.", **{'x_url_open': '', 'x_url_close': ''}) body += '
' return subject, body def tmpl_group_info(self, nb_admin_groups=0, nb_member_groups=0, nb_total_groups=0, ln=CFG_SITE_LANG): """ display infos about groups (used by myaccount.py) @param nb_admin_group: number of groups the user is admin of @param nb_member_group: number of groups the user is member of @param total_group: number of groups the user belongs to @param ln: language return: html output. """ _ = gettext_set_language(ln) out = _("You can consult the list of %(x_url_open)s%(x_nb_total)i groups%(x_url_close)s you are subscribed to (%(x_nb_member)i) or administering (%(x_nb_admin)i).") out %= {'x_url_open': '', 'x_nb_total': nb_total_groups, 'x_url_close': '', 'x_nb_admin': nb_admin_groups, 'x_nb_member': nb_member_groups} return out def tmpl_general_warnings(self, warning_list, ln=CFG_SITE_LANG): """ display information to the admin user about possible ssecurity problems in the system. """ message = "" _ = gettext_set_language(ln) #Try and connect to the mysql database with the default invenio password if "warning_mysql_password_equal_to_invenio_password" in warning_list: message += "

" message += _("Warning: The password set for MySQL root user is the same as the default Invenio password. For security purposes, you may want to change the password.") message += "

" #Try and connect to the invenio database with the default invenio password if "warning_invenio_password_equal_to_default" in warning_list: message += "

" message += _("Warning: The password set for the Invenio MySQL user is the same as the shipped default. For security purposes, you may want to change the password.") message += "

" #Check if the admin password is empty if "warning_empty_admin_password" in warning_list: message += "

" message += _("Warning: The password set for the Invenio admin user is currently empty. For security purposes, it is strongly recommended that you add a password.") message += "

" #Check if the admin email has been changed from the default if "warning_site_support_email_equal_to_default" in warning_list: message += "

" message += _("Warning: The email address set for support email is currently set to info@invenio-software.org. It is recommended that you change this to your own address.") message += "

" #Check for a new release if "note_new_release_available" in warning_list: message += "

" message += _("A newer version of Invenio is available for download. You may want to visit ") message += "http://invenio-software.org/wiki/Installation/Download" message += "

" #Error downloading release notes if "error_cannot_download_release_notes" in warning_list: message += "

" message += _("Cannot download or parse release notes from http://invenio-software.org/repo/invenio/tree/RELEASE-NOTES") message += "

" if "email_auto_generated" in warning_list: message += "

" message += _("Your e-mail is auto-generated by the system. Please change your e-mail from account settings.", x_site=CFG_SITE_SECURE_URL, x_link=ln) message += "

" return message def tmpl_external_login_button(self, provider, referer = '', icon_size = 48, classes = ""): """ Template of the login button for providers which don't need username. @param provider: The name of the provider @type provider: str @param referer: The referer URL - will be redirected upon after login @type referer: str @param icon_size: The size of the icon of the provider @type icon_size: int @param classes: Additional classes for the login form @type classes: str @rtype: str """ login_url = CFG_SITE_SECURE_URL + "/youraccount/" if provider in CFG_OPENID_PROVIDERS: login_url += 'openid' elif provider in CFG_OAUTH2_PROVIDERS: login_url += 'oauth2' elif provider in CFG_OAUTH1_PROVIDERS: login_url += 'oauth1' login_url += '?' if referer: if not 'youraccount/login' in referer: login_url += "referer=" + referer + "&" out = "" out += """
""" % { 'loginurl': login_url, 'imgurl': CFG_SITE_SECURE_URL + "/img", 'provider': provider, 'class': classes, 'icon_size': icon_size } return out def tmpl_external_login_form(self, provider, referer = '', icon_size = 48, classes = "", label = "%(provider)s username"): """ Template of the login form for providers which need an username for verification. @param provider: The name of the provider @type provider: str @param referer: The referer URL - will be redirected upon after login @type referer: str @param icon_size: The size of the icon of the provider @type icon_size: int @param classes: Additional classes for the login form @type classes: str @param label: The label for text input. @param label: str @rtype: str """ login_url = CFG_SITE_SECURE_URL + "/youraccount/" if provider in CFG_OPENID_PROVIDERS: login_url += 'openid' elif provider in CFG_OAUTH2_PROVIDERS: login_url += 'oauth2' elif provider in CFG_OAUTH1_PROVIDERS: login_url += 'oauth1' label %= {'provider': provider} out = "" out += """
""" % { 'loginurl': login_url, 'imgurl': CFG_SITE_SECURE_URL + "/img", 'provider': provider, 'label': label, 'referer': referer, 'class': classes, 'icon_size': icon_size } return out def tmpl_external_login_panel(self, ln, referer): """ Template for external login buttons """ from invenio.legacy.websession.websession_config import CFG_EXTERNAL_LOGIN_LARGE from invenio.legacy.websession.websession_config import CFG_EXTERNAL_LOGIN_BUTTON_ORDER from invenio.legacy.websession.websession_config import CFG_EXTERNAL_LOGIN_FORM_LABELS from invenio.modules.access.local_config import CFG_OPENID_CONFIGURATIONS def construct_button(provider, size, button_class): """ Constructs a button for given provider. @param provider: the name of the provider. @type provider: str @param size: the size of the login button @type size: int @param button_class: the additional class for the login button @type button_class: str @rtype str """ _ = gettext_set_language(ln) # Look if the login button needs a form. config = CFG_OPENID_CONFIGURATIONS.get(provider, {}) identifier = config.get('identifier', '') if "{0}" in identifier: label = CFG_EXTERNAL_LOGIN_FORM_LABELS.get(provider, "%(provider)s username") return self.tmpl_external_login_form(provider, referer = referer, icon_size = size, classes = button_class, label = _(label)) else: return self.tmpl_external_login_button(provider, referer = referer, icon_size = size, classes = button_class) activated_providers = CFG_OPENID_PROVIDERS * CFG_OPENID_AUTHENTICATION \ + CFG_OAUTH1_PROVIDERS * CFG_OAUTH1_AUTHENTICATION \ + CFG_OAUTH2_PROVIDERS * CFG_OAUTH2_AUTHENTICATION if not len(activated_providers): return "" out = "" out += "
" out += "You may login with:" out += "
" for provider in CFG_EXTERNAL_LOGIN_LARGE: if provider in activated_providers: out += construct_button(provider, 48, "login_button_big") out += "
" out += "
" providers = CFG_EXTERNAL_LOGIN_BUTTON_ORDER if (len(activated_providers) - len(CFG_EXTERNAL_LOGIN_LARGE)) != \ len(providers): # Not all the providers ordered. Add the unsorted ones to the end. for provider in sorted(activated_providers): if not provider in providers: providers.append(provider) for provider in providers: if not provider in CFG_EXTERNAL_LOGIN_LARGE: out += construct_button(provider, 24, "login_button_small") out += "
" out += "
" out += "
" out += "
" out += """ """ return out diff --git a/invenio/modules/accounts/models.py b/invenio/modules/accounts/models.py index d707465aa..e3e8cb82f 100644 --- a/invenio/modules/accounts/models.py +++ b/invenio/modules/accounts/models.py @@ -1,157 +1,239 @@ # -*- coding: utf-8 -*- # ## This file is part of Invenio. -## Copyright (C) 2011, 2012 CERN. +## Copyright (C) 2011, 2012, 2013, 2014 CERN. ## ## Invenio 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. ## ## Invenio 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 Invenio; if not, write to the Free Software Foundation, Inc., ## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. -""" -WebSession database models. -""" +"""Account database models.""" # General imports. from invenio.ext.sqlalchemy import db from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy_utils.types.choice import ChoiceType # Create your models here. def get_default_user_preferences(): + """Return default user preferences.""" from invenio.modules.access.local_config import CFG_EXTERNAL_AUTHENTICATION, \ CFG_EXTERNAL_AUTH_DEFAULT user_preference = { 'login_method': 'Local'} if CFG_EXTERNAL_AUTH_DEFAULT in CFG_EXTERNAL_AUTHENTICATION: user_preference['login_method'] = CFG_EXTERNAL_AUTH_DEFAULT return user_preference class User(db.Model): + """Represents a User record.""" + def __str__(self): return "%s <%s>" % (self.nickname, self.email) + __tablename__ = 'user' id = db.Column(db.Integer(15, unsigned=True), primary_key=True, autoincrement=True) email = db.Column(db.String(255), nullable=False, server_default='', index=True) _password = db.Column(db.LargeBinary, name="password", nullable=False) note = db.Column(db.String(255), nullable=True) settings = db.Column(db.MutableDict.as_mutable(db.MarshalBinary( default_value=get_default_user_preferences, force_type=dict)), nullable=True) nickname = db.Column(db.String(255), nullable=False, server_default='', index=True) last_login = db.Column(db.DateTime, nullable=False, server_default='1900-01-01 00:00:00') #TODO re_invalid_nickname = re.compile(""".*[,'@]+.*""") _password_comparator = db.PasswordComparator(_password) @hybrid_property def password(self): return self._password @password.setter def password(self, password): self._password = self._password_comparator.hash(password) @password.comparator def password(self): return self._password_comparator @property def guest(self): return False if self.email else True # # Basic functions for user authentification. # def get_id(self): return self.id def is_guest(self): return self.guest def is_authenticated(self): return True if self.email else False def is_active(self): return True class Usergroup(db.Model): - """Represents a Usergroup record.""" + + """Represent a Usergroup record.""" + def __str__(self): return "%s <%s>" % (self.name, self.description) + __tablename__ = 'usergroup' - #FIXME Unique(login_method(70), name) - id = db.Column(db.Integer(15, unsigned=True), - nullable=False, - primary_key=True, autoincrement=True) + + JOIN_POLICIES = { + 'VISIBLEOPEN': 'VO', + 'VISIBLEMAIL': 'VM', + 'INVISIBLEOPEN': 'IO', + 'INVISIBLEMAIL': 'IM', + 'VISIBLEEXTERNAL': 'VE', + } + + LOGIN_METHODS = { + 'INTERNAL': 'INTERNAL', + 'EXTERNAL': 'EXTERNAL', + } + + id = db.Column(db.Integer(15, unsigned=True), nullable=False, + primary_key=True, autoincrement=True) name = db.Column(db.String(255), nullable=False, - server_default='', index=True) + server_default='', index=True) description = db.Column(db.Text, nullable=True) - join_policy = db.Column(db.CHAR(2), nullable=False, - server_default='') - login_method = db.Column(db.String(255), nullable=False, - server_default='INTERNAL') - #all_users = db.relationship(User, secondary=lambda: UserUsergroup.__table__, - # collection_class=set) - + join_policy = db.Column( + ChoiceType( + map(lambda (k, v): (v, k), JOIN_POLICIES.items()), + impl=db.CHAR(2) + ), nullable=False, server_default='') + login_method = db.Column( + ChoiceType(map(lambda (k, v): (v, k), LOGIN_METHODS.items())), + nullable=False, server_default='INTERNAL') + + # FIXME Unique(login_method(70), name) __table_args__ = (db.Index('login_method_name', login_method, name, mysql_length=[60, None]), db.Model.__table_args__) + @classmethod + def filter_visible(cls): + """Return query object with filtered out invisible groups.""" + visible = filter(lambda k: k[0] == 0, + cls.JOIN_POLICIES.values()) + assert len(visible) > 1 # if implementation chage use == instead of in + return cls.query.filter(cls.join_policy.in_(visible)) + + def join(self, id_user=None, status=None): + """Join user to group. + + If ``id_user`` is not defined the current user's id is used. + + :param id_user: User identifier. + """ + if id_user is None: + from flask.ext.login import current_user + id_user = current_user.get_id() + self.users.append( + UserUsergroup( + id_user=id_user or current_user.get_id(), + user_status=status or self.new_user_status, + ) + ) + + def leave(self, id_user=None): + """Remove user from group. + + If ``id_user`` is not defined the current user's id is used. + + :param id_user: User identifier. + """ + if id_user is None: + from flask.ext.login import current_user + id_user = current_user.get_id() + + # FIXME check that I'm not the last admin before leaving the group. + UserUsergroup.query.filter_by( + id_usergroup=self.id, + id_user=id_user, + ).delete() + + @property + def new_user_status(self): + """Return user status for new user.""" + if not self.join_policy.code.endswith('O'): + return UserUsergroup.USER_STATUS['PENDING'] + return UserUsergroup.USER_STATUS['MEMBER'] + + class UserUsergroup(db.Model): - """Represents a UserUsergroup record.""" + + """Represent a UserUsergroup record.""" + + USER_STATUS = { + 'ADMIN': 'A', + 'MEMBER': 'M', + 'PENDING': 'P', + } + def __str__(self): return "%s:%s" % (self.user.nickname, self.usergroup.name) + __tablename__ = 'user_usergroup' + id_user = db.Column(db.Integer(15, unsigned=True), - db.ForeignKey(User.id), - nullable=False, server_default='0', - primary_key=True) + db.ForeignKey(User.id), + nullable=False, server_default='0', + primary_key=True) id_usergroup = db.Column(db.Integer(15, unsigned=True), - db.ForeignKey(Usergroup.id), - nullable=False, server_default='0', - primary_key=True) - user_status = db.Column(db.CHAR(1), nullable=False, - server_default='') + db.ForeignKey(Usergroup.id), + nullable=False, server_default='0', + primary_key=True) + user_status = db.Column(db.CHAR(1), nullable=False, server_default='') user_status_date = db.Column(db.DateTime, nullable=False, - server_default='1900-01-01 00:00:00') + server_default='1900-01-01 00:00:00') user = db.relationship(User, backref='usergroups') usergroup = db.relationship(Usergroup, backref='users') class UserEXT(db.Model): - """Represents a UserEXT record.""" + + """Represent a UserEXT record.""" + __tablename__ = 'userEXT' id = db.Column(db.VARBINARY(255), primary_key=True, nullable=False) method = db.Column(db.String(50), primary_key=True, nullable=False) id_user = db.Column(db.Integer(15, unsigned=True), - db.ForeignKey(User.id), nullable=False) + db.ForeignKey(User.id), nullable=False) __table_args__ = (db.Index('id_user', id_user, method, unique=True), db.Model.__table_args__) __all__ = ['User', 'Usergroup', 'UserUsergroup', 'UserEXT'] diff --git a/invenio/modules/accounts/user_settings/email.py b/invenio/modules/accounts/user_settings/email.py index b2f0372f4..3b45e73d6 100644 --- a/invenio/modules/accounts/user_settings/email.py +++ b/invenio/modules/accounts/user_settings/email.py @@ -1,54 +1,47 @@ # -*- coding: utf-8 -*- ## ## This file is part of Invenio. -## Copyright (C) 2012, 2013 CERN. +## Copyright (C) 2012, 2013, 2014 CERN. ## ## Invenio 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. ## ## Invenio 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 Invenio; if not, write to the Free Software Foundation, Inc., ## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. """WebAccount User Settings""" from flask import url_for, g -from invenio.modules.accounts.models import User from flask.ext.login import current_user from invenio.ext.template import render_template_to_string -from invenio.modules.dashboard.settings import Settings, ModelSettingsStorageBuilder -from invenio.modules.accounts.forms import ChangeUserEmailSettingsForm +from invenio.modules.dashboard.settings import Settings class WebAccountSettings(Settings): - keys = ['email'] - form_builder = ChangeUserEmailSettingsForm - storage_builder = ModelSettingsStorageBuilder( - lambda: User.query.get(current_user.get_id())) - def __init__(self): super(WebAccountSettings, self).__init__() self.icon = 'user' self.title = g._('Account') - self.edit = url_for('webaccount.edit', name=self.name) + self.edit = url_for('accounts_settings.profile') def widget(self): return render_template_to_string('accounts/widget.html') widget.size = 4 @property def is_authorized(self): return current_user.is_authenticated() ## Compulsory plugin interface settings = WebAccountSettings #__all__ = ['WebAccountSettings'] diff --git a/invenio/modules/groups/forms.py b/invenio/modules/groups/forms.py new file mode 100644 index 000000000..7c9357a97 --- /dev/null +++ b/invenio/modules/groups/forms.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +## +## This file is part of Invenio. +## Copyright (C) 2014 CERN. +## +## Invenio 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. +## +## Invenio 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 Invenio; if not, write to the Free Software Foundation, Inc., +## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. + +"""Group Forms""" + +from wtforms_alchemy import model_form_factory +from wtforms import validators, widgets +from wtforms.ext.sqlalchemy.fields import QuerySelectField +from invenio.base.i18n import _ +from invenio.utils.forms import InvenioBaseForm +from invenio.modules.accounts.models import Usergroup + +ModelForm = model_form_factory(InvenioBaseForm) + + +class UsergroupForm(ModelForm): + + """Create new Usergroup.""" + + class Meta: + + """Meta class model for *WTForms-Alchemy*.""" + + model = Usergroup + strip_string_fields = True + field_args = dict( + name=dict( + label=_('Name'), + validators=[validators.Required()], + widget=widgets.TextInput(), + ), + description=dict(label=_('Description')), + join_policy=dict(label=_('Join policy')), + login_method=dict(label=_('Login method')) + ) + + +class JoinUsergroupForm(InvenioBaseForm): + + """Join existing group.""" + + id_usergroup = QuerySelectField( + _('Join group'), + get_pk=lambda i: i.id, + get_label=lambda i: i.name, + query_factory=lambda: Usergroup.query.all() + ) diff --git a/invenio/modules/groups/templates/groups/index_base.html b/invenio/modules/groups/templates/groups/index_base.html index 69454850a..6e636933d 100644 --- a/invenio/modules/groups/templates/groups/index_base.html +++ b/invenio/modules/groups/templates/groups/index_base.html @@ -1,84 +1,83 @@ {# ## This file is part of Invenio. ## Copyright (C) 2012, 2014 CERN. ## ## Invenio 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. ## ## Invenio 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 Invenio; if not, write to the Free Software Foundation, Inc., ## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. #} -{% extends "page.html" %} -{% set title = _("Your Groups") %} -{% set personalize_selected = True %} -{% block header %} - {{ super() }} - {%- js url_for("static", filename="js/jquery.tokeninput.js") -%} - {%- css url_for("static", filename="css/token-input.css") -%} -{% endblock %} -{% block javascript %} - -{% endblock javascript %} -{% block body %} -

{{ _("You are a member of the following groups:") }}

- -
- -
+{% block settings_body %} +{{helpers.panel_start( + 'Groups', + icon='fa fa-group fa-fw', + btn=_('New group'), + btn_icon='fa fa-plus', + btn_href=url_for('webgroup.new'), + with_body=False)}} +
+

{{ _("You are a member of the following groups:") }}

+
+ + +{{helpers.panel_end(with_body=False)}} {% endblock %} diff --git a/invenio/modules/groups/templates/groups/new.html b/invenio/modules/groups/templates/groups/new.html new file mode 100644 index 000000000..cb4a53e0f --- /dev/null +++ b/invenio/modules/groups/templates/groups/new.html @@ -0,0 +1,19 @@ +{# +## This file is part of Invenio. +## Copyright (C) 2014 CERN. +## +## Invenio 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. +## +## Invenio 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 Invenio; if not, write to the Free Software Foundation, Inc., +## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. +#} +{%- extends "groups/new_base.html" -%} diff --git a/invenio/modules/groups/templates/groups/new_base.html b/invenio/modules/groups/templates/groups/new_base.html new file mode 100644 index 000000000..be2e722e7 --- /dev/null +++ b/invenio/modules/groups/templates/groups/new_base.html @@ -0,0 +1,44 @@ +{# +## This file is part of Invenio. +## Copyright (C) 2014 CERN. +## +## Invenio 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. +## +## Invenio 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 Invenio; if not, write to the Free Software Foundation, Inc., +## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. +#} + +{%- import "accounts/settings/helpers.html" as helpers with context %} +{%- from "_formhelpers.html" import render_field with context %} +{%- extends "accounts/settings/index.html" %} + +{% block settings_body %} +{{helpers.panel_start( + _('New Group'), +)}} +
+{%- for field in form %} +{{ render_field(field, show_description=True) }} +{%- endfor %} +
+
+ + {{ _('Cancel') }} + + +
+
+
+{{helpers.panel_end()}} +{% endblock %} diff --git a/invenio/modules/groups/views.py b/invenio/modules/groups/views.py index 3ec34e1c2..50bb7ac85 100644 --- a/invenio/modules/groups/views.py +++ b/invenio/modules/groups/views.py @@ -1,121 +1,156 @@ # -*- coding: utf-8 -*- ## ## This file is part of Invenio. -## Copyright (C) 2012, 2013 CERN. +## Copyright (C) 2012, 2013, 2014 CERN. ## ## Invenio 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. ## ## Invenio 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 Invenio; if not, write to the Free Software Foundation, Inc., ## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. -"""WebGroup Flask Blueprint""" +"""Groups Flask Blueprint.""" -from flask import Blueprint, render_template, request, jsonify +from flask import (Blueprint, render_template, request, jsonify, flash, + url_for, redirect, abort) from flask.ext.login import current_user, login_required +from flask.ext.menu import register_menu from invenio.base.decorators import wash_arguments from invenio.base.i18n import _ from flask.ext.breadcrumbs import default_breadcrumb_root, register_breadcrumb from invenio.ext.sqlalchemy import db from invenio.modules.accounts.models import User, Usergroup, UserUsergroup +from .forms import JoinUsergroupForm, UsergroupForm + blueprint = Blueprint('webgroup', __name__, url_prefix="/yourgroups", template_folder='templates', static_folder='static') -default_breadcrumb_root(blueprint, '.webaccount.webgroup') - -def filter_by_user_status(uid, user_status, login_method='INTERNAL'): - return db.and_(UserUsergroup.id_user == uid, - UserUsergroup.user_status == user_status, - Usergroup.login_method == login_method) +default_breadcrumb_root(blueprint, '.settings.groups') @blueprint.route('/') -@blueprint.route('/index', methods=['GET', 'POST']) -@register_breadcrumb(blueprint, '.', _('Your Groups')) +@blueprint.route('/index') +@register_menu( + blueprint, 'settings.groups', + _('%(icon)s Groups', icon=''), + order=0, + active_when=lambda: request.endpoint.startswith("webgroup.") +) +@register_breadcrumb(blueprint, '.', _('Groups')) @login_required def index(): + """List all user groups.""" uid = current_user.get_id() - mg = Usergroup.query.join(Usergroup.users).\ - filter(UserUsergroup.id_user==uid).all() - #filter_by_user_status(uid, - #CFG_WEBSESSION_USERGROUP_STATUS["MEMBER"])).\ - #all() + current_user.reload() + mg = UserUsergroup.query.join(UserUsergroup.usergroup).filter( + UserUsergroup.id_user == uid).all() + member_groups = dict(map(lambda ug: (ug.usergroup.name, ug), mg)) + + return render_template( + 'groups/index.html', + member_groups=member_groups, + form=JoinUsergroupForm(), + ) + + +@blueprint.route('/new', methods=['GET', 'POST']) +@register_breadcrumb(blueprint, '.new', _('New Group')) +@login_required +def new(): + """Create new user group.""" + form = UsergroupForm(request.form) + + if form.validate_on_submit(): + ug = Usergroup() + form.populate_obj(ug) + ug.join(status=UserUsergroup.USER_STATUS['ADMIN']) + db.session.add(ug) + db.session.commit() + current_user.reload() + return redirect(url_for(".index")) + + return render_template( + "groups/new.html", + form=form, + ) - return render_template('groups/index.html', member_groups=map(dict, mg)) + +@blueprint.route('/leave/') +@login_required +def leave(id_usergroup): + """Leave user group. + + :param id_usergroup: Identifier of user group. + """ + group = Usergroup.query.get(id_usergroup) + if group is None: + return abort(400) + group.leave() + db.session.merge(group) + db.session.commit() + current_user.reload() + flash(_('You left a group %(name)s.', name=group.name), 'success') + return redirect(url_for('.index')) + + +@blueprint.route('/join', methods=['GET', 'POST']) +@blueprint.route('/join/', methods=['GET', 'POST']) +@login_required +@wash_arguments({"id_usergroup": (int, 0)}) +def join(id_usergroup, status=None): + """Join group.""" + group = Usergroup.query.get(id_usergroup) + if group is None: + return abort(400) + group.join() + db.session.merge(group) + db.session.commit() + current_user.reload() + flash(_('You join a group %(name)s.', name=group.name), 'success') + return redirect(url_for('.index')) + + +@blueprint.route('/manage/', methods=['GET', 'POST']) +def manage(id_usergroup): + """Manage user group.""" + raise NotImplemented() + + +@blueprint.route('/members/', methods=['GET', 'POST']) +def members(id_usergroup): + """List user group members.""" + raise NotImplemented() @blueprint.route("/search", methods=['GET', 'POST']) @wash_arguments({"query": (unicode, ""), "term": (unicode, "")}) def search(query, term): + """Search user groups.""" if query == 'users' and len(term) >= 3: res = db.session.query(User.nickname).filter( User.nickname.like("%s%%" % term)).limit(10).all() return jsonify(nicknames=[elem for elem, in res]) elif query == 'groups' and len(term) >= 3: res = db.session.query(db.func.distinct(Usergroup.name)).\ join(UserUsergroup).filter( Usergroup.name.like("%s%%" % term)).limit(10).all() return jsonify(groups=[elem for elem, in res]) return jsonify() @blueprint.route("/tokenize", methods=['GET', 'POST']) @wash_arguments({"q": (unicode, "")}) def tokenize(q): + """FIXME.""" res = Usergroup.query.filter( Usergroup.name.like("%s%%" % q)).limit(10).all() return jsonify(data=map(dict, res)) - - -@blueprint.route("/join", methods=['GET', 'POST']) -@blueprint.route("/leave", methods=['GET', 'POST']) -@wash_arguments({"id": (int, 0)}) -def _manipulate_group(id): - uid = current_user.get_id() - try: - user = User.query.filter(User.id == uid).one() - group = Usergroup.query.filter(Usergroup.id == id).one() - if request.path.find("/join") > 0: - user.usergroups.append(UserUsergroup(usergroup=group)) - db.session.add(user) - else: - [db.session.delete(ug) for ug in user.usergroups - if ug.id_usergroup == id] - #UserUsergroup.query.filter(and_( - # UserUsergroup.id_user==uid, - # UserUsergroup.id_userusergroup==id_usergroup)).delete() - db.session.commit() - return jsonify(result=dict({'status':True})) - except: - db.session.rollback() - return jsonify(result=dict({'status':False})) - - - -#@blueprint.route("/add", methods=['GET', 'POST']) -#@login_required -#def add(): -# uid = current_user.get_id() -# form = AddMsgMESSAGEForm(request.form) -# if form.validate_on_submit(): -# m = MsgMESSAGE() -# form.populate_obj(m) -# try: -# db.session.add(m) -# db.session.commit() -# flash(_('Message was sent'), "info") -# return redirect(url_for('.display')) -# except: -# db.session.rollback() -# -# return render_template('webgroup_add.html', form=form) -# diff --git a/requirements.txt b/requirements.txt index e31c9e6f3..e24b559bb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,73 +1,73 @@ MySQL-python==1.2.5 rdflib==2.4.2 reportlab==2.5 python-dateutil<=1.9999 python-magic==0.4.2 https://www.reportlab.com/ftp/pyRXP-1.16-daily-unix.tar.gz#egg=pyRXP-1.16 numpy==1.7.0 lxml==3.1.2 mechanize==0.2.5 python-Levenshtein==0.10.2 http://pybrary.net/pyPdf/pyPdf-1.13.tar.gz#egg=pyPdf-1.13 PyStemmer==1.3.0 https://py-editdist.googlecode.com/files/py-editdist-0.3.tar.gz#egg=editdist-0.3 feedparser==5.1.3 BeautifulSoup==3.2.1 beautifulsoup4==4.3.2 python-twitter==0.8.7 celery==3.0.17 msgpack-python==0.3.0 pyparsing==2.0.1 git+git://github.com/lnielsen-cern/dictdiffer.git#egg=dictdiffer-0.0.3 git+git://github.com/romanchyla/workflow.git@e41299579501704b1486c72cc2509a9f82e63ea6#egg=workflow-1.1.0 requests==1.2.3 PyPDF2 rauth unidecode https://pyfilesystem.googlecode.com/files/fs-0.4.0.tar.gz#egg=fs-0.4.0 libmagic==1.0 Babel==1.3 git+https://github.com/inveniosoftware/intbitset#egg=intbitset-2.0 Jinja2==2.7.2 SQLAlchemy==0.8.3 Flask==0.10.1 # Flask-WTF 0.9.5 doesn't support WTForms 2.0 as of yet. WTForms>=1.0.5,<2.0 fixture==1.5 redis==2.8.0 unittest2==0.5.1 git+https://github.com/david-e/flask-admin.git@bootstrap3#egg=Flask-Admin-1.0.7 Flask-Assets==0.9 Flask-Babel==0.9 Flask-Cache==0.12 Flask-DebugToolbar==0.9.0 Flask-Email==1.4.4 Flask-Gravatar==0.4.0 Flask-Login==0.2.7 Flask-Principal==0.4.0 Flask-RESTful==0.2.12 Flask-OAuthlib==0.4.3 #Flask-SQLAlchemy==1.0 git+https://github.com/mitsuhiko/flask-sqlalchemy#egg=Flask-SQLAlchemy-1.99 git+https://github.com/inveniosoftware/flask-menu.git#egg=Flask-Menu-0.1 git+https://github.com/inveniosoftware/flask-breadcrumbs.git#egg=Flask-Breadcrumbs-0.1 git+https://github.com/inveniosoftware/flask-registry.git#egg=Flask-Registry-0.1 Flask-Script>=2.0.3 #Flask-Testing==0.4 https://github.com/jarus/flask-testing/zipball/master#egg=Flask-Testing-0.4.1 Flask-WTF==0.9.5 Cerberus==0.5 #Flask-Collect>=0.2.2 git+https://github.com/greut/Flask-Collect.git@setup-py-fix#egg=Flask-Collect-0.2.3-dev Sphinx alembic==0.6.2 git+https://github.com/lnielsen-cern/setuptools-bower.git#egg=setuptools-bower-0.1 pytz #PyLD=0.5.0 + python 2.6 patches git+https://github.com/inveniosoftware/pyld.git@python2.6#egg=PyLD-26.0.5.0-dev -SQLAlchemy-Utils==0.23.1 +SQLAlchemy-Utils<0.24.0 wtforms-alchemy==0.12.4 mock==1.0.1 setuptools>=2.0 httpretty==0.8.0 raven==4.2.1