trytond-nereid-zz/routing.py

540 lines
16 KiB
Python

# 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::
<model>.<method>
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)