# This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import pytz from werkzeug import abort, redirect from wtforms import Form, TextField, PasswordField, validators from nereid import jsonify, flash, render_template, url_for, cache from nereid.globals import session, request from nereid.helpers import login_required, key_from_list, get_flashed_messages from nereid.signals import login, failed_login, logout from trytond.model import ModelView, ModelSQL, fields from trytond.transaction import Transaction from trytond.pool import Pool from .i18n import _ __all__ = ['URLMap', 'WebSite', 'WebSiteLocale', 'URLRule', 'URLRuleDefaults', 'WebsiteCountry', 'WebsiteCurrency', 'WebsiteWebsiteLocale'] class URLMap(ModelSQL, ModelView): """ URL Map ~~~~~~~ A collection of URLs for a website. This is analogous to werkzeug's URL Map. :param name: Name of the URL Map :param default_subdomain: Default subdomain for URLs in this Map :param active: Whether the URL Map is active or not. Rules: ~~~~~~ :param rules: O2M URLRules Advanced: ~~~~~~~~~ :param charset: default value - utf-8 :param strict_slashes: Boolean field if / in url map is taken seriously :param unique_urls: Enable `redirect_defaults` in the URL Map and redirects the defaults to the URL """ __name__ = "nereid.url_map" name = fields.Char( 'Name', required=True, select=True, ) default_subdomain = fields.Char( 'Default Subdomain', ) rules = fields.One2Many( 'nereid.url_rule', 'url_map', 'Rules' ) charset = fields.Char('Char Set') strict_slashes = fields.Boolean('Strict Slashes') unique_urls = fields.Boolean('Unique URLs') active = fields.Boolean('Active') @staticmethod def default_active(): "By default URL is active" return True @staticmethod def default_charset(): "By default characterset is utf-8" return 'utf-8' def get_rules_arguments(self): """ Constructs a list of dictionary of arguments needed for URL Rule construction. A wrapper around the URL RULE get_rule_arguments """ rule_args = [] for rule in self.rules: rule_args.append(rule.get_rule_arguments()) return rule_args class LoginForm(Form): "Default Login Form" email = TextField(_('e-mail'), [validators.Required(), validators.Email()]) password = PasswordField(_('Password'), [validators.Required()]) class WebSiteLocale(ModelSQL, ModelView): 'Web Site Locale' __name__ = "nereid.website.locale" _rec_name = 'code' code = fields.Char('Code', required=True) language = fields.Many2One( 'ir.lang', 'Default Language', required=True ) currency = fields.Many2One( 'currency.currency', 'Currency', ondelete='CASCADE', required=True ) @classmethod def __setup__(cls): super(WebSiteLocale, cls).__setup__() cls._sql_constraints += [ ('unique_code', 'UNIQUE(code)', 'Code must be unique'), ] class WebSite(ModelSQL, ModelView): """ One of the most powerful features of Nereid is the ability to manage multiple websites from one back-end. A web site in nereid represents a collection or URLs, settings. :param name: Name of the web site :param base_url: The unique URL of the website, You cannot have two websites, with the same base_url :param url_map: The active URL Map for the website (M2O URLMap) :param company: The company linked with the website. :param active: Whether the website is active or not. """ __name__ = "nereid.website" #: The name field is used for both information and also as #: the site identifier for nereid. The WSGI application requires #: SITE argument. The SITE argument is then used to load URLs and #: other settings for the website. Needs to be unique name = fields.Char('Name', required=True, select=True) #: The URLMap is made as a different object which functions as a #: collection of Rules. This will allow easy replication of sites #: which perform with same URL structures but different templates url_map = fields.Many2One('nereid.url_map', 'URL Map', required=True) #: The company to which the website belongs. Useful when creating #: records like sale order which require a company to be present company = fields.Many2One('company.company', 'Company', required=True) active = fields.Boolean('Active') #: The list of countries this website operates in. Used for generating #: Countries list in the registration form etc. countries = fields.Many2Many( 'nereid.website-country.country', 'website', 'country', 'Countries Available') #: Allowed currencies in the website currencies = fields.Many2Many( 'nereid.website-currency.currency', 'website', 'currency', 'Currencies Available') #: Default locale default_locale = fields.Many2One( 'nereid.website.locale', 'Default Locale', required=True ) #: Allowed locales in the website locales = fields.Many2Many( 'nereid.website-nereid.website.locale', 'website', 'locale', 'Languages Available') #: The res.user with which the nereid application will be loaded #: .. versionadded: 0.3 application_user = fields.Many2One( 'res.user', 'Application User', required=True ) guest_user = fields.Many2One( 'nereid.user', 'Guest user', required=True ) timezone = fields.Selection( [(x, x) for x in pytz.common_timezones], 'Timezone', translate=False ) @staticmethod def default_timezone(): return 'UTC' @staticmethod def default_active(): return True @classmethod def __setup__(cls): super(WebSite, cls).__setup__() cls._sql_constraints = [ ('name_uniq', 'UNIQUE(name)', 'Another site with the same name already exists!') ] @classmethod def country_list(cls): """ Return the list of countries in JSON """ return jsonify(result=[ {'key': c.id, 'value': c.name} for c in request.nereid_website.countries ]) @staticmethod def subdivision_list(): """ Return the list of states for given country """ country = int(request.args.get('country', 0)) if country not in [c.id for c in request.nereid_website.countries]: abort(404) Subdivision = Pool().get('country.subdivision') subdivisions = Subdivision.search([('country', '=', country)]) return jsonify( result=[{ 'id': s.id, 'name': s.name, 'code': s.code, } for s in subdivisions ] ) def get_urls(self, name): """ Return complete list of URLs """ URLMap = Pool().get('nereid.url_map') websites = self.search([('name', '=', name)]) if not websites: raise RuntimeError("Website with Name %s not found" % name) return URLMap.get_rules_arguments(websites[0].url_map.id) def stats(self, **arguments): """ Test method. """ return u'Request: %s\nArguments: %s\nEnviron: %s\n' \ % (request, arguments, request.environ) @classmethod def home(cls): "A dummy home method which just renders home.jinja" return render_template('home.jinja') @classmethod def login(cls): """ Simple login based on the email and password Required post data see :class:LoginForm """ login_form = LoginForm(request.form) if not request.is_guest_user and request.args.get('next'): return redirect(request.args['next']) if request.method == 'POST' and login_form.validate(): NereidUser = Pool().get('nereid.user') result = NereidUser.authenticate( login_form.email.data, login_form.password.data ) # Result can be the following: # 1 - Browse record of User (successful login) # 2 - None - Login failure without message # 3 - Any other false value (no message is shown. useful if you # want to handle the message shown to user) if result: # NOTE: Translators leave %s as such flash(_("You are now logged in. Welcome %(name)s", name=result.display_name)) session['user'] = result.id login.send() if request.is_xhr: return 'OK' else: return redirect( request.values.get( 'next', url_for('nereid.website.home') ) ) elif result is None: flash(_("Invalid login credentials")) failed_login.send(form=login_form) if request.is_xhr: return 'NOK' return render_template('login.jinja', login_form=login_form) @classmethod def logout(cls): "Log the user out" session.pop('user', None) logout.send() flash( _('You have been logged out successfully. Thanks for visiting us') ) return redirect( request.args.get('next', url_for('nereid.website.home')) ) @staticmethod def account_context(): """This fills the account context for the template rendering my account. Additional modules might want to fill extra data into the context """ return dict( user=request.nereid_user, party=request.nereid_user.party, ) @classmethod @login_required def account(cls): return render_template('account.jinja', **cls.account_context()) def get_currencies(self): """Returns available currencies for current site .. note:: A special method is required so that the fetch can be speeded up, by pushing the categories to the central cache which cannot be done directly on a browse node. """ cache_key = key_from_list([ Transaction().cursor.dbname, Transaction().user, 'nereid.website.get_currencies', ]) # The website is automatically appended to the cache prefix rv = cache.get(cache_key) if rv is None: rv = [{ 'id': c.id, 'name': c.name, 'symbol': c.symbol, } for c in self.currencies ] cache.set(cache_key, rv, 60 * 60) return rv @staticmethod def _user_status(): """Returns the commonly required status parameters of the user This method could be inherited and components could be added """ rv = { 'messages': get_flashed_messages() } if request.is_guest_user: rv.update({ 'logged_id': False }) else: rv.update({ 'logged_in': True, 'name': request.nereid_user.display_name }) return rv @classmethod def user_status(cls): """ Returns a JSON of the user_status """ return jsonify(status=cls._user_status()) class URLRule(ModelSQL, ModelView): """ URL Rule ~~~~~~~~ A rule that represents a single URL pattern :param path: Path of the URL :param name: Name of the URL. This is used for reverse mapping, hence needs to be unique :param handler: The handler of this URL or the target model.method which is called. The representation is:: . For example: To call list_parties method in party.party use: party.party.list_parties The signature of the method being called should be: def method(self, **arguments): return "Hello World" where request is the request object and arguments is the dictionary of the values generated from the match of the URL :param active: Whether the website is active or not. Advanced ~~~~~~~~~ :param defaults: Defaults of the URL (O2M - URLRuleDefaults) :param method: POST, GET, :param only_for_generation: URL will not be mapped, but can be used for URL generation. Example for static pages, where content delivery is managed by apache, but URL generation is necessary :param redirect_to: (M2O self) Another URL to which the redirect has to be done :param sequence: Numeric sequence of the URL Map. :param url_map: Relation field for url_rule o2m """ __name__ = "nereid.url_rule" _rec_name = 'rule' rule = fields.Char('Rule', required=True, select=True,) endpoint = fields.Char('Endpoint', select=True,) active = fields.Boolean('Active') defaults = fields.One2Many('nereid.url_rule_defaults', 'rule', 'Defaults') # Supported HTTP methods http_method_get = fields.Boolean('GET') http_method_post = fields.Boolean('POST') http_method_patch = fields.Boolean('PATCH') http_method_put = fields.Boolean('PUT') http_method_delete = fields.Boolean('DELETE') only_for_genaration = fields.Boolean('Only for Generation') redirect_to = fields.Char('Redirect To') sequence = fields.Integer('Sequence', required=True,) url_map = fields.Many2One('nereid.url_map', 'URL Map') @classmethod def __setup__(cls): super(URLRule, cls).__setup__() cls._order.insert(0, ('sequence', 'ASC')) @staticmethod def default_active(): return True @staticmethod def default_http_method_get(): return True def get_http_methods(self): """ Returns an iterable of HTTP methods that the URL has to support. .. versionadded: 2.4.0.6 """ methods = [] if self.http_method_get: methods.append('GET') if self.http_method_post: methods.append('POST') if self.http_method_put: methods.append('PUT') if self.http_method_delete: methods.append('DELETE') if self.http_method_patch: methods.append('PATCH') return methods def get_rule_arguments(self): """ Return the arguments of a Rule in the corresponding format """ defaults = dict( [(i.key, i.value) for i in self.defaults] ) return { 'rule': self.rule, 'endpoint': self.endpoint, 'methods': self.get_http_methods(), 'build_only': self.only_for_genaration, 'defaults': defaults, 'redirect_to': self.redirect_to or None, } class URLRuleDefaults(ModelSQL, ModelView): """ Defaults for the URL :param key: The char for the default's key :param value: The Value for the default's Value :param Rule: M2O Rule """ __name__ = "nereid.url_rule_defaults" _rec_name = 'key' key = fields.Char('Key', required=True, select=True) value = fields.Char('Value', required=True, select=True) rule = fields.Many2One( 'nereid.url_rule', 'Rule', required=True, select=True ) class WebsiteCountry(ModelSQL): "Website Country Relations" __name__ = 'nereid.website-country.country' website = fields.Many2One('nereid.website', 'Website') country = fields.Many2One('country.country', 'Country') class WebsiteCurrency(ModelSQL): "Currencies to be made available on website" __name__ = 'nereid.website-currency.currency' _table = 'website_currency_rel' website = fields.Many2One( 'nereid.website', 'Website', ondelete='CASCADE', select=1, required=True) currency = fields.Many2One( 'currency.currency', 'Currency', ondelete='CASCADE', select=1, required=True) class WebsiteWebsiteLocale(ModelSQL): "Languages to be made available on website" __name__ = 'nereid.website-nereid.website.locale' _table = 'website_locale_rel' website = fields.Many2One( 'nereid.website', 'Website', ondelete='CASCADE', select=1, required=True) locale = fields.Many2One( 'nereid.website.locale', 'Locale', ondelete='CASCADE', select=1, required=True)