3
0
Fork 0

fix: format all code with black

and from now on should not deviate from that...
This commit is contained in:
Lance Edgar 2025-08-31 12:26:43 -05:00
parent 8a09fb1a3c
commit 4d0693862d
68 changed files with 6693 additions and 5659 deletions

View file

@ -8,41 +8,41 @@
from importlib.metadata import version as get_version
project = 'WuttaWeb'
copyright = '2024, Lance Edgar'
author = 'Lance Edgar'
release = get_version('WuttaWeb')
project = "WuttaWeb"
copyright = "2024, Lance Edgar"
author = "Lance Edgar"
release = get_version("WuttaWeb")
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.intersphinx',
'sphinx.ext.viewcode',
'sphinx.ext.todo',
'sphinxcontrib.programoutput',
"sphinx.ext.autodoc",
"sphinx.ext.intersphinx",
"sphinx.ext.viewcode",
"sphinx.ext.todo",
"sphinxcontrib.programoutput",
]
templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
templates_path = ["_templates"]
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
intersphinx_mapping = {
'colander': ('https://docs.pylonsproject.org/projects/colander/en/latest/', None),
'deform': ('https://docs.pylonsproject.org/projects/deform/en/latest/', None),
'fanstatic': ('https://www.fanstatic.org/en/latest/', None),
'pyramid': ('https://docs.pylonsproject.org/projects/pyramid/en/latest/', None),
'python': ('https://docs.python.org/3/', None),
'rattail-manual': ('https://docs.wuttaproject.org/rattail-manual/', None),
'sqlalchemy': ('http://docs.sqlalchemy.org/en/latest/', None),
'webhelpers2': ('https://webhelpers2.readthedocs.io/en/latest/', None),
'wuttjamaican': ('https://docs.wuttaproject.org/wuttjamaican/', None),
'wutta-continuum': ('https://docs.wuttaproject.org/wutta-continuum/', None),
"colander": ("https://docs.pylonsproject.org/projects/colander/en/latest/", None),
"deform": ("https://docs.pylonsproject.org/projects/deform/en/latest/", None),
"fanstatic": ("https://www.fanstatic.org/en/latest/", None),
"pyramid": ("https://docs.pylonsproject.org/projects/pyramid/en/latest/", None),
"python": ("https://docs.python.org/3/", None),
"rattail-manual": ("https://docs.wuttaproject.org/rattail-manual/", None),
"sqlalchemy": ("http://docs.sqlalchemy.org/en/latest/", None),
"webhelpers2": ("https://webhelpers2.readthedocs.io/en/latest/", None),
"wuttjamaican": ("https://docs.wuttaproject.org/wuttjamaican/", None),
"wutta-continuum": ("https://docs.wuttaproject.org/wutta-continuum/", None),
}
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = 'furo'
html_static_path = ['_static']
html_theme = "furo"
html_static_path = ["_static"]

View file

@ -3,4 +3,4 @@
from importlib.metadata import version
__version__ = version('wuttaweb')
__version__ = version("wuttaweb")

View file

@ -48,8 +48,9 @@ class WebAppProvider(AppProvider):
registers some :term:`email templates <email template>` for the
app, etc.
"""
email_modules = ['wuttaweb.emails']
email_templates = ['wuttaweb:email-templates']
email_modules = ["wuttaweb.emails"]
email_templates = ["wuttaweb:email-templates"]
def get_web_handler(self, **kwargs):
"""
@ -64,9 +65,11 @@ class WebAppProvider(AppProvider):
:returns: Instance of :class:`~wuttaweb.handler.WebHandler`.
"""
if 'web_handler' not in self.__dict__:
spec = self.config.get(f'{self.appname}.web.handler_spec',
default='wuttaweb.handler:WebHandler')
if "web_handler" not in self.__dict__:
spec = self.config.get(
f"{self.appname}.web.handler_spec",
default="wuttaweb.handler:WebHandler",
)
self.web_handler = self.app.load_object(spec)(self.config)
return self.web_handler
@ -96,23 +99,25 @@ def make_wutta_config(settings, config_maker=None, **kwargs):
If this config file path cannot be discovered, an error is raised.
"""
wutta_config = settings.get('wutta_config')
wutta_config = settings.get("wutta_config")
if not wutta_config:
# validate config file path
path = settings.get('wutta.config')
path = settings.get("wutta.config")
if not path or not os.path.exists(path):
raise ValueError("Please set 'wutta.config' in [app:main] "
"section of config to the path of your "
"config file. Lame, but necessary.")
raise ValueError(
"Please set 'wutta.config' in [app:main] "
"section of config to the path of your "
"config file. Lame, but necessary."
)
# make config, add to settings
config_maker = config_maker or make_config
wutta_config = config_maker(path, **kwargs)
settings['wutta_config'] = wutta_config
settings["wutta_config"] = wutta_config
# configure database sessions
if hasattr(wutta_config, 'appdb_engine'):
if hasattr(wutta_config, "appdb_engine"):
wuttaweb.db.Session.configure(bind=wutta_config.appdb_engine)
return wutta_config
@ -128,10 +133,11 @@ def make_pyramid_config(settings):
:returns: Instance of
:class:`pyramid:pyramid.config.Configurator`.
"""
settings.setdefault('fanstatic.versioning', 'true')
settings.setdefault('mako.directories', ['wuttaweb:templates'])
settings.setdefault('pyramid_deform.template_search_path',
'wuttaweb:templates/deform')
settings.setdefault("fanstatic.versioning", "true")
settings.setdefault("mako.directories", ["wuttaweb:templates"])
settings.setdefault(
"pyramid_deform.template_search_path", "wuttaweb:templates/deform"
)
# update settings per current theme
establish_theme(settings)
@ -142,21 +148,21 @@ def make_pyramid_config(settings):
pyramid_config.set_security_policy(WuttaSecurityPolicy())
# require CSRF token for POST
pyramid_config.set_default_csrf_options(require_csrf=True,
token='_csrf',
header='X-CSRF-TOKEN')
pyramid_config.set_default_csrf_options(
require_csrf=True, token="_csrf", header="X-CSRF-TOKEN"
)
pyramid_config.include('pyramid_beaker')
pyramid_config.include('pyramid_deform')
pyramid_config.include('pyramid_fanstatic')
pyramid_config.include('pyramid_mako')
pyramid_config.include('pyramid_tm')
pyramid_config.include("pyramid_beaker")
pyramid_config.include("pyramid_deform")
pyramid_config.include("pyramid_fanstatic")
pyramid_config.include("pyramid_mako")
pyramid_config.include("pyramid_tm")
# add some permissions magic
pyramid_config.add_directive('add_wutta_permission_group',
'wuttaweb.auth.add_permission_group')
pyramid_config.add_directive('add_wutta_permission',
'wuttaweb.auth.add_permission')
pyramid_config.add_directive(
"add_wutta_permission_group", "wuttaweb.auth.add_permission_group"
)
pyramid_config.add_directive("add_wutta_permission", "wuttaweb.auth.add_permission")
return pyramid_config
@ -179,9 +185,9 @@ def main(global_config, **settings):
wutta_config = make_wutta_config(settings)
pyramid_config = make_pyramid_config(settings)
pyramid_config.include('wuttaweb.static')
pyramid_config.include('wuttaweb.subscribers')
pyramid_config.include('wuttaweb.views')
pyramid_config.include("wuttaweb.static")
pyramid_config.include("wuttaweb.subscribers")
pyramid_config.include("wuttaweb.views")
return pyramid_config.make_wsgi_app()
@ -226,10 +232,10 @@ def make_wsgi_app(main_app=None, config=None):
app = config.get_app()
# extract pyramid settings
settings = config.get_dict('app:main')
settings = config.get_dict("app:main")
# keep same config object
settings['wutta_config'] = config
settings["wutta_config"] = config
# determine the app factory
if isinstance(main_app, str):
@ -270,15 +276,15 @@ def establish_theme(settings):
will update ``settings['mako.directories']`` such that the theme's
template path is listed first.
"""
config = settings['wutta_config']
config = settings["wutta_config"]
theme = get_effective_theme(config)
settings['wuttaweb.theme'] = theme
settings["wuttaweb.theme"] = theme
directories = settings['mako.directories']
directories = settings["mako.directories"]
if isinstance(directories, str):
directories = config.parse_list(directories)
path = get_theme_template_path(config)
directories.insert(0, path)
settings['mako.directories'] = directories
settings["mako.directories"] = directories

View file

@ -108,7 +108,7 @@ class WuttaSecurityPolicy:
self.db_session = db_session or Session()
def load_identity(self, request):
config = request.registry.settings['wutta_config']
config = request.registry.settings["wutta_config"]
app = config.get_app()
model = app.model
@ -141,10 +141,10 @@ class WuttaSecurityPolicy:
def permits(self, request, context, permission):
# nb. root user can do anything
if getattr(request, 'is_root', False):
if getattr(request, "is_root", False):
return True
config = request.registry.settings['wutta_config']
config = request.registry.settings["wutta_config"]
app = config.get_app()
auth = app.get_auth_handler()
user = self.identity(request)
@ -183,14 +183,16 @@ def add_permission_group(pyramid_config, groupkey, label=None, overwrite=True):
See also :func:`add_permission()`.
"""
config = pyramid_config.get_settings()['wutta_config']
config = pyramid_config.get_settings()["wutta_config"]
app = config.get_app()
def action():
perms = pyramid_config.get_settings().get('wutta_permissions', {})
perms = pyramid_config.get_settings().get("wutta_permissions", {})
if overwrite or groupkey not in perms:
group = perms.setdefault(groupkey, {'key': groupkey})
group['label'] = label or app.make_title(groupkey)
pyramid_config.add_settings({'wutta_permissions': perms})
group = perms.setdefault(groupkey, {"key": groupkey})
group["label"] = label or app.make_title(groupkey)
pyramid_config.add_settings({"wutta_permissions": perms})
pyramid_config.action(None, action)
@ -229,13 +231,15 @@ def add_permission(pyramid_config, groupkey, key, label=None):
See also :func:`add_permission_group()`.
"""
def action():
config = pyramid_config.get_settings()['wutta_config']
config = pyramid_config.get_settings()["wutta_config"]
app = config.get_app()
perms = pyramid_config.get_settings().get('wutta_permissions', {})
group = perms.setdefault(groupkey, {'key': groupkey})
group.setdefault('label', app.make_title(groupkey))
perm = group.setdefault('perms', {}).setdefault(key, {'key': key})
perm['label'] = label or app.make_title(key)
pyramid_config.add_settings({'wutta_permissions': perms})
perms = pyramid_config.get_settings().get("wutta_permissions", {})
group = perms.setdefault(groupkey, {"key": groupkey})
group.setdefault("label", app.make_title(groupkey))
perm = group.setdefault("perms", {}).setdefault(key, {"key": key})
perm["label"] = label or app.make_title(key)
pyramid_config.add_settings({"wutta_permissions": perms})
pyramid_config.action(None, action)

View file

@ -36,11 +36,11 @@ from wuttjamaican.cli import wutta_typer
@wutta_typer.command()
def webapp(
ctx: typer.Context,
auto_reload: Annotated[
bool,
typer.Option('--reload', '-r',
help="Auto-reload web app when files change.")] = False,
ctx: typer.Context,
auto_reload: Annotated[
bool,
typer.Option("--reload", "-r", help="Auto-reload web app when files change."),
] = False,
):
"""
Run the configured web app
@ -52,34 +52,38 @@ def webapp(
sys.stderr.write("no config files found!\n")
sys.exit(1)
runner = config.get(f'{config.appname}.web.app.runner', default='pserve')
if runner == 'pserve':
runner = config.get(f"{config.appname}.web.app.runner", default="pserve")
if runner == "pserve":
# run pserve
argv = ['pserve', f'file+ini:{config.files_read[0]}']
if ctx.params['auto_reload']:
argv.append('--reload')
argv = ["pserve", f"file+ini:{config.files_read[0]}"]
if ctx.params["auto_reload"]:
argv.append("--reload")
pserve.main(argv=argv)
elif runner == 'uvicorn':
elif runner == "uvicorn":
import uvicorn
# need service details from config
spec = config.require(f'{config.appname}.web.app.spec')
spec = config.require(f"{config.appname}.web.app.spec")
kw = {
'host': config.get(f'{config.appname}.web.app.host', default='127.0.0.1'),
'port': config.get_int(f'{config.appname}.web.app.port', default=8000),
'reload': ctx.params['auto_reload'],
'reload_dirs': config.get_list(f'{config.appname}.web.app.reload_dirs'),
'factory': config.get_bool(f'{config.appname}.web.app.factory', default=False),
'interface': config.get(f'{config.appname}.web.app.interface', default='auto'),
'root_path': config.get(f'{config.appname}.web.app.root_path', default=''),
"host": config.get(f"{config.appname}.web.app.host", default="127.0.0.1"),
"port": config.get_int(f"{config.appname}.web.app.port", default=8000),
"reload": ctx.params["auto_reload"],
"reload_dirs": config.get_list(f"{config.appname}.web.app.reload_dirs"),
"factory": config.get_bool(
f"{config.appname}.web.app.factory", default=False
),
"interface": config.get(
f"{config.appname}.web.app.interface", default="auto"
),
"root_path": config.get(f"{config.appname}.web.app.root_path", default=""),
}
# also must inject our config files to env, since there is no
# other way to specify when running via uvicorn
os.environ['WUTTA_CONFIG_FILES'] = os.pathsep.join(config.files_read)
os.environ["WUTTA_CONFIG_FILES"] = os.pathsep.join(config.files_read)
# run uvicorn
uvicorn.run(spec, **kw)

View file

@ -35,9 +35,12 @@ class WuttaWebConfigExtension(WuttaConfigExtension):
only relevant if Wutta-Continuum is installed and enabled. For
more info see :doc:`wutta-continuum:index`.
"""
key = 'wuttaweb'
key = "wuttaweb"
def configure(self, config):
""" """
config.setdefault('wutta_continuum.wutta_plugin_spec',
'wuttaweb.db.continuum:WuttaWebContinuumPlugin')
config.setdefault(
"wutta_continuum.wutta_plugin_spec",
"wuttaweb.db.continuum:WuttaWebContinuumPlugin",
)

View file

@ -28,7 +28,7 @@ from pyramid.threadlocal import get_current_request
try:
from wutta_continuum.conf import WuttaContinuumPlugin
except ImportError: # pragma: no cover
except ImportError: # pragma: no cover
pass
else:

View file

@ -31,18 +31,19 @@ class feedback(EmailSetting):
"""
Sent when user submits feedback via the web app.
"""
default_subject = "User Feedback"
def sample_data(self):
""" """
model = self.app.model
person = model.Person(full_name="Barney Rubble")
user = model.User(username='barney', person=person)
user = model.User(username="barney", person=person)
return {
'user': user,
'user_name': str(person),
'user_url': '#',
'referrer': 'http://example.com/',
'client_ip': '127.0.0.1',
'message': "This app is cool but needs a new feature.\n\nAllow me to describe...",
"user": user,
"user_name": str(person),
"user_url": "#",
"referrer": "http://example.com/",
"client_ip": "127.0.0.1",
"message": "This app is cool but needs a new feature.\n\nAllow me to describe...",
}

View file

@ -268,35 +268,35 @@ class Form:
"""
def __init__(
self,
request,
fields=None,
schema=None,
model_class=None,
model_instance=None,
nodes={},
widgets={},
validators={},
defaults={},
readonly=False,
readonly_fields=[],
required_fields={},
labels={},
action_method='post',
action_url=None,
reset_url=None,
cancel_url=None,
cancel_url_fallback=None,
vue_tagname='wutta-form',
align_buttons_right=False,
auto_disable_submit=True,
button_label_submit="Save",
button_icon_submit='save',
button_type_submit='is-primary',
show_button_reset=False,
show_button_cancel=True,
button_label_cancel="Cancel",
auto_disable_cancel=True,
self,
request,
fields=None,
schema=None,
model_class=None,
model_instance=None,
nodes={},
widgets={},
validators={},
defaults={},
readonly=False,
readonly_fields=[],
required_fields={},
labels={},
action_method="post",
action_url=None,
reset_url=None,
cancel_url=None,
cancel_url_fallback=None,
vue_tagname="wutta-form",
align_buttons_right=False,
auto_disable_submit=True,
button_label_submit="Save",
button_icon_submit="save",
button_type_submit="is-primary",
show_button_reset=False,
show_button_cancel=True,
button_label_cancel="Cancel",
auto_disable_cancel=True,
):
self.request = request
self.schema = schema
@ -367,8 +367,8 @@ class Form:
This is a generated value based on :attr:`vue_tagname`.
"""
words = self.vue_tagname.split('-')
return ''.join([word.capitalize() for word in words])
words = self.vue_tagname.split("-")
return "".join([word.capitalize() for word in words])
def get_cancel_url(self):
"""
@ -392,8 +392,8 @@ class Form:
# nb. use fake default to avoid normal default logic;
# that way if we get something it's a real referrer
url = self.request.get_referrer(default='NOPE')
if url and url != 'NOPE':
url = self.request.get_referrer(default="NOPE")
if url and url != "NOPE":
return url
# use fallback URL if set
@ -471,8 +471,8 @@ class Form:
# assume nodeinfo is a complete node
node = nodeinfo
else: # assume nodeinfo is a schema type
kwargs.setdefault('name', key)
else: # assume nodeinfo is a schema type
kwargs.setdefault("name", key)
node = ObjectNode(nodeinfo, **kwargs)
self.nodes[key] = node
@ -534,7 +534,7 @@ class Form:
"""
from wuttaweb.forms import widgets
if widget_type == 'notes':
if widget_type == "notes":
return widgets.NotesWidget(**kwargs)
def set_default_widgets(self):
@ -565,7 +565,7 @@ class Form:
attr = getattr(self.model_class, key, None)
if attr:
prop = getattr(attr, 'prop', None)
prop = getattr(attr, "prop", None)
if prop and isinstance(prop, orm.ColumnProperty):
column = prop.columns[0]
if isinstance(column.type, sa.Date):
@ -596,8 +596,10 @@ class Form:
raise ValueError("grid must have a key!")
if grid.key in self.grid_vue_context:
log.warning("grid data with key '%s' already registered, "
"but will be replaced", grid.key)
log.warning(
"grid data with key '%s' already registered, " "but will be replaced",
grid.key,
)
self.grid_vue_context[grid.key] = grid.get_vue_context()
@ -748,7 +750,7 @@ class Form:
Otherwise ``None`` is returned.
"""
if hasattr(self, 'fields') and self.fields:
if hasattr(self, "fields") and self.fields:
return self.fields
if self.schema:
@ -769,8 +771,9 @@ class Form:
fields. If not set, the form's :attr:`model_class` is
assumed.
"""
return get_model_fields(self.config,
model_class=model_class or self.model_class)
return get_model_fields(
self.config, model_class=model_class or self.model_class
)
def get_schema(self):
"""
@ -802,8 +805,7 @@ class Form:
includes.append(key)
# make initial schema with ColanderAlchemy magic
schema = SQLAlchemySchemaNode(self.model_class,
includes=includes)
schema = SQLAlchemySchemaNode(self.model_class, includes=includes)
# fill in the blanks if anything got missed
for key in fields:
@ -824,9 +826,7 @@ class Form:
if not node:
# otherwise make simple string node
node = colander.SchemaNode(
colander.String(),
name=key)
node = colander.SchemaNode(colander.String(), name=key)
schema.add(node)
@ -844,7 +844,7 @@ class Form:
if key is None:
# nb. this one is form-wide
schema.validator = validator
elif key in schema: # field-level
elif key in schema: # field-level
schema[key].validator = validator
# apply default value overrides
@ -867,7 +867,7 @@ class Form:
Return the :class:`deform:deform.Form` instance for the form,
generating it automatically if necessary.
"""
if not hasattr(self, 'deform_form'):
if not hasattr(self, "deform_form"):
model = self.app.model
schema = self.get_schema()
kwargs = {}
@ -897,9 +897,9 @@ class Form:
# this is what we are trying currently...
if isinstance(schema, SQLAlchemySchemaNode):
kwargs['appstruct'] = schema.dictify(self.model_instance)
kwargs["appstruct"] = schema.dictify(self.model_instance)
else:
kwargs['appstruct'] = self.model_instance
kwargs["appstruct"] = self.model_instance
# create the Deform instance
# nb. must give a reference back to wutta form; this is
@ -926,10 +926,7 @@ class Form:
"""
return HTML.tag(self.vue_tagname, **kwargs)
def render_vue_template(
self,
template='/forms/vue_template.mako',
**context):
def render_vue_template(self, template="/forms/vue_template.mako", **context):
"""
Render the Vue template block for the form.
@ -962,29 +959,29 @@ class Form:
:param template: Path to Mako template which is used to render
the output.
"""
context['form'] = self
context['dform'] = self.get_deform()
context.setdefault('request', self.request)
context['model_data'] = self.get_vue_model_data()
context["form"] = self
context["dform"] = self.get_deform()
context.setdefault("request", self.request)
context["model_data"] = self.get_vue_model_data()
# set form method, enctype
context.setdefault('form_attrs', {})
context['form_attrs'].setdefault('method', self.action_method)
if self.action_method == 'post':
context['form_attrs'].setdefault('enctype', 'multipart/form-data')
context.setdefault("form_attrs", {})
context["form_attrs"].setdefault("method", self.action_method)
if self.action_method == "post":
context["form_attrs"].setdefault("enctype", "multipart/form-data")
# auto disable button on submit
if self.auto_disable_submit:
context['form_attrs']['@submit'] = 'formSubmitting = true'
context["form_attrs"]["@submit"] = "formSubmitting = true"
output = render(template, context)
return HTML.literal(output)
def render_vue_field(
self,
fieldname,
readonly=None,
**kwargs,
self,
fieldname,
readonly=None,
**kwargs,
):
"""
Render the given field completely, i.e. ``<b-field>`` wrapper
@ -1026,7 +1023,7 @@ class Form:
field = dform[fieldname]
kw = {}
if readonly:
kw['readonly'] = True
kw["readonly"] = True
html = field.serialize(**kw)
else:
@ -1034,20 +1031,20 @@ class Form:
# TODO: need to abstract this somehow
if self.model_instance:
value = self.model_instance[fieldname]
html = str(value) if value is not None else ''
html = str(value) if value is not None else ""
else:
html = ''
html = ""
# mark all that as safe
html = HTML.literal(html or '&nbsp;')
html = HTML.literal(html or "&nbsp;")
# render field label
label = self.get_label(fieldname)
# b-field attrs
attrs = {
':horizontal': 'true',
'label': label,
":horizontal": "true",
"label": label,
}
# next we will build array of messages to display..some
@ -1059,22 +1056,21 @@ class Form:
# show errors if present
errors = self.get_field_errors(fieldname)
if errors:
field_type = 'is-danger'
field_type = "is-danger"
messages.extend(errors)
# ..okay now we can declare the field messages and type
if field_type:
attrs['type'] = field_type
attrs["type"] = field_type
if messages:
cls = 'is-size-7'
if field_type == 'is-danger':
cls += ' has-text-danger'
messages = [HTML.tag('p', c=[msg], class_=cls)
for msg in messages]
slot = HTML.tag('slot', name='messages', c=messages)
html = HTML.tag('div', c=[html, slot])
cls = "is-size-7"
if field_type == "is-danger":
cls += " has-text-danger"
messages = [HTML.tag("p", c=[msg], class_=cls) for msg in messages]
slot = HTML.tag("slot", name="messages", c=messages)
html = HTML.tag("div", c=[html, slot])
return HTML.tag('b-field', c=[html], **attrs)
return HTML.tag("b-field", c=[html], **attrs)
def render_vue_finalize(self):
"""
@ -1094,11 +1090,10 @@ class Form:
"""
set_data = f"{self.vue_component}.data = function() {{ return {self.vue_component}Data }}"
make_component = f"Vue.component('{self.vue_tagname}', {self.vue_component})"
return HTML.tag('script', c=['\n',
HTML.literal(set_data),
'\n',
HTML.literal(make_component),
'\n'])
return HTML.tag(
"script",
c=["\n", HTML.literal(set_data), "\n", HTML.literal(make_component), "\n"],
)
def get_vue_model_data(self):
"""
@ -1191,10 +1186,10 @@ class Form:
:returns: Data dict, or ``False``.
"""
if hasattr(self, 'validated'):
if hasattr(self, "validated"):
del self.validated
if self.request.method != 'POST':
if self.request.method != "POST":
return False
# remove all readonly fields from deform / schema

View file

@ -55,8 +55,8 @@ class WuttaDateTime(colander.DateTime):
return colander.null
formats = [
'%Y-%m-%dT%H:%M:%S',
'%Y-%m-%dT%I:%M %p',
"%Y-%m-%dT%H:%M:%S",
"%Y-%m-%dT%I:%M %p",
]
for fmt in formats:
@ -98,7 +98,7 @@ class ObjectNode(colander.SchemaNode):
If the node's type does not have a ``dictify()`` method, this
will just convert the object to a string and return that.
"""
if hasattr(self.typ, 'dictify'):
if hasattr(self.typ, "dictify"):
return self.typ.dictify(obj)
# TODO: this is better than raising an error, as it previously
@ -122,7 +122,7 @@ class ObjectNode(colander.SchemaNode):
If the node's type does not have an ``objectify()`` method,
this will raise ``NotImplementeError``.
"""
if hasattr(self.typ, 'objectify'):
if hasattr(self.typ, "objectify"):
return self.typ.objectify(value)
class_name = self.typ.__class__.__name__
@ -148,9 +148,10 @@ class WuttaEnum(colander.Enum):
def widget_maker(self, **kwargs):
""" """
if 'values' not in kwargs:
kwargs['values'] = [(getattr(e, self.attr), getattr(e, self.attr))
for e in self.enum_cls]
if "values" not in kwargs:
kwargs["values"] = [
(getattr(e, self.attr), getattr(e, self.attr)) for e in self.enum_cls
]
return widgets.SelectWidget(**kwargs)
@ -180,8 +181,8 @@ class WuttaDictEnum(colander.String):
def widget_maker(self, **kwargs):
""" """
if 'values' not in kwargs:
kwargs['values'] = [(k, v) for k, v in self.enum_dct.items()]
if "values" not in kwargs:
kwargs["values"] = [(k, v) for k, v in self.enum_dct.items()]
return widgets.SelectWidget(**kwargs)
@ -201,7 +202,7 @@ class WuttaMoney(colander.Money):
"""
def __init__(self, request, *args, **kwargs):
self.scale = kwargs.pop('scale', None)
self.scale = kwargs.pop("scale", None)
super().__init__(*args, **kwargs)
self.request = request
self.config = self.request.wutta_config
@ -210,7 +211,7 @@ class WuttaMoney(colander.Money):
def widget_maker(self, **kwargs):
""" """
if self.scale:
kwargs.setdefault('scale', self.scale)
kwargs.setdefault("scale", self.scale)
return widgets.WuttaMoneyInputWidget(self.request, **kwargs)
@ -284,17 +285,17 @@ class ObjectRef(colander.SchemaType):
Note that in the latter, ``value`` must be a string.
"""
default_empty_option = ('', "(none)")
default_empty_option = ("", "(none)")
def __init__(
self,
request,
empty_option=None,
*args,
**kwargs,
self,
request,
empty_option=None,
*args,
**kwargs,
):
# nb. allow session injection for tests
self.session = kwargs.pop('session', Session())
self.session = kwargs.pop("session", Session())
super().__init__(*args, **kwargs)
self.request = request
self.config = self.request.wutta_config
@ -307,7 +308,7 @@ class ObjectRef(colander.SchemaType):
elif isinstance(empty_option, tuple) and len(empty_option) == 2:
self.empty_option = empty_option
else:
self.empty_option = ('', str(empty_option))
self.empty_option = ("", str(empty_option))
else:
self.empty_option = None
@ -427,17 +428,16 @@ class ObjectRef(colander.SchemaType):
:class:`~wuttaweb.forms.widgets.ObjectRefWidget`.
"""
if 'values' not in kwargs:
if "values" not in kwargs:
query = self.get_query()
objects = query.all()
values = [(self.serialize_object(obj), str(obj))
for obj in objects]
values = [(self.serialize_object(obj), str(obj)) for obj in objects]
if self.empty_option:
values.insert(0, self.empty_option)
kwargs['values'] = values
kwargs["values"] = values
if 'url' not in kwargs:
kwargs['url'] = self.get_object_url
if "url" not in kwargs:
kwargs["url"] = self.get_object_url
return widgets.ObjectRefWidget(self.request, **kwargs)
@ -475,7 +475,7 @@ class PersonRef(ObjectRef):
def get_object_url(self, person):
""" """
return self.request.route_url('people.view', uuid=person.uuid)
return self.request.route_url("people.view", uuid=person.uuid)
class RoleRef(ObjectRef):
@ -499,7 +499,7 @@ class RoleRef(ObjectRef):
def get_object_url(self, role):
""" """
return self.request.route_url('roles.view', uuid=role.uuid)
return self.request.route_url("roles.view", uuid=role.uuid)
class UserRef(ObjectRef):
@ -523,7 +523,7 @@ class UserRef(ObjectRef):
def get_object_url(self, user):
""" """
return self.request.route_url('users.view', uuid=user.uuid)
return self.request.route_url("users.view", uuid=user.uuid)
class RoleRefs(WuttaSet):
@ -544,9 +544,9 @@ class RoleRefs(WuttaSet):
:returns: Instance of
:class:`~wuttaweb.forms.widgets.RoleRefsWidget`.
"""
session = kwargs.setdefault('session', Session())
session = kwargs.setdefault("session", Session())
if 'values' not in kwargs:
if "values" not in kwargs:
model = self.app.model
auth = self.app.get_auth_handler()
@ -562,12 +562,14 @@ class RoleRefs(WuttaSet):
avoid.add(auth.get_role_administrator(session).uuid)
# everything else can be (un)assigned for users
roles = session.query(model.Role)\
.filter(~model.Role.uuid.in_(avoid))\
.order_by(model.Role.name)\
.all()
roles = (
session.query(model.Role)
.filter(~model.Role.uuid.in_(avoid))
.order_by(model.Role.name)
.all()
)
values = [(role.uuid.hex, role.name) for role in roles]
kwargs['values'] = values
kwargs["values"] = values
return widgets.RoleRefsWidget(self.request, **kwargs)
@ -598,15 +600,15 @@ class Permissions(WuttaSet):
:returns: Instance of
:class:`~wuttaweb.forms.widgets.PermissionsWidget`.
"""
kwargs.setdefault('session', Session())
kwargs.setdefault('permissions', self.permissions)
kwargs.setdefault("session", Session())
kwargs.setdefault("permissions", self.permissions)
if 'values' not in kwargs:
if "values" not in kwargs:
values = []
for gkey, group in self.permissions.items():
for pkey, perm in group['perms'].items():
values.append((pkey, perm['label']))
kwargs['values'] = values
for pkey, perm in group["perms"].items():
values.append((pkey, perm["label"]))
kwargs["values"] = values
return widgets.PermissionsWidget(self.request, **kwargs)
@ -631,7 +633,7 @@ class FileDownload(colander.String):
"""
def __init__(self, request, *args, **kwargs):
self.url = kwargs.pop('url', None)
self.url = kwargs.pop("url", None)
super().__init__(*args, **kwargs)
self.request = request
self.config = self.request.wutta_config
@ -639,7 +641,7 @@ class FileDownload(colander.String):
def widget_maker(self, **kwargs):
""" """
kwargs.setdefault('url', self.url)
kwargs.setdefault("url", self.url)
return widgets.FileDownloadWidget(self.request, **kwargs)
@ -653,16 +655,15 @@ class EmailRecipients(colander.String):
if appstruct is colander.null:
return colander.null
return '\n'.join(parse_list(appstruct))
return "\n".join(parse_list(appstruct))
def deserialize(self, node, cstruct):
""" """
if cstruct is colander.null:
return colander.null
values = [value for value in parse_list(cstruct)
if value]
return ', '.join(values)
values = [value for value in parse_list(cstruct) if value]
return ", ".join(values)
def widget_maker(self, **kwargs):
"""
@ -675,4 +676,4 @@ class EmailRecipients(colander.String):
# nb. colanderalchemy schema overrides
sa.DateTime.__colanderalchemy_config__ = {'typ': WuttaDateTime}
sa.DateTime.__colanderalchemy_config__ = {"typ": WuttaDateTime}

View file

@ -47,10 +47,19 @@ import os
import colander
import humanize
from deform.widget import (Widget, TextInputWidget, TextAreaWidget,
PasswordWidget, CheckedPasswordWidget,
CheckboxWidget, SelectWidget, CheckboxChoiceWidget,
DateInputWidget, DateTimeInputWidget, MoneyInputWidget)
from deform.widget import (
Widget,
TextInputWidget,
TextAreaWidget,
PasswordWidget,
CheckedPasswordWidget,
CheckboxWidget,
SelectWidget,
CheckboxChoiceWidget,
DateInputWidget,
DateTimeInputWidget,
MoneyInputWidget,
)
from webhelpers2.html import HTML
from wuttjamaican.conf import parse_list
@ -93,7 +102,8 @@ class ObjectRefWidget(SelectWidget):
when the :class:`~wuttaweb.forms.schema.ObjectRef` type
instance (associated with the node) is serialized.
"""
readonly_template = 'readonly/objectref'
readonly_template = "readonly/objectref"
def __init__(self, request, url=None, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -105,10 +115,14 @@ class ObjectRefWidget(SelectWidget):
values = super().get_template_values(field, cstruct, kw)
# add url, only if rendering readonly
readonly = kw.get('readonly', self.readonly)
readonly = kw.get("readonly", self.readonly)
if readonly:
if 'url' not in values and self.url and getattr(field.schema, 'model_instance', None):
values['url'] = self.url(field.schema.model_instance)
if (
"url" not in values
and self.url
and getattr(field.schema, "model_instance", None)
):
values["url"] = self.url(field.schema.model_instance)
return values
@ -128,7 +142,8 @@ class NotesWidget(TextAreaWidget):
* ``textarea``
* ``readonly/notes``
"""
readonly_template = 'readonly/notes'
readonly_template = "readonly/notes"
class WuttaCheckboxChoiceWidget(CheckboxChoiceWidget):
@ -165,7 +180,8 @@ class WuttaCheckedPasswordWidget(PasswordWidget):
* ``wutta_checked_password``
"""
template = 'wutta_checked_password'
template = "wutta_checked_password"
class WuttaDateWidget(DateInputWidget):
@ -197,7 +213,7 @@ class WuttaDateWidget(DateInputWidget):
def serialize(self, field, cstruct, **kw):
""" """
readonly = kw.get('readonly', self.readonly)
readonly = kw.get("readonly", self.readonly)
if readonly and cstruct:
dt = datetime.datetime.fromisoformat(cstruct)
return self.app.render_date(dt)
@ -234,7 +250,7 @@ class WuttaDateTimeWidget(DateTimeInputWidget):
def serialize(self, field, cstruct, **kw):
""" """
readonly = kw.get('readonly', self.readonly)
readonly = kw.get("readonly", self.readonly)
if readonly and cstruct:
dt = datetime.datetime.fromisoformat(cstruct)
return self.app.render_datetime(dt)
@ -264,7 +280,7 @@ class WuttaMoneyInputWidget(MoneyInputWidget):
"""
def __init__(self, request, *args, **kwargs):
self.scale = kwargs.pop('scale', 2)
self.scale = kwargs.pop("scale", 2)
super().__init__(*args, **kwargs)
self.request = request
self.config = self.request.wutta_config
@ -272,13 +288,13 @@ class WuttaMoneyInputWidget(MoneyInputWidget):
def serialize(self, field, cstruct, **kw):
""" """
readonly = kw.get('readonly', self.readonly)
readonly = kw.get("readonly", self.readonly)
if readonly:
if cstruct in (colander.null, None):
return HTML.tag('span')
return HTML.tag("span")
cstruct = decimal.Decimal(cstruct)
text = self.app.render_currency(cstruct, scale=self.scale)
return HTML.tag('span', c=[text])
return HTML.tag("span", c=[text])
return super().serialize(field, cstruct, **kw)
@ -301,10 +317,11 @@ class FileDownloadWidget(Widget):
:param url: Optional URL for hyperlink. If not specified, file
name/size is shown with no hyperlink.
"""
readonly_template = 'readonly/filedownload'
readonly_template = "readonly/filedownload"
def __init__(self, request, *args, **kwargs):
self.url = kwargs.pop('url', None)
self.url = kwargs.pop("url", None)
super().__init__(*args, **kwargs)
self.request = request
self.config = self.request.wutta_config
@ -313,21 +330,21 @@ class FileDownloadWidget(Widget):
def serialize(self, field, cstruct, **kw):
""" """
# nb. readonly is the only way this rolls
kw['readonly'] = True
kw["readonly"] = True
template = self.readonly_template
path = cstruct or None
if path:
kw.setdefault('filename', os.path.basename(path))
kw.setdefault('filesize', self.readable_size(path))
kw.setdefault("filename", os.path.basename(path))
kw.setdefault("filesize", self.readable_size(path))
if self.url:
kw.setdefault('url', self.url)
kw.setdefault("url", self.url)
else:
kw.setdefault('filename', None)
kw.setdefault('filesize', None)
kw.setdefault("filename", None)
kw.setdefault("filesize", None)
kw.setdefault('url', None)
kw.setdefault("url", None)
values = self.get_template_values(field, cstruct, kw)
return field.renderer(template, **values)
@ -371,7 +388,7 @@ class GridWidget(Widget):
:meth:`~wuttaweb.grids.base.Grid.render_table_element()` on
the ``grid`` to serialize.
"""
readonly = kw.get('readonly', self.readonly)
readonly = kw.get("readonly", self.readonly)
if not readonly:
raise NotImplementedError("edit not allowed for this widget")
@ -387,14 +404,15 @@ class RoleRefsWidget(WuttaCheckboxChoiceWidget):
This is a subclass of :class:`WuttaCheckboxChoiceWidget`.
"""
readonly_template = 'readonly/rolerefs'
readonly_template = "readonly/rolerefs"
def serialize(self, field, cstruct, **kw):
""" """
model = self.app.model
# special logic when field is editable
readonly = kw.get('readonly', self.readonly)
readonly = kw.get("readonly", self.readonly)
if not readonly:
# but does not apply if current user is root
@ -404,12 +422,11 @@ class RoleRefsWidget(WuttaCheckboxChoiceWidget):
# prune admin role from values list; it should not be
# one of the options since current user is not admin
values = kw.get('values', self.values)
values = [val for val in values
if val[0] != admin.uuid]
kw['values'] = values
values = kw.get("values", self.values)
values = [val for val in values if val[0] != admin.uuid]
kw["values"] = values
else: # readonly
else: # readonly
# roles
roles = []
@ -418,11 +435,11 @@ class RoleRefsWidget(WuttaCheckboxChoiceWidget):
role = self.session.get(model.Role, uuid)
if role:
roles.append(role)
kw['roles'] = roles
kw["roles"] = roles
# url
url = lambda role: self.request.route_url('roles.view', uuid=role.uuid)
kw['url'] = url
url = lambda role: self.request.route_url("roles.view", uuid=role.uuid)
kw["url"] = url
# default logic from here
return super().serialize(field, cstruct, **kw)
@ -440,19 +457,20 @@ class PermissionsWidget(WuttaCheckboxChoiceWidget):
* ``permissions``
* ``readonly/permissions``
"""
template = 'permissions'
readonly_template = 'readonly/permissions'
template = "permissions"
readonly_template = "readonly/permissions"
def serialize(self, field, cstruct, **kw):
""" """
kw.setdefault('permissions', self.permissions)
kw.setdefault("permissions", self.permissions)
if 'values' not in kw:
if "values" not in kw:
values = []
for gkey, group in self.permissions.items():
for pkey, perm in group['perms'].items():
values.append((pkey, perm['label']))
kw['values'] = values
for pkey, perm in group["perms"].items():
values.append((pkey, perm["label"]))
kw["values"] = values
return super().serialize(field, cstruct, **kw)
@ -472,13 +490,14 @@ class EmailRecipientsWidget(TextAreaWidget):
See also the :class:`~wuttaweb.forms.schema.EmailRecipients`
schema type, which uses this widget.
"""
readonly_template = 'readonly/email_recips'
readonly_template = "readonly/email_recips"
def serialize(self, field, cstruct, **kw):
""" """
readonly = kw.get('readonly', self.readonly)
readonly = kw.get("readonly", self.readonly)
if readonly:
kw['recips'] = parse_list(cstruct or '')
kw["recips"] = parse_list(cstruct or "")
return super().serialize(field, cstruct, **kw)
@ -487,9 +506,8 @@ class EmailRecipientsWidget(TextAreaWidget):
if pstruct is colander.null:
return colander.null
values = [value for value in parse_list(pstruct)
if value]
return ', '.join(values)
values = [value for value in parse_list(pstruct) if value]
return ", ".join(values)
class BatchIdWidget(Widget):
@ -508,4 +526,4 @@ class BatchIdWidget(Widget):
return colander.null
batch_id = int(cstruct)
return f'{batch_id:08d}'
return f"{batch_id:08d}"

View file

@ -48,13 +48,14 @@ from wuttaweb.grids.filters import default_sqlalchemy_filters, VerbNotSupported
log = logging.getLogger(__name__)
SortInfo = namedtuple('SortInfo', ['sortkey', 'sortdir'])
SortInfo = namedtuple("SortInfo", ["sortkey", "sortdir"])
SortInfo.__doc__ = """
Named tuple to track sorting info.
Elements of :attr:`~Grid.sort_defaults` will be of this type.
"""
class Grid:
"""
Base class for all :term:`grids <grid>`.
@ -374,37 +375,37 @@ class Grid:
"""
def __init__(
self,
request,
vue_tagname='wutta-grid',
model_class=None,
key=None,
columns=None,
data=None,
labels={},
renderers={},
enums={},
checkable=False,
row_class=None,
actions=[],
linked_columns=[],
hidden_columns=[],
sortable=False,
sort_multiple=True,
sort_on_backend=True,
sorters=None,
sort_defaults=None,
paginated=False,
paginate_on_backend=True,
pagesize_options=None,
pagesize=None,
page=1,
searchable_columns=None,
filterable=False,
filters=None,
filter_defaults=None,
joiners=None,
tools=None,
self,
request,
vue_tagname="wutta-grid",
model_class=None,
key=None,
columns=None,
data=None,
labels={},
renderers={},
enums={},
checkable=False,
row_class=None,
actions=[],
linked_columns=[],
hidden_columns=[],
sortable=False,
sort_multiple=True,
sort_on_backend=True,
sorters=None,
sort_defaults=None,
paginated=False,
paginate_on_backend=True,
pagesize_options=None,
pagesize=None,
page=1,
searchable_columns=None,
filterable=False,
filters=None,
filter_defaults=None,
joiners=None,
tools=None,
):
self.request = request
self.vue_tagname = vue_tagname
@ -434,7 +435,9 @@ class Grid:
self.sortable = sortable
self.sort_multiple = sort_multiple
if self.sort_multiple and self.request.use_oruga:
log.warning("grid.sort_multiple is not implemented for Oruga-based templates")
log.warning(
"grid.sort_multiple is not implemented for Oruga-based templates"
)
self.sort_multiple = False
self.sort_on_backend = sort_on_backend
if sorters is not None:
@ -482,7 +485,7 @@ class Grid:
Otherwise ``None`` is returned.
"""
if hasattr(self, 'columns') and self.columns:
if hasattr(self, "columns") and self.columns:
return self.columns
columns = self.get_model_columns()
@ -500,8 +503,9 @@ class Grid:
fields. If not set, the grid's :attr:`model_class` is
assumed.
"""
return get_model_fields(self.config,
model_class=model_class or self.model_class)
return get_model_fields(
self.config, model_class=model_class or self.model_class
)
@property
def vue_component(self):
@ -510,8 +514,8 @@ class Grid:
This is a generated value based on :attr:`vue_tagname`.
"""
words = self.vue_tagname.split('-')
return ''.join([word.capitalize() for word in words])
words = self.vue_tagname.split("-")
return "".join([word.capitalize() for word in words])
def set_columns(self, columns):
"""
@ -576,7 +580,7 @@ class Grid:
if hidden:
if key not in self.hidden_columns:
self.hidden_columns.append(key)
else: # un-hide
else: # un-hide
if self.hidden_columns and key in self.hidden_columns:
self.hidden_columns.remove(key)
@ -678,13 +682,13 @@ class Grid:
Renderer overrides are tracked via :attr:`renderers`.
"""
builtins = {
'batch_id': self.render_batch_id,
'boolean': self.render_boolean,
'currency': self.render_currency,
'date': self.render_date,
'datetime': self.render_datetime,
'quantity': self.render_quantity,
'percent': self.render_percent,
"batch_id": self.render_batch_id,
"boolean": self.render_boolean,
"currency": self.render_currency,
"date": self.render_date,
"datetime": self.render_datetime,
"quantity": self.render_quantity,
"percent": self.render_percent,
}
if renderer in builtins:
@ -722,7 +726,7 @@ class Grid:
attr = getattr(self.model_class, key, None)
if attr:
prop = getattr(attr, 'prop', None)
prop = getattr(attr, "prop", None)
if prop and isinstance(prop, orm.ColumnProperty):
column = prop.columns[0]
if isinstance(column.type, sa.Date):
@ -780,7 +784,7 @@ class Grid:
if link:
if key not in self.linked_columns:
self.linked_columns.append(key)
else: # unlink
else: # unlink
if self.linked_columns and key in self.linked_columns:
self.linked_columns.remove(key)
@ -946,8 +950,11 @@ class Grid:
if key in sorters:
continue
prop = getattr(self.model_class, key, None)
if (prop and hasattr(prop, 'property')
and isinstance(prop.property, orm.ColumnProperty)):
if (
prop
and hasattr(prop, "property")
and isinstance(prop.property, orm.ColumnProperty)
):
sorters[prop.key] = self.make_sorter(prop)
return sorters
@ -1039,7 +1046,9 @@ class Grid:
# query is sorted with order_by()
if isinstance(data, orm.Query):
if not model_property:
raise TypeError(f"grid sorter for '{key}' does not map to a model property")
raise TypeError(
f"grid sorter for '{key}' does not map to a model property"
)
query = data
return query.order_by(getattr(model_property, direction)())
@ -1052,16 +1061,16 @@ class Grid:
# TODO: may need this for String etc. as well?
if isinstance(model_property.type, sa.Text):
if foldcase:
kfunc = lambda obj: (obj[key] or '').lower()
kfunc = lambda obj: (obj[key] or "").lower()
else:
kfunc = lambda obj: obj[key] or ''
kfunc = lambda obj: obj[key] or ""
if not kfunc:
# nb. sorting with this can raise error if data
# contains varying types, e.g. str and None
kfunc = lambda obj: obj[key]
# then sort the data and return
return sorted(data, key=kfunc, reverse=direction == 'desc')
return sorted(data, key=kfunc, reverse=direction == "desc")
# TODO: this should be improved; is needed in tailbone for
# multi-column sorting with sqlalchemy queries
@ -1168,13 +1177,15 @@ class Grid:
sort_defaults = []
if len(args) == 1:
if isinstance(args[0], str):
sort_defaults = [SortInfo(args[0], 'asc')]
sort_defaults = [SortInfo(args[0], "asc")]
elif isinstance(args[0], tuple) and len(args[0]) == 2:
sort_defaults = [SortInfo(*args[0])]
elif isinstance(args[0], list):
sort_defaults = [SortInfo(*tup) for tup in args[0]]
else:
raise ValueError("for just one positional arg, must pass string, 2-tuple or list")
raise ValueError(
"for just one positional arg, must pass string, 2-tuple or list"
)
elif len(args) == 2:
sort_defaults = [SortInfo(*args)]
else:
@ -1182,9 +1193,12 @@ class Grid:
# prune if multi-column requested but not supported
if len(sort_defaults) > 1 and not self.sort_multiple:
log.warning("multi-column sorting is not enabled for the instance; "
"list will be pruned to first element for '%s' grid: %s",
self.key, sort_defaults)
log.warning(
"multi-column sorting is not enabled for the instance; "
"list will be pruned to first element for '%s' grid: %s",
self.key,
sort_defaults,
)
sort_defaults = [sort_defaults[0]]
self.sort_defaults = sort_defaults
@ -1270,8 +1284,7 @@ class Grid:
continue
# do not create filter for UUID field
if (len(prop.columns) == 1
and isinstance(prop.columns[0].type, UUID)):
if len(prop.columns) == 1 and isinstance(prop.columns[0].type, UUID):
continue
attr = getattr(self.model_class, prop.key)
@ -1294,12 +1307,12 @@ class Grid:
:returns: A :class:`~wuttaweb.grids.filters.GridFilter`
instance.
"""
key = kwargs.pop('key', None)
key = kwargs.pop("key", None)
# model_property is required
model_property = None
if kwargs.get('model_property'):
model_property = kwargs['model_property']
if kwargs.get("model_property"):
model_property = kwargs["model_property"]
elif isinstance(columninfo, str):
key = columninfo
if self.model_class:
@ -1310,7 +1323,7 @@ class Grid:
model_property = columninfo
# optional factory override
factory = kwargs.pop('factory', None)
factory = kwargs.pop("factory", None)
if not factory:
typ = model_property.type
factory = default_sqlalchemy_filters.get(type(typ))
@ -1318,7 +1331,7 @@ class Grid:
factory = default_sqlalchemy_filters[None]
# make filter
kwargs['model_property'] = model_property
kwargs["model_property"] = model_property
return factory(self.request, key or model_property.key, **kwargs)
def set_filter(self, key, filterinfo=None, **kwargs):
@ -1349,8 +1362,8 @@ class Grid:
# filtr = filterinfo
raise NotImplementedError
else:
kwargs['key'] = key
kwargs.setdefault('label', self.get_label(key))
kwargs["key"] = key
kwargs.setdefault("label", self.get_label(key))
filtr = self.make_filter(filterinfo or key, **kwargs)
self.filters[key] = filtr
@ -1384,7 +1397,7 @@ class Grid:
Filter defaults are tracked via :attr:`filter_defaults`.
"""
filter_defaults = dict(getattr(self, 'filter_defaults', {}))
filter_defaults = dict(getattr(self, "filter_defaults", {}))
for key, values in defaults.items():
filtr = filter_defaults.setdefault(key, {})
@ -1411,10 +1424,9 @@ class Grid:
This method is intended for use in the constructor. Code can
instead access :attr:`pagesize_options` directly.
"""
options = self.config.get_list('wuttaweb.grids.default_pagesize_options')
options = self.config.get_list("wuttaweb.grids.default_pagesize_options")
if options:
options = [int(size) for size in options
if size.isdigit()]
options = [int(size) for size in options if size.isdigit()]
if options:
return options
@ -1434,7 +1446,7 @@ class Grid:
This method is intended for use in the constructor. Code can
instead access :attr:`pagesize` directly.
"""
size = self.config.get_int('wuttaweb.grids.default_pagesize')
size = self.config.get_int("wuttaweb.grids.default_pagesize")
if size:
return size
@ -1485,51 +1497,54 @@ class Grid:
if self.filterable:
for filtr in self.filters.values():
defaults = self.filter_defaults.get(filtr.key, {})
settings[f'filter.{filtr.key}.active'] = defaults.get('active',
filtr.default_active)
settings[f'filter.{filtr.key}.verb'] = defaults.get('verb',
filtr.get_default_verb())
settings[f'filter.{filtr.key}.value'] = defaults.get('value',
filtr.default_value)
settings[f"filter.{filtr.key}.active"] = defaults.get(
"active", filtr.default_active
)
settings[f"filter.{filtr.key}.verb"] = defaults.get(
"verb", filtr.get_default_verb()
)
settings[f"filter.{filtr.key}.value"] = defaults.get(
"value", filtr.default_value
)
if self.sortable:
if self.sort_defaults:
# nb. as of writing neither Buefy nor Oruga support a
# multi-column *default* sort; so just use first sorter
sortinfo = self.sort_defaults[0]
settings['sorters.length'] = 1
settings['sorters.1.key'] = sortinfo.sortkey
settings['sorters.1.dir'] = sortinfo.sortdir
settings["sorters.length"] = 1
settings["sorters.1.key"] = sortinfo.sortkey
settings["sorters.1.dir"] = sortinfo.sortdir
else:
settings['sorters.length'] = 0
settings["sorters.length"] = 0
if self.paginated and self.paginate_on_backend:
settings['pagesize'] = self.pagesize
settings['page'] = self.page
settings["pagesize"] = self.pagesize
settings["page"] = self.page
# update settings dict based on what we find in the request
# and/or user session. always prioritize the former.
# nb. do not read settings if user wants a reset
if self.request.GET.get('reset-view'):
if self.request.GET.get("reset-view"):
# at this point we only have default settings, and we want
# to keep those *and* persist them for next time, below
pass
elif self.request_has_settings('filter'):
self.update_filter_settings(settings, src='request')
if self.request_has_settings('sort'):
self.update_sort_settings(settings, src='request')
elif self.request_has_settings("filter"):
self.update_filter_settings(settings, src="request")
if self.request_has_settings("sort"):
self.update_sort_settings(settings, src="request")
else:
self.update_sort_settings(settings, src='session')
self.update_sort_settings(settings, src="session")
self.update_page_settings(settings)
elif self.request_has_settings('sort'):
self.update_filter_settings(settings, src='session')
self.update_sort_settings(settings, src='request')
elif self.request_has_settings("sort"):
self.update_filter_settings(settings, src="session")
self.update_sort_settings(settings, src="request")
self.update_page_settings(settings)
elif self.request_has_settings('page'):
self.update_filter_settings(settings, src='session')
self.update_sort_settings(settings, src='session')
elif self.request_has_settings("page"):
self.update_filter_settings(settings, src="session")
self.update_sort_settings(settings, src="session")
self.update_page_settings(settings)
else:
@ -1537,32 +1552,36 @@ class Grid:
persist = False
# but still should load whatever is in user session
self.update_filter_settings(settings, src='session')
self.update_sort_settings(settings, src='session')
self.update_filter_settings(settings, src="session")
self.update_sort_settings(settings, src="session")
self.update_page_settings(settings)
# maybe save settings in user session, for next time
if persist:
self.persist_settings(settings, dest='session')
self.persist_settings(settings, dest="session")
# update ourself to reflect settings dict..
# filtering
if self.filterable:
for filtr in self.filters.values():
filtr.active = settings[f'filter.{filtr.key}.active']
filtr.verb = settings[f'filter.{filtr.key}.verb'] or filtr.get_default_verb()
filtr.value = settings[f'filter.{filtr.key}.value']
filtr.active = settings[f"filter.{filtr.key}.active"]
filtr.verb = (
settings[f"filter.{filtr.key}.verb"] or filtr.get_default_verb()
)
filtr.value = settings[f"filter.{filtr.key}.value"]
# sorting
if self.sortable:
# nb. doing this for frontend sorting also
self.active_sorters = []
for i in range(1, settings['sorters.length'] + 1):
self.active_sorters.append({
'key': settings[f'sorters.{i}.key'],
'dir': settings[f'sorters.{i}.dir'],
})
for i in range(1, settings["sorters.length"] + 1):
self.active_sorters.append(
{
"key": settings[f"sorters.{i}.key"],
"dir": settings[f"sorters.{i}.dir"],
}
)
# TODO: i thought this was needed, but now idk?
# # nb. when showing full index page (i.e. not partial)
# # this implies we must set the default sorter for Vue
@ -1572,35 +1591,36 @@ class Grid:
# paging
if self.paginated and self.paginate_on_backend:
self.pagesize = settings['pagesize']
self.page = settings['page']
self.pagesize = settings["pagesize"]
self.page = settings["page"]
def request_has_settings(self, typ):
""" """
if typ == 'filter' and self.filterable:
if typ == "filter" and self.filterable:
for filtr in self.filters.values():
if filtr.key in self.request.GET:
return True
if 'filter' in self.request.GET: # user may be applying empty filters
if "filter" in self.request.GET: # user may be applying empty filters
return True
elif typ == 'sort' and self.sortable and self.sort_on_backend:
if 'sort1key' in self.request.GET:
elif typ == "sort" and self.sortable and self.sort_on_backend:
if "sort1key" in self.request.GET:
return True
elif typ == 'page' and self.paginated and self.paginate_on_backend:
for key in ['pagesize', 'page']:
elif typ == "page" and self.paginated and self.paginate_on_backend:
for key in ["pagesize", "page"]:
if key in self.request.GET:
return True
return False
def get_setting(self, settings, key, src='session', default=None,
normalize=lambda v: v):
def get_setting(
self, settings, key, src="session", default=None, normalize=lambda v: v
):
""" """
if src == 'request':
if src == "request":
value = self.request.GET.get(key)
if value is not None:
try:
@ -1608,8 +1628,8 @@ class Grid:
except ValueError:
pass
elif src == 'session':
value = self.request.session.get(f'grid.{self.key}.{key}')
elif src == "session":
value = self.request.session.get(f"grid.{self.key}.{key}")
if value is not None:
return normalize(value)
@ -1627,52 +1647,62 @@ class Grid:
return
for filtr in self.filters.values():
prefix = f'filter.{filtr.key}'
prefix = f"filter.{filtr.key}"
if src == 'request':
if src == "request":
# consider filter active if query string contains a value for it
settings[f'{prefix}.active'] = filtr.key in self.request.GET
settings[f'{prefix}.verb'] = self.get_setting(
settings, f'{filtr.key}.verb', src='request', default='')
settings[f'{prefix}.value'] = self.get_setting(
settings, filtr.key, src='request', default='')
settings[f"{prefix}.active"] = filtr.key in self.request.GET
settings[f"{prefix}.verb"] = self.get_setting(
settings, f"{filtr.key}.verb", src="request", default=""
)
settings[f"{prefix}.value"] = self.get_setting(
settings, filtr.key, src="request", default=""
)
elif src == 'session':
settings[f'{prefix}.active'] = self.get_setting(
settings, f'{prefix}.active', src='session',
normalize=lambda v: str(v).lower() == 'true', default=False)
settings[f'{prefix}.verb'] = self.get_setting(
settings, f'{prefix}.verb', src='session', default='')
settings[f'{prefix}.value'] = self.get_setting(
settings, f'{prefix}.value', src='session', default='')
elif src == "session":
settings[f"{prefix}.active"] = self.get_setting(
settings,
f"{prefix}.active",
src="session",
normalize=lambda v: str(v).lower() == "true",
default=False,
)
settings[f"{prefix}.verb"] = self.get_setting(
settings, f"{prefix}.verb", src="session", default=""
)
settings[f"{prefix}.value"] = self.get_setting(
settings, f"{prefix}.value", src="session", default=""
)
def update_sort_settings(self, settings, src=None):
""" """
if not (self.sortable and self.sort_on_backend):
return
if src == 'request':
if src == "request":
i = 1
while True:
skey = f'sort{i}key'
skey = f"sort{i}key"
if skey in self.request.GET:
settings[f'sorters.{i}.key'] = self.get_setting(settings, skey,
src='request')
settings[f'sorters.{i}.dir'] = self.get_setting(settings, f'sort{i}dir',
src='request',
default='asc')
settings[f"sorters.{i}.key"] = self.get_setting(
settings, skey, src="request"
)
settings[f"sorters.{i}.dir"] = self.get_setting(
settings, f"sort{i}dir", src="request", default="asc"
)
else:
break
i += 1
settings['sorters.length'] = i - 1
settings["sorters.length"] = i - 1
elif src == 'session':
settings['sorters.length'] = self.get_setting(settings, 'sorters.length',
src='session', normalize=int)
for i in range(1, settings['sorters.length'] + 1):
for key in ('key', 'dir'):
skey = f'sorters.{i}.{key}'
settings[skey] = self.get_setting(settings, skey, src='session')
elif src == "session":
settings["sorters.length"] = self.get_setting(
settings, "sorters.length", src="session", normalize=int
)
for i in range(1, settings["sorters.length"] + 1):
for key in ("key", "dir"):
skey = f"sorters.{i}.{key}"
settings[skey] = self.get_setting(settings, skey, src="session")
def update_page_settings(self, settings):
""" """
@ -1682,34 +1712,34 @@ class Grid:
# update the settings dict from request and/or user session
# pagesize
pagesize = self.request.GET.get('pagesize')
pagesize = self.request.GET.get("pagesize")
if pagesize is not None:
if pagesize.isdigit():
settings['pagesize'] = int(pagesize)
settings["pagesize"] = int(pagesize)
else:
pagesize = self.request.session.get(f'grid.{self.key}.pagesize')
pagesize = self.request.session.get(f"grid.{self.key}.pagesize")
if pagesize is not None:
settings['pagesize'] = pagesize
settings["pagesize"] = pagesize
# page
page = self.request.GET.get('page')
page = self.request.GET.get("page")
if page is not None:
if page.isdigit():
settings['page'] = int(page)
settings["page"] = int(page)
else:
page = self.request.session.get(f'grid.{self.key}.page')
page = self.request.session.get(f"grid.{self.key}.page")
if page is not None:
settings['page'] = int(page)
settings["page"] = int(page)
def persist_settings(self, settings, dest=None):
""" """
if dest not in ('session',):
if dest not in ("session",):
raise ValueError(f"invalid dest identifier: {dest}")
# func to save a setting value to user session
def persist(key, value=lambda k: settings.get(k)):
assert dest == 'session'
skey = f'grid.{self.key}.{key}'
assert dest == "session"
skey = f"grid.{self.key}.{key}"
self.request.session[skey] = value(key)
# filter settings
@ -1717,10 +1747,12 @@ class Grid:
# always save all filters, with status
for filtr in self.filters.values():
persist(f'filter.{filtr.key}.active',
value=lambda k: 'true' if settings.get(k) else 'false')
persist(f'filter.{filtr.key}.verb')
persist(f'filter.{filtr.key}.value')
persist(
f"filter.{filtr.key}.active",
value=lambda k: "true" if settings.get(k) else "false",
)
persist(f"filter.{filtr.key}.verb")
persist(f"filter.{filtr.key}.value")
# sort settings
if self.sortable and self.sort_on_backend:
@ -1729,26 +1761,26 @@ class Grid:
# because number of sort settings will vary, so we delete
# all and then write all
if dest == 'session':
if dest == "session":
# remove sort settings from user session
prefix = f'grid.{self.key}.sorters.'
prefix = f"grid.{self.key}.sorters."
for key in list(self.request.session):
if key.startswith(prefix):
del self.request.session[key]
# now save sort settings to dest
if 'sorters.length' in settings:
persist('sorters.length')
for i in range(1, settings['sorters.length'] + 1):
persist(f'sorters.{i}.key')
persist(f'sorters.{i}.dir')
if "sorters.length" in settings:
persist("sorters.length")
for i in range(1, settings["sorters.length"] + 1):
persist(f"sorters.{i}.key")
persist(f"sorters.{i}.dir")
# pagination settings
if self.paginated and self.paginate_on_backend:
# save to dest
persist('pagesize')
persist('page')
persist("pagesize")
persist("page")
##############################
# data methods
@ -1794,8 +1826,7 @@ class Grid:
This inspects each :class:`~wuttaweb.grids.filters.GridFilter`
in :attr:`filters` and only returns the ones marked active.
"""
return [filtr for filtr in self.filters.values()
if filtr.active]
return [filtr for filtr in self.filters.values() if filtr.active]
def filter_data(self, data, filters=None):
"""
@ -1848,8 +1879,8 @@ class Grid:
sorters = reversed(sorters)
for sorter in sorters:
sortkey = sorter['key']
sortdir = sorter['dir']
sortkey = sorter["key"]
sortdir = sorter["dir"]
# cannot sort unless we have a sorter callable
sortfunc = self.sorters.get(sortkey)
@ -1876,20 +1907,18 @@ class Grid:
This method is called by :meth:`get_visible_data()`.
"""
if isinstance(data, orm.Query):
pager = SqlalchemyOrmPage(data,
items_per_page=self.pagesize,
page=self.page)
pager = SqlalchemyOrmPage(
data, items_per_page=self.pagesize, page=self.page
)
else:
pager = paginate.Page(data,
items_per_page=self.pagesize,
page=self.page)
pager = paginate.Page(data, items_per_page=self.pagesize, page=self.page)
# pager may have detected that our current page is outside the
# valid range. if so we should update ourself to match
if pager.page != self.page:
self.page = pager.page
key = f'grid.{self.key}.page'
key = f"grid.{self.key}.page"
if key in self.request.session:
self.request.session[key] = self.page
@ -1914,7 +1943,7 @@ class Grid:
return ""
batch_id = int(value)
return f'{batch_id:08d}'
return f"{batch_id:08d}"
def render_boolean(self, obj, key, value):
"""
@ -2034,10 +2063,8 @@ class Grid:
return self.app.render_quantity(value)
def render_table_element(
self,
form=None,
template='/grids/table_element.mako',
**context):
self, form=None, template="/grids/table_element.mako", **context
):
"""
Render a simple Vue table element for the grid.
@ -2100,10 +2127,7 @@ class Grid:
"""
return HTML.tag(self.vue_tagname, **kwargs)
def render_vue_template(
self,
template='/grids/vue_template.mako',
**context):
def render_vue_template(self, template="/grids/vue_template.mako", **context):
"""
Render the Vue template block for the grid.
@ -2141,8 +2165,8 @@ class Grid:
:param template: Path to Mako template which is used to render
the output.
"""
context['grid'] = self
context.setdefault('request', self.request)
context["grid"] = self
context.setdefault("request", self.request)
output = render(template, context)
return HTML.literal(output)
@ -2164,11 +2188,10 @@ class Grid:
"""
set_data = f"{self.vue_component}.data = function() {{ return {self.vue_component}Data }}"
make_component = f"Vue.component('{self.vue_tagname}', {self.vue_component})"
return HTML.tag('script', c=['\n',
HTML.literal(set_data),
'\n',
HTML.literal(make_component),
'\n'])
return HTML.tag(
"script",
c=["\n", HTML.literal(set_data), "\n", HTML.literal(make_component), "\n"],
)
def get_vue_columns(self):
"""
@ -2195,13 +2218,15 @@ class Grid:
columns = []
for name in self.columns:
columns.append({
'field': name,
'label': self.get_label(name),
'hidden': self.is_hidden(name),
'sortable': self.is_sortable(name),
'searchable': self.is_searchable(name),
})
columns.append(
{
"field": name,
"label": self.get_label(name),
"hidden": self.is_hidden(name),
"sortable": self.is_sortable(name),
"searchable": self.is_searchable(name),
}
)
return columns
def get_vue_active_sorters(self):
@ -2223,8 +2248,7 @@ class Grid:
"""
sorters = []
for sorter in self.active_sorters:
sorters.append({'field': sorter['key'],
'order': sorter['dir']})
sorters.append({"field": sorter["key"], "order": sorter["dir"]})
return sorters
def get_vue_first_sorter(self):
@ -2247,10 +2271,10 @@ class Grid:
:returns: The first sorter in format ``[sortkey, sortdir]``,
or ``None``.
"""
if hasattr(self, 'active_sorters'):
if hasattr(self, "active_sorters"):
if self.active_sorters:
sorter = self.active_sorters[0]
return [sorter['key'], sorter['dir']]
return [sorter["key"], sorter["dir"]]
elif self.sort_defaults:
sorter = self.sort_defaults[0]
@ -2272,20 +2296,22 @@ class Grid:
choices = list(filtr.choices)
choice_labels = dict(filtr.choices)
filters.append({
'key': filtr.key,
'data_type': filtr.data_type,
'active': filtr.active,
'visible': filtr.active,
'verbs': filtr.get_verbs(),
'verb_labels': filtr.get_verb_labels(),
'valueless_verbs': filtr.get_valueless_verbs(),
'verb': filtr.verb,
'choices': choices,
'choice_labels': choice_labels,
'value': filtr.value,
'label': filtr.label,
})
filters.append(
{
"key": filtr.key,
"data_type": filtr.data_type,
"active": filtr.active,
"visible": filtr.active,
"verbs": filtr.get_verbs(),
"verb_labels": filtr.get_verb_labels(),
"valueless_verbs": filtr.get_valueless_verbs(),
"verb": filtr.verb,
"choices": choices,
"choice_labels": choice_labels,
"value": filtr.value,
"label": filtr.label,
}
)
return filters
def object_to_dict(self, obj):
@ -2294,7 +2320,7 @@ class Grid:
dct = dict(obj)
except TypeError:
dct = dict(obj.__dict__)
dct.pop('_sa_instance_state', None)
dct.pop("_sa_instance_state", None)
return dct
def get_vue_context(self):
@ -2345,7 +2371,7 @@ class Grid:
# add action urls to each record
for action in self.actions:
key = f'_action_url_{action.key}'
key = f"_action_url_{action.key}"
if key not in record:
url = action.get_url(original_record, i)
if url:
@ -2355,21 +2381,24 @@ class Grid:
css_class = self.get_row_class(original_record, record, i)
if css_class:
# nb. use *string* zero-based index, for js compat
row_classes[str(i-1)] = css_class
row_classes[str(i - 1)] = css_class
data.append(record)
return {
'data': data,
'row_classes': row_classes,
"data": data,
"row_classes": row_classes,
}
def get_vue_data(self):
""" """
warnings.warn("grid.get_vue_data() is deprecated; "
"please use grid.get_vue_context() instead",
DeprecationWarning, stacklevel=2)
return self.get_vue_context()['data']
warnings.warn(
"grid.get_vue_data() is deprecated; "
"please use grid.get_vue_context() instead",
DeprecationWarning,
stacklevel=2,
)
return self.get_vue_context()["data"]
def get_row_class(self, obj, data, i):
"""
@ -2402,12 +2431,12 @@ class Grid:
"""
pager = self.pager
return {
'item_count': pager.item_count,
'items_per_page': pager.items_per_page,
'page': pager.page,
'page_count': pager.page_count,
'first_item': pager.first_item,
'last_item': pager.last_item,
"item_count": pager.item_count,
"items_per_page": pager.items_per_page,
"page": pager.page,
"page_count": pager.page_count,
"first_item": pager.first_item,
"last_item": pager.last_item,
}
@ -2487,15 +2516,15 @@ class GridAction:
"""
def __init__(
self,
request,
key,
label=None,
url=None,
target=None,
click_handler=None,
icon=None,
link_class=None,
self,
request,
key,
label=None,
url=None,
target=None,
click_handler=None,
icon=None,
link_class=None,
):
self.request = request
self.config = self.request.wutta_config
@ -2506,7 +2535,7 @@ class GridAction:
self.click_handler = click_handler
self.label = label or self.app.make_title(key)
self.icon = icon or key
self.link_class = link_class or ''
self.link_class = link_class or ""
def render_icon_and_label(self):
"""
@ -2519,7 +2548,7 @@ class GridAction:
self.render_icon(),
self.render_label(),
]
return HTML.literal(' ').join(html)
return HTML.literal(" ").join(html)
def render_icon(self):
"""
@ -2535,9 +2564,9 @@ class GridAction:
See also :meth:`render_icon_and_label()`.
"""
if self.request.use_oruga:
return HTML.tag('o-icon', icon=self.icon)
return HTML.tag("o-icon", icon=self.icon)
return HTML.tag('i', class_=f'fas fa-{self.icon}')
return HTML.tag("i", class_=f"fas fa-{self.icon}")
def render_label(self):
"""

View file

@ -27,9 +27,10 @@ Grid Filters
import datetime
import logging
from collections import OrderedDict
try:
from enum import EnumType
except ImportError: # pragma: no cover
except ImportError: # pragma: no cover
# nb. python <= 3.10
from enum import EnumMeta as EnumType
@ -137,47 +138,48 @@ class GridFilter:
See also :attr:`default_verb`.
"""
data_type = 'string'
default_verbs = ['equal', 'not_equal']
data_type = "string"
default_verbs = ["equal", "not_equal"]
default_verb_labels = {
'is_any': "is any",
'equal': "equal to",
'not_equal': "not equal to",
'greater_than': "greater than",
'greater_equal': "greater than or equal to",
'less_than': "less than",
'less_equal': "less than or equal to",
"is_any": "is any",
"equal": "equal to",
"not_equal": "not equal to",
"greater_than": "greater than",
"greater_equal": "greater than or equal to",
"less_than": "less than",
"less_equal": "less than or equal to",
# 'between': "between",
'is_true': "is true",
'is_false': "is false",
'is_false_null': "is false or null",
'is_null': "is null",
'is_not_null': "is not null",
'contains': "contains",
'does_not_contain': "does not contain",
"is_true": "is true",
"is_false": "is false",
"is_false_null": "is false or null",
"is_null": "is null",
"is_not_null": "is not null",
"contains": "contains",
"does_not_contain": "does not contain",
}
valueless_verbs = [
'is_any',
'is_true',
'is_false',
'is_false_null',
'is_null',
'is_not_null',
"is_any",
"is_true",
"is_false",
"is_false_null",
"is_null",
"is_not_null",
]
def __init__(
self,
request,
key,
label=None,
verbs=None,
choices={},
default_active=False,
default_verb=None,
default_value=None,
**kwargs,
self,
request,
key,
label=None,
verbs=None,
choices={},
default_active=False,
default_verb=None,
default_value=None,
**kwargs,
):
self.request = request
self.key = key
@ -205,12 +207,14 @@ class GridFilter:
self.__dict__.update(kwargs)
def __repr__(self):
verb = getattr(self, 'verb', None)
return (f"{self.__class__.__name__}("
f"key='{self.key}', "
f"active={self.active}, "
f"verb={repr(verb)}, "
f"value={repr(self.value)})")
verb = getattr(self, "verb", None)
return (
f"{self.__class__.__name__}("
f"key='{self.key}', "
f"active={self.active}, "
f"verb={repr(verb)}, "
f"value={repr(self.value)})"
)
def get_verbs(self):
"""
@ -218,7 +222,7 @@ class GridFilter:
"""
verbs = None
if hasattr(self, 'verbs'):
if hasattr(self, "verbs"):
verbs = self.verbs
else:
@ -229,13 +233,13 @@ class GridFilter:
verbs = list(verbs)
if self.nullable:
if 'is_null' not in verbs:
verbs.append('is_null')
if 'is_not_null' not in verbs:
verbs.append('is_not_null')
if "is_null" not in verbs:
verbs.append("is_null")
if "is_not_null" not in verbs:
verbs.append("is_not_null")
if 'is_any' not in verbs:
verbs.append('is_any')
if "is_any" not in verbs:
verbs.append("is_any")
return verbs
@ -260,10 +264,10 @@ class GridFilter:
"""
verb = None
if hasattr(self, 'default_verb'):
if hasattr(self, "default_verb"):
verb = self.default_verb
elif hasattr(self, 'verb'):
elif hasattr(self, "verb"):
verb = self.verb
if not verb:
@ -291,10 +295,10 @@ class GridFilter:
"""
if choices:
self.choices = self.normalize_choices(choices)
self.data_type = 'choice'
self.data_type = "choice"
else:
self.choices = {}
self.data_type = 'string'
self.data_type = "string"
def normalize_choices(self, choices):
"""
@ -320,22 +324,18 @@ class GridFilter:
normalized = choices
if isinstance(choices, EnumType):
normalized = OrderedDict([
(member.name, member.value)
for member in choices])
normalized = OrderedDict(
[(member.name, member.value) for member in choices]
)
elif isinstance(choices, OrderedDict):
normalized = choices
elif isinstance(choices, dict):
normalized = OrderedDict([
(key, choices[key])
for key in sorted(choices)])
normalized = OrderedDict([(key, choices[key]) for key in sorted(choices)])
elif isinstance(choices, list):
normalized = OrderedDict([
(key, key)
for key in choices])
normalized = OrderedDict([(key, key) for key in choices])
return normalized
@ -358,8 +358,11 @@ class GridFilter:
verb = self.verb
if not verb:
verb = self.get_default_verb()
log.warn("missing verb for '%s' filter, will use default verb: %s",
self.key, verb)
log.warn(
"missing verb for '%s' filter, will use default verb: %s",
self.key,
verb,
)
# only attempt for known verbs
if verb not in self.get_verbs():
@ -370,7 +373,7 @@ class GridFilter:
value = self.value
# locate filter method
func = getattr(self, f'filter_{verb}', None)
func = getattr(self, f"filter_{verb}", None)
if not func:
raise VerbNotSupported(verb)
@ -403,7 +406,7 @@ class AlchemyFilter(GridFilter):
"""
def __init__(self, *args, **kwargs):
nullable = kwargs.pop('nullable', None)
nullable = kwargs.pop("nullable", None)
super().__init__(*args, **kwargs)
self.nullable = nullable
@ -441,10 +444,12 @@ class AlchemyFilter(GridFilter):
# sql probably excludes null values from results, but user
# probably does not expect that, so explicitly include them.
return query.filter(sa.or_(
self.model_property == None,
self.model_property != value,
))
return query.filter(
sa.or_(
self.model_property == None,
self.model_property != value,
)
)
def filter_greater_than(self, query, value):
"""
@ -502,8 +507,8 @@ class StringAlchemyFilter(AlchemyFilter):
Subclass of :class:`AlchemyFilter`.
"""
default_verbs = ['contains', 'does_not_contain',
'equal', 'not_equal']
default_verbs = ["contains", "does_not_contain", "equal", "not_equal"]
def coerce_value(self, value):
""" """
@ -522,8 +527,8 @@ class StringAlchemyFilter(AlchemyFilter):
criteria = []
for val in value.split():
val = val.replace('_', r'\_')
val = f'%{val}%'
val = val.replace("_", r"\_")
val = f"%{val}%"
criteria.append(self.model_property.ilike(val))
return query.filter(sa.and_(*criteria))
@ -538,15 +543,13 @@ class StringAlchemyFilter(AlchemyFilter):
criteria = []
for val in value.split():
val = val.replace('_', r'\_')
val = f'%{val}%'
val = val.replace("_", r"\_")
val = f"%{val}%"
criteria.append(~self.model_property.ilike(val))
# sql probably excludes null values from results, but user
# probably does not expect that, so explicitly include them.
return query.filter(sa.or_(
self.model_property == None,
sa.and_(*criteria)))
return query.filter(sa.or_(self.model_property == None, sa.and_(*criteria)))
class NumericAlchemyFilter(AlchemyFilter):
@ -555,9 +558,15 @@ class NumericAlchemyFilter(AlchemyFilter):
Subclass of :class:`AlchemyFilter`.
"""
default_verbs = ['equal', 'not_equal',
'greater_than', 'greater_equal',
'less_than', 'less_equal']
default_verbs = [
"equal",
"not_equal",
"greater_than",
"greater_equal",
"less_than",
"less_equal",
]
class IntegerAlchemyFilter(NumericAlchemyFilter):
@ -582,26 +591,27 @@ class BooleanAlchemyFilter(AlchemyFilter):
Subclass of :class:`AlchemyFilter`.
"""
default_verbs = ['is_true', 'is_false']
default_verbs = ["is_true", "is_false"]
def get_verbs(self):
""" """
# get basic verbs from caller, or default list
verbs = getattr(self, 'verbs', self.default_verbs)
verbs = getattr(self, "verbs", self.default_verbs)
if callable(verbs):
verbs = verbs()
verbs = list(verbs)
# add some more if column is nullable
if self.nullable:
for verb in ('is_false_null', 'is_null', 'is_not_null'):
for verb in ("is_false_null", "is_null", "is_not_null"):
if verb not in verbs:
verbs.append(verb)
# add wildcard
if 'is_any' not in verbs:
verbs.append('is_any')
if "is_any" not in verbs:
verbs.append("is_any")
return verbs
@ -629,8 +639,9 @@ class BooleanAlchemyFilter(AlchemyFilter):
Filter data with "is false or null" condition. The value is
ignored.
"""
return query.filter(sa.or_(self.model_property == False,
self.model_property == None))
return query.filter(
sa.or_(self.model_property == False, self.model_property == None)
)
class DateAlchemyFilter(AlchemyFilter):
@ -640,24 +651,25 @@ class DateAlchemyFilter(AlchemyFilter):
Subclass of :class:`AlchemyFilter`.
"""
data_type = 'date'
data_type = "date"
default_verbs = [
'equal',
'not_equal',
'greater_than',
'greater_equal',
'less_than',
'less_equal',
"equal",
"not_equal",
"greater_than",
"greater_equal",
"less_than",
"less_equal",
# 'between',
]
default_verb_labels = {
'equal': "on",
'not_equal': "not on",
'greater_than': "after",
'greater_equal': "on or after",
'less_than': "before",
'less_equal': "on or before",
"equal": "on",
"not_equal": "not on",
"greater_than": "after",
"greater_equal": "on or after",
"less_than": "before",
"less_equal": "on or before",
# 'between': "between",
}
@ -668,7 +680,7 @@ class DateAlchemyFilter(AlchemyFilter):
return value
try:
dt = datetime.datetime.strptime(value, '%Y-%m-%d')
dt = datetime.datetime.strptime(value, "%Y-%m-%d")
except ValueError:
log.warning("invalid date value: %s", value)
else:

View file

@ -49,8 +49,8 @@ class WebHandler(GenericHandler):
:param resource: :class:`fanstatic:fanstatic.Resource`
instance representing an image file or other resource.
"""
needed = request.environ['fanstatic.needed']
url = needed.library_url(resource.library) + '/'
needed = request.environ["fanstatic.needed"]
url = needed.library_url(resource.library) + "/"
if request.script_name:
url = request.script_name + url
return url + resource.relpath
@ -67,7 +67,7 @@ class WebHandler(GenericHandler):
[wuttaweb]
favicon_url = http://example.com/favicon.ico
"""
url = self.config.get('wuttaweb.favicon_url')
url = self.config.get("wuttaweb.favicon_url")
if url:
return url
return self.get_fanstatic_url(request, static.favicon)
@ -85,7 +85,7 @@ class WebHandler(GenericHandler):
[wuttaweb]
header_logo_url = http://example.com/logo.png
"""
url = self.config.get('wuttaweb.header_logo_url')
url = self.config.get("wuttaweb.header_logo_url")
if url:
return url
return self.get_favicon_url(request)
@ -102,7 +102,7 @@ class WebHandler(GenericHandler):
[wuttaweb]
logo_url = http://example.com/logo.png
"""
url = self.config.get('wuttaweb.logo_url')
url = self.config.get("wuttaweb.logo_url")
if url:
return url
return self.get_fanstatic_url(request, static.logo)
@ -120,16 +120,20 @@ class WebHandler(GenericHandler):
:returns: Instance of :class:`~wuttaweb.menus.MenuHandler`.
"""
spec = self.config.get(f'{self.appname}.web.menus.handler.spec')
spec = self.config.get(f"{self.appname}.web.menus.handler.spec")
if not spec:
spec = self.config.get(f'{self.appname}.web.menus.handler_spec')
spec = self.config.get(f"{self.appname}.web.menus.handler_spec")
if spec:
warnings.warn(f"setting '{self.appname}.web.menus.handler_spec' is deprecated; "
f"please use '{self.appname}.web.menus.handler.spec' instead",
DeprecationWarning)
warnings.warn(
f"setting '{self.appname}.web.menus.handler_spec' is deprecated; "
f"please use '{self.appname}.web.menus.handler.spec' instead",
DeprecationWarning,
)
else:
spec = self.config.get(f'{self.appname}.web.menus.handler.default_spec',
default='wuttaweb.menus:MenuHandler')
spec = self.config.get(
f"{self.appname}.web.menus.handler.default_spec",
default="wuttaweb.menus:MenuHandler",
)
factory = self.app.load_object(spec)
return factory(self.config)
@ -175,13 +179,15 @@ class WebHandler(GenericHandler):
handlers.extend(default)
# configured default, if applicable
default = self.config.get(f'{self.config.appname}.web.menus.handler.default_spec')
default = self.config.get(
f"{self.config.appname}.web.menus.handler.default_spec"
)
if default and default not in handlers:
handlers.append(default)
# registered via entry points
registered = []
for Handler in load_entry_points(f'{self.appname}.web.menus').values():
for Handler in load_entry_points(f"{self.appname}.web.menus").values():
spec = Handler.get_spec()
if spec not in handlers:
registered.append(spec)

View file

@ -121,13 +121,13 @@ class MenuHandler(GenericHandler):
dicts as described in :class:`MenuHandler`.
"""
return {
'title': "People",
'type': 'menu',
'items': [
"title": "People",
"type": "menu",
"items": [
{
'title': "All People",
'route': 'people',
'perm': 'people.list',
"title": "All People",
"route": "people",
"perm": "people.list",
},
],
}
@ -150,59 +150,63 @@ class MenuHandler(GenericHandler):
"""
items = []
if kwargs.get('include_people'):
items.extend([
{
'title': "All People",
'route': 'people',
'perm': 'people.list',
},
])
if kwargs.get("include_people"):
items.extend(
[
{
"title": "All People",
"route": "people",
"perm": "people.list",
},
]
)
items.extend([
{
'title': "Users",
'route': 'users',
'perm': 'users.list',
},
{
'title': "Roles",
'route': 'roles',
'perm': 'roles.list',
},
{
'title': "Permissions",
'route': 'permissions',
'perm': 'permissions.list',
},
{'type': 'sep'},
{
'title': "Email Settings",
'route': 'email_settings',
'perm': 'email_settings.list',
},
{'type': 'sep'},
{
'title': "App Info",
'route': 'appinfo',
'perm': 'appinfo.list',
},
{
'title': "Raw Settings",
'route': 'settings',
'perm': 'settings.list',
},
{
'title': "Upgrades",
'route': 'upgrades',
'perm': 'upgrades.list',
},
])
items.extend(
[
{
"title": "Users",
"route": "users",
"perm": "users.list",
},
{
"title": "Roles",
"route": "roles",
"perm": "roles.list",
},
{
"title": "Permissions",
"route": "permissions",
"perm": "permissions.list",
},
{"type": "sep"},
{
"title": "Email Settings",
"route": "email_settings",
"perm": "email_settings.list",
},
{"type": "sep"},
{
"title": "App Info",
"route": "appinfo",
"perm": "appinfo.list",
},
{
"title": "Raw Settings",
"route": "settings",
"perm": "settings.list",
},
{
"title": "Upgrades",
"route": "upgrades",
"perm": "upgrades.list",
},
]
)
return {
'title': kwargs.get('title', "Admin"),
'type': 'menu',
'items': items,
"title": kwargs.get("title", "Admin"),
"type": "menu",
"items": items,
}
##############################
@ -227,64 +231,68 @@ class MenuHandler(GenericHandler):
final_menus = []
for topitem in raw_menus:
if topitem['allowed']:
if topitem["allowed"]:
if topitem.get('type') == 'link':
if topitem.get("type") == "link":
final_menus.append(self._make_menu_entry(request, topitem))
else: # assuming 'menu' type
else: # assuming 'menu' type
menu_items = []
for item in topitem['items']:
if not item['allowed']:
for item in topitem["items"]:
if not item["allowed"]:
continue
# nested submenu
if item.get('type') == 'menu':
if item.get("type") == "menu":
submenu_items = []
for subitem in item['items']:
if subitem['allowed']:
submenu_items.append(self._make_menu_entry(request, subitem))
menu_items.append({
'type': 'submenu',
'title': item['title'],
'items': submenu_items,
'is_menu': True,
'is_sep': False,
})
for subitem in item["items"]:
if subitem["allowed"]:
submenu_items.append(
self._make_menu_entry(request, subitem)
)
menu_items.append(
{
"type": "submenu",
"title": item["title"],
"items": submenu_items,
"is_menu": True,
"is_sep": False,
}
)
elif item.get('type') == 'sep':
elif item.get("type") == "sep":
# we only want to add a sep, *if* we already have some
# menu items (i.e. there is something to separate)
# *and* the last menu item is not a sep (avoid doubles)
if menu_items and not menu_items[-1]['is_sep']:
if menu_items and not menu_items[-1]["is_sep"]:
menu_items.append(self._make_menu_entry(request, item))
else: # standard menu item
else: # standard menu item
menu_items.append(self._make_menu_entry(request, item))
# remove final separator if present
if menu_items and menu_items[-1]['is_sep']:
if menu_items and menu_items[-1]["is_sep"]:
menu_items.pop()
# only add if we wound up with something
assert menu_items
if menu_items:
group = {
'type': 'menu',
'key': topitem.get('key'),
'title': topitem['title'],
'items': menu_items,
'is_menu': True,
'is_link': False,
"type": "menu",
"key": topitem.get("key"),
"title": topitem["title"],
"items": menu_items,
"is_menu": True,
"is_link": False,
}
# topitem w/ no key likely means it did not come
# from config but rather explicit definition in
# code. so we are free to "invent" a (safe) key
# for it, since that is only for editing config
if not group['key']:
group['key'] = self._make_menu_key(topitem['title'])
if not group["key"]:
group["key"] = self._make_menu_key(topitem["title"])
final_menus.append(group)
@ -305,7 +313,7 @@ class MenuHandler(GenericHandler):
Logic to determine if a given menu item is "allowed" for
current user.
"""
perm = item.get('perm')
perm = item.get("perm")
if perm:
return request.has_perm(perm)
return True
@ -317,30 +325,30 @@ class MenuHandler(GenericHandler):
"""
for topitem in menus:
if topitem.get('type', 'menu') == 'link':
topitem['allowed'] = True
if topitem.get("type", "menu") == "link":
topitem["allowed"] = True
elif topitem.get('type', 'menu') == 'menu':
topitem['allowed'] = False
elif topitem.get("type", "menu") == "menu":
topitem["allowed"] = False
for item in topitem['items']:
for item in topitem["items"]:
if item.get('type') == 'menu':
for subitem in item['items']:
subitem['allowed'] = self._is_allowed(request, subitem)
if item.get("type") == "menu":
for subitem in item["items"]:
subitem["allowed"] = self._is_allowed(request, subitem)
item['allowed'] = False
for subitem in item['items']:
if subitem['allowed'] and subitem.get('type') != 'sep':
item['allowed'] = True
item["allowed"] = False
for subitem in item["items"]:
if subitem["allowed"] and subitem.get("type") != "sep":
item["allowed"] = True
break
else:
item['allowed'] = self._is_allowed(request, item)
item["allowed"] = self._is_allowed(request, item)
for item in topitem['items']:
if item['allowed'] and item.get('type') != 'sep':
topitem['allowed'] = True
for item in topitem["items"]:
if item["allowed"] and item.get("type") != "sep":
topitem["allowed"] = True
break
def _make_menu_entry(self, request, item):
@ -349,39 +357,39 @@ class MenuHandler(GenericHandler):
object, for use in constructing final menu.
"""
# separator
if item.get('type') == 'sep':
if item.get("type") == "sep":
return {
'type': 'sep',
'is_menu': False,
'is_sep': True,
"type": "sep",
"is_menu": False,
"is_sep": True,
}
# standard menu item
entry = {
'type': 'item',
'title': item['title'],
'perm': item.get('perm'),
'target': item.get('target'),
'is_link': True,
'is_menu': False,
'is_sep': False,
"type": "item",
"title": item["title"],
"perm": item.get("perm"),
"target": item.get("target"),
"is_link": True,
"is_menu": False,
"is_sep": False,
}
if item.get('route'):
entry['route'] = item['route']
if item.get("route"):
entry["route"] = item["route"]
try:
entry['url'] = request.route_url(entry['route'])
except KeyError: # happens if no such route
entry["url"] = request.route_url(entry["route"])
except KeyError: # happens if no such route
log.warning("invalid route name for menu entry: %s", entry)
entry['url'] = entry['route']
entry['key'] = entry['route']
entry["url"] = entry["route"]
entry["key"] = entry["route"]
else:
if item.get('url'):
entry['url'] = item['url']
entry['key'] = self._make_menu_key(entry['title'])
if item.get("url"):
entry["url"] = item["url"]
entry["key"] = self._make_menu_key(entry["title"])
return entry
def _make_menu_key(self, value):
"""
Generate a normalized menu key for the given value.
"""
return re.sub(r'\W', '', value.lower())
return re.sub(r"\W", "", value.lower())

View file

@ -33,7 +33,7 @@ def get_basic_session(request, **kwargs):
"""
Create/get a "basic" Beaker session object.
"""
kwargs['use_cookies'] = False
kwargs["use_cookies"] = False
return BeakerSession(request, **kwargs)
@ -41,7 +41,7 @@ def get_progress_session(request, key, **kwargs):
"""
Create/get a Beaker session object, to be used for progress.
"""
kwargs['id'] = f'{request.session.id}.progress.{key}'
kwargs["id"] = f"{request.session.id}.progress.{key}"
return get_basic_session(request, **kwargs)
@ -91,7 +91,9 @@ class SessionProgress(ProgressBase):
:attr:`success_url`.
"""
def __init__(self, request, key, success_msg=None, success_url=None, error_url=None):
def __init__(
self, request, key, success_msg=None, success_url=None, error_url=None
):
self.request = request
self.config = self.request.wutta_config
self.app = self.config.get_app()
@ -104,24 +106,24 @@ class SessionProgress(ProgressBase):
def __call__(self, message, maximum):
self.clear()
self.session['message'] = message
self.session['maximum'] = maximum
self.session['maximum_display'] = f'{maximum:,d}'
self.session['value'] = 0
self.session["message"] = message
self.session["maximum"] = maximum
self.session["maximum_display"] = f"{maximum:,d}"
self.session["value"] = 0
self.session.save()
return self
def clear(self):
""" """
self.session.clear()
self.session['complete'] = False
self.session['error'] = False
self.session["complete"] = False
self.session["error"] = False
self.session.save()
def update(self, value):
""" """
self.session.load()
self.session['value'] = value
self.session["value"] = value
self.session.save()
def handle_error(self, error, error_url=None):
@ -139,9 +141,9 @@ class SessionProgress(ProgressBase):
:attr:`error_url` is used.
"""
self.session.load()
self.session['error'] = True
self.session['error_msg'] = self.app.render_error(error)
self.session['error_url'] = error_url or self.error_url
self.session["error"] = True
self.session["error_msg"] = self.app.render_error(error)
self.session["error_url"] = error_url or self.error_url
self.session.save()
def handle_success(self, success_msg=None, success_url=None):
@ -162,7 +164,7 @@ class SessionProgress(ProgressBase):
:attr:`success_url` is used.
"""
self.session.load()
self.session['complete'] = True
self.session['success_msg'] = success_msg or self.success_msg
self.session['success_url'] = success_url or self.success_url
self.session["complete"] = True
self.session["success_msg"] = success_msg or self.success_msg
self.session["success_url"] = success_url or self.success_url
self.session.save()

View file

@ -62,13 +62,13 @@ from fanstatic import Library, Resource
# fanstatic img library
img = Library('wuttaweb_img', 'img')
favicon = Resource(img, 'favicon.ico')
img = Library("wuttaweb_img", "img")
favicon = Resource(img, "favicon.ico")
# nb. mock out the renderers here, to appease fanstatic
logo = Resource(img, 'logo.png', renderer=True)
testing = Resource(img, 'testing.png', renderer=True)
logo = Resource(img, "logo.png", renderer=True)
testing = Resource(img, "testing.png", renderer=True)
# TODO: should consider deprecating this?
def includeme(config):
config.add_static_view('wuttaweb', 'wuttaweb:static')
config.add_static_view("wuttaweb", "wuttaweb:static")

View file

@ -120,32 +120,35 @@ def new_request(event):
</script>
"""
request = event.request
config = request.registry.settings['wutta_config']
config = request.registry.settings["wutta_config"]
app = config.get_app()
request.wutta_config = config
def get_referrer(default=None):
if request.params.get('referrer'):
return request.params['referrer']
if request.session.get('referrer'):
return request.session.pop('referrer')
referrer = getattr(request, 'referrer', None)
if (not referrer or referrer == request.current_route_url()
or not referrer.startswith(request.host_url)):
referrer = default or request.route_url('home')
if request.params.get("referrer"):
return request.params["referrer"]
if request.session.get("referrer"):
return request.session.pop("referrer")
referrer = getattr(request, "referrer", None)
if (
not referrer
or referrer == request.current_route_url()
or not referrer.startswith(request.host_url)
):
referrer = default or request.route_url("home")
return referrer
request.get_referrer = get_referrer
def use_oruga(request):
spec = config.get('wuttaweb.oruga_detector.spec')
spec = config.get("wuttaweb.oruga_detector.spec")
if spec:
func = app.load_object(spec)
return func(request)
theme = request.registry.settings.get('wuttaweb.theme')
if theme == 'butterfly':
theme = request.registry.settings.get("wuttaweb.theme")
if theme == "butterfly":
return True
return False
@ -156,16 +159,18 @@ def new_request(event):
Register a Vue 3 component, so the base template knows to
declare it for use within the app (page).
"""
if not hasattr(request, '_wuttaweb_registered_components'):
if not hasattr(request, "_wuttaweb_registered_components"):
request._wuttaweb_registered_components = OrderedDict()
if tagname in request._wuttaweb_registered_components:
log.warning("component with tagname '%s' already registered "
"with class '%s' but we are replacing that "
"with class '%s'",
tagname,
request._wuttaweb_registered_components[tagname],
classname)
log.warning(
"component with tagname '%s' already registered "
"with class '%s' but we are replacing that "
"with class '%s'",
tagname,
request._wuttaweb_registered_components[tagname],
classname,
)
request._wuttaweb_registered_components[tagname] = classname
@ -188,9 +193,9 @@ def default_user_getter(request, db_session=None):
def new_request_set_user(
event,
user_getter=default_user_getter,
db_session=None,
event,
user_getter=default_user_getter,
db_session=None,
):
"""
Event hook called when processing a new :term:`request`, for sake
@ -258,32 +263,35 @@ def new_request_set_user(
"""
request = event.request
config = request.registry.settings['wutta_config']
config = request.registry.settings["wutta_config"]
app = config.get_app()
auth = app.get_auth_handler()
# request.user
if db_session:
user_getter = functools.partial(user_getter, db_session=db_session)
request.set_property(user_getter, name='user', reify=True)
request.set_property(user_getter, name="user", reify=True)
# request.is_admin
def is_admin(request):
return auth.user_is_admin(request.user)
request.set_property(is_admin, reify=True)
# request.is_root
def is_root(request):
if request.is_admin:
if request.session.get('is_root', False):
if request.session.get("is_root", False):
return True
return False
request.set_property(is_root, reify=True)
# request.user_permissions
def user_permissions(request):
session = db_session or Session()
return auth.get_permissions(session, request.user)
request.set_property(user_permissions, reify=True)
# request.has_perm()
@ -293,6 +301,7 @@ def new_request_set_user(
if name in request.user_permissions:
return True
return False
request.has_perm = has_perm
# request.has_any_perm()
@ -301,6 +310,7 @@ def new_request_set_user(
if request.has_perm(name):
return True
return False
request.has_any_perm = has_any_perm
@ -371,35 +381,36 @@ def before_render(event):
allowed to change theme. Only set/relevant if
``expose_theme_picker`` is true (see above).
"""
request = event.get('request') or threadlocal.get_current_request()
request = event.get("request") or threadlocal.get_current_request()
config = request.wutta_config
app = config.get_app()
web = app.get_web_handler()
context = event
context['config'] = config
context['app'] = app
context['web'] = web
context['h'] = helpers
context['url'] = request.route_url
context['json'] = json
context['b'] = 'o' if request.use_oruga else 'b' # for buefy
context["config"] = config
context["app"] = app
context["web"] = web
context["h"] = helpers
context["url"] = request.route_url
context["json"] = json
context["b"] = "o" if request.use_oruga else "b" # for buefy
# TODO: this should be avoided somehow, for non-traditional web
# apps, esp. "API" web apps. (in the meantime can configure the
# app to use NullMenuHandler which avoids most of the overhead.)
menus = web.get_menu_handler()
context['menus'] = menus.do_make_menus(request)
context["menus"] = menus.do_make_menus(request)
# theme
context['theme'] = request.registry.settings.get('wuttaweb.theme', 'default')
context['expose_theme_picker'] = config.get_bool('wuttaweb.themes.expose_picker',
default=False)
if context['expose_theme_picker']:
context['available_themes'] = get_available_themes(config)
context["theme"] = request.registry.settings.get("wuttaweb.theme", "default")
context["expose_theme_picker"] = config.get_bool(
"wuttaweb.themes.expose_picker", default=False
)
if context["expose_theme_picker"]:
context["available_themes"] = get_available_themes(config)
def includeme(config):
config.add_subscriber(new_request, 'pyramid.events.NewRequest')
config.add_subscriber(new_request_set_user, 'pyramid.events.NewRequest')
config.add_subscriber(before_render, 'pyramid.events.BeforeRender')
config.add_subscriber(new_request, "pyramid.events.NewRequest")
config.add_subscriber(new_request_set_user, "pyramid.events.NewRequest")
config.add_subscriber(before_render, "pyramid.events.BeforeRender")

View file

@ -45,22 +45,28 @@ class WebTestCase(DataTestCase):
def setup_web(self):
self.setup_db()
self.request = self.make_request()
self.pyramid_config = testing.setUp(request=self.request, settings={
'wutta_config': self.config,
'mako.directories': ['wuttaweb:templates'],
'pyramid_deform.template_search_path': 'wuttaweb:templates/deform',
})
self.pyramid_config = testing.setUp(
request=self.request,
settings={
"wutta_config": self.config,
"mako.directories": ["wuttaweb:templates"],
"pyramid_deform.template_search_path": "wuttaweb:templates/deform",
},
)
# init web
self.pyramid_config.include('pyramid_deform')
self.pyramid_config.include('pyramid_mako')
self.pyramid_config.add_directive('add_wutta_permission_group',
'wuttaweb.auth.add_permission_group')
self.pyramid_config.add_directive('add_wutta_permission',
'wuttaweb.auth.add_permission')
self.pyramid_config.add_subscriber('wuttaweb.subscribers.before_render',
'pyramid.events.BeforeRender')
self.pyramid_config.include('wuttaweb.static')
self.pyramid_config.include("pyramid_deform")
self.pyramid_config.include("pyramid_mako")
self.pyramid_config.add_directive(
"add_wutta_permission_group", "wuttaweb.auth.add_permission_group"
)
self.pyramid_config.add_directive(
"add_wutta_permission", "wuttaweb.auth.add_permission"
)
self.pyramid_config.add_subscriber(
"wuttaweb.subscribers.before_render", "pyramid.events.BeforeRender"
)
self.pyramid_config.include("wuttaweb.static")
# nb. mock out fanstatic env..good enough for now to avoid errors..
needed = fanstatic.init_needed()
@ -69,9 +75,13 @@ class WebTestCase(DataTestCase):
# setup new request w/ anonymous user
event = MagicMock(request=self.request)
subscribers.new_request(event)
def user_getter(request, **kwargs): pass
subscribers.new_request_set_user(event, db_session=self.session,
user_getter=user_getter)
def user_getter(request, **kwargs):
pass
subscribers.new_request_set_user(
event, db_session=self.session, user_getter=user_getter
)
def tearDown(self):
self.teardown_web()

View file

@ -69,8 +69,9 @@ class FieldList(list):
i = self.index(field)
self.insert(i, newfield)
else:
log.warning("field '%s' not found, will append new field: %s",
field, newfield)
log.warning(
"field '%s' not found, will append new field: %s", field, newfield
)
self.append(newfield)
def insert_after(self, field, newfield):
@ -86,8 +87,9 @@ class FieldList(list):
i = self.index(field)
self.insert(i + 1, newfield)
else:
log.warning("field '%s' not found, will append new field: %s",
field, newfield)
log.warning(
"field '%s' not found, will append new field: %s", field, newfield
)
self.append(newfield)
def set_sequence(self, fields):
@ -132,18 +134,19 @@ def get_form_data(request):
# there is a better way? see also
# https://docs.pylonsproject.org/projects/pyramid/en/latest/api/request.html#pyramid.request.Request.is_xhr
if not request.POST and (
getattr(request, 'is_xhr', False)
or getattr(request, 'content_type', None) == 'application/json'):
getattr(request, "is_xhr", False)
or getattr(request, "content_type", None) == "application/json"
):
return request.json_body
return request.POST
def get_libver(
request,
key,
configured_only=False,
default_only=False,
prefix='wuttaweb',
request,
key,
configured_only=False,
default_only=False,
prefix="wuttaweb",
):
"""
Return the appropriate version string for the web resource library
@ -194,88 +197,94 @@ def get_libver(
if not default_only:
# nb. new/preferred setting
version = config.get(f'wuttaweb.libver.{key}')
version = config.get(f"wuttaweb.libver.{key}")
if version:
return version
# fallback to caller-specified prefix
if prefix != 'wuttaweb':
version = config.get(f'{prefix}.libver.{key}')
if prefix != "wuttaweb":
version = config.get(f"{prefix}.libver.{key}")
if version:
warnings.warn(f"config for {prefix}.libver.{key} is deprecated; "
f"please set wuttaweb.libver.{key} instead",
DeprecationWarning)
warnings.warn(
f"config for {prefix}.libver.{key} is deprecated; "
f"please set wuttaweb.libver.{key} instead",
DeprecationWarning,
)
return version
if key == 'buefy':
if key == "buefy":
if not default_only:
# nb. old/legacy setting
version = config.get(f'{prefix}.buefy_version')
version = config.get(f"{prefix}.buefy_version")
if version:
warnings.warn(f"config for {prefix}.buefy_version is deprecated; "
"please set wuttaweb.libver.buefy instead",
DeprecationWarning)
warnings.warn(
f"config for {prefix}.buefy_version is deprecated; "
"please set wuttaweb.libver.buefy instead",
DeprecationWarning,
)
return version
if not configured_only:
return '0.9.25'
return "0.9.25"
elif key == 'buefy.css':
elif key == "buefy.css":
# nb. this always returns something
return get_libver(request, 'buefy',
default_only=default_only,
configured_only=configured_only)
return get_libver(
request, "buefy", default_only=default_only, configured_only=configured_only
)
elif key == 'vue':
elif key == "vue":
if not default_only:
# nb. old/legacy setting
version = config.get(f'{prefix}.vue_version')
version = config.get(f"{prefix}.vue_version")
if version:
warnings.warn(f"config for {prefix}.vue_version is deprecated; "
"please set wuttaweb.libver.vue instead",
DeprecationWarning)
warnings.warn(
f"config for {prefix}.vue_version is deprecated; "
"please set wuttaweb.libver.vue instead",
DeprecationWarning,
)
return version
if not configured_only:
return '2.6.14'
return "2.6.14"
elif key == 'vue_resource':
elif key == "vue_resource":
if not configured_only:
return '1.5.3'
return "1.5.3"
elif key == 'fontawesome':
elif key == "fontawesome":
if not configured_only:
return '5.3.1'
return "5.3.1"
elif key == 'bb_vue':
elif key == "bb_vue":
if not configured_only:
return '3.5.18'
return "3.5.18"
elif key == 'bb_oruga':
elif key == "bb_oruga":
if not configured_only:
return '0.11.4'
return "0.11.4"
elif key in ('bb_oruga_bulma', 'bb_oruga_bulma_css'):
elif key in ("bb_oruga_bulma", "bb_oruga_bulma_css"):
if not configured_only:
return '0.7.3'
return "0.7.3"
elif key == 'bb_fontawesome_svg_core':
elif key == "bb_fontawesome_svg_core":
if not configured_only:
return '7.0.0'
return "7.0.0"
elif key == 'bb_free_solid_svg_icons':
elif key == "bb_free_solid_svg_icons":
if not configured_only:
return '7.0.0'
return "7.0.0"
elif key == 'bb_vue_fontawesome':
elif key == "bb_vue_fontawesome":
if not configured_only:
return '3.1.1'
return "3.1.1"
def get_liburl(
request,
key,
configured_only=False,
default_only=False,
prefix='wuttaweb',
request,
key,
configured_only=False,
default_only=False,
prefix="wuttaweb",
):
"""
Return the appropriate URL for the web resource library identified
@ -346,100 +355,106 @@ def get_liburl(
if not default_only:
# nb. new/preferred setting
url = config.get(f'wuttaweb.liburl.{key}')
url = config.get(f"wuttaweb.liburl.{key}")
if url:
return url
# fallback to caller-specified prefix
url = config.get(f'{prefix}.liburl.{key}')
url = config.get(f"{prefix}.liburl.{key}")
if url:
warnings.warn(f"config for {prefix}.liburl.{key} is deprecated; "
f"please set wuttaweb.liburl.{key} instead",
DeprecationWarning)
warnings.warn(
f"config for {prefix}.liburl.{key} is deprecated; "
f"please set wuttaweb.liburl.{key} instead",
DeprecationWarning,
)
return url
if configured_only:
return
version = get_libver(request, key, prefix=prefix,
configured_only=False,
default_only=default_only)
version = get_libver(
request, key, prefix=prefix, configured_only=False, default_only=default_only
)
# load fanstatic libcache if configured
static = config.get('wuttaweb.static_libcache.module')
static = config.get("wuttaweb.static_libcache.module")
if not static:
static = config.get(f'{prefix}.static_libcache.module')
static = config.get(f"{prefix}.static_libcache.module")
if static:
warnings.warn(f"config for {prefix}.static_libcache.module is deprecated; "
"please set wuttaweb.static_libcache.module instead",
DeprecationWarning)
warnings.warn(
f"config for {prefix}.static_libcache.module is deprecated; "
"please set wuttaweb.static_libcache.module instead",
DeprecationWarning,
)
if static:
static = importlib.import_module(static)
needed = request.environ['fanstatic.needed']
liburl = needed.library_url(static.libcache) + '/'
needed = request.environ["fanstatic.needed"]
liburl = needed.library_url(static.libcache) + "/"
# nb. add custom url prefix if needed, e.g. /wutta
if request.script_name:
liburl = request.script_name + liburl
if key == 'buefy':
if static and hasattr(static, 'buefy_js'):
if key == "buefy":
if static and hasattr(static, "buefy_js"):
return liburl + static.buefy_js.relpath
return f'https://unpkg.com/buefy@{version}/dist/buefy.min.js'
return f"https://unpkg.com/buefy@{version}/dist/buefy.min.js"
elif key == 'buefy.css':
if static and hasattr(static, 'buefy_css'):
elif key == "buefy.css":
if static and hasattr(static, "buefy_css"):
return liburl + static.buefy_css.relpath
return f'https://unpkg.com/buefy@{version}/dist/buefy.min.css'
return f"https://unpkg.com/buefy@{version}/dist/buefy.min.css"
elif key == 'vue':
if static and hasattr(static, 'vue_js'):
elif key == "vue":
if static and hasattr(static, "vue_js"):
return liburl + static.vue_js.relpath
return f'https://unpkg.com/vue@{version}/dist/vue.min.js'
return f"https://unpkg.com/vue@{version}/dist/vue.min.js"
elif key == 'vue_resource':
if static and hasattr(static, 'vue_resource_js'):
elif key == "vue_resource":
if static and hasattr(static, "vue_resource_js"):
return liburl + static.vue_resource_js.relpath
return f'https://cdn.jsdelivr.net/npm/vue-resource@{version}'
return f"https://cdn.jsdelivr.net/npm/vue-resource@{version}"
elif key == 'fontawesome':
if static and hasattr(static, 'fontawesome_js'):
elif key == "fontawesome":
if static and hasattr(static, "fontawesome_js"):
return liburl + static.fontawesome_js.relpath
return f'https://use.fontawesome.com/releases/v{version}/js/all.js'
return f"https://use.fontawesome.com/releases/v{version}/js/all.js"
elif key == 'bb_vue':
if static and hasattr(static, 'bb_vue_js'):
elif key == "bb_vue":
if static and hasattr(static, "bb_vue_js"):
return liburl + static.bb_vue_js.relpath
return f'https://unpkg.com/vue@{version}/dist/vue.esm-browser.prod.js'
return f"https://unpkg.com/vue@{version}/dist/vue.esm-browser.prod.js"
elif key == 'bb_oruga':
if static and hasattr(static, 'bb_oruga_js'):
elif key == "bb_oruga":
if static and hasattr(static, "bb_oruga_js"):
return liburl + static.bb_oruga_js.relpath
return f'https://unpkg.com/@oruga-ui/oruga-next@{version}/dist/oruga.mjs'
return f"https://unpkg.com/@oruga-ui/oruga-next@{version}/dist/oruga.mjs"
elif key == 'bb_oruga_bulma':
if static and hasattr(static, 'bb_oruga_bulma_js'):
elif key == "bb_oruga_bulma":
if static and hasattr(static, "bb_oruga_bulma_js"):
return liburl + static.bb_oruga_bulma_js.relpath
return f'https://unpkg.com/@oruga-ui/theme-bulma@{version}/dist/bulma.js'
return f"https://unpkg.com/@oruga-ui/theme-bulma@{version}/dist/bulma.js"
elif key == 'bb_oruga_bulma_css':
if static and hasattr(static, 'bb_oruga_bulma_css'):
elif key == "bb_oruga_bulma_css":
if static and hasattr(static, "bb_oruga_bulma_css"):
return liburl + static.bb_oruga_bulma_css.relpath
return f'https://unpkg.com/@oruga-ui/theme-bulma@{version}/dist/bulma.css'
return f"https://unpkg.com/@oruga-ui/theme-bulma@{version}/dist/bulma.css"
elif key == 'bb_fontawesome_svg_core':
if static and hasattr(static, 'bb_fontawesome_svg_core_js'):
elif key == "bb_fontawesome_svg_core":
if static and hasattr(static, "bb_fontawesome_svg_core_js"):
return liburl + static.bb_fontawesome_svg_core_js.relpath
return f'https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-svg-core@{version}/+esm'
return f"https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-svg-core@{version}/+esm"
elif key == 'bb_free_solid_svg_icons':
if static and hasattr(static, 'bb_free_solid_svg_icons_js'):
elif key == "bb_free_solid_svg_icons":
if static and hasattr(static, "bb_free_solid_svg_icons_js"):
return liburl + static.bb_free_solid_svg_icons_js.relpath
return f'https://cdn.jsdelivr.net/npm/@fortawesome/free-solid-svg-icons@{version}/+esm'
return f"https://cdn.jsdelivr.net/npm/@fortawesome/free-solid-svg-icons@{version}/+esm"
elif key == 'bb_vue_fontawesome':
if static and hasattr(static, 'bb_vue_fontawesome_js'):
elif key == "bb_vue_fontawesome":
if static and hasattr(static, "bb_vue_fontawesome_js"):
return liburl + static.bb_vue_fontawesome_js.relpath
return f'https://cdn.jsdelivr.net/npm/@fortawesome/vue-fontawesome@{version}/+esm'
return (
f"https://cdn.jsdelivr.net/npm/@fortawesome/vue-fontawesome@{version}/+esm"
)
def get_csrf_token(request):
@ -455,7 +470,7 @@ def get_csrf_token(request):
return token
def render_csrf_token(request, name='_csrf'):
def render_csrf_token(request, name="_csrf"):
"""
Convenience function, returns CSRF hidden input inside hidden div,
e.g.:
@ -480,7 +495,9 @@ def render_csrf_token(request, name='_csrf'):
See also :func:`get_csrf_token()`.
"""
token = get_csrf_token(request)
return HTML.tag('div', tags.hidden(name, value=token, id=None), style='display:none;')
return HTML.tag(
"div", tags.hidden(name, value=token, id=None), style="display:none;"
)
def get_model_fields(config, model_class, include_fk=False):
@ -511,13 +528,16 @@ def get_model_fields(config, model_class, include_fk=False):
if include_fk:
fields = [prop.key for prop in mapper.iterate_properties]
else:
fields = [prop.key for prop in mapper.iterate_properties
if not prop_is_fk(mapper, prop)]
fields = [
prop.key
for prop in mapper.iterate_properties
if not prop_is_fk(mapper, prop)
]
# nb. we never want the continuum 'versions' prop
app = config.get_app()
if app.continuum_is_enabled() and 'versions' in fields:
fields.remove('versions')
if app.continuum_is_enabled() and "versions" in fields:
fields.remove("versions")
return fields
@ -599,6 +619,7 @@ def make_json_safe(value, key=None, warn=True):
# theme functions
##############################
def get_available_themes(config):
"""
Returns the official list of theme names which are available for
@ -621,16 +642,17 @@ def get_available_themes(config):
:param config: App :term:`config object`.
"""
# get available list from config, if it has one
available = config.get_list('wuttaweb.themes.keys',
default=['default', 'butterfly'])
available = config.get_list(
"wuttaweb.themes.keys", default=["default", "butterfly"]
)
# sort the list by name
available.sort()
# make default theme the first option
if 'default' in available:
available.remove('default')
available.insert(0, 'default')
if "default" in available:
available.remove("default")
available.insert(0, "default")
return available
@ -663,7 +685,7 @@ def get_effective_theme(config, theme=None, session=None):
if not theme:
with app.short_session(session=session) as s:
theme = app.get_setting(s, 'wuttaweb.theme') or 'default'
theme = app.get_setting(s, "wuttaweb.theme") or "default"
# confirm requested theme is available
available = get_available_themes(config)
@ -701,8 +723,9 @@ def get_theme_template_path(config, theme=None, session=None):
:returns: Path on disk to theme template folder.
"""
theme = get_effective_theme(config, theme=theme, session=session)
theme_path = config.get(f'wuttaweb.theme.{theme}',
default=f'wuttaweb:templates/themes/{theme}')
theme_path = config.get(
f"wuttaweb.theme.{theme}", default=f"wuttaweb:templates/themes/{theme}"
)
return resource_path(theme_path)
@ -731,7 +754,7 @@ def set_app_theme(request, theme, session=None):
# there's only one global template lookup; can get to it via any renderer
# but should *not* use /base.mako since that one is about to get volatile
renderer = get_renderer('/menu.mako')
renderer = get_renderer("/menu.mako")
lookup = renderer.lookup
# overwrite first entry in lookup's directory list
@ -742,7 +765,7 @@ def set_app_theme(request, theme, session=None):
# persist current theme in db settings
with app.short_session(session=session) as s:
app.save_setting(s, 'wuttaweb.theme', theme)
app.save_setting(s, "wuttaweb.theme", theme)
# and cache in live app settings
request.registry.settings['wuttaweb.theme'] = theme
request.registry.settings["wuttaweb.theme"] = theme

View file

@ -35,4 +35,4 @@ from .master import MasterView
def includeme(config):
config.include('wuttaweb.views.essential')
config.include("wuttaweb.views.essential")

View file

@ -54,28 +54,32 @@ class AuthView(View):
# nb. redirect to /setup if no users exist
user = session.query(model.User).first()
if not user:
return self.redirect(self.request.route_url('setup'))
return self.redirect(self.request.route_url("setup"))
referrer = self.request.get_referrer()
# redirect if already logged in
if self.request.user:
self.request.session.flash(f"{self.request.user} is already logged in", 'error')
self.request.session.flash(
f"{self.request.user} is already logged in", "error"
)
return self.redirect(referrer)
form = self.make_form(schema=self.login_make_schema(),
align_buttons_right=True,
show_button_cancel=False,
show_button_reset=True,
button_label_submit="Login",
button_icon_submit='user')
form = self.make_form(
schema=self.login_make_schema(),
align_buttons_right=True,
show_button_cancel=False,
show_button_reset=True,
button_label_submit="Login",
button_icon_submit="user",
)
# validate basic form data (sanity check)
data = form.validate()
if data:
# truly validate user credentials
user = auth.authenticate_user(session, data['username'], data['password'])
user = auth.authenticate_user(session, data["username"], data["password"])
if user:
# okay now they're truly logged in
@ -83,11 +87,11 @@ class AuthView(View):
return self.redirect(referrer, headers=headers)
else:
self.request.session.flash("Invalid user credentials", 'error')
self.request.session.flash("Invalid user credentials", "error")
return {
'index_title': self.app.get_title(),
'form': form,
"index_title": self.app.get_title(),
"form": form,
# TODO
# 'referrer': referrer,
}
@ -99,19 +103,29 @@ class AuthView(View):
# specify the ref attribute. this is needed for autofocus and
# keydown behavior for login form.
schema.add(colander.SchemaNode(
colander.String(),
name='username',
widget=widgets.TextInputWidget(attributes={
'ref': 'username',
})))
schema.add(
colander.SchemaNode(
colander.String(),
name="username",
widget=widgets.TextInputWidget(
attributes={
"ref": "username",
}
),
)
)
schema.add(colander.SchemaNode(
colander.String(),
name='password',
widget=widgets.PasswordWidget(attributes={
'ref': 'password',
})))
schema.add(
colander.SchemaNode(
colander.String(),
name="password",
widget=widgets.PasswordWidget(
attributes={
"ref": "password",
}
),
)
)
return schema
@ -138,7 +152,7 @@ class AuthView(View):
# otherwise redirect to referrer, with 'login' page as fallback
# TODO: should call request.get_referrer()
# referrer = self.request.get_referrer(default=self.request.route_url('login'))
referrer = self.request.route_url('login')
referrer = self.request.route_url("login")
return self.redirect(referrer, headers=headers)
def change_password(self):
@ -155,46 +169,55 @@ class AuthView(View):
* template: ``/auth/change_password.mako``
"""
if not self.request.user:
return self.redirect(self.request.route_url('home'))
return self.redirect(self.request.route_url("home"))
if self.request.user.prevent_edit:
raise self.forbidden()
form = self.make_form(schema=self.change_password_make_schema(),
show_button_cancel=False,
show_button_reset=True)
form = self.make_form(
schema=self.change_password_make_schema(),
show_button_cancel=False,
show_button_reset=True,
)
data = form.validate()
if data:
auth = self.app.get_auth_handler()
auth.set_user_password(self.request.user, data['new_password'])
auth.set_user_password(self.request.user, data["new_password"])
self.request.session.flash("Your password has been changed.")
# TODO: should use request.get_referrer() instead
referrer = self.request.route_url('home')
referrer = self.request.route_url("home")
return self.redirect(referrer)
return {'index_title': str(self.request.user),
'form': form}
return {"index_title": str(self.request.user), "form": form}
def change_password_make_schema(self):
""" """
schema = colander.Schema()
schema.add(colander.SchemaNode(
colander.String(),
name='current_password',
widget=widgets.PasswordWidget(),
validator=self.change_password_validate_current_password))
schema.add(
colander.SchemaNode(
colander.String(),
name="current_password",
widget=widgets.PasswordWidget(),
validator=self.change_password_validate_current_password,
)
)
# nb. must use different widget for Vue 3 + Oruga
widget = (widgets.WuttaCheckedPasswordWidget()
if self.request.use_oruga
else widgets.CheckedPasswordWidget())
schema.add(colander.SchemaNode(
colander.String(),
name='new_password',
widget=widget,
validator=self.change_password_validate_new_password))
widget = (
widgets.WuttaCheckedPasswordWidget()
if self.request.use_oruga
else widgets.CheckedPasswordWidget()
)
schema.add(
colander.SchemaNode(
colander.String(),
name="new_password",
widget=widget,
validator=self.change_password_validate_new_password,
)
)
return schema
@ -220,14 +243,16 @@ class AuthView(View):
See also :meth:`stop_root()`.
"""
if self.request.method != 'POST':
if self.request.method != "POST":
raise self.forbidden()
if not self.request.is_admin:
raise self.forbidden()
self.request.session['is_root'] = True
self.request.session.flash("You have been elevated to 'root' and now have full system access")
self.request.session["is_root"] = True
self.request.session.flash(
"You have been elevated to 'root' and now have full system access"
)
url = self.request.get_referrer()
return self.redirect(url)
@ -240,13 +265,13 @@ class AuthView(View):
See also :meth:`become_root()`.
"""
if self.request.method != 'POST':
if self.request.method != "POST":
raise self.forbidden()
if not self.request.is_admin:
raise self.forbidden()
self.request.session['is_root'] = False
self.request.session["is_root"] = False
self.request.session.flash("Your normal system access has been restored")
url = self.request.get_referrer()
@ -260,39 +285,37 @@ class AuthView(View):
def _auth_defaults(cls, config):
# login
config.add_route('login', '/login')
config.add_view(cls, attr='login',
route_name='login',
renderer='/auth/login.mako')
config.add_route("login", "/login")
config.add_view(
cls, attr="login", route_name="login", renderer="/auth/login.mako"
)
# logout
config.add_route('logout', '/logout')
config.add_view(cls, attr='logout',
route_name='logout')
config.add_route("logout", "/logout")
config.add_view(cls, attr="logout", route_name="logout")
# change password
config.add_route('change_password', '/change-password')
config.add_view(cls, attr='change_password',
route_name='change_password',
renderer='/auth/change_password.mako')
config.add_route("change_password", "/change-password")
config.add_view(
cls,
attr="change_password",
route_name="change_password",
renderer="/auth/change_password.mako",
)
# become root
config.add_route('become_root', '/root/yes',
request_method='POST')
config.add_view(cls, attr='become_root',
route_name='become_root')
config.add_route("become_root", "/root/yes", request_method="POST")
config.add_view(cls, attr="become_root", route_name="become_root")
# stop root
config.add_route('stop_root', '/root/no',
request_method='POST')
config.add_view(cls, attr='stop_root',
route_name='stop_root')
config.add_route("stop_root", "/root/no", request_method="POST")
config.add_view(cls, attr="stop_root", route_name="stop_root")
def defaults(config, **kwargs):
base = globals()
AuthView = kwargs.get('AuthView', base['AuthView'])
AuthView = kwargs.get("AuthView", base["AuthView"])
AuthView.defaults(config)

View file

@ -169,4 +169,4 @@ class View:
:returns: A :term:`response` with JSON content type.
"""
return render_to_response('json', context, request=self.request)
return render_to_response("json", context, request=self.request)

View file

@ -52,18 +52,18 @@ class BatchMasterView(MasterView):
"""
labels = {
'id': "Batch ID",
'status_code': "Status",
"id": "Batch ID",
"status_code": "Status",
}
sort_defaults = ('id', 'desc')
sort_defaults = ("id", "desc")
has_rows = True
rows_title = "Batch Rows"
rows_sort_defaults = 'sequence'
rows_sort_defaults = "sequence"
row_labels = {
'status_code': "Status",
"status_code": "Status",
}
def __init__(self, request, context=None):
@ -89,7 +89,7 @@ class BatchMasterView(MasterView):
batches.
"""
templates = super().get_fallback_templates(template)
templates.insert(0, f'/batch/{template}.mako')
templates.insert(0, f"/batch/{template}.mako")
return templates
def render_to_response(self, template, context):
@ -105,16 +105,19 @@ class BatchMasterView(MasterView):
* ``why_not_execute`` - text of reason (if any) not to execute batch
* ``execution_described`` - HTML (rendered from markdown) describing batch execution
"""
if template == 'view':
batch = context['instance']
context['batch'] = batch
context['batch_handler'] = self.batch_handler
context['why_not_execute'] = self.batch_handler.why_not_execute(batch)
if template == "view":
batch = context["instance"]
context["batch"] = batch
context["batch_handler"] = self.batch_handler
context["why_not_execute"] = self.batch_handler.why_not_execute(batch)
description = (self.batch_handler.describe_execution(batch)
or "Handler does not say! Your guess is as good as mine.")
context['execution_described'] = markdown.markdown(
description, extensions=['fenced_code', 'codehilite'])
description = (
self.batch_handler.describe_execution(batch)
or "Handler does not say! Your guess is as good as mine."
)
context["execution_described"] = markdown.markdown(
description, extensions=["fenced_code", "codehilite"]
)
return super().render_to_response(template, context)
@ -125,24 +128,27 @@ class BatchMasterView(MasterView):
# created_by
CreatedBy = orm.aliased(model.User)
g.set_joiner('created_by',
lambda q: q.join(CreatedBy,
CreatedBy.uuid == self.model_class.created_by_uuid))
g.set_sorter('created_by', CreatedBy.username)
g.set_joiner(
"created_by",
lambda q: q.join(
CreatedBy, CreatedBy.uuid == self.model_class.created_by_uuid
),
)
g.set_sorter("created_by", CreatedBy.username)
# g.set_filter('created_by', CreatedBy.username, label="Created By Username")
# id
g.set_renderer('id', self.render_batch_id)
g.set_link('id')
g.set_renderer("id", self.render_batch_id)
g.set_link("id")
# description
g.set_link('description')
g.set_link("description")
def render_batch_id(self, batch, key, value):
""" """
if value:
batch_id = int(value)
return f'{batch_id:08d}'
return f"{batch_id:08d}"
def get_instance_title(self, batch):
""" """
@ -157,55 +163,55 @@ class BatchMasterView(MasterView):
# id
if self.creating:
f.remove('id')
f.remove("id")
else:
f.set_readonly('id')
f.set_widget('id', BatchIdWidget())
f.set_readonly("id")
f.set_widget("id", BatchIdWidget())
# notes
f.set_widget('notes', 'notes')
f.set_widget("notes", "notes")
# rows
f.remove('rows')
f.remove("rows")
if self.creating:
f.remove('row_count')
f.remove("row_count")
else:
f.set_readonly('row_count')
f.set_readonly("row_count")
# status
f.remove('status_text')
f.remove("status_text")
if self.creating:
f.remove('status_code')
f.remove("status_code")
else:
f.set_readonly('status_code')
f.set_readonly("status_code")
# created
if self.creating:
f.remove('created')
f.remove("created")
else:
f.set_readonly('created')
f.set_readonly("created")
# created_by
f.remove('created_by_uuid')
f.remove("created_by_uuid")
if self.creating:
f.remove('created_by')
f.remove("created_by")
else:
f.set_node('created_by', UserRef(self.request))
f.set_readonly('created_by')
f.set_node("created_by", UserRef(self.request))
f.set_readonly("created_by")
# executed
if self.creating or not batch.executed:
f.remove('executed')
f.remove("executed")
else:
f.set_readonly('executed')
f.set_readonly("executed")
# executed_by
f.remove('executed_by_uuid')
f.remove("executed_by_uuid")
if self.creating or not batch.executed:
f.remove('executed_by')
f.remove("executed_by")
else:
f.set_node('executed_by', UserRef(self.request))
f.set_readonly('executed_by')
f.set_node("executed_by", UserRef(self.request))
f.set_readonly("executed_by")
def objectify(self, form, **kwargs):
"""
@ -226,12 +232,16 @@ class BatchMasterView(MasterView):
batch = schema.objectify(form.validated, context=form.model_instance)
# then we collect attributes from the new batch
kw = dict([(key, getattr(batch, key))
for key in form.validated
if hasattr(batch, key)])
kw = dict(
[
(key, getattr(batch, key))
for key in form.validated
if hasattr(batch, key)
]
)
# and set attribute for user creating the batch
kw['created_by'] = self.request.user
kw["created_by"] = self.request.user
# plus caller can override anything
kw.update(kwargs)
@ -252,15 +262,19 @@ class BatchMasterView(MasterView):
"""
# just view batch if should not populate
if not self.batch_handler.should_populate(batch):
return self.redirect(self.get_action_url('view', batch))
return self.redirect(self.get_action_url("view", batch))
# setup thread to populate batch
route_prefix = self.get_route_prefix()
key = f'{route_prefix}.populate'
progress = self.make_progress(key, success_url=self.get_action_url('view', batch))
thread = threading.Thread(target=self.populate_thread,
args=(batch.uuid,),
kwargs=dict(progress=progress))
key = f"{route_prefix}.populate"
progress = self.make_progress(
key, success_url=self.get_action_url("view", batch)
)
thread = threading.Thread(
target=self.populate_thread,
args=(batch.uuid,),
kwargs=dict(progress=progress),
)
# start thread and show progress page
thread.start()
@ -316,9 +330,12 @@ class BatchMasterView(MasterView):
except Exception as error:
session.rollback()
log.warning("failed to populate %s: %s",
self.get_model_title(), batch,
exc_info=True)
log.warning(
"failed to populate %s: %s",
self.get_model_title(),
batch,
exc_info=True,
)
if progress:
progress.handle_error(error)
@ -351,9 +368,9 @@ class BatchMasterView(MasterView):
self.batch_handler.do_execute(batch, self.request.user)
except Exception as error:
log.warning("failed to execute batch: %s", batch, exc_info=True)
self.request.session.flash(f"Execution failed!: {error}", 'error')
self.request.session.flash(f"Execution failed!: {error}", "error")
return self.redirect(self.get_action_url('view', batch))
return self.redirect(self.get_action_url("view", batch))
##############################
# row methods
@ -362,7 +379,7 @@ class BatchMasterView(MasterView):
@classmethod
def get_row_model_class(cls):
""" """
if hasattr(cls, 'row_model_class'):
if hasattr(cls, "row_model_class"):
return cls.row_model_class
Batch = cls.get_model_class()
@ -375,17 +392,16 @@ class BatchMasterView(MasterView):
data.
"""
BatchRow = self.get_row_model_class()
query = self.Session.query(BatchRow)\
.filter(BatchRow.batch == batch)
query = self.Session.query(BatchRow).filter(BatchRow.batch == batch)
return query
def configure_row_grid(self, g):
""" """
super().configure_row_grid(g)
g.set_label('sequence', "Seq.", column_only=True)
g.set_label("sequence", "Seq.", column_only=True)
g.set_renderer('status_code', self.render_row_status)
g.set_renderer("status_code", self.render_row_status)
def render_row_status(self, row, key, value):
""" """
@ -409,12 +425,17 @@ class BatchMasterView(MasterView):
instance_url_prefix = cls.get_instance_url_prefix()
# execute
config.add_route(f'{route_prefix}.execute',
f'{instance_url_prefix}/execute',
request_method='POST')
config.add_view(cls, attr='execute',
route_name=f'{route_prefix}.execute',
permission=f'{permission_prefix}.execute')
config.add_wutta_permission(permission_prefix,
f'{permission_prefix}.execute',
f"Execute {model_title}")
config.add_route(
f"{route_prefix}.execute",
f"{instance_url_prefix}/execute",
request_method="POST",
)
config.add_view(
cls,
attr="execute",
route_name=f"{route_prefix}.execute",
permission=f"{permission_prefix}.execute",
)
config.add_wutta_permission(
permission_prefix, f"{permission_prefix}.execute", f"Execute {model_title}"
)

View file

@ -58,15 +58,15 @@ class CommonView(View):
# nb. redirect to /setup if no users exist
user = session.query(model.User).first()
if not user:
return self.redirect(self.request.route_url('setup'))
return self.redirect(self.request.route_url("setup"))
# maybe auto-redirect anons to login
if not self.request.user:
if self.config.get_bool('wuttaweb.home_redirect_to_login'):
return self.redirect(self.request.route_url('login'))
if self.config.get_bool("wuttaweb.home_redirect_to_login"):
return self.redirect(self.request.route_url("login"))
return {
'index_title': self.app.get_title(),
"index_title": self.app.get_title(),
}
def forbidden_view(self):
@ -75,7 +75,7 @@ class CommonView(View):
Template: ``/forbidden.mako``
"""
return {'index_title': self.app.get_title()}
return {"index_title": self.app.get_title()}
def notfound_view(self):
"""
@ -83,7 +83,7 @@ class CommonView(View):
Template: ``/notfound.mako``
"""
return {'index_title': self.app.get_title()}
return {"index_title": self.app.get_title()}
def feedback(self):
""" """
@ -96,46 +96,45 @@ class CommonView(View):
if not form.validate():
# TODO: native Form class should better expose error(s)
dform = form.get_deform()
return {'error': str(dform.error)}
return {"error": str(dform.error)}
# build email template context
context = dict(form.validated)
if context['user_uuid']:
context['user'] = session.get(model.User, context['user_uuid'])
context['user_url'] = self.request.route_url('users.view', uuid=context['user_uuid'])
context['client_ip'] = self.request.client_addr
if context["user_uuid"]:
context["user"] = session.get(model.User, context["user_uuid"])
context["user_url"] = self.request.route_url(
"users.view", uuid=context["user_uuid"]
)
context["client_ip"] = self.request.client_addr
# send email
try:
self.feedback_send(context)
except Exception as error:
log.warning("failed to send feedback email", exc_info=True)
return {'error': str(error) or error.__class__.__name__}
return {"error": str(error) or error.__class__.__name__}
return {'ok': True}
return {"ok": True}
def feedback_make_schema(self):
""" """
schema = colander.Schema()
schema.add(colander.SchemaNode(colander.String(),
name='referrer'))
schema.add(colander.SchemaNode(colander.String(), name="referrer"))
schema.add(colander.SchemaNode(colander.String(),
name='user_uuid',
missing=None))
schema.add(
colander.SchemaNode(colander.String(), name="user_uuid", missing=None)
)
schema.add(colander.SchemaNode(colander.String(),
name='user_name'))
schema.add(colander.SchemaNode(colander.String(), name="user_name"))
schema.add(colander.SchemaNode(colander.String(),
name='message'))
schema.add(colander.SchemaNode(colander.String(), name="message"))
return schema
def feedback_send(self, context):
""" """
self.app.send_email('feedback', context)
self.app.send_email("feedback", context)
def setup(self, session=None):
"""
@ -162,87 +161,99 @@ class CommonView(View):
# nb. this view only available until first user is created
user = session.query(model.User).first()
if user:
return self.redirect(self.request.route_url('home'))
return self.redirect(self.request.route_url("home"))
form = self.make_form(fields=['username', 'password', 'first_name', 'last_name'],
show_button_cancel=False,
show_button_reset=True)
form.set_widget('password', widgets.CheckedPasswordWidget())
form.set_required('first_name', False)
form.set_required('last_name', False)
form = self.make_form(
fields=["username", "password", "first_name", "last_name"],
show_button_cancel=False,
show_button_reset=True,
)
form.set_widget("password", widgets.CheckedPasswordWidget())
form.set_required("first_name", False)
form.set_required("last_name", False)
if form.validate():
auth = self.app.get_auth_handler()
data = form.validated
# make user
user = auth.make_user(session=session, username=data['username'])
auth.set_user_password(user, data['password'])
user = auth.make_user(session=session, username=data["username"])
auth.set_user_password(user, data["password"])
# assign admin role
admin = auth.get_role_administrator(session)
user.roles.append(admin)
admin.notes = ("users in this role may \"become root\".\n\n"
"it's recommended not to grant other perms to this role.")
admin.notes = (
'users in this role may "become root".\n\n'
"it's recommended not to grant other perms to this role."
)
# initialize built-in roles
authed = auth.get_role_authenticated(session)
authed.notes = ("this role represents any user who *is* logged in.\n\n"
"you may grant any perms you like to it.")
authed.notes = (
"this role represents any user who *is* logged in.\n\n"
"you may grant any perms you like to it."
)
anon = auth.get_role_anonymous(session)
anon.notes = ("this role represents any user who is *not* logged in.\n\n"
"you may grant any perms you like to it.")
anon.notes = (
"this role represents any user who is *not* logged in.\n\n"
"you may grant any perms you like to it."
)
# also make "Site Admin" role
site_admin_perms = [
'appinfo.list',
'appinfo.configure',
'people.list',
'people.create',
'people.view',
'people.edit',
'people.delete',
'roles.list',
'roles.create',
'roles.view',
'roles.edit',
'roles.edit_builtin',
'roles.delete',
'settings.list',
'settings.create',
'settings.view',
'settings.edit',
'settings.delete',
'settings.delete_bulk',
'upgrades.list',
'upgrades.create',
'upgrades.view',
'upgrades.edit',
'upgrades.delete',
'upgrades.execute',
'upgrades.download',
'upgrades.configure',
'users.list',
'users.create',
'users.view',
'users.edit',
'users.delete',
"appinfo.list",
"appinfo.configure",
"people.list",
"people.create",
"people.view",
"people.edit",
"people.delete",
"roles.list",
"roles.create",
"roles.view",
"roles.edit",
"roles.edit_builtin",
"roles.delete",
"settings.list",
"settings.create",
"settings.view",
"settings.edit",
"settings.delete",
"settings.delete_bulk",
"upgrades.list",
"upgrades.create",
"upgrades.view",
"upgrades.edit",
"upgrades.delete",
"upgrades.execute",
"upgrades.download",
"upgrades.configure",
"users.list",
"users.create",
"users.view",
"users.edit",
"users.delete",
]
admin2 = model.Role(name="Site Admin")
admin2.notes = ("this is the \"daily driver\" admin role.\n\n"
"you may grant any perms you like to it.")
admin2.notes = (
'this is the "daily driver" admin role.\n\n'
"you may grant any perms you like to it."
)
session.add(admin2)
user.roles.append(admin2)
for perm in site_admin_perms:
auth.grant_permission(admin2, perm)
# maybe make person
if data['first_name'] or data['last_name']:
first = data['first_name']
last = data['last_name']
person = model.Person(first_name=first,
last_name=last,
full_name=(f"{first} {last}").strip())
if data["first_name"] or data["last_name"]:
first = data["first_name"]
last = data["last_name"]
person = model.Person(
first_name=first,
last_name=last,
full_name=(f"{first} {last}").strip(),
)
session.add(person)
user.person = person
@ -250,11 +261,11 @@ class CommonView(View):
# send user to /login
self.request.session.flash("Account created! Please login below.")
return self.redirect(self.request.route_url('login'))
return self.redirect(self.request.route_url("login"))
return {
'index_title': self.app.get_title(),
'form': form,
"index_title": self.app.get_title(),
"form": form,
}
def setup_enhance_admin_user(self, user):
@ -273,14 +284,14 @@ class CommonView(View):
This view will set the global app theme, then redirect back to
the referring page.
"""
theme = self.request.params.get('theme')
theme = self.request.params.get("theme")
if theme:
try:
set_app_theme(self.request, theme, session=Session())
except Exception as error:
error = self.app.render_error(error)
self.request.session.flash(f"Failed to set theme: {error}", 'error')
referrer = self.request.params.get('referrer') or self.request.get_referrer()
self.request.session.flash(f"Failed to set theme: {error}", "error")
referrer = self.request.params.get("referrer") or self.request.get_referrer()
return self.redirect(referrer)
@classmethod
@ -290,51 +301,52 @@ class CommonView(View):
@classmethod
def _defaults(cls, config):
config.add_wutta_permission_group('common', "(common)", overwrite=False)
config.add_wutta_permission_group("common", "(common)", overwrite=False)
# home page
config.add_route('home', '/')
config.add_view(cls, attr='home',
route_name='home',
renderer='/home.mako')
config.add_route("home", "/")
config.add_view(cls, attr="home", route_name="home", renderer="/home.mako")
# forbidden
config.add_forbidden_view(cls, attr='forbidden_view',
renderer='/forbidden.mako')
config.add_forbidden_view(
cls, attr="forbidden_view", renderer="/forbidden.mako"
)
# notfound
# nb. also, auto-correct URLs which require trailing slash
config.add_notfound_view(cls, attr='notfound_view',
append_slash=True,
renderer='/notfound.mako')
config.add_notfound_view(
cls, attr="notfound_view", append_slash=True, renderer="/notfound.mako"
)
# feedback
config.add_route('feedback', '/feedback',
request_method='POST')
config.add_view(cls, attr='feedback',
route_name='feedback',
permission='common.feedback',
renderer='json')
config.add_wutta_permission('common', 'common.feedback',
"Send user feedback about the app")
config.add_route("feedback", "/feedback", request_method="POST")
config.add_view(
cls,
attr="feedback",
route_name="feedback",
permission="common.feedback",
renderer="json",
)
config.add_wutta_permission(
"common", "common.feedback", "Send user feedback about the app"
)
# setup
config.add_route('setup', '/setup')
config.add_view(cls, attr='setup',
route_name='setup',
renderer='/setup.mako')
config.add_route("setup", "/setup")
config.add_view(cls, attr="setup", route_name="setup", renderer="/setup.mako")
# change theme
config.add_route('change_theme', '/change-theme', request_method='POST')
config.add_view(cls, attr='change_theme', route_name='change_theme')
config.add_wutta_permission('common', 'common.change_theme',
"Change global theme")
config.add_route("change_theme", "/change-theme", request_method="POST")
config.add_view(cls, attr="change_theme", route_name="change_theme")
config.add_wutta_permission(
"common", "common.change_theme", "Change global theme"
)
def defaults(config, **kwargs):
base = globals()
CommonView = kwargs.get('CommonView', base['CommonView'])
CommonView = kwargs.get("CommonView", base["CommonView"])
CommonView.defaults(config)

View file

@ -34,10 +34,11 @@ class EmailSettingView(MasterView):
"""
Master view for :term:`email settings <email setting>`.
"""
model_name = 'email_setting'
model_key = 'key'
model_name = "email_setting"
model_key = "key"
model_title = "Email Setting"
url_prefix = '/email/settings'
url_prefix = "/email/settings"
filterable = False
sortable = True
sort_on_backend = False
@ -46,31 +47,31 @@ class EmailSettingView(MasterView):
deletable = False
labels = {
'key': "Email Key",
'replyto': "Reply-To",
"key": "Email Key",
"replyto": "Reply-To",
}
grid_columns = [
'key',
'subject',
'to',
'enabled',
"key",
"subject",
"to",
"enabled",
]
# TODO: why does this not work?
sort_defaults = 'key'
sort_defaults = "key"
form_fields = [
'key',
'description',
'subject',
'sender',
'replyto',
'to',
'cc',
'bcc',
'notes',
'enabled',
"key",
"description",
"subject",
"sender",
"replyto",
"to",
"cc",
"bcc",
"notes",
"enabled",
]
def __init__(self, request, context=None):
@ -92,16 +93,18 @@ class EmailSettingView(MasterView):
""" """
key = setting.__name__
return {
'key': key,
'description': setting.__doc__,
'subject': self.email_handler.get_auto_subject(key, rendered=False, setting=setting),
'sender': self.email_handler.get_auto_sender(key),
'replyto': self.email_handler.get_auto_replyto(key) or colander.null,
'to': self.email_handler.get_auto_to(key),
'cc': self.email_handler.get_auto_cc(key),
'bcc': self.email_handler.get_auto_bcc(key),
'notes': self.email_handler.get_notes(key) or colander.null,
'enabled': self.email_handler.is_enabled(key),
"key": key,
"description": setting.__doc__,
"subject": self.email_handler.get_auto_subject(
key, rendered=False, setting=setting
),
"sender": self.email_handler.get_auto_sender(key),
"replyto": self.email_handler.get_auto_replyto(key) or colander.null,
"to": self.email_handler.get_auto_to(key),
"cc": self.email_handler.get_auto_cc(key),
"bcc": self.email_handler.get_auto_bcc(key),
"notes": self.email_handler.get_notes(key) or colander.null,
"enabled": self.email_handler.is_enabled(key),
}
def configure_grid(self, g):
@ -109,15 +112,15 @@ class EmailSettingView(MasterView):
super().configure_grid(g)
# key
g.set_searchable('key')
g.set_link('key')
g.set_searchable("key")
g.set_link("key")
# subject
g.set_searchable('subject')
g.set_link('subject')
g.set_searchable("subject")
g.set_link("subject")
# to
g.set_renderer('to', self.render_to_short)
g.set_renderer("to", self.render_to_short)
def render_to_short(self, setting, field, value):
""" """
@ -126,14 +129,14 @@ class EmailSettingView(MasterView):
return
if len(recips) < 3:
return ', '.join(recips)
return ", ".join(recips)
recips = ', '.join(recips[:2])
recips = ", ".join(recips[:2])
return f"{recips}, ..."
def get_instance(self):
""" """
key = self.request.matchdict['key']
key = self.request.matchdict["key"]
setting = self.email_handler.get_email_setting(key, instance=False)
if setting:
return self.normalize_setting(setting)
@ -142,99 +145,105 @@ class EmailSettingView(MasterView):
def get_instance_title(self, setting):
""" """
return setting['subject']
return setting["subject"]
def configure_form(self, f):
""" """
super().configure_form(f)
# description
f.set_readonly('description')
f.set_readonly("description")
# replyto
f.set_required('replyto', False)
f.set_required("replyto", False)
# to
f.set_node('to', EmailRecipients())
f.set_node("to", EmailRecipients())
# cc
f.set_node('cc', EmailRecipients())
f.set_node("cc", EmailRecipients())
# bcc
f.set_node('bcc', EmailRecipients())
f.set_node("bcc", EmailRecipients())
# notes
f.set_widget('notes', 'notes')
f.set_required('notes', False)
f.set_widget("notes", "notes")
f.set_required("notes", False)
# enabled
f.set_node('enabled', colander.Boolean())
f.set_node("enabled", colander.Boolean())
def persist(self, setting):
""" """
session = self.Session()
key = self.request.matchdict['key']
key = self.request.matchdict["key"]
def save(name, value):
self.app.save_setting(session, f'{self.config.appname}.email.{key}.{name}', value)
self.app.save_setting(
session, f"{self.config.appname}.email.{key}.{name}", value
)
def delete(name):
self.app.delete_setting(session, f'{self.config.appname}.email.{key}.{name}')
self.app.delete_setting(
session, f"{self.config.appname}.email.{key}.{name}"
)
# subject
if setting['subject']:
save('subject', setting['subject'])
if setting["subject"]:
save("subject", setting["subject"])
else:
delete('subject')
delete("subject")
# sender
if setting['sender']:
save('sender', setting['sender'])
if setting["sender"]:
save("sender", setting["sender"])
else:
delete('sender')
delete("sender")
# replyto
if setting['replyto']:
save('replyto', setting['replyto'])
if setting["replyto"]:
save("replyto", setting["replyto"])
else:
delete('replyto')
delete("replyto")
# to
if setting['to']:
save('to', setting['to'])
if setting["to"]:
save("to", setting["to"])
else:
delete('to')
delete("to")
# cc
if setting['cc']:
save('cc', setting['cc'])
if setting["cc"]:
save("cc", setting["cc"])
else:
delete('cc')
delete("cc")
# bcc
if setting['bcc']:
save('bcc', setting['bcc'])
if setting["bcc"]:
save("bcc", setting["bcc"])
else:
delete('bcc')
delete("bcc")
# notes
if setting['notes']:
save('notes', setting['notes'])
if setting["notes"]:
save("notes", setting["notes"])
else:
delete('notes')
delete("notes")
# enabled
save('enabled', 'true' if setting['enabled'] else 'false')
save("enabled", "true" if setting["enabled"] else "false")
def render_to_response(self, template, context):
""" """
if self.viewing:
setting = context['instance']
context['setting'] = setting
context['has_html_template'] = self.email_handler.get_auto_body_template(
setting['key'], 'html')
context['has_txt_template'] = self.email_handler.get_auto_body_template(
setting['key'], 'txt')
setting = context["instance"]
context["setting"] = setting
context["has_html_template"] = self.email_handler.get_auto_body_template(
setting["key"], "html"
)
context["has_txt_template"] = self.email_handler.get_auto_body_template(
setting["key"], "txt"
)
return super().render_to_response(template, context)
@ -245,16 +254,16 @@ class EmailSettingView(MasterView):
This will render the email template according to the "mode"
requested - i.e. HTML or TXT.
"""
key = self.request.matchdict['key']
key = self.request.matchdict["key"]
setting = self.email_handler.get_email_setting(key)
context = setting.sample_data()
mode = self.request.params.get('mode', 'html')
mode = self.request.params.get("mode", "html")
if mode == 'txt':
if mode == "txt":
body = self.email_handler.get_auto_txt_body(key, context)
self.request.response.content_type = 'text/plain'
self.request.response.content_type = "text/plain"
else: # html
else: # html
body = self.email_handler.get_auto_html_body(key, context)
self.request.response.text = body
@ -275,22 +284,24 @@ class EmailSettingView(MasterView):
instance_url_prefix = cls.get_instance_url_prefix()
# fix permission group
config.add_wutta_permission_group(permission_prefix,
model_title_plural,
overwrite=False)
config.add_wutta_permission_group(
permission_prefix, model_title_plural, overwrite=False
)
# preview
config.add_route(f'{route_prefix}.preview',
f'{instance_url_prefix}/preview')
config.add_view(cls, attr='preview',
route_name=f'{route_prefix}.preview',
permission=f'{permission_prefix}.view')
config.add_route(f"{route_prefix}.preview", f"{instance_url_prefix}/preview")
config.add_view(
cls,
attr="preview",
route_name=f"{route_prefix}.preview",
permission=f"{permission_prefix}.view",
)
def defaults(config, **kwargs):
base = globals()
EmailSettingView = kwargs.get('EmailSettingView', base['EmailSettingView'])
EmailSettingView = kwargs.get("EmailSettingView", base["EmailSettingView"])
EmailSettingView.defaults(config)

View file

@ -44,15 +44,15 @@ That will in turn include the following modules:
def defaults(config, **kwargs):
mod = lambda spec: kwargs.get(spec, spec)
config.include(mod('wuttaweb.views.common'))
config.include(mod('wuttaweb.views.auth'))
config.include(mod('wuttaweb.views.email'))
config.include(mod('wuttaweb.views.settings'))
config.include(mod('wuttaweb.views.progress'))
config.include(mod('wuttaweb.views.people'))
config.include(mod('wuttaweb.views.roles'))
config.include(mod('wuttaweb.views.users'))
config.include(mod('wuttaweb.views.upgrades'))
config.include(mod("wuttaweb.views.common"))
config.include(mod("wuttaweb.views.auth"))
config.include(mod("wuttaweb.views.email"))
config.include(mod("wuttaweb.views.settings"))
config.include(mod("wuttaweb.views.progress"))
config.include(mod("wuttaweb.views.people"))
config.include(mod("wuttaweb.views.roles"))
config.include(mod("wuttaweb.views.users"))
config.include(mod("wuttaweb.views.upgrades"))
def includeme(config):

File diff suppressed because it is too large Load diff

View file

@ -44,29 +44,30 @@ class PersonView(MasterView):
* ``/people/XXX/edit``
* ``/people/XXX/delete``
"""
model_class = Person
model_title_plural = "People"
route_prefix = 'people'
sort_defaults = 'full_name'
route_prefix = "people"
sort_defaults = "full_name"
has_autocomplete = True
grid_columns = [
'full_name',
'first_name',
'middle_name',
'last_name',
"full_name",
"first_name",
"middle_name",
"last_name",
]
filter_defaults = {
'full_name': {'active': True},
"full_name": {"active": True},
}
form_fields = [
'full_name',
'first_name',
'middle_name',
'last_name',
'users',
"full_name",
"first_name",
"middle_name",
"last_name",
"users",
]
def configure_grid(self, g):
@ -74,13 +75,13 @@ class PersonView(MasterView):
super().configure_grid(g)
# full_name
g.set_link('full_name')
g.set_link("full_name")
# first_name
g.set_link('first_name')
g.set_link("first_name")
# last_name
g.set_link('last_name')
g.set_link("last_name")
def configure_form(self, f):
""" """
@ -89,11 +90,11 @@ class PersonView(MasterView):
# full_name
if self.creating or self.editing:
f.remove('full_name')
f.remove("full_name")
# users
if self.viewing:
f.set_grid('users', self.make_users_grid(person))
f.set_grid("users", self.make_users_grid(person))
def make_users_grid(self, person):
"""
@ -107,22 +108,24 @@ class PersonView(MasterView):
model = self.app.model
route_prefix = self.get_route_prefix()
grid = self.make_grid(key=f'{route_prefix}.view.users',
model_class=model.User,
data=person.users,
columns=[
'username',
'active',
])
grid = self.make_grid(
key=f"{route_prefix}.view.users",
model_class=model.User,
data=person.users,
columns=[
"username",
"active",
],
)
if self.request.has_perm('users.view'):
url = lambda user, i: self.request.route_url('users.view', uuid=user.uuid)
grid.add_action('view', icon='eye', url=url)
grid.set_link('username')
if self.request.has_perm("users.view"):
url = lambda user, i: self.request.route_url("users.view", uuid=user.uuid)
grid.add_action("view", icon="eye", url=url)
grid.set_link("username")
if self.request.has_perm('users.edit'):
url = lambda user, i: self.request.route_url('users.edit', uuid=user.uuid)
grid.add_action('edit', url=url)
if self.request.has_perm("users.edit"):
url = lambda user, i: self.request.route_url("users.edit", uuid=user.uuid)
grid.add_action("edit", url=url)
return grid
@ -131,8 +134,7 @@ class PersonView(MasterView):
person = super().objectify(form)
# full_name
person.full_name = self.app.make_full_name(person.first_name,
person.last_name)
person.full_name = self.app.make_full_name(person.first_name, person.last_name)
return person
@ -141,24 +143,22 @@ class PersonView(MasterView):
model = self.app.model
session = self.Session()
query = session.query(model.Person)
criteria = [model.Person.full_name.ilike(f'%{word}%')
for word in term.split()]
query = query.filter(sa.and_(*criteria))\
.order_by(model.Person.full_name)
criteria = [model.Person.full_name.ilike(f"%{word}%") for word in term.split()]
query = query.filter(sa.and_(*criteria)).order_by(model.Person.full_name)
return query
def view_profile(self, session=None):
""" """
person = self.get_instance(session=session)
context = {
'person': person,
'instance': person,
"person": person,
"instance": person,
}
return self.render_to_response('view_profile', context)
return self.render_to_response("view_profile", context)
def make_user(self):
""" """
self.request.session.flash("TODO: this feature is not yet supported", 'error')
self.request.session.flash("TODO: this feature is not yet supported", "error")
return self.redirect(self.request.get_referrer())
@classmethod
@ -166,7 +166,7 @@ class PersonView(MasterView):
""" """
# nb. Person may come from custom model
wutta_config = config.registry.settings['wutta_config']
wutta_config = config.registry.settings["wutta_config"]
app = wutta_config.get_app()
cls.model_class = app.model.Person
@ -181,26 +181,36 @@ class PersonView(MasterView):
permission_prefix = cls.get_permission_prefix()
# view profile
config.add_route(f'{route_prefix}.view_profile',
f'{instance_url_prefix}/profile',
request_method='GET')
config.add_view(cls, attr='view_profile',
route_name=f'{route_prefix}.view_profile',
permission=f'{permission_prefix}.view_profile')
config.add_route(
f"{route_prefix}.view_profile",
f"{instance_url_prefix}/profile",
request_method="GET",
)
config.add_view(
cls,
attr="view_profile",
route_name=f"{route_prefix}.view_profile",
permission=f"{permission_prefix}.view_profile",
)
# make user for person
config.add_route(f'{route_prefix}.make_user',
f'{url_prefix}/make-user',
request_method='POST')
config.add_view(cls, attr='make_user',
route_name=f'{route_prefix}.make_user',
permission='users.create')
config.add_route(
f"{route_prefix}.make_user",
f"{url_prefix}/make-user",
request_method="POST",
)
config.add_view(
cls,
attr="make_user",
route_name=f"{route_prefix}.make_user",
permission="users.create",
)
def defaults(config, **kwargs):
base = globals()
PersonView = kwargs.get('PersonView', base['PersonView'])
PersonView = kwargs.get("PersonView", base["PersonView"])
PersonView.defaults(config)

View file

@ -38,24 +38,24 @@ def progress(request):
session storage. See also
:class:`~wuttaweb.progress.SessionProgress`.
"""
key = request.matchdict['key']
key = request.matchdict["key"]
session = get_progress_session(request, key)
# session has 'complete' flag set when operation is over
if session.get('complete'):
if session.get("complete"):
# set a flash msg for user if one is defined. this is the
# time to do it since user is about to get redirected.
msg = session.get('success_msg')
msg = session.get("success_msg")
if msg:
request.session.flash(msg)
elif session.get('error'): # uh-oh
elif session.get("error"): # uh-oh
# set an error flash msg for user. this is the time to do it
# since user is about to get redirected.
msg = session.get('error_msg', "An unspecified error occurred.")
request.session.flash(msg, 'error')
msg = session.get("error_msg", "An unspecified error occurred.")
request.session.flash(msg, "error")
# nb. we return the session as-is; since it is dict-like (and only
# contains relevant progress data) it can be used directly for the
@ -66,9 +66,9 @@ def progress(request):
def defaults(config, **kwargs):
base = globals()
progress = kwargs.get('progress', base['progress'])
config.add_route('progress', '/progress/{key}')
config.add_view(progress, route_name='progress', renderer='json')
progress = kwargs.get("progress", base["progress"])
config.add_route("progress", "/progress/{key}")
config.add_view(progress, route_name="progress", renderer="json")
def includeme(config):

View file

@ -47,24 +47,25 @@ class ReportView(MasterView):
* ``/reports/``
* ``/reports/XXX``
"""
model_title = "Report"
model_key = 'report_key'
model_key = "report_key"
filterable = False
sort_on_backend = False
creatable = False
editable = False
deletable = False
route_prefix = 'reports'
template_prefix = '/reports'
route_prefix = "reports"
template_prefix = "/reports"
grid_columns = [
'report_title',
'help_text',
'report_key',
"report_title",
"help_text",
"report_key",
]
form_fields = [
'help_text',
"help_text",
]
def __init__(self, request, context=None):
@ -81,9 +82,9 @@ class ReportView(MasterView):
def normalize_report(self, report):
""" """
return {
'report_key': report.report_key,
'report_title': report.report_title,
'help_text': report.__doc__,
"report_key": report.report_key,
"report_title": report.report_title,
"help_text": report.__doc__,
}
def configure_grid(self, g):
@ -91,18 +92,18 @@ class ReportView(MasterView):
super().configure_grid(g)
# report_key
g.set_link('report_key')
g.set_link("report_key")
# report_title
g.set_link('report_title')
g.set_searchable('report_title')
g.set_link("report_title")
g.set_searchable("report_title")
# help_text
g.set_searchable('help_text')
g.set_searchable("help_text")
def get_instance(self):
""" """
key = self.request.matchdict['report_key']
key = self.request.matchdict["report_key"]
report = self.report_handler.get_report(key)
if report:
return self.normalize_report(report)
@ -111,7 +112,7 @@ class ReportView(MasterView):
def get_instance_title(self, report):
""" """
return report['report_title']
return report["report_title"]
def view(self):
"""
@ -119,41 +120,43 @@ class ReportView(MasterView):
means showing them a form with report params, so they can run
it.
"""
key = self.request.matchdict['report_key']
key = self.request.matchdict["report_key"]
report = self.report_handler.get_report(key)
normal = self.normalize_report(report)
report_url = self.get_action_url('view', normal)
form = self.make_model_form(normal,
action_method='get',
action_url=report_url,
cancel_url=self.get_index_url(),
show_button_reset=True,
reset_url=report_url,
button_label_submit="Run Report",
button_icon_submit='arrow-circle-right')
report_url = self.get_action_url("view", normal)
form = self.make_model_form(
normal,
action_method="get",
action_url=report_url,
cancel_url=self.get_index_url(),
show_button_reset=True,
reset_url=report_url,
button_label_submit="Run Report",
button_icon_submit="arrow-circle-right",
)
context = {
'instance': normal,
'report': report,
'form': form,
'xref_buttons': self.get_xref_buttons(report),
"instance": normal,
"report": report,
"form": form,
"xref_buttons": self.get_xref_buttons(report),
}
if self.request.GET:
form.show_button_cancel = False
context = self.run_report(report, context)
return self.render_to_response('view', context)
return self.render_to_response("view", context)
def configure_form(self, f):
""" """
super().configure_form(f)
key = self.request.matchdict['report_key']
key = self.request.matchdict["report_key"]
report = self.report_handler.get_report(key)
# help_text
f.set_readonly('help_text')
f.set_readonly("help_text")
# add widget fields for all report params
schema = f.get_schema()
@ -174,12 +177,12 @@ class ReportView(MasterView):
:returns: Final view template context.
"""
form = context['form']
form = context["form"]
controls = list(self.request.GET.items())
# TODO: must re-inject help_text value for some reason,
# otherwise its absence screws things up. why?
controls.append(('help_text', report.__doc__))
controls.append(("help_text", report.__doc__))
dform = form.get_deform()
try:
@ -191,20 +194,20 @@ class ReportView(MasterView):
data = self.report_handler.make_report_data(report, params)
columns = self.normalize_columns(report.get_output_columns())
context['report_columns'] = columns
context["report_columns"] = columns
format_cols = [col for col in columns if col.get('formatter')]
format_cols = [col for col in columns if col.get("formatter")]
if format_cols:
for record in data['data']:
for record in data["data"]:
for column in format_cols:
if column['name'] in record:
value = record[column['name']]
record[column['name']] = column['formatter'](value)
if column["name"] in record:
value = record[column["name"]]
record[column["name"]] = column["formatter"](value)
params.pop('help_text')
context['report_params'] = params
context['report_data'] = data
context['report_generated'] = datetime.datetime.now()
params.pop("help_text")
context["report_params"] = params
context["report_data"] = data
context["report_generated"] = datetime.datetime.now()
return context
def normalize_columns(self, columns):
@ -212,14 +215,14 @@ class ReportView(MasterView):
normal = []
for column in columns:
if isinstance(column, str):
column = {'name': column}
column.setdefault('label', column['name'])
column = {"name": column}
column.setdefault("label", column["name"])
normal.append(column)
return normal
def get_download_data(self):
""" """
key = self.request.matchdict['report_key']
key = self.request.matchdict["report_key"]
report = self.report_handler.get_report(key)
params = dict(self.request.GET)
columns = self.normalize_columns(report.get_output_columns())
@ -244,21 +247,22 @@ class ReportView(MasterView):
model_title = cls.get_model_title()
# overwrite title for "view" perm since it also implies "run"
config.add_wutta_permission(permission_prefix,
f'{permission_prefix}.view',
f"View / run {model_title}")
config.add_wutta_permission(
permission_prefix, f"{permission_prefix}.view", f"View / run {model_title}"
)
# separate permission to download report files
config.add_wutta_permission(permission_prefix,
f'{permission_prefix}.download',
f"Download {model_title}")
config.add_wutta_permission(
permission_prefix,
f"{permission_prefix}.download",
f"Download {model_title}",
)
def defaults(config, **kwargs):
base = globals()
ReportView = kwargs.get('ReportView', base['ReportView'])
ReportView = kwargs.get("ReportView", base["ReportView"])
ReportView.defaults(config)

View file

@ -45,17 +45,18 @@ class RoleView(MasterView):
* ``/roles/XXX/edit``
* ``/roles/XXX/delete``
"""
model_class = Role
grid_columns = [
'name',
'notes',
"name",
"notes",
]
filter_defaults = {
'name': {'active': True},
"name": {"active": True},
}
sort_defaults = 'name'
sort_defaults = "name"
# TODO: master should handle this, possibly via configure_form()
def get_query(self, session=None):
@ -69,10 +70,10 @@ class RoleView(MasterView):
super().configure_grid(g)
# name
g.set_link('name')
g.set_link("name")
# notes
g.set_renderer('notes', self.grid_render_notes)
g.set_renderer("notes", self.grid_render_notes)
def is_editable(self, role):
""" """
@ -84,9 +85,11 @@ class RoleView(MasterView):
return self.request.is_root
# other built-in roles require special perm
if role in (auth.get_role_authenticated(session),
auth.get_role_anonymous(session)):
return self.has_perm('edit_builtin')
if role in (
auth.get_role_authenticated(session),
auth.get_role_anonymous(session),
):
return self.has_perm("edit_builtin")
return True
@ -111,26 +114,27 @@ class RoleView(MasterView):
role = f.model_instance
# never show these
f.remove('permission_refs',
'user_refs')
f.remove("permission_refs", "user_refs")
# name
f.set_validator('name', self.unique_name)
f.set_validator("name", self.unique_name)
# notes
f.set_widget('notes', widgets.NotesWidget())
f.set_widget("notes", widgets.NotesWidget())
# users
if not (self.creating or self.editing):
f.append('users')
f.set_grid('users', self.make_users_grid(role))
f.append("users")
f.set_grid("users", self.make_users_grid(role))
# permissions
f.append('permissions')
f.append("permissions")
self.wutta_permissions = self.get_available_permissions()
f.set_node('permissions', Permissions(self.request, permissions=self.wutta_permissions))
f.set_node(
"permissions", Permissions(self.request, permissions=self.wutta_permissions)
)
if not self.creating:
f.set_default('permissions', list(role.permissions))
f.set_default("permissions", list(role.permissions))
def make_users_grid(self, role):
"""
@ -144,24 +148,26 @@ class RoleView(MasterView):
model = self.app.model
route_prefix = self.get_route_prefix()
grid = self.make_grid(key=f'{route_prefix}.view.users',
model_class=model.User,
data=role.users,
columns=[
'username',
'person',
'active',
])
grid = self.make_grid(
key=f"{route_prefix}.view.users",
model_class=model.User,
data=role.users,
columns=[
"username",
"person",
"active",
],
)
if self.request.has_perm('users.view'):
url = lambda user, i: self.request.route_url('users.view', uuid=user.uuid)
grid.add_action('view', icon='eye', url=url)
grid.set_link('person')
grid.set_link('username')
if self.request.has_perm("users.view"):
url = lambda user, i: self.request.route_url("users.view", uuid=user.uuid)
grid.add_action("view", icon="eye", url=url)
grid.set_link("person")
grid.set_link("username")
if self.request.has_perm('users.edit'):
url = lambda user, i: self.request.route_url('users.edit', uuid=user.uuid)
grid.add_action('edit', url=url)
if self.request.has_perm("users.edit"):
url = lambda user, i: self.request.route_url("users.edit", uuid=user.uuid)
grid.add_action("edit", url=url)
return grid
@ -170,11 +176,10 @@ class RoleView(MasterView):
model = self.app.model
session = Session()
query = session.query(model.Role)\
.filter(model.Role.name == value)
query = session.query(model.Role).filter(model.Role.name == value)
if self.editing:
uuid = self.request.matchdict['uuid']
uuid = self.request.matchdict["uuid"]
query = query.filter(model.Role.uuid != uuid)
if query.count():
@ -236,7 +241,7 @@ class RoleView(MasterView):
"""
# get all known permissions from settings cache
permissions = self.request.registry.settings.get('wutta_permissions', {})
permissions = self.request.registry.settings.get("wutta_permissions", {})
# when viewing, we allow all permissions to be exposed for all users
if self.viewing:
@ -249,15 +254,15 @@ class RoleView(MasterView):
# non-admin user can only see permissions they're granted
available = {}
for gkey, group in permissions.items():
for pkey, perm in group['perms'].items():
for pkey, perm in group["perms"].items():
if self.request.has_perm(pkey):
if gkey not in available:
available[gkey] = {
'key': gkey,
'label': group['label'],
'perms': {},
"key": gkey,
"label": group["label"],
"perms": {},
}
available[gkey]['perms'][pkey] = perm
available[gkey]["perms"][pkey] = perm
return available
@ -273,15 +278,15 @@ class RoleView(MasterView):
def update_permissions(self, role, form):
""" """
if 'permissions' not in form.validated:
if "permissions" not in form.validated:
return
auth = self.app.get_auth_handler()
available = self.wutta_permissions
permissions = form.validated['permissions']
permissions = form.validated["permissions"]
for gkey, group in available.items():
for pkey, perm in group['perms'].items():
for pkey, perm in group["perms"].items():
if pkey in permissions:
auth.grant_permission(role, pkey)
else:
@ -299,9 +304,11 @@ class RoleView(MasterView):
model_title_plural = cls.get_model_title_plural()
# perm to edit built-in roles
config.add_wutta_permission(permission_prefix,
f'{permission_prefix}.edit_builtin',
f"Edit the Built-in {model_title_plural}")
config.add_wutta_permission(
permission_prefix,
f"{permission_prefix}.edit_builtin",
f"Edit the Built-in {model_title_plural}",
)
class PermissionView(MasterView):
@ -316,20 +323,21 @@ class PermissionView(MasterView):
* ``/permissions/XXX``
* ``/permissions/XXX/delete``
"""
model_class = Permission
creatable = False
editable = False
grid_columns = [
'role',
'permission',
"role",
"permission",
]
sort_defaults = 'role'
sort_defaults = "role"
form_fields = [
'role',
'permission',
"role",
"permission",
]
def get_query(self, **kwargs):
@ -348,28 +356,28 @@ class PermissionView(MasterView):
model = self.app.model
# role
g.set_sorter('role', model.Role.name)
g.set_filter('role', model.Role.name, label="Role Name")
g.set_link('role')
g.set_sorter("role", model.Role.name)
g.set_filter("role", model.Role.name, label="Role Name")
g.set_link("role")
# permission
g.set_link('permission')
g.set_link("permission")
def configure_form(self, f):
""" """
super().configure_form(f)
# role
f.set_node('role', RoleRef(self.request))
f.set_node("role", RoleRef(self.request))
def defaults(config, **kwargs):
base = globals()
RoleView = kwargs.get('RoleView', base['RoleView'])
RoleView = kwargs.get("RoleView", base["RoleView"])
RoleView.defaults(config)
PermissionView = kwargs.get('PermissionView', base['PermissionView'])
PermissionView = kwargs.get("PermissionView", base["PermissionView"])
PermissionView.defaults(config)

View file

@ -48,12 +48,13 @@ class AppInfoView(MasterView):
See also :class:`SettingView`.
"""
model_name = 'AppInfo'
model_name = "AppInfo"
model_title_plural = "App Info"
route_prefix = 'appinfo'
route_prefix = "appinfo"
filterable = False
sort_on_backend = False
sort_defaults = 'name'
sort_defaults = "name"
paginated = False
creatable = False
viewable = False
@ -62,9 +63,9 @@ class AppInfoView(MasterView):
configurable = True
grid_columns = [
'name',
'version',
'editable_project_location',
"name",
"version",
"editable_project_location",
]
# TODO: for tailbone backward compat with get_liburl() etc.
@ -74,19 +75,19 @@ class AppInfoView(MasterView):
""" """
# nb. init with empty data, only load it upon user request
if not self.request.GET.get('partial'):
if not self.request.GET.get("partial"):
return []
# TODO: pretty sure this is not cross-platform. probably some
# sort of pip methods belong on the app handler? or it should
# have a pip handler for all that?
pip = os.path.join(sys.prefix, 'bin', 'pip')
output = subprocess.check_output([pip, 'list', '--format=json'], text=True)
pip = os.path.join(sys.prefix, "bin", "pip")
output = subprocess.check_output([pip, "list", "--format=json"], text=True)
data = json.loads(output.strip())
# must avoid null values for sort to work right
for pkg in data:
pkg.setdefault('editable_project_location', '')
pkg.setdefault("editable_project_location", "")
return data
@ -97,74 +98,76 @@ class AppInfoView(MasterView):
g.sort_multiple = False
# name
g.set_searchable('name')
g.set_searchable("name")
# editable_project_location
g.set_searchable('editable_project_location')
g.set_searchable("editable_project_location")
def get_weblibs(self):
""" """
return OrderedDict([
('vue', "(Vue2) Vue"),
('vue_resource', "(Vue2) vue-resource"),
('buefy', "(Vue2) Buefy"),
('buefy.css', "(Vue2) Buefy CSS"),
('fontawesome', "(Vue2) FontAwesome"),
('bb_vue', "(Vue3) vue"),
('bb_oruga', "(Vue3) @oruga-ui/oruga-next"),
('bb_oruga_bulma', "(Vue3) @oruga-ui/theme-bulma (JS)"),
('bb_oruga_bulma_css', "(Vue3) @oruga-ui/theme-bulma (CSS)"),
('bb_fontawesome_svg_core', "(Vue3) @fortawesome/fontawesome-svg-core"),
('bb_free_solid_svg_icons', "(Vue3) @fortawesome/free-solid-svg-icons"),
('bb_vue_fontawesome', "(Vue3) @fortawesome/vue-fontawesome"),
])
return OrderedDict(
[
("vue", "(Vue2) Vue"),
("vue_resource", "(Vue2) vue-resource"),
("buefy", "(Vue2) Buefy"),
("buefy.css", "(Vue2) Buefy CSS"),
("fontawesome", "(Vue2) FontAwesome"),
("bb_vue", "(Vue3) vue"),
("bb_oruga", "(Vue3) @oruga-ui/oruga-next"),
("bb_oruga_bulma", "(Vue3) @oruga-ui/theme-bulma (JS)"),
("bb_oruga_bulma_css", "(Vue3) @oruga-ui/theme-bulma (CSS)"),
("bb_fontawesome_svg_core", "(Vue3) @fortawesome/fontawesome-svg-core"),
("bb_free_solid_svg_icons", "(Vue3) @fortawesome/free-solid-svg-icons"),
("bb_vue_fontawesome", "(Vue3) @fortawesome/vue-fontawesome"),
]
)
def configure_get_simple_settings(self):
""" """
simple_settings = [
# basics
{'name': f'{self.config.appname}.app_title'},
{'name': f'{self.config.appname}.node_type'},
{'name': f'{self.config.appname}.node_title'},
{'name': f'{self.config.appname}.production',
'type': bool},
{'name': 'wuttaweb.themes.expose_picker',
'type': bool},
{'name': f'{self.config.appname}.web.menus.handler.spec'},
{"name": f"{self.config.appname}.app_title"},
{"name": f"{self.config.appname}.node_type"},
{"name": f"{self.config.appname}.node_title"},
{"name": f"{self.config.appname}.production", "type": bool},
{"name": "wuttaweb.themes.expose_picker", "type": bool},
{"name": f"{self.config.appname}.web.menus.handler.spec"},
# nb. this is deprecated; we define so it is auto-deleted
# when we replace with newer setting
{'name': f'{self.config.appname}.web.menus.handler_spec'},
{"name": f"{self.config.appname}.web.menus.handler_spec"},
# user/auth
{'name': 'wuttaweb.home_redirect_to_login',
'type': bool, 'default': False},
{"name": "wuttaweb.home_redirect_to_login", "type": bool, "default": False},
# email
{'name': f'{self.config.appname}.mail.send_emails',
'type': bool, 'default': False},
{'name': f'{self.config.appname}.email.default.sender'},
{'name': f'{self.config.appname}.email.default.subject'},
{'name': f'{self.config.appname}.email.default.to'},
{'name': f'{self.config.appname}.email.feedback.subject'},
{'name': f'{self.config.appname}.email.feedback.to'},
{
"name": f"{self.config.appname}.mail.send_emails",
"type": bool,
"default": False,
},
{"name": f"{self.config.appname}.email.default.sender"},
{"name": f"{self.config.appname}.email.default.subject"},
{"name": f"{self.config.appname}.email.default.to"},
{"name": f"{self.config.appname}.email.feedback.subject"},
{"name": f"{self.config.appname}.email.feedback.to"},
]
def getval(key):
return self.config.get(f'wuttaweb.{key}')
return self.config.get(f"wuttaweb.{key}")
weblibs = self.get_weblibs()
for key, title in weblibs.items():
simple_settings.append({
'name': f'wuttaweb.libver.{key}',
'default': getval(f'libver.{key}'),
})
simple_settings.append({
'name': f'wuttaweb.liburl.{key}',
'default': getval(f'liburl.{key}'),
})
simple_settings.append(
{
"name": f"wuttaweb.libver.{key}",
"default": getval(f"libver.{key}"),
}
)
simple_settings.append(
{
"name": f"wuttaweb.liburl.{key}",
"default": getval(f"liburl.{key}"),
}
)
return simple_settings
@ -175,34 +178,42 @@ class AppInfoView(MasterView):
# add registered menu handlers
web = self.app.get_web_handler()
handlers = web.get_menu_handler_specs()
handlers = [{'spec': spec} for spec in handlers]
context['menu_handlers'] = handlers
handlers = [{"spec": spec} for spec in handlers]
context["menu_handlers"] = handlers
# add `weblibs` to context, based on config values
weblibs = self.get_weblibs()
for key in weblibs:
title = weblibs[key]
weblibs[key] = {
'key': key,
'title': title,
"key": key,
"title": title,
# nb. these values are exactly as configured, and are
# used for editing the settings
'configured_version': get_libver(self.request, key,
prefix=self.weblib_config_prefix,
configured_only=True),
'configured_url': get_liburl(self.request, key,
prefix=self.weblib_config_prefix,
configured_only=True),
"configured_version": get_libver(
self.request,
key,
prefix=self.weblib_config_prefix,
configured_only=True,
),
"configured_url": get_liburl(
self.request,
key,
prefix=self.weblib_config_prefix,
configured_only=True,
),
# nb. these are for display only
'default_version': get_libver(self.request, key,
prefix=self.weblib_config_prefix,
default_only=True),
'live_url': get_liburl(self.request, key,
prefix=self.weblib_config_prefix),
"default_version": get_libver(
self.request,
key,
prefix=self.weblib_config_prefix,
default_only=True,
),
"live_url": get_liburl(
self.request, key, prefix=self.weblib_config_prefix
),
}
context['weblibs'] = list(weblibs.values())
context["weblibs"] = list(weblibs.values())
return context
@ -219,13 +230,14 @@ class SettingView(MasterView):
See also :class:`AppInfoView`.
"""
model_class = Setting
model_title = "Raw Setting"
deletable_bulk = True
filter_defaults = {
'name': {'active': True},
"name": {"active": True},
}
sort_defaults = 'name'
sort_defaults = "name"
# TODO: master should handle this (per model key)
def configure_grid(self, g):
@ -233,29 +245,28 @@ class SettingView(MasterView):
super().configure_grid(g)
# name
g.set_link('name')
g.set_link("name")
def configure_form(self, f):
""" """
super().configure_form(f)
# name
f.set_validator('name', self.unique_name)
f.set_validator("name", self.unique_name)
# value
# TODO: master should handle this (per column nullable)
f.set_required('value', False)
f.set_required("value", False)
def unique_name(self, node, value):
""" """
model = self.app.model
session = self.Session()
query = session.query(model.Setting)\
.filter(model.Setting.name == value)
query = session.query(model.Setting).filter(model.Setting.name == value)
if self.editing:
name = self.request.matchdict['name']
name = self.request.matchdict["name"]
query = query.filter(model.Setting.name != name)
if query.count():
@ -265,10 +276,10 @@ class SettingView(MasterView):
def defaults(config, **kwargs):
base = globals()
AppInfoView = kwargs.get('AppInfoView', base['AppInfoView'])
AppInfoView = kwargs.get("AppInfoView", base["AppInfoView"])
AppInfoView.defaults(config)
SettingView = kwargs.get('SettingView', base['SettingView'])
SettingView = kwargs.get("SettingView", base["SettingView"])
SettingView.defaults(config)

View file

@ -55,21 +55,22 @@ class UpgradeView(MasterView):
* ``/upgrades/XXX/edit``
* ``/upgrades/XXX/delete``
"""
model_class = Upgrade
executable = True
execute_progress_template = '/upgrade.mako'
execute_progress_template = "/upgrade.mako"
downloadable = True
configurable = True
grid_columns = [
'created',
'description',
'status',
'executed',
'executed_by',
"created",
"description",
"status",
"executed",
"executed_by",
]
sort_defaults = ('created', 'desc')
sort_defaults = ("created", "desc")
def configure_grid(self, g):
""" """
@ -78,40 +79,44 @@ class UpgradeView(MasterView):
enum = self.app.enum
# description
g.set_link('description')
g.set_link("description")
# created
g.set_renderer('created', self.grid_render_datetime)
g.set_renderer("created", self.grid_render_datetime)
# created_by
g.set_link('created_by')
g.set_link("created_by")
Creator = orm.aliased(model.User)
g.set_joiner('created_by', lambda q: q.join(Creator,
Creator.uuid == model.Upgrade.created_by_uuid))
g.set_filter('created_by', Creator.username,
label="Created By Username")
g.set_joiner(
"created_by",
lambda q: q.join(Creator, Creator.uuid == model.Upgrade.created_by_uuid),
)
g.set_filter("created_by", Creator.username, label="Created By Username")
# status
g.set_renderer('status', self.grid_render_enum, enum=enum.UpgradeStatus)
g.set_renderer("status", self.grid_render_enum, enum=enum.UpgradeStatus)
# executed
g.set_renderer('executed', self.grid_render_datetime)
g.set_renderer("executed", self.grid_render_datetime)
# executed_by
g.set_link('executed_by')
g.set_link("executed_by")
Executor = orm.aliased(model.User)
g.set_joiner('executed_by', lambda q: q.outerjoin(Executor,
Executor.uuid == model.Upgrade.executed_by_uuid))
g.set_filter('executed_by', Executor.username,
label="Executed By Username")
g.set_joiner(
"executed_by",
lambda q: q.outerjoin(
Executor, Executor.uuid == model.Upgrade.executed_by_uuid
),
)
g.set_filter("executed_by", Executor.username, label="Executed By Username")
def grid_row_class(self, upgrade, data, i):
""" """
enum = self.app.enum
if upgrade.status == enum.UpgradeStatus.EXECUTING:
return 'has-background-warning'
return "has-background-warning"
if upgrade.status == enum.UpgradeStatus.FAILURE:
return 'has-background-warning'
return "has-background-warning"
def configure_form(self, f):
""" """
@ -120,71 +125,81 @@ class UpgradeView(MasterView):
upgrade = f.model_instance
# never show these
f.remove('created_by_uuid',
'executing',
'executed_by_uuid')
f.remove("created_by_uuid", "executing", "executed_by_uuid")
# sequence sanity
f.fields.set_sequence([
'description',
'notes',
'status',
'created',
'created_by',
'executed',
'executed_by',
])
f.fields.set_sequence(
[
"description",
"notes",
"status",
"created",
"created_by",
"executed",
"executed_by",
]
)
# created
if self.creating or self.editing:
f.remove('created')
f.remove("created")
# created_by
if self.creating or self.editing:
f.remove('created_by')
f.remove("created_by")
else:
f.set_node('created_by', UserRef(self.request))
f.set_node("created_by", UserRef(self.request))
# notes
f.set_widget('notes', 'notes')
f.set_widget("notes", "notes")
# status
if self.creating:
f.remove('status')
f.remove("status")
else:
f.set_node('status', WuttaEnum(self.request, enum.UpgradeStatus))
f.set_node("status", WuttaEnum(self.request, enum.UpgradeStatus))
# executed
if self.creating or self.editing or not upgrade.executed:
f.remove('executed')
f.remove("executed")
# executed_by
if self.creating or self.editing or not upgrade.executed:
f.remove('executed_by')
f.remove("executed_by")
else:
f.set_node('executed_by', UserRef(self.request))
f.set_node("executed_by", UserRef(self.request))
# exit_code
if self.creating or self.editing or not upgrade.executed:
f.remove('exit_code')
f.remove("exit_code")
# stdout / stderr
if not (self.creating or self.editing) and upgrade.status in (
enum.UpgradeStatus.SUCCESS, enum.UpgradeStatus.FAILURE):
enum.UpgradeStatus.SUCCESS,
enum.UpgradeStatus.FAILURE,
):
# stdout_file
f.append('stdout_file')
f.set_label('stdout_file', "STDOUT")
url = self.get_action_url('download', upgrade, _query={'filename': 'stdout.log'})
f.set_node('stdout_file', FileDownload(self.request, url=url))
f.set_default('stdout_file', self.get_upgrade_filepath(upgrade, 'stdout.log'))
f.append("stdout_file")
f.set_label("stdout_file", "STDOUT")
url = self.get_action_url(
"download", upgrade, _query={"filename": "stdout.log"}
)
f.set_node("stdout_file", FileDownload(self.request, url=url))
f.set_default(
"stdout_file", self.get_upgrade_filepath(upgrade, "stdout.log")
)
# stderr_file
f.append('stderr_file')
f.set_label('stderr_file', "STDERR")
url = self.get_action_url('download', upgrade, _query={'filename': 'stderr.log'})
f.set_node('stderr_file', FileDownload(self.request, url=url))
f.set_default('stderr_file', self.get_upgrade_filepath(upgrade, 'stderr.log'))
f.append("stderr_file")
f.set_label("stderr_file", "STDERR")
url = self.get_action_url(
"download", upgrade, _query={"filename": "stderr.log"}
)
f.set_node("stderr_file", FileDownload(self.request, url=url))
f.set_default(
"stderr_file", self.get_upgrade_filepath(upgrade, "stderr.log")
)
def delete_instance(self, upgrade):
"""
@ -217,8 +232,9 @@ class UpgradeView(MasterView):
def get_upgrade_filepath(self, upgrade, filename=None, create=True):
""" """
uuid = str(upgrade.uuid)
path = self.app.get_appdir('data', 'upgrades', uuid[:2], uuid[2:],
create=create)
path = self.app.get_appdir(
"data", "upgrades", uuid[:2], uuid[2:], create=create
)
if filename:
path = os.path.join(path, filename)
return path
@ -239,9 +255,9 @@ class UpgradeView(MasterView):
enum = self.app.enum
# locate file paths
script = self.config.require(f'{self.app.appname}.upgrades.command')
stdout_path = self.get_upgrade_filepath(upgrade, 'stdout.log')
stderr_path = self.get_upgrade_filepath(upgrade, 'stderr.log')
script = self.config.require(f"{self.app.appname}.upgrades.command")
stdout_path = self.get_upgrade_filepath(upgrade, "stdout.log")
stderr_path = self.get_upgrade_filepath(upgrade, "stderr.log")
# record the fact that execution has begun for this upgrade
# nb. this is done in separate session to ensure it sticks,
@ -253,10 +269,11 @@ class UpgradeView(MasterView):
# run the command
log.debug("running upgrade command: %s", script)
with open(stdout_path, 'wb') as stdout:
with open(stderr_path, 'wb') as stderr:
upgrade.exit_code = subprocess.call(script, shell=True, text=True,
stdout=stdout, stderr=stderr)
with open(stdout_path, "wb") as stdout:
with open(stderr_path, "wb") as stderr:
upgrade.exit_code = subprocess.call(
script, shell=True, text=True, stdout=stdout, stderr=stderr
)
logger = log.warning if upgrade.exit_code != 0 else log.debug
logger("upgrade command had exit code: %s", upgrade.exit_code)
@ -272,30 +289,30 @@ class UpgradeView(MasterView):
""" """
route_prefix = self.get_route_prefix()
upgrade = self.get_instance()
session = get_progress_session(self.request, f'{route_prefix}.execute')
session = get_progress_session(self.request, f"{route_prefix}.execute")
# session has 'complete' flag set when operation is over
if session.get('complete'):
if session.get("complete"):
# set a flash msg for user if one is defined. this is the
# time to do it since user is about to get redirected.
msg = session.get('success_msg')
msg = session.get("success_msg")
if msg:
self.request.session.flash(msg)
elif session.get('error'): # uh-oh
elif session.get("error"): # uh-oh
# set an error flash msg for user. this is the time to do it
# since user is about to get redirected.
msg = session.get('error_msg', "An unspecified error occurred.")
self.request.session.flash(msg, 'error')
msg = session.get("error_msg", "An unspecified error occurred.")
self.request.session.flash(msg, "error")
# our return value will include all from progress session
data = dict(session)
# add whatever might be new from upgrade process STDOUT
path = self.get_upgrade_filepath(upgrade, filename='stdout.log')
offset = session.get('stdout.offset', 0)
path = self.get_upgrade_filepath(upgrade, filename="stdout.log")
offset = session.get("stdout.offset", 0)
if os.path.exists(path):
size = os.path.getsize(path) - offset
if size > 0:
@ -304,8 +321,8 @@ class UpgradeView(MasterView):
f.seek(offset)
chunk = f.read(size)
# data['stdout'] = chunk.decode('utf8').replace('\n', '<br />')
data['stdout'] = chunk.replace('\n', '<br />')
session['stdout.offset'] = offset + size
data["stdout"] = chunk.replace("\n", "<br />")
session["stdout.offset"] = offset + size
session.save()
return data
@ -313,16 +330,13 @@ class UpgradeView(MasterView):
def configure_get_simple_settings(self):
""" """
script = self.config.get(f'{self.app.appname}.upgrades.command')
script = self.config.get(f"{self.app.appname}.upgrades.command")
if not script:
pass
return [
# basics
{'name': f'{self.app.appname}.upgrades.command',
'default': script},
{"name": f"{self.app.appname}.upgrades.command", "default": script},
]
@classmethod
@ -330,7 +344,7 @@ class UpgradeView(MasterView):
""" """
# nb. Upgrade may come from custom model
wutta_config = config.registry.settings['wutta_config']
wutta_config = config.registry.settings["wutta_config"]
app = wutta_config.get_app()
cls.model_class = app.model.Upgrade
@ -344,18 +358,23 @@ class UpgradeView(MasterView):
instance_url_prefix = cls.get_instance_url_prefix()
# execution progress
config.add_route(f'{route_prefix}.execute_progress',
f'{instance_url_prefix}/execute/progress')
config.add_view(cls, attr='execute_progress',
route_name=f'{route_prefix}.execute_progress',
permission=f'{permission_prefix}.execute',
renderer='json')
config.add_route(
f"{route_prefix}.execute_progress",
f"{instance_url_prefix}/execute/progress",
)
config.add_view(
cls,
attr="execute_progress",
route_name=f"{route_prefix}.execute_progress",
permission=f"{permission_prefix}.execute",
renderer="json",
)
def defaults(config, **kwargs):
base = globals()
UpgradeView = kwargs.get('UpgradeView', base['UpgradeView'])
UpgradeView = kwargs.get("UpgradeView", base["UpgradeView"])
UpgradeView.defaults(config)

View file

@ -46,31 +46,32 @@ class UserView(MasterView):
* ``/users/XXX/edit``
* ``/users/XXX/delete``
"""
model_class = User
labels = {
'api_tokens': "API Tokens",
"api_tokens": "API Tokens",
}
grid_columns = [
'username',
'person',
'active',
"username",
"person",
"active",
]
filter_defaults = {
'username': {'active': True},
'active': {'active': True, 'verb': 'is_true'},
"username": {"active": True},
"active": {"active": True, "verb": "is_true"},
}
sort_defaults = 'username'
sort_defaults = "username"
form_fields = [
'username',
'person',
'active',
'prevent_edit',
'roles',
'api_tokens',
"username",
"person",
"active",
"prevent_edit",
"roles",
"api_tokens",
]
def get_query(self, session=None):
@ -89,23 +90,20 @@ class UserView(MasterView):
model = self.app.model
# never show these
g.remove('person_uuid',
'role_refs',
'password')
g.remove("person_uuid", "role_refs", "password")
# username
g.set_link('username')
g.set_link("username")
# person
g.set_link('person')
g.set_sorter('person', model.Person.full_name)
g.set_filter('person', model.Person.full_name,
label="Person Full Name")
g.set_link("person")
g.set_sorter("person", model.Person.full_name)
g.set_filter("person", model.Person.full_name, label="Person Full Name")
def grid_row_class(self, user, data, i):
""" """
if not user.active:
return 'has-background-warning'
return "has-background-warning"
def is_editable(self, user):
""" """
@ -122,56 +120,55 @@ class UserView(MasterView):
user = f.model_instance
# username
f.set_validator('username', self.unique_username)
f.set_validator("username", self.unique_username)
# person
if self.creating or self.editing:
f.fields.insert_after('person', 'first_name')
f.set_required('first_name', False)
f.fields.insert_after('first_name', 'last_name')
f.set_required('last_name', False)
f.remove('person')
f.fields.insert_after("person", "first_name")
f.set_required("first_name", False)
f.fields.insert_after("first_name", "last_name")
f.set_required("last_name", False)
f.remove("person")
if self.editing:
person = user.person
if person:
f.set_default('first_name', person.first_name)
f.set_default('last_name', person.last_name)
f.set_default("first_name", person.first_name)
f.set_default("last_name", person.last_name)
else:
f.set_node('person', PersonRef(self.request))
f.set_node("person", PersonRef(self.request))
# password
# nb. we must avoid 'password' as field name since
# ColanderAlchemy wants to handle the raw/hashed value
f.remove('password')
f.remove("password")
# nb. no need for password field if readonly
if self.creating or self.editing:
# nb. use 'set_password' as field name
f.append('set_password')
f.set_required('set_password', False)
f.set_widget('set_password', widgets.CheckedPasswordWidget())
f.append("set_password")
f.set_required("set_password", False)
f.set_widget("set_password", widgets.CheckedPasswordWidget())
# roles
f.append('roles')
f.set_node('roles', RoleRefs(self.request))
f.append("roles")
f.set_node("roles", RoleRefs(self.request))
if not self.creating:
f.set_default('roles', [role.uuid.hex for role in user.roles])
f.set_default("roles", [role.uuid.hex for role in user.roles])
# api_tokens
if self.viewing and self.has_perm('manage_api_tokens'):
f.set_grid('api_tokens', self.make_api_tokens_grid(user))
if self.viewing and self.has_perm("manage_api_tokens"):
f.set_grid("api_tokens", self.make_api_tokens_grid(user))
else:
f.remove('api_tokens')
f.remove("api_tokens")
def unique_username(self, node, value):
""" """
model = self.app.model
session = self.Session()
query = session.query(model.User)\
.filter(model.User.username == value)
query = session.query(model.User).filter(model.User.username == value)
if self.editing:
uuid = self.request.matchdict['uuid']
uuid = self.request.matchdict["uuid"]
query = query.filter(model.User.uuid != uuid)
if query.count():
@ -187,29 +184,34 @@ class UserView(MasterView):
user = super().objectify(form)
# maybe update person name
if 'first_name' in form or 'last_name' in form:
first_name = data.get('first_name')
last_name = data.get('last_name')
if "first_name" in form or "last_name" in form:
first_name = data.get("first_name")
last_name = data.get("last_name")
if self.creating and (first_name or last_name):
user.person = auth.make_person(first_name=first_name, last_name=last_name)
user.person = auth.make_person(
first_name=first_name, last_name=last_name
)
elif self.editing:
if first_name or last_name:
if user.person:
person = user.person
if 'first_name' in form:
if "first_name" in form:
person.first_name = first_name
if 'last_name' in form:
if "last_name" in form:
person.last_name = last_name
person.full_name = self.app.make_full_name(person.first_name,
person.last_name)
person.full_name = self.app.make_full_name(
person.first_name, person.last_name
)
else:
user.person = auth.make_person(first_name=first_name, last_name=last_name)
user.person = auth.make_person(
first_name=first_name, last_name=last_name
)
elif user.person:
user.person = None
# maybe set user password
if 'set_password' in form and data.get('set_password'):
auth.set_user_password(user, data['set_password'])
if "set_password" in form and data.get("set_password"):
auth.set_user_password(user, data["set_password"])
# update roles for user
# TODO
@ -224,7 +226,7 @@ class UserView(MasterView):
# if not self.has_perm('edit_roles'):
# return
data = form.validated
if 'roles' not in data:
if "roles" not in data:
return
model = self.app.model
@ -232,7 +234,7 @@ class UserView(MasterView):
auth = self.app.get_auth_handler()
old_roles = set([role.uuid for role in user.roles])
new_roles = data['roles']
new_roles = data["roles"]
admin = auth.get_role_administrator(session)
ignored = {
@ -274,33 +276,46 @@ class UserView(MasterView):
model = self.app.model
route_prefix = self.get_route_prefix()
grid = self.make_grid(key=f'{route_prefix}.view.api_tokens',
data=[self.normalize_api_token(t) for t in user.api_tokens],
columns=[
'description',
'created',
],
sortable=True,
sort_on_backend=False,
sort_defaults=[('created', 'desc')])
grid = self.make_grid(
key=f"{route_prefix}.view.api_tokens",
data=[self.normalize_api_token(t) for t in user.api_tokens],
columns=[
"description",
"created",
],
sortable=True,
sort_on_backend=False,
sort_defaults=[("created", "desc")],
)
if self.has_perm('manage_api_tokens'):
if self.has_perm("manage_api_tokens"):
# create token
button = self.make_button("New", primary=True, icon_left='plus', **{'@click': "$emit('new-token')"})
grid.add_tool(button, key='create')
button = self.make_button(
"New",
primary=True,
icon_left="plus",
**{"@click": "$emit('new-token')"},
)
grid.add_tool(button, key="create")
# delete token
grid.add_action('delete', url='#', icon='trash', link_class='has-text-danger', click_handler="$emit('delete-token', props.row)")
grid.add_action(
"delete",
url="#",
icon="trash",
link_class="has-text-danger",
click_handler="$emit('delete-token', props.row)",
)
return grid
def normalize_api_token(self, token):
""" """
return {
'uuid': token.uuid.hex,
'description': token.description,
'created': self.app.render_datetime(token.created),
"uuid": token.uuid.hex,
"description": token.description,
"created": self.app.render_datetime(token.created),
}
def add_api_token(self):
@ -316,13 +331,13 @@ class UserView(MasterView):
user = self.get_instance()
data = self.request.json_body
token = auth.add_api_token(user, data['description'])
token = auth.add_api_token(user, data["description"])
session.flush()
session.refresh(token)
result = self.normalize_api_token(token)
result['token_string'] = token.token_string
result['_action_url_delete'] = '#'
result["token_string"] = token.token_string
result["_action_url_delete"] = "#"
return result
def delete_api_token(self):
@ -339,12 +354,12 @@ class UserView(MasterView):
user = self.get_instance()
data = self.request.json_body
token = session.get(model.UserAPIToken, data['uuid'])
token = session.get(model.UserAPIToken, data["uuid"])
if not token:
return {'error': "API token not found"}
return {"error": "API token not found"}
if token.user is not user:
return {'error': "API token not found"}
return {"error": "API token not found"}
auth.delete_api_token(token)
return {}
@ -354,7 +369,7 @@ class UserView(MasterView):
""" """
# nb. User may come from custom model
wutta_config = config.registry.settings['wutta_config']
wutta_config = config.registry.settings["wutta_config"]
app = wutta_config.get_app()
cls.model_class = app.model.User
@ -372,29 +387,41 @@ class UserView(MasterView):
model_title = cls.get_model_title()
# manage API tokens
config.add_wutta_permission(permission_prefix,
f'{permission_prefix}.manage_api_tokens',
f"Manage API tokens for any {model_title}")
config.add_route(f'{route_prefix}.add_api_token',
f'{instance_url_prefix}/add-api-token',
request_method='POST')
config.add_view(cls, attr='add_api_token',
route_name=f'{route_prefix}.add_api_token',
permission=f'{permission_prefix}.manage_api_tokens',
renderer='json')
config.add_route(f'{route_prefix}.delete_api_token',
f'{instance_url_prefix}/delete-api-token',
request_method='POST')
config.add_view(cls, attr='delete_api_token',
route_name=f'{route_prefix}.delete_api_token',
permission=f'{permission_prefix}.manage_api_tokens',
renderer='json')
config.add_wutta_permission(
permission_prefix,
f"{permission_prefix}.manage_api_tokens",
f"Manage API tokens for any {model_title}",
)
config.add_route(
f"{route_prefix}.add_api_token",
f"{instance_url_prefix}/add-api-token",
request_method="POST",
)
config.add_view(
cls,
attr="add_api_token",
route_name=f"{route_prefix}.add_api_token",
permission=f"{permission_prefix}.manage_api_tokens",
renderer="json",
)
config.add_route(
f"{route_prefix}.delete_api_token",
f"{instance_url_prefix}/delete-api-token",
request_method="POST",
)
config.add_view(
cls,
attr="delete_api_token",
route_name=f"{route_prefix}.delete_api_token",
permission=f"{permission_prefix}.manage_api_tokens",
renderer="json",
)
def defaults(config, **kwargs):
base = globals()
UserView = kwargs.get('UserView', base['UserView'])
UserView = kwargs.get("UserView", base["UserView"])
UserView.defaults(config)

View file

@ -15,14 +15,14 @@ def release(c, skip_tests=False):
Release a new version of WuttJamaican
"""
if not skip_tests:
c.run('pytest')
c.run("pytest")
# rebuild pkg
if os.path.exists('dist'):
shutil.rmtree('dist')
if os.path.exists('WuttJamaican.egg-info'):
shutil.rmtree('WuttJamaican.egg-info')
c.run('python -m build --sdist')
if os.path.exists("dist"):
shutil.rmtree("dist")
if os.path.exists("WuttJamaican.egg-info"):
shutil.rmtree("WuttJamaican.egg-info")
c.run("python -m build --sdist")
# upload
c.run('twine upload dist/*')
c.run("twine upload dist/*")

View file

@ -10,8 +10,8 @@ from wuttaweb.cli import webapp as mod
class TestWebapp(ConfigTestCase):
def make_context(self, **kwargs):
params = {'auto_reload': False}
params.update(kwargs.get('params', {}))
params = {"auto_reload": False}
params.update(kwargs.get("params", {}))
ctx = MagicMock(params=params)
ctx.parent.wutta_config = self.config
return ctx
@ -19,7 +19,7 @@ class TestWebapp(ConfigTestCase):
def test_missing_config_file(self):
# nb. our default config has no files, so can test w/ that
ctx = self.make_context()
with patch.object(mod, 'sys') as sys:
with patch.object(mod, "sys") as sys:
sys.exit.side_effect = RuntimeError
self.assertRaises(RuntimeError, mod.webapp, ctx)
sys.stderr.write.assert_called_once_with("no config files found!\n")
@ -28,14 +28,17 @@ class TestWebapp(ConfigTestCase):
def test_invalid_runner(self):
# make new config from file, with bad setting
path = self.write_file('my.conf', """
path = self.write_file(
"my.conf",
"""
[wutta.web]
app.runner = bogus
""")
""",
)
self.config = self.make_config(files=[path])
ctx = self.make_context()
with patch.object(mod, 'sys') as sys:
with patch.object(mod, "sys") as sys:
sys.exit.side_effect = RuntimeError
self.assertRaises(RuntimeError, mod.webapp, ctx)
sys.stderr.write.assert_called_once_with("unknown web app runner: bogus\n")
@ -43,64 +46,76 @@ app.runner = bogus
def test_pserve(self):
path = self.write_file('my.conf', """
path = self.write_file(
"my.conf",
"""
[wutta.web]
app.runner = pserve
""")
""",
)
self.config = self.make_config(files=[path])
# normal
with patch.object(mod, 'pserve') as pserve:
with patch.object(mod, "pserve") as pserve:
ctx = self.make_context()
mod.webapp(ctx)
pserve.main.assert_called_once_with(argv=['pserve', f'file+ini:{path}'])
pserve.main.assert_called_once_with(argv=["pserve", f"file+ini:{path}"])
# with reload
with patch.object(mod, 'pserve') as pserve:
ctx = self.make_context(params={'auto_reload': True})
with patch.object(mod, "pserve") as pserve:
ctx = self.make_context(params={"auto_reload": True})
mod.webapp(ctx)
pserve.main.assert_called_once_with(argv=['pserve', f'file+ini:{path}', '--reload'])
pserve.main.assert_called_once_with(
argv=["pserve", f"file+ini:{path}", "--reload"]
)
def test_uvicorn(self):
path = self.write_file('my.conf', """
path = self.write_file(
"my.conf",
"""
[wutta.web]
app.runner = uvicorn
app.spec = wuttaweb.app:make_wsgi_app
""")
""",
)
self.config = self.make_config(files=[path])
orig_import = __import__
uvicorn = MagicMock()
def mock_import(name, *args, **kwargs):
if name == 'uvicorn':
if name == "uvicorn":
return uvicorn
return orig_import(name, *args, **kwargs)
# normal
with patch('builtins.__import__', side_effect=mock_import):
with patch("builtins.__import__", side_effect=mock_import):
ctx = self.make_context()
mod.webapp(ctx)
uvicorn.run.assert_called_once_with('wuttaweb.app:make_wsgi_app',
host='127.0.0.1',
port=8000,
reload=False,
reload_dirs=None,
factory=False,
interface='auto',
root_path='')
uvicorn.run.assert_called_once_with(
"wuttaweb.app:make_wsgi_app",
host="127.0.0.1",
port=8000,
reload=False,
reload_dirs=None,
factory=False,
interface="auto",
root_path="",
)
# with reload
uvicorn.run.reset_mock()
with patch('builtins.__import__', side_effect=mock_import):
ctx = self.make_context(params={'auto_reload': True})
with patch("builtins.__import__", side_effect=mock_import):
ctx = self.make_context(params={"auto_reload": True})
mod.webapp(ctx)
uvicorn.run.assert_called_once_with('wuttaweb.app:make_wsgi_app',
host='127.0.0.1',
port=8000,
reload=True,
reload_dirs=None,
factory=False,
interface='auto',
root_path='')
uvicorn.run.assert_called_once_with(
"wuttaweb.app:make_wsgi_app",
host="127.0.0.1",
port=8000,
reload=True,
reload_dirs=None,
factory=False,
interface="auto",
root_path="",
)

View file

@ -11,7 +11,7 @@ from wuttaweb.testing import WebTestCase
class TestWuttaWebContinuumPlugin(WebTestCase):
def setUp(self):
if not hasattr(mod, 'WuttaWebContinuumPlugin'):
if not hasattr(mod, "WuttaWebContinuumPlugin"):
pytest.skip("test not relevant without sqlalchemy-continuum")
self.setup_web()
@ -21,17 +21,17 @@ class TestWuttaWebContinuumPlugin(WebTestCase):
def test_get_remote_addr(self):
plugin = self.make_plugin()
with patch.object(mod, 'get_current_request', return_value=None):
with patch.object(mod, "get_current_request", return_value=None):
self.assertIsNone(plugin.get_remote_addr(None, self.session))
self.request.client_addr = '127.0.0.1'
self.assertEqual(plugin.get_remote_addr(None, self.session), '127.0.0.1')
self.request.client_addr = "127.0.0.1"
self.assertEqual(plugin.get_remote_addr(None, self.session), "127.0.0.1")
def test_get_user_id(self):
plugin = self.make_plugin()
with patch.object(mod, 'get_current_request', return_value=None):
with patch.object(mod, "get_current_request", return_value=None):
self.assertIsNone(plugin.get_user_id(None, self.session))
self.request.user = MagicMock(uuid='some-random-uuid')
self.assertEqual(plugin.get_user_id(None, self.session), 'some-random-uuid')
self.request.user = MagicMock(uuid="some-random-uuid")
self.assertEqual(plugin.get_user_id(None, self.session), "some-random-uuid")

View file

@ -18,17 +18,22 @@ from wuttaweb.grids import Grid
class TestForm(TestCase):
def setUp(self):
self.config = WuttaConfig(defaults={
'wutta.web.menus.handler_spec': 'tests.util:NullMenuHandler',
})
self.config = WuttaConfig(
defaults={
"wutta.web.menus.handler_spec": "tests.util:NullMenuHandler",
}
)
self.app = self.config.get_app()
self.request = testing.DummyRequest(wutta_config=self.config, use_oruga=False)
self.pyramid_config = testing.setUp(request=self.request, settings={
'wutta_config': self.config,
'mako.directories': ['wuttaweb:templates'],
'pyramid_deform.template_search_path': 'wuttaweb:templates/deform',
})
self.pyramid_config = testing.setUp(
request=self.request,
settings={
"wutta_config": self.config,
"mako.directories": ["wuttaweb:templates"],
"pyramid_deform.template_search_path": "wuttaweb:templates/deform",
},
)
event = MagicMock(request=self.request)
subscribers.new_request(event)
@ -40,12 +45,12 @@ class TestForm(TestCase):
return base.Form(self.request, **kwargs)
def make_schema(self):
schema = colander.Schema(children=[
colander.SchemaNode(colander.String(),
name='foo'),
colander.SchemaNode(colander.String(),
name='bar'),
])
schema = colander.Schema(
children=[
colander.SchemaNode(colander.String(), name="foo"),
colander.SchemaNode(colander.String(), name="bar"),
]
)
return schema
def test_init_with_none(self):
@ -53,126 +58,126 @@ class TestForm(TestCase):
self.assertEqual(form.fields, [])
def test_init_with_fields(self):
form = self.make_form(fields=['foo', 'bar'])
self.assertEqual(form.fields, ['foo', 'bar'])
form = self.make_form(fields=["foo", "bar"])
self.assertEqual(form.fields, ["foo", "bar"])
def test_init_with_schema(self):
schema = self.make_schema()
form = self.make_form(schema=schema)
self.assertEqual(form.fields, ['foo', 'bar'])
self.assertEqual(form.fields, ["foo", "bar"])
def test_vue_tagname(self):
form = self.make_form()
self.assertEqual(form.vue_tagname, 'wutta-form')
self.assertEqual(form.vue_tagname, "wutta-form")
def test_vue_component(self):
form = self.make_form()
self.assertEqual(form.vue_component, 'WuttaForm')
self.assertEqual(form.vue_component, "WuttaForm")
def test_contains(self):
form = self.make_form(fields=['foo', 'bar'])
self.assertIn('foo', form)
self.assertNotIn('baz', form)
form = self.make_form(fields=["foo", "bar"])
self.assertIn("foo", form)
self.assertNotIn("baz", form)
def test_iter(self):
form = self.make_form(fields=['foo', 'bar'])
form = self.make_form(fields=["foo", "bar"])
fields = list(iter(form))
self.assertEqual(fields, ['foo', 'bar'])
self.assertEqual(fields, ["foo", "bar"])
fields = []
for field in form:
fields.append(field)
self.assertEqual(fields, ['foo', 'bar'])
self.assertEqual(fields, ["foo", "bar"])
def test_set_fields(self):
form = self.make_form(fields=['foo', 'bar'])
self.assertEqual(form.fields, ['foo', 'bar'])
form.set_fields(['baz'])
self.assertEqual(form.fields, ['baz'])
form = self.make_form(fields=["foo", "bar"])
self.assertEqual(form.fields, ["foo", "bar"])
form.set_fields(["baz"])
self.assertEqual(form.fields, ["baz"])
def test_append(self):
form = self.make_form(fields=['one', 'two'])
self.assertEqual(form.fields, ['one', 'two'])
form.append('one', 'two', 'three')
self.assertEqual(form.fields, ['one', 'two', 'three'])
form = self.make_form(fields=["one", "two"])
self.assertEqual(form.fields, ["one", "two"])
form.append("one", "two", "three")
self.assertEqual(form.fields, ["one", "two", "three"])
def test_remove(self):
form = self.make_form(fields=['one', 'two', 'three', 'four'])
self.assertEqual(form.fields, ['one', 'two', 'three', 'four'])
form.remove('two', 'three')
self.assertEqual(form.fields, ['one', 'four'])
form = self.make_form(fields=["one", "two", "three", "four"])
self.assertEqual(form.fields, ["one", "two", "three", "four"])
form.remove("two", "three")
self.assertEqual(form.fields, ["one", "four"])
def test_set_node(self):
form = self.make_form(fields=['foo', 'bar'])
form = self.make_form(fields=["foo", "bar"])
self.assertEqual(form.nodes, {})
# complete node
node = colander.SchemaNode(colander.Bool(), name='foo')
form.set_node('foo', node)
self.assertIs(form.nodes['foo'], node)
node = colander.SchemaNode(colander.Bool(), name="foo")
form.set_node("foo", node)
self.assertIs(form.nodes["foo"], node)
# type only
typ = colander.Bool()
form.set_node('foo', typ)
node = form.nodes['foo']
form.set_node("foo", typ)
node = form.nodes["foo"]
self.assertIsInstance(node, colander.SchemaNode)
self.assertIsInstance(node.typ, colander.Bool)
self.assertEqual(node.name, 'foo')
self.assertEqual(node.name, "foo")
# schema is updated if already present
schema = form.get_schema()
self.assertIsNotNone(schema)
typ = colander.Date()
form.set_node('foo', typ)
node = form.nodes['foo']
form.set_node("foo", typ)
node = form.nodes["foo"]
self.assertIsInstance(node, colander.SchemaNode)
self.assertIsInstance(node.typ, colander.Date)
self.assertEqual(node.name, 'foo')
self.assertEqual(node.name, "foo")
def test_set_widget(self):
form = self.make_form(fields=['foo', 'bar'])
form = self.make_form(fields=["foo", "bar"])
self.assertEqual(form.widgets, {})
# basic
widget = widgets.SelectWidget()
form.set_widget('foo', widget)
self.assertIs(form.widgets['foo'], widget)
form.set_widget("foo", widget)
self.assertIs(form.widgets["foo"], widget)
# schema is updated if already present
schema = form.get_schema()
self.assertIsNotNone(schema)
self.assertIs(schema['foo'].widget, widget)
self.assertIs(schema["foo"].widget, widget)
new_widget = widgets.TextInputWidget()
form.set_widget('foo', new_widget)
self.assertIs(form.widgets['foo'], new_widget)
self.assertIs(schema['foo'].widget, new_widget)
form.set_widget("foo", new_widget)
self.assertIs(form.widgets["foo"], new_widget)
self.assertIs(schema["foo"].widget, new_widget)
# can also just specify widget pseudo-type (invalid)
self.assertNotIn('bar', form.widgets)
self.assertRaises(ValueError, form.set_widget, 'bar', 'ldjfadjfadj')
self.assertNotIn("bar", form.widgets)
self.assertRaises(ValueError, form.set_widget, "bar", "ldjfadjfadj")
# can also just specify widget pseudo-type (valid)
self.assertNotIn('bar', form.widgets)
form.set_widget('bar', 'notes')
self.assertIsInstance(form.widgets['bar'], widgets.NotesWidget)
self.assertNotIn("bar", form.widgets)
form.set_widget("bar", "notes")
self.assertIsInstance(form.widgets["bar"], widgets.NotesWidget)
def test_make_widget(self):
form = self.make_form(fields=['foo', 'bar'])
form = self.make_form(fields=["foo", "bar"])
# notes
widget = form.make_widget('notes')
widget = form.make_widget("notes")
self.assertIsInstance(widget, widgets.NotesWidget)
# invalid
widget = form.make_widget('fdajvdafjjf')
widget = form.make_widget("fdajvdafjjf")
self.assertIsNone(widget)
def test_set_default_widgets(self):
model = self.app.model
# no defaults for "plain" schema
form = self.make_form(fields=['foo', 'bar'])
form = self.make_form(fields=["foo", "bar"])
self.assertEqual(form.widgets, {})
# no defaults for "plain" mapped class
@ -184,52 +189,56 @@ class TestForm(TestCase):
# widget set for datetime mapped field
form = self.make_form(model_class=model.Upgrade)
self.assertIn('created', form.widgets)
self.assertIsNot(form.widgets['created'], MyWidget)
self.assertNotIsInstance(form.widgets['created'], MyWidget)
self.assertIn("created", form.widgets)
self.assertIsNot(form.widgets["created"], MyWidget)
self.assertNotIsInstance(form.widgets["created"], MyWidget)
# widget *not* set for datetime, if override present
form = self.make_form(model_class=model.Upgrade,
widgets={'created': MyWidget()})
self.assertIn('created', form.widgets)
self.assertIsInstance(form.widgets['created'], MyWidget)
form = self.make_form(
model_class=model.Upgrade, widgets={"created": MyWidget()}
)
self.assertIn("created", form.widgets)
self.assertIsInstance(form.widgets["created"], MyWidget)
# mock up a table with all relevant column types
class Whatever(model.Base):
__tablename__ = 'whatever'
__tablename__ = "whatever"
id = sa.Column(sa.Integer(), primary_key=True)
date = sa.Column(sa.Date())
date_time = sa.Column(sa.DateTime())
# widget set for all known types
form = self.make_form(model_class=Whatever)
self.assertIsInstance(form.widgets['date'], widgets.WuttaDateWidget)
self.assertIsInstance(form.widgets['date_time'], widgets.WuttaDateTimeWidget)
self.assertIsInstance(form.widgets["date"], widgets.WuttaDateWidget)
self.assertIsInstance(form.widgets["date_time"], widgets.WuttaDateTimeWidget)
def test_set_grid(self):
form = self.make_form(fields=['foo', 'bar'])
self.assertNotIn('foo', form.widgets)
self.assertNotIn('foogrid', form.grid_vue_context)
form = self.make_form(fields=["foo", "bar"])
self.assertNotIn("foo", form.widgets)
self.assertNotIn("foogrid", form.grid_vue_context)
grid = Grid(self.request, key='foogrid',
columns=['a', 'b'],
data=[{'a': 1, 'b': 2}, {'a': 3, 'b': 4}])
grid = Grid(
self.request,
key="foogrid",
columns=["a", "b"],
data=[{"a": 1, "b": 2}, {"a": 3, "b": 4}],
)
form.set_grid('foo', grid)
self.assertIn('foo', form.widgets)
self.assertIsInstance(form.widgets['foo'], widgets.GridWidget)
self.assertIn('foogrid', form.grid_vue_context)
form.set_grid("foo", grid)
self.assertIn("foo", form.widgets)
self.assertIsInstance(form.widgets["foo"], widgets.GridWidget)
self.assertIn("foogrid", form.grid_vue_context)
def test_set_validator(self):
form = self.make_form(fields=['foo', 'bar'])
form = self.make_form(fields=["foo", "bar"])
self.assertEqual(form.validators, {})
def validate1(node, value):
pass
# basic
form.set_validator('foo', validate1)
self.assertIs(form.validators['foo'], validate1)
form.set_validator("foo", validate1)
self.assertIs(form.validators["foo"], validate1)
def validate2(node, value):
pass
@ -237,18 +246,18 @@ class TestForm(TestCase):
# schema is updated if already present
schema = form.get_schema()
self.assertIsNotNone(schema)
self.assertIs(schema['foo'].validator, validate1)
form.set_validator('foo', validate2)
self.assertIs(form.validators['foo'], validate2)
self.assertIs(schema['foo'].validator, validate2)
self.assertIs(schema["foo"].validator, validate1)
form.set_validator("foo", validate2)
self.assertIs(form.validators["foo"], validate2)
self.assertIs(schema["foo"].validator, validate2)
def test_set_default(self):
form = self.make_form(fields=['foo', 'bar'])
form = self.make_form(fields=["foo", "bar"])
self.assertEqual(form.defaults, {})
# basic
form.set_default('foo', 42)
self.assertEqual(form.defaults['foo'], 42)
form.set_default("foo", 42)
self.assertEqual(form.defaults["foo"], 42)
def test_get_schema(self):
model = self.app.model
@ -262,10 +271,10 @@ class TestForm(TestCase):
self.assertIs(form.get_schema(), schema)
# schema is auto-generated if fields provided
form = self.make_form(fields=['foo', 'bar'])
form = self.make_form(fields=["foo", "bar"])
schema = form.get_schema()
self.assertEqual(len(schema.children), 2)
self.assertEqual(schema['foo'].name, 'foo')
self.assertEqual(schema["foo"].name, "foo")
# but auto-generating without fields is not supported
form = self.make_form()
@ -276,58 +285,62 @@ class TestForm(TestCase):
form = self.make_form(model_class=model.Setting)
schema = form.get_schema()
self.assertEqual(len(schema.children), 2)
self.assertIn('name', schema)
self.assertIn('value', schema)
self.assertIn("name", schema)
self.assertIn("value", schema)
# but node overrides are honored when auto-generating
form = self.make_form(model_class=model.Setting)
value_node = colander.SchemaNode(colander.Bool(), name='value')
form.set_node('value', value_node)
value_node = colander.SchemaNode(colander.Bool(), name="value")
form.set_node("value", value_node)
schema = form.get_schema()
self.assertIs(schema['value'], value_node)
self.assertIs(schema["value"], value_node)
# schema is auto-generated if model_instance provided
form = self.make_form(model_instance=model.Setting(name='uhoh'))
self.assertEqual(form.fields, ['name', 'value'])
form = self.make_form(model_instance=model.Setting(name="uhoh"))
self.assertEqual(form.fields, ["name", "value"])
self.assertIsNone(form.schema)
# nb. force method to get new fields
del form.fields
schema = form.get_schema()
self.assertEqual(len(schema.children), 2)
self.assertIn('name', schema)
self.assertIn('value', schema)
self.assertIn("name", schema)
self.assertIn("value", schema)
# ColanderAlchemy schema still has *all* requested fields
form = self.make_form(model_instance=model.Setting(name='uhoh'),
fields=['name', 'value', 'foo', 'bar'])
self.assertEqual(form.fields, ['name', 'value', 'foo', 'bar'])
form = self.make_form(
model_instance=model.Setting(name="uhoh"),
fields=["name", "value", "foo", "bar"],
)
self.assertEqual(form.fields, ["name", "value", "foo", "bar"])
self.assertIsNone(form.schema)
schema = form.get_schema()
self.assertEqual(len(schema.children), 4)
self.assertIn('name', schema)
self.assertIn('value', schema)
self.assertIn('foo', schema)
self.assertIn('bar', schema)
self.assertIn("name", schema)
self.assertIn("value", schema)
self.assertIn("foo", schema)
self.assertIn("bar", schema)
# schema nodes are required by default
form = self.make_form(fields=['foo', 'bar'])
form = self.make_form(fields=["foo", "bar"])
schema = form.get_schema()
self.assertIs(schema['foo'].missing, colander.required)
self.assertIs(schema['bar'].missing, colander.required)
self.assertIs(schema["foo"].missing, colander.required)
self.assertIs(schema["bar"].missing, colander.required)
# but fields can be marked *not* required
form = self.make_form(fields=['foo', 'bar'])
form.set_required('bar', False)
form = self.make_form(fields=["foo", "bar"])
form.set_required("bar", False)
schema = form.get_schema()
self.assertIs(schema['foo'].missing, colander.required)
self.assertIs(schema['bar'].missing, colander.null)
self.assertIs(schema["foo"].missing, colander.required)
self.assertIs(schema["bar"].missing, colander.null)
# validator overrides are honored
def validate(node, value): pass
def validate(node, value):
pass
form = self.make_form(model_class=model.Setting)
form.set_validator('name', validate)
form.set_validator("name", validate)
schema = form.get_schema()
self.assertIs(schema['name'].validator, validate)
self.assertIs(schema["name"].validator, validate)
# validator can be set for whole form
form = self.make_form(model_class=model.Setting)
@ -340,9 +353,9 @@ class TestForm(TestCase):
# default value overrides are honored
form = self.make_form(model_class=model.Setting)
form.set_default('name', 'foo')
form.set_default("name", "foo")
schema = form.get_schema()
self.assertEqual(schema['name'].default, 'foo')
self.assertEqual(schema["name"].default, "foo")
def test_get_deform(self):
model = self.app.model
@ -350,139 +363,142 @@ class TestForm(TestCase):
# basic
form = self.make_form(schema=schema)
self.assertFalse(hasattr(form, 'deform_form'))
self.assertFalse(hasattr(form, "deform_form"))
dform = form.get_deform()
self.assertIsInstance(dform, deform.Form)
self.assertIs(form.deform_form, dform)
# with model instance as dict
myobj = {'foo': 'one', 'bar': 'two'}
myobj = {"foo": "one", "bar": "two"}
form = self.make_form(schema=schema, model_instance=myobj)
dform = form.get_deform()
self.assertEqual(dform.cstruct, myobj)
# with sqlalchemy model instance
myobj = model.Setting(name='foo', value='bar')
myobj = model.Setting(name="foo", value="bar")
form = self.make_form(model_instance=myobj)
dform = form.get_deform()
self.assertEqual(dform.cstruct, {'name': 'foo', 'value': 'bar'})
self.assertEqual(dform.cstruct, {"name": "foo", "value": "bar"})
# sqlalchemy instance with null value
myobj = model.Setting(name='foo', value=None)
myobj = model.Setting(name="foo", value=None)
form = self.make_form(model_instance=myobj)
dform = form.get_deform()
self.assertEqual(dform.cstruct, {'name': 'foo', 'value': colander.null})
self.assertEqual(dform.cstruct, {"name": "foo", "value": colander.null})
def test_get_cancel_url(self):
# is referrer by default
form = self.make_form()
self.request.get_referrer = MagicMock(return_value='/cancel-default')
self.assertEqual(form.get_cancel_url(), '/cancel-default')
self.request.get_referrer = MagicMock(return_value="/cancel-default")
self.assertEqual(form.get_cancel_url(), "/cancel-default")
del self.request.get_referrer
# or can be static URL
form = self.make_form(cancel_url='/cancel-static')
self.assertEqual(form.get_cancel_url(), '/cancel-static')
form = self.make_form(cancel_url="/cancel-static")
self.assertEqual(form.get_cancel_url(), "/cancel-static")
# or can be fallback URL (nb. 'NOPE' indicates no referrer)
form = self.make_form(cancel_url_fallback='/cancel-fallback')
self.request.get_referrer = MagicMock(return_value='NOPE')
self.assertEqual(form.get_cancel_url(), '/cancel-fallback')
form = self.make_form(cancel_url_fallback="/cancel-fallback")
self.request.get_referrer = MagicMock(return_value="NOPE")
self.assertEqual(form.get_cancel_url(), "/cancel-fallback")
del self.request.get_referrer
# or can be referrer fallback, i.e. home page
form = self.make_form()
def get_referrer(default=None):
if default == 'NOPE':
return 'NOPE'
return '/home-page'
if default == "NOPE":
return "NOPE"
return "/home-page"
self.request.get_referrer = get_referrer
self.assertEqual(form.get_cancel_url(), '/home-page')
self.assertEqual(form.get_cancel_url(), "/home-page")
del self.request.get_referrer
def test_get_label(self):
form = self.make_form(fields=['foo', 'bar'])
self.assertEqual(form.get_label('foo'), "Foo")
form.set_label('foo', "Baz")
self.assertEqual(form.get_label('foo'), "Baz")
form = self.make_form(fields=["foo", "bar"])
self.assertEqual(form.get_label("foo"), "Foo")
form.set_label("foo", "Baz")
self.assertEqual(form.get_label("foo"), "Baz")
def test_set_label(self):
form = self.make_form(fields=['foo', 'bar'])
self.assertEqual(form.get_label('foo'), "Foo")
form.set_label('foo', "Baz")
self.assertEqual(form.get_label('foo'), "Baz")
form = self.make_form(fields=["foo", "bar"])
self.assertEqual(form.get_label("foo"), "Foo")
form.set_label("foo", "Baz")
self.assertEqual(form.get_label("foo"), "Baz")
# schema should be updated when setting label
schema = self.make_schema()
form = self.make_form(schema=schema)
form.set_label('foo', "Woohoo")
self.assertEqual(form.get_label('foo'), "Woohoo")
self.assertEqual(schema['foo'].title, "Woohoo")
form.set_label("foo", "Woohoo")
self.assertEqual(form.get_label("foo"), "Woohoo")
self.assertEqual(schema["foo"].title, "Woohoo")
def test_readonly_fields(self):
form = self.make_form(fields=['foo', 'bar'])
form = self.make_form(fields=["foo", "bar"])
self.assertEqual(form.readonly_fields, set())
self.assertFalse(form.is_readonly('foo'))
self.assertFalse(form.is_readonly("foo"))
form.set_readonly('foo')
self.assertEqual(form.readonly_fields, {'foo'})
self.assertTrue(form.is_readonly('foo'))
self.assertFalse(form.is_readonly('bar'))
form.set_readonly("foo")
self.assertEqual(form.readonly_fields, {"foo"})
self.assertTrue(form.is_readonly("foo"))
self.assertFalse(form.is_readonly("bar"))
form.set_readonly('bar')
self.assertEqual(form.readonly_fields, {'foo', 'bar'})
self.assertTrue(form.is_readonly('foo'))
self.assertTrue(form.is_readonly('bar'))
form.set_readonly("bar")
self.assertEqual(form.readonly_fields, {"foo", "bar"})
self.assertTrue(form.is_readonly("foo"))
self.assertTrue(form.is_readonly("bar"))
form.set_readonly('foo', False)
self.assertEqual(form.readonly_fields, {'bar'})
self.assertFalse(form.is_readonly('foo'))
self.assertTrue(form.is_readonly('bar'))
form.set_readonly("foo", False)
self.assertEqual(form.readonly_fields, {"bar"})
self.assertFalse(form.is_readonly("foo"))
self.assertTrue(form.is_readonly("bar"))
def test_required_fields(self):
form = self.make_form(fields=['foo', 'bar'])
form = self.make_form(fields=["foo", "bar"])
self.assertEqual(form.required_fields, {})
self.assertIsNone(form.is_required('foo'))
self.assertIsNone(form.is_required("foo"))
form.set_required('foo')
self.assertEqual(form.required_fields, {'foo': True})
self.assertTrue(form.is_required('foo'))
self.assertIsNone(form.is_required('bar'))
form.set_required("foo")
self.assertEqual(form.required_fields, {"foo": True})
self.assertTrue(form.is_required("foo"))
self.assertIsNone(form.is_required("bar"))
form.set_required('bar')
self.assertEqual(form.required_fields, {'foo': True, 'bar': True})
self.assertTrue(form.is_required('foo'))
self.assertTrue(form.is_required('bar'))
form.set_required("bar")
self.assertEqual(form.required_fields, {"foo": True, "bar": True})
self.assertTrue(form.is_required("foo"))
self.assertTrue(form.is_required("bar"))
form.set_required('foo', False)
self.assertEqual(form.required_fields, {'foo': False, 'bar': True})
self.assertFalse(form.is_required('foo'))
self.assertTrue(form.is_required('bar'))
form.set_required("foo", False)
self.assertEqual(form.required_fields, {"foo": False, "bar": True})
self.assertFalse(form.is_required("foo"))
self.assertTrue(form.is_required("bar"))
def test_render_vue_tag(self):
schema = self.make_schema()
form = self.make_form(schema=schema)
html = form.render_vue_tag()
self.assertEqual(html, '<wutta-form></wutta-form>')
self.assertEqual(html, "<wutta-form></wutta-form>")
def test_render_vue_template(self):
self.pyramid_config.include('pyramid_mako')
self.pyramid_config.add_subscriber('wuttaweb.subscribers.before_render',
'pyramid.events.BeforeRender')
self.pyramid_config.include("pyramid_mako")
self.pyramid_config.add_subscriber(
"wuttaweb.subscribers.before_render", "pyramid.events.BeforeRender"
)
# form button is disabled on @submit by default
schema = self.make_schema()
form = self.make_form(schema=schema, cancel_url='/')
form = self.make_form(schema=schema, cancel_url="/")
html = form.render_vue_template()
self.assertIn('<script type="text/x-template" id="wutta-form-template">', html)
self.assertIn('@submit', html)
self.assertIn("@submit", html)
# but not if form is configured otherwise
form = self.make_form(schema=schema, auto_disable_submit=False, cancel_url='/')
form = self.make_form(schema=schema, auto_disable_submit=False, cancel_url="/")
html = form.render_vue_template()
self.assertIn('<script type="text/x-template" id="wutta-form-template">', html)
self.assertNotIn('@submit', html)
self.assertNotIn("@submit", html)
def test_add_grid_vue_context(self):
form = self.make_form()
@ -492,76 +508,84 @@ class TestForm(TestCase):
self.assertRaises(ValueError, form.add_grid_vue_context, grid)
# otherwise it works
grid = Grid(self.request, key='foo')
grid = Grid(self.request, key="foo")
self.assertEqual(len(form.grid_vue_context), 0)
form.add_grid_vue_context(grid)
self.assertEqual(len(form.grid_vue_context), 1)
self.assertIn('foo', form.grid_vue_context)
self.assertEqual(form.grid_vue_context['foo'], {
'data': [],
'row_classes': {},
})
self.assertIn("foo", form.grid_vue_context)
self.assertEqual(
form.grid_vue_context["foo"],
{
"data": [],
"row_classes": {},
},
)
# calling again with same key will replace data
records = [{'foo': 1}, {'foo': 2}]
grid = Grid(self.request, key='foo', columns=['foo'], data=records)
records = [{"foo": 1}, {"foo": 2}]
grid = Grid(self.request, key="foo", columns=["foo"], data=records)
form.add_grid_vue_context(grid)
self.assertEqual(len(form.grid_vue_context), 1)
self.assertIn('foo', form.grid_vue_context)
self.assertEqual(form.grid_vue_context['foo'], {
'data': records,
'row_classes': {},
})
self.assertIn("foo", form.grid_vue_context)
self.assertEqual(
form.grid_vue_context["foo"],
{
"data": records,
"row_classes": {},
},
)
def test_render_vue_finalize(self):
form = self.make_form()
html = form.render_vue_finalize()
self.assertIn('<script>', html)
self.assertIn("<script>", html)
self.assertIn("Vue.component('wutta-form', WuttaForm)", html)
def test_render_vue_field(self):
self.pyramid_config.include('pyramid_deform')
self.pyramid_config.include("pyramid_deform")
schema = self.make_schema()
form = self.make_form(schema=schema)
dform = form.get_deform()
# typical
html = form.render_vue_field('foo')
html = form.render_vue_field("foo")
self.assertIn('<b-field :horizontal="true" label="Foo">', html)
self.assertIn('<b-input name="foo"', html)
# nb. no error message
self.assertNotIn('message', html)
self.assertNotIn("message", html)
# readonly
html = form.render_vue_field('foo', readonly=True)
html = form.render_vue_field("foo", readonly=True)
self.assertIn('<b-field :horizontal="true" label="Foo">', html)
self.assertNotIn('<b-input name="foo"', html)
# nb. no error message
self.assertNotIn('message', html)
self.assertNotIn("message", html)
# with error message
with patch.object(form, 'get_field_errors', return_value=['something is wrong']):
html = form.render_vue_field('foo')
self.assertIn('something is wrong', html)
with patch.object(
form, "get_field_errors", return_value=["something is wrong"]
):
html = form.render_vue_field("foo")
self.assertIn("something is wrong", html)
# add another field, but not to deform, so it should still
# display but with no widget
form.fields.append('zanzibar')
html = form.render_vue_field('zanzibar')
form.fields.append("zanzibar")
html = form.render_vue_field("zanzibar")
self.assertIn('<b-field :horizontal="true" label="Zanzibar">', html)
self.assertNotIn('<b-input', html)
self.assertNotIn("<b-input", html)
# nb. no error message
self.assertNotIn('message', html)
self.assertNotIn("message", html)
# try that once more but with a model record instance
with patch.object(form, 'model_instance', new={'zanzibar': 'omgwtfbbq'}):
html = form.render_vue_field('zanzibar')
self.assertIn('<b-field', html)
with patch.object(form, "model_instance", new={"zanzibar": "omgwtfbbq"}):
html = form.render_vue_field("zanzibar")
self.assertIn("<b-field", html)
self.assertIn('label="Zanzibar"', html)
self.assertNotIn('<b-input', html)
self.assertIn('>omgwtfbbq<', html)
self.assertNotIn("<b-input", html)
self.assertIn(">omgwtfbbq<", html)
# nb. no error message
self.assertNotIn('message', html)
self.assertNotIn("message", html)
def test_get_vue_field_value(self):
schema = self.make_schema()
@ -569,20 +593,20 @@ class TestForm(TestCase):
# TODO: yikes what a hack (?)
dform = form.get_deform()
dform.set_appstruct({'foo': 'one', 'bar': 'two'})
dform.set_appstruct({"foo": "one", "bar": "two"})
# null for missing field
value = form.get_vue_field_value('doesnotexist')
value = form.get_vue_field_value("doesnotexist")
self.assertIsNone(value)
# normal value is returned
value = form.get_vue_field_value('foo')
self.assertEqual(value, 'one')
value = form.get_vue_field_value("foo")
self.assertEqual(value, "one")
# but not if we remove field from deform
# TODO: what is the use case here again?
dform.children.remove(dform['foo'])
value = form.get_vue_field_value('foo')
dform.children.remove(dform["foo"])
value = form.get_vue_field_value("foo")
self.assertIsNone(value)
def test_get_vue_model_data(self):
@ -594,19 +618,22 @@ class TestForm(TestCase):
self.assertEqual(len(data), 2)
# still just 2 fields even if we request more
form.set_fields(['foo', 'bar', 'baz'])
form.set_fields(["foo", "bar", "baz"])
data = form.get_vue_model_data()
self.assertEqual(len(data), 2)
# confirm bool values make it thru as-is
schema.add(colander.SchemaNode(colander.Bool(), name='baz'))
form = self.make_form(schema=schema, model_instance={
'foo': 'one',
'bar': 'two',
'baz': True,
})
schema.add(colander.SchemaNode(colander.Bool(), name="baz"))
form = self.make_form(
schema=schema,
model_instance={
"foo": "one",
"bar": "two",
"baz": True,
},
)
data = form.get_vue_model_data()
self.assertEqual(list(data.values()), ['one', 'two', True])
self.assertEqual(list(data.values()), ["one", "two", True])
def test_has_global_errors(self):
@ -617,8 +644,8 @@ class TestForm(TestCase):
schema.validator = fail
form = self.make_form(schema=schema)
self.assertFalse(form.has_global_errors())
self.request.method = 'POST'
self.request.POST = {'foo': 'one', 'bar': 'two'}
self.request.method = "POST"
self.request.POST = {"foo": "one", "bar": "two"}
self.assertFalse(form.validate())
self.assertTrue(form.has_global_errors())
@ -631,8 +658,8 @@ class TestForm(TestCase):
schema.validator = fail
form = self.make_form(schema=schema)
self.assertEqual(form.get_global_errors(), [])
self.request.method = 'POST'
self.request.POST = {'foo': 'one', 'bar': 'two'}
self.request.method = "POST"
self.request.POST = {"foo": "one", "bar": "two"}
self.assertFalse(form.validate())
self.assertTrue(form.get_global_errors(), ["things are bad!"])
@ -641,55 +668,55 @@ class TestForm(TestCase):
# simple 'Required' validation failure
form = self.make_form(schema=schema)
self.request.method = 'POST'
self.request.POST = {'foo': 'one'}
self.request.method = "POST"
self.request.POST = {"foo": "one"}
self.assertFalse(form.validate())
errors = form.get_field_errors('bar')
self.assertEqual(errors, ['Required'])
errors = form.get_field_errors("bar")
self.assertEqual(errors, ["Required"])
# no errors
form = self.make_form(schema=schema)
self.request.POST = {'foo': 'one', 'bar': 'two'}
self.request.POST = {"foo": "one", "bar": "two"}
self.assertTrue(form.validate())
errors = form.get_field_errors('bar')
errors = form.get_field_errors("bar")
self.assertEqual(errors, [])
def test_validate(self):
schema = self.make_schema()
form = self.make_form(schema=schema)
self.assertFalse(hasattr(form, 'validated'))
self.assertFalse(hasattr(form, "validated"))
# will not validate unless request is POST
self.request.POST = {'foo': 'blarg', 'bar': 'baz'}
self.request.method = 'GET'
self.request.POST = {"foo": "blarg", "bar": "baz"}
self.request.method = "GET"
self.assertFalse(form.validate())
self.request.method = 'POST'
self.request.method = "POST"
data = form.validate()
self.assertEqual(data, {'foo': 'blarg', 'bar': 'baz'})
self.assertEqual(data, {"foo": "blarg", "bar": "baz"})
# validating a second time updates form.validated
self.request.POST = {'foo': 'BLARG', 'bar': 'BAZ'}
self.request.POST = {"foo": "BLARG", "bar": "BAZ"}
data = form.validate()
self.assertEqual(data, {'foo': 'BLARG', 'bar': 'BAZ'})
self.assertEqual(data, {"foo": "BLARG", "bar": "BAZ"})
self.assertIs(form.validated, data)
# bad data does not validate
self.request.POST = {'foo': 42, 'bar': None}
self.request.POST = {"foo": 42, "bar": None}
self.assertFalse(form.validate())
dform = form.get_deform()
self.assertEqual(len(dform.error.children), 2)
self.assertEqual(dform['foo'].errormsg, "Pstruct is not a string")
self.assertEqual(dform["foo"].errormsg, "Pstruct is not a string")
# when a form has readonly fields, validating it will *remove*
# those fields from deform/schema as well as final data dict
schema = self.make_schema()
form = self.make_form(schema=schema)
form.set_readonly('foo')
self.request.POST = {'foo': 'one', 'bar': 'two'}
form.set_readonly("foo")
self.request.POST = {"foo": "one", "bar": "two"}
data = form.validate()
self.assertEqual(data, {'bar': 'two'})
self.assertEqual(data, {"bar": "two"})
dform = form.get_deform()
self.assertNotIn('foo', schema)
self.assertNotIn('foo', dform)
self.assertIn('bar', schema)
self.assertIn('bar', dform)
self.assertNotIn("foo", schema)
self.assertNotIn("foo", dform)
self.assertIn("bar", schema)
self.assertIn("bar", dform)

View file

@ -25,17 +25,17 @@ class TestWuttaDateTime(TestCase):
result = typ.deserialize(node, colander.null)
self.assertIs(result, colander.null)
result = typ.deserialize(node, '2024-12-11T10:33 PM')
result = typ.deserialize(node, "2024-12-11T10:33 PM")
self.assertIsInstance(result, datetime.datetime)
self.assertEqual(result, datetime.datetime(2024, 12, 11, 22, 33))
self.assertIsNone(result.tzinfo)
result = typ.deserialize(node, '2024-12-11T22:33:00')
result = typ.deserialize(node, "2024-12-11T22:33:00")
self.assertIsInstance(result, datetime.datetime)
self.assertEqual(result, datetime.datetime(2024, 12, 11, 22, 33))
self.assertIsNone(result.tzinfo)
self.assertRaises(colander.Invalid, typ.deserialize, node, 'bogus')
self.assertRaises(colander.Invalid, typ.deserialize, node, "bogus")
class TestObjectNode(DataTestCase):
@ -84,20 +84,24 @@ class TestWuttaEnum(WebTestCase):
MOCK_STATUS_ONE = 1
MOCK_STATUS_TWO = 2
MOCK_STATUS = {
MOCK_STATUS_ONE: 'one',
MOCK_STATUS_TWO: 'two',
MOCK_STATUS_ONE: "one",
MOCK_STATUS_TWO: "two",
}
class TestWuttaDictEnum(WebTestCase):
def test_widget_maker(self):
typ = mod.WuttaDictEnum(self.request, MOCK_STATUS)
widget = typ.widget_maker()
self.assertIsInstance(widget, widgets.SelectWidget)
self.assertEqual(widget.values, [
(1, 'one'),
(2, 'two'),
])
self.assertEqual(
widget.values,
[
(1, "one"),
(2, "two"),
],
)
class TestWuttaMoney(WebTestCase):
@ -132,13 +136,13 @@ class TestWuttaQuantity(WebTestCase):
# quantity
result = typ.serialize(node, 42)
self.assertEqual(result, '42')
self.assertEqual(result, "42")
result = typ.serialize(node, 42.00)
self.assertEqual(result, '42')
result = typ.serialize(node, decimal.Decimal('42.00'))
self.assertEqual(result, '42')
self.assertEqual(result, "42")
result = typ.serialize(node, decimal.Decimal("42.00"))
self.assertEqual(result, "42")
result = typ.serialize(node, 42.13)
self.assertEqual(result, '42.13')
self.assertEqual(result, "42.13")
class TestObjectRef(DataTestCase):
@ -155,19 +159,19 @@ class TestObjectRef(DataTestCase):
# passing true yields default empty option
typ = mod.ObjectRef(self.request, empty_option=True)
self.assertEqual(typ.empty_option, ('', "(none)"))
self.assertEqual(typ.empty_option, ("", "(none)"))
# can set explicitly
typ = mod.ObjectRef(self.request, empty_option=('foo', 'bar'))
self.assertEqual(typ.empty_option, ('foo', 'bar'))
typ = mod.ObjectRef(self.request, empty_option=("foo", "bar"))
self.assertEqual(typ.empty_option, ("foo", "bar"))
# can set just a label
typ = mod.ObjectRef(self.request, empty_option="(empty)")
self.assertEqual(typ.empty_option, ('', "(empty)"))
self.assertEqual(typ.empty_option, ("", "(empty)"))
def test_model_class(self):
typ = mod.ObjectRef(self.request)
self.assertRaises(NotImplementedError, getattr, typ, 'model_class')
self.assertRaises(NotImplementedError, getattr, typ, "model_class")
def test_serialize(self):
model = self.app.model
@ -188,9 +192,9 @@ class TestObjectRef(DataTestCase):
self.assertEqual(value, person.uuid.hex)
# null w/ empty option
typ = mod.ObjectRef(self.request, empty_option=('bad', 'BAD'))
typ = mod.ObjectRef(self.request, empty_option=("bad", "BAD"))
value = typ.serialize(node, colander.null)
self.assertEqual(value, 'bad')
self.assertEqual(value, "bad")
def test_deserialize(self):
model = self.app.model
@ -206,8 +210,8 @@ class TestObjectRef(DataTestCase):
self.session.add(person)
self.session.commit()
self.assertIsNotNone(person.uuid)
with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
with patch.object(mod, 'Session', return_value=self.session):
with patch.object(mod.ObjectRef, "model_class", new=model.Person):
with patch.object(mod, "Session", return_value=self.session):
typ = mod.ObjectRef(self.request)
value = typ.deserialize(node, person.uuid)
self.assertIs(value, person)
@ -234,14 +238,14 @@ class TestObjectRef(DataTestCase):
value = typ.objectify(None)
self.assertIsNone(value)
with patch.object(mod, 'Session', return_value=self.session):
with patch.object(mod, "Session", return_value=self.session):
# model instance
person = model.Person(full_name="Betty Boop")
self.session.add(person)
self.session.commit()
self.assertIsNotNone(person.uuid)
with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
with patch.object(mod.ObjectRef, "model_class", new=model.Person):
# can specify as uuid
typ = mod.ObjectRef(self.request)
@ -254,22 +258,22 @@ class TestObjectRef(DataTestCase):
self.assertIs(value, person)
# error if not found
with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
with patch.object(mod.ObjectRef, "model_class", new=model.Person):
typ = mod.ObjectRef(self.request)
self.assertRaises(ValueError, typ.objectify, 'WRONG-UUID')
self.assertRaises(ValueError, typ.objectify, "WRONG-UUID")
def test_get_query(self):
model = self.app.model
with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
with patch.object(mod, 'Session', return_value=self.session):
with patch.object(mod.ObjectRef, "model_class", new=model.Person):
with patch.object(mod, "Session", return_value=self.session):
typ = mod.ObjectRef(self.request)
query = typ.get_query()
self.assertIsInstance(query, orm.Query)
def test_sort_query(self):
model = self.app.model
with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
with patch.object(mod, 'Session', return_value=self.session):
with patch.object(mod.ObjectRef, "model_class", new=model.Person):
with patch.object(mod, "Session", return_value=self.session):
typ = mod.ObjectRef(self.request)
query = typ.get_query()
sorted_query = typ.sort_query(query)
@ -282,16 +286,16 @@ class TestObjectRef(DataTestCase):
self.session.commit()
# basic
with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
with patch.object(mod, 'Session', return_value=self.session):
with patch.object(mod.ObjectRef, "model_class", new=model.Person):
with patch.object(mod, "Session", return_value=self.session):
typ = mod.ObjectRef(self.request)
widget = typ.widget_maker()
self.assertEqual(len(widget.values), 1)
self.assertEqual(widget.values[0][1], "Betty Boop")
# empty option
with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
with patch.object(mod, 'Session', return_value=self.session):
with patch.object(mod.ObjectRef, "model_class", new=model.Person):
with patch.object(mod, "Session", return_value=self.session):
typ = mod.ObjectRef(self.request, empty_option=True)
widget = typ.widget_maker()
self.assertEqual(len(widget.values), 2)
@ -302,7 +306,7 @@ class TestObjectRef(DataTestCase):
class TestPersonRef(WebTestCase):
def test_sort_query(self):
with patch.object(mod, 'Session', return_value=self.session):
with patch.object(mod, "Session", return_value=self.session):
typ = mod.PersonRef(self.request)
query = typ.get_query()
self.assertIsInstance(query, orm.Query)
@ -311,9 +315,9 @@ class TestPersonRef(WebTestCase):
self.assertIsNot(sorted_query, query)
def test_get_object_url(self):
self.pyramid_config.add_route('people.view', '/people/{uuid}')
self.pyramid_config.add_route("people.view", "/people/{uuid}")
model = self.app.model
with patch.object(mod, 'Session', return_value=self.session):
with patch.object(mod, "Session", return_value=self.session):
typ = mod.PersonRef(self.request)
person = model.Person(full_name="Barney Rubble")
@ -322,13 +326,13 @@ class TestPersonRef(WebTestCase):
url = typ.get_object_url(person)
self.assertIsNotNone(url)
self.assertIn(f'/people/{person.uuid}', url)
self.assertIn(f"/people/{person.uuid}", url)
class TestRoleRef(WebTestCase):
def test_sort_query(self):
with patch.object(mod, 'Session', return_value=self.session):
with patch.object(mod, "Session", return_value=self.session):
typ = mod.RoleRef(self.request)
query = typ.get_query()
self.assertIsInstance(query, orm.Query)
@ -337,24 +341,24 @@ class TestRoleRef(WebTestCase):
self.assertIsNot(sorted_query, query)
def test_get_object_url(self):
self.pyramid_config.add_route('roles.view', '/roles/{uuid}')
self.pyramid_config.add_route("roles.view", "/roles/{uuid}")
model = self.app.model
with patch.object(mod, 'Session', return_value=self.session):
with patch.object(mod, "Session", return_value=self.session):
typ = mod.RoleRef(self.request)
role = model.Role(name='Manager')
role = model.Role(name="Manager")
self.session.add(role)
self.session.commit()
url = typ.get_object_url(role)
self.assertIsNotNone(url)
self.assertIn(f'/roles/{role.uuid}', url)
self.assertIn(f"/roles/{role.uuid}", url)
class TestUserRef(WebTestCase):
def test_sort_query(self):
with patch.object(mod, 'Session', return_value=self.session):
with patch.object(mod, "Session", return_value=self.session):
typ = mod.UserRef(self.request)
query = typ.get_query()
self.assertIsInstance(query, orm.Query)
@ -363,18 +367,18 @@ class TestUserRef(WebTestCase):
self.assertIsNot(sorted_query, query)
def test_get_object_url(self):
self.pyramid_config.add_route('users.view', '/users/{uuid}')
self.pyramid_config.add_route("users.view", "/users/{uuid}")
model = self.app.model
with patch.object(mod, 'Session', return_value=self.session):
with patch.object(mod, "Session", return_value=self.session):
typ = mod.UserRef(self.request)
user = model.User(username='barney')
user = model.User(username="barney")
self.session.add(user)
self.session.commit()
url = typ.get_object_url(user)
self.assertIsNotNone(url)
self.assertIn(f'/users/{user.uuid}', url)
self.assertIn(f"/users/{user.uuid}", url)
class TestRoleRefs(DataTestCase):
@ -393,7 +397,7 @@ class TestRoleRefs(DataTestCase):
self.session.add(blokes)
self.session.commit()
with patch.object(mod, 'Session', return_value=self.session):
with patch.object(mod, "Session", return_value=self.session):
# with root access, default values include: admin, blokes
self.request.is_root = True
@ -427,11 +431,11 @@ class TestPermissions(DataTestCase):
# supported permissions are morphed to values
permissions = {
'widgets': {
'label': "Widgets",
'perms': {
'widgets.polish': {
'label': "Polish the widgets",
"widgets": {
"label": "Widgets",
"perms": {
"widgets.polish": {
"label": "Polish the widgets",
},
},
},
@ -439,7 +443,7 @@ class TestPermissions(DataTestCase):
typ = mod.Permissions(self.request, permissions)
widget = typ.widget_maker()
self.assertEqual(len(widget.values), 1)
self.assertEqual(widget.values[0], ('widgets.polish', "Polish the widgets"))
self.assertEqual(widget.values[0], ("widgets.polish", "Polish the widgets"))
class TestFileDownload(DataTestCase):
@ -451,10 +455,10 @@ class TestFileDownload(DataTestCase):
def test_widget_maker(self):
# sanity / coverage check
typ = mod.FileDownload(self.request, url='/foo')
typ = mod.FileDownload(self.request, url="/foo")
widget = typ.widget_maker()
self.assertIsInstance(widget, widgets.FileDownloadWidget)
self.assertEqual(widget.url, '/foo')
self.assertEqual(widget.url, "/foo")
class TestEmailRecipients(TestCase):
@ -464,14 +468,14 @@ class TestEmailRecipients(TestCase):
node = colander.SchemaNode(typ)
recips = [
'alice@example.com',
'bob@example.com',
"alice@example.com",
"bob@example.com",
]
recips_str = ', '.join(recips)
recips_str = ", ".join(recips)
# values
result = typ.serialize(node, recips_str)
self.assertEqual(result, '\n'.join(recips))
self.assertEqual(result, "\n".join(recips))
# null
result = typ.serialize(node, colander.null)
@ -482,10 +486,10 @@ class TestEmailRecipients(TestCase):
node = colander.SchemaNode(typ)
recips = [
'alice@example.com',
'bob@example.com',
"alice@example.com",
"bob@example.com",
]
recips_str = ', '.join(recips)
recips_str = ", ".join(recips)
# values
result = typ.deserialize(node, recips_str)

View file

@ -11,8 +11,14 @@ from pyramid import testing
from wuttaweb import grids
from wuttaweb.forms import widgets as mod
from wuttaweb.forms import schema
from wuttaweb.forms.schema import (FileDownload, PersonRef, RoleRefs, Permissions,
WuttaDateTime, EmailRecipients)
from wuttaweb.forms.schema import (
FileDownload,
PersonRef,
RoleRefs,
Permissions,
WuttaDateTime,
EmailRecipients,
)
from wuttaweb.testing import WebTestCase
@ -21,7 +27,7 @@ class TestObjectRefWidget(WebTestCase):
def make_field(self, node, **kwargs):
# TODO: not sure why default renderer is in use even though
# pyramid_deform was included in setup? but this works..
kwargs.setdefault('renderer', deform.Form.default_renderer)
kwargs.setdefault("renderer", deform.Form.default_renderer)
return deform.Field(node, **kwargs)
def make_widget(self, **kwargs):
@ -33,14 +39,14 @@ class TestObjectRefWidget(WebTestCase):
self.session.add(person)
self.session.commit()
with patch.object(schema, 'Session', return_value=self.session):
with patch.object(schema, "Session", return_value=self.session):
# standard (editable)
node = colander.SchemaNode(PersonRef(self.request))
widget = self.make_widget()
field = self.make_field(node)
html = widget.serialize(field, person.uuid)
self.assertIn('<b-select ', html)
self.assertIn("<b-select ", html)
# readonly
node = colander.SchemaNode(PersonRef(self.request))
@ -48,17 +54,17 @@ class TestObjectRefWidget(WebTestCase):
widget = self.make_widget()
field = self.make_field(node)
html = widget.serialize(field, person.uuid, readonly=True)
self.assertIn('Betty Boop', html)
self.assertNotIn('<a', html)
self.assertIn("Betty Boop", html)
self.assertNotIn("<a", html)
# with hyperlink
node = colander.SchemaNode(PersonRef(self.request))
node.model_instance = person
widget = self.make_widget(url=lambda p: '/foo')
widget = self.make_widget(url=lambda p: "/foo")
field = self.make_field(node)
html = widget.serialize(field, person.uuid, readonly=True)
self.assertIn('Betty Boop', html)
self.assertIn('<a', html)
self.assertIn("Betty Boop", html)
self.assertIn("<a", html)
self.assertIn('href="/foo"', html)
def test_get_template_values(self):
@ -67,24 +73,25 @@ class TestObjectRefWidget(WebTestCase):
self.session.add(person)
self.session.commit()
with patch.object(schema, 'Session', return_value=self.session):
with patch.object(schema, "Session", return_value=self.session):
# standard
node = colander.SchemaNode(PersonRef(self.request))
widget = self.make_widget()
field = self.make_field(node)
values = widget.get_template_values(field, person.uuid, {})
self.assertIn('cstruct', values)
self.assertNotIn('url', values)
self.assertIn("cstruct", values)
self.assertNotIn("url", values)
# readonly w/ empty option
node = colander.SchemaNode(PersonRef(self.request,
empty_option=('_empty_', '(empty)')))
widget = self.make_widget(readonly=True, url=lambda obj: '/foo')
node = colander.SchemaNode(
PersonRef(self.request, empty_option=("_empty_", "(empty)"))
)
widget = self.make_widget(readonly=True, url=lambda obj: "/foo")
field = self.make_field(node)
values = widget.get_template_values(field, '_empty_', {})
self.assertIn('cstruct', values)
self.assertNotIn('url', values)
values = widget.get_template_values(field, "_empty_", {})
self.assertIn("cstruct", values)
self.assertNotIn("url", values)
class TestWuttaDateWidget(WebTestCase):
@ -92,7 +99,7 @@ class TestWuttaDateWidget(WebTestCase):
def make_field(self, node, **kwargs):
# TODO: not sure why default renderer is in use even though
# pyramid_deform was included in setup? but this works..
kwargs.setdefault('renderer', deform.Form.default_renderer)
kwargs.setdefault("renderer", deform.Form.default_renderer)
return deform.Field(node, **kwargs)
def make_widget(self, **kwargs):
@ -108,11 +115,11 @@ class TestWuttaDateWidget(WebTestCase):
# editable widget has normal picker html
result = widget.serialize(field, str(dt))
self.assertIn('<wutta-datepicker', result)
self.assertIn("<wutta-datepicker", result)
# readonly is rendered per app convention
result = widget.serialize(field, str(dt), readonly=True)
self.assertEqual(result, '2025-01-15')
self.assertEqual(result, "2025-01-15")
# now try again with datetime
widget = self.make_widget()
@ -120,11 +127,11 @@ class TestWuttaDateWidget(WebTestCase):
# editable widget has normal picker html
result = widget.serialize(field, str(dt))
self.assertIn('<wutta-datepicker', result)
self.assertIn("<wutta-datepicker", result)
# readonly is rendered per app convention
result = widget.serialize(field, str(dt), readonly=True)
self.assertEqual(result, '2025-01-15')
self.assertEqual(result, "2025-01-15")
class TestWuttaDateTimeWidget(WebTestCase):
@ -132,7 +139,7 @@ class TestWuttaDateTimeWidget(WebTestCase):
def make_field(self, node, **kwargs):
# TODO: not sure why default renderer is in use even though
# pyramid_deform was included in setup? but this works..
kwargs.setdefault('renderer', deform.Form.default_renderer)
kwargs.setdefault("renderer", deform.Form.default_renderer)
return deform.Field(node, **kwargs)
def make_widget(self, **kwargs):
@ -146,11 +153,11 @@ class TestWuttaDateTimeWidget(WebTestCase):
# editable widget has normal picker html
result = widget.serialize(field, str(dt))
self.assertIn('<wutta-datepicker', result)
self.assertIn("<wutta-datepicker", result)
# readonly is rendered per app convention
result = widget.serialize(field, str(dt), readonly=True)
self.assertEqual(result, '2024-12-12 13:49+0000')
self.assertEqual(result, "2024-12-12 13:49+0000")
class TestWuttaMoneyInputWidget(WebTestCase):
@ -158,7 +165,7 @@ class TestWuttaMoneyInputWidget(WebTestCase):
def make_field(self, node, **kwargs):
# TODO: not sure why default renderer is in use even though
# pyramid_deform was included in setup? but this works..
kwargs.setdefault('renderer', deform.Form.default_renderer)
kwargs.setdefault("renderer", deform.Form.default_renderer)
return deform.Field(node, **kwargs)
def make_widget(self, **kwargs):
@ -168,19 +175,19 @@ class TestWuttaMoneyInputWidget(WebTestCase):
node = colander.SchemaNode(schema.WuttaMoney(self.request))
field = self.make_field(node)
widget = self.make_widget()
amount = decimal.Decimal('12.34')
amount = decimal.Decimal("12.34")
# editable widget has normal text input
result = widget.serialize(field, str(amount))
self.assertIn('<b-input', result)
self.assertIn("<b-input", result)
# readonly is rendered per app convention
result = widget.serialize(field, str(amount), readonly=True)
self.assertEqual(result, '<span>$12.34</span>')
self.assertEqual(result, "<span>$12.34</span>")
# readonly w/ null value
result = widget.serialize(field, None, readonly=True)
self.assertEqual(result, '<span></span>')
self.assertEqual(result, "<span></span>")
class TestFileDownloadWidget(WebTestCase):
@ -188,7 +195,7 @@ class TestFileDownloadWidget(WebTestCase):
def make_field(self, node, **kwargs):
# TODO: not sure why default renderer is in use even though
# pyramid_deform was included in setup? but this works..
kwargs.setdefault('renderer', deform.Form.default_renderer)
kwargs.setdefault("renderer", deform.Form.default_renderer)
return deform.Field(node, **kwargs)
def test_serialize(self):
@ -201,31 +208,31 @@ class TestFileDownloadWidget(WebTestCase):
# null value
html = widget.serialize(field, None, readonly=True)
self.assertNotIn('<a ', html)
self.assertIn('<span>', html)
self.assertNotIn("<a ", html)
self.assertIn("<span>", html)
# path to nonexistent file
html = widget.serialize(field, '/this/path/does/not/exist', readonly=True)
self.assertNotIn('<a ', html)
self.assertIn('<span>', html)
html = widget.serialize(field, "/this/path/does/not/exist", readonly=True)
self.assertNotIn("<a ", html)
self.assertIn("<span>", html)
# path to actual file
datfile = self.write_file('data.txt', "hello\n" * 1000)
datfile = self.write_file("data.txt", "hello\n" * 1000)
html = widget.serialize(field, datfile, readonly=True)
self.assertNotIn('<a ', html)
self.assertIn('<span>', html)
self.assertIn('data.txt', html)
self.assertIn('kB)', html)
self.assertNotIn("<a ", html)
self.assertIn("<span>", html)
self.assertIn("data.txt", html)
self.assertIn("kB)", html)
# path to file, w/ url
node = colander.SchemaNode(FileDownload(self.request, url='/download/blarg'))
node = colander.SchemaNode(FileDownload(self.request, url="/download/blarg"))
field = self.make_field(node)
widget = field.widget
html = widget.serialize(field, datfile, readonly=True)
self.assertNotIn('<span>', html)
self.assertNotIn("<span>", html)
self.assertIn('<a href="/download/blarg">', html)
self.assertIn('data.txt', html)
self.assertIn('kB)', html)
self.assertIn("data.txt", html)
self.assertIn("kB)", html)
# nb. same readonly output even if we ask for editable
html2 = widget.serialize(field, datfile, readonly=False)
@ -237,13 +244,15 @@ class TestGridWidget(WebTestCase):
def make_field(self, node, **kwargs):
# TODO: not sure why default renderer is in use even though
# pyramid_deform was included in setup? but this works..
kwargs.setdefault('renderer', deform.Form.default_renderer)
kwargs.setdefault("renderer", deform.Form.default_renderer)
return deform.Field(node, **kwargs)
def test_serialize(self):
grid = grids.Grid(self.request,
columns=['foo', 'bar'],
data=[{'foo': 1, 'bar': 2}, {'foo': 3, 'bar': 4}])
grid = grids.Grid(
self.request,
columns=["foo", "bar"],
data=[{"foo": 1, "bar": 2}, {"foo": 3, "bar": 4}],
)
node = colander.SchemaNode(colander.String())
widget = mod.GridWidget(self.request, grid)
@ -251,7 +260,7 @@ class TestGridWidget(WebTestCase):
# readonly works okay
html = widget.serialize(field, None, readonly=True)
self.assertIn('<b-table ', html)
self.assertIn("<b-table ", html)
# but otherwise, error
self.assertRaises(NotImplementedError, widget.serialize, field, None)
@ -262,11 +271,11 @@ class TestRoleRefsWidget(WebTestCase):
def make_field(self, node, **kwargs):
# TODO: not sure why default renderer is in use even though
# pyramid_deform was included in setup? but this works..
kwargs.setdefault('renderer', deform.Form.default_renderer)
kwargs.setdefault("renderer", deform.Form.default_renderer)
return deform.Field(node, **kwargs)
def test_serialize(self):
self.pyramid_config.add_route('roles.view', '/roles/{uuid}')
self.pyramid_config.add_route("roles.view", "/roles/{uuid}")
model = self.app.model
auth = self.app.get_auth_handler()
admin = auth.get_role_administrator(self.session)
@ -275,7 +284,7 @@ class TestRoleRefsWidget(WebTestCase):
self.session.commit()
# nb. we let the field construct the widget via our type
with patch.object(schema, 'Session', return_value=self.session):
with patch.object(schema, "Session", return_value=self.session):
node = colander.SchemaNode(RoleRefs(self.request))
field = self.make_field(node)
widget = field.widget
@ -305,16 +314,16 @@ class TestPermissionsWidget(WebTestCase):
def make_field(self, node, **kwargs):
# TODO: not sure why default renderer is in use even though
# pyramid_deform was included in setup? but this works..
kwargs.setdefault('renderer', deform.Form.default_renderer)
kwargs.setdefault("renderer", deform.Form.default_renderer)
return deform.Field(node, **kwargs)
def test_serialize(self):
permissions = {
'widgets': {
'label': "Widgets",
'perms': {
'widgets.polish': {
'label': "Polish the widgets",
"widgets": {
"label": "Widgets",
"perms": {
"widgets.polish": {
"label": "Polish the widgets",
},
},
},
@ -330,7 +339,7 @@ class TestPermissionsWidget(WebTestCase):
self.assertNotIn("Polish the widgets", html)
# readonly output includes the perm if set
html = widget.serialize(field, {'widgets.polish'}, readonly=True)
html = widget.serialize(field, {"widgets.polish"}, readonly=True)
self.assertIn("Polish the widgets", html)
# editable output always includes the perm
@ -343,7 +352,7 @@ class TestEmailRecipientsWidget(WebTestCase):
def make_field(self, node, **kwargs):
# TODO: not sure why default renderer is in use even though
# pyramid_deform was included in setup? but this works..
kwargs.setdefault('renderer', deform.Form.default_renderer)
kwargs.setdefault("renderer", deform.Form.default_renderer)
return deform.Field(node, **kwargs)
def test_serialize(self):
@ -352,19 +361,19 @@ class TestEmailRecipientsWidget(WebTestCase):
widget = mod.EmailRecipientsWidget()
recips = [
'alice@example.com',
'bob@example.com',
"alice@example.com",
"bob@example.com",
]
recips_str = ', '.join(recips)
recips_str = ", ".join(recips)
# readonly
result = widget.serialize(field, recips_str, readonly=True)
self.assertIn('<ul>', result)
self.assertIn('<li>alice@example.com</li>', result)
self.assertIn("<ul>", result)
self.assertIn("<li>alice@example.com</li>", result)
# editable
result = widget.serialize(field, recips_str)
self.assertIn('<b-input', result)
self.assertIn("<b-input", result)
self.assertIn('type="textarea"', result)
def test_deserialize(self):
@ -373,10 +382,10 @@ class TestEmailRecipientsWidget(WebTestCase):
widget = mod.EmailRecipientsWidget()
recips = [
'alice@example.com',
'bob@example.com',
"alice@example.com",
"bob@example.com",
]
recips_str = ', '.join(recips)
recips_str = ", ".join(recips)
# values
result = widget.deserialize(field, recips_str)
@ -392,7 +401,7 @@ class TestBatchIdWidget(WebTestCase):
def make_field(self, node, **kwargs):
# TODO: not sure why default renderer is in use even though
# pyramid_deform was included in setup? but this works..
kwargs.setdefault('renderer', deform.Form.default_renderer)
kwargs.setdefault("renderer", deform.Form.default_renderer)
return deform.Field(node, **kwargs)
def test_serialize(self):
@ -404,4 +413,4 @@ class TestBatchIdWidget(WebTestCase):
self.assertIs(result, colander.null)
result = widget.serialize(field, 42)
self.assertEqual(result, '00000042')
self.assertEqual(result, "00000042")

File diff suppressed because it is too large Load diff

View file

@ -21,24 +21,24 @@ class TestGridFilter(WebTestCase):
model = self.app.model
self.sample_data = [
{'name': 'foo1', 'value': 'ONE'},
{'name': 'foo2', 'value': 'two'},
{'name': 'foo3', 'value': 'ggg'},
{'name': 'foo4', 'value': 'ggg'},
{'name': 'foo5', 'value': 'ggg'},
{'name': 'foo6', 'value': 'six'},
{'name': 'foo7', 'value': 'seven'},
{'name': 'foo8', 'value': 'eight'},
{'name': 'foo9', 'value': 'nine'},
{"name": "foo1", "value": "ONE"},
{"name": "foo2", "value": "two"},
{"name": "foo3", "value": "ggg"},
{"name": "foo4", "value": "ggg"},
{"name": "foo5", "value": "ggg"},
{"name": "foo6", "value": "six"},
{"name": "foo7", "value": "seven"},
{"name": "foo8", "value": "eight"},
{"name": "foo9", "value": "nine"},
]
for setting in self.sample_data:
self.app.save_setting(self.session, setting['name'], setting['value'])
self.app.save_setting(self.session, setting["name"], setting["value"])
self.session.commit()
self.sample_query = self.session.query(model.Setting)
def make_filter(self, model_property, **kwargs):
factory = kwargs.pop('factory', mod.GridFilter)
kwargs['model_property'] = model_property
factory = kwargs.pop("factory", mod.GridFilter)
kwargs["model_property"] = model_property
return factory(self.request, model_property.key, **kwargs)
def test_constructor(self):
@ -46,158 +46,193 @@ class TestGridFilter(WebTestCase):
# verbs is not set by default, but can be set
filtr = self.make_filter(model.Setting.name)
self.assertFalse(hasattr(filtr, 'verbs'))
filtr = self.make_filter(model.Setting.name, verbs=['foo', 'bar'])
self.assertEqual(filtr.verbs, ['foo', 'bar'])
self.assertFalse(hasattr(filtr, "verbs"))
filtr = self.make_filter(model.Setting.name, verbs=["foo", "bar"])
self.assertEqual(filtr.verbs, ["foo", "bar"])
# verb is not set by default, but can be set
filtr = self.make_filter(model.Setting.name)
self.assertFalse(hasattr(filtr, 'verb'))
filtr = self.make_filter(model.Setting.name, verb='foo')
self.assertEqual(filtr.verb, 'foo')
self.assertFalse(hasattr(filtr, "verb"))
filtr = self.make_filter(model.Setting.name, verb="foo")
self.assertEqual(filtr.verb, "foo")
# default verb is not set by default, but can be set
filtr = self.make_filter(model.Setting.name)
self.assertFalse(hasattr(filtr, 'default_verb'))
filtr = self.make_filter(model.Setting.name, default_verb='foo')
self.assertEqual(filtr.default_verb, 'foo')
self.assertFalse(hasattr(filtr, "default_verb"))
filtr = self.make_filter(model.Setting.name, default_verb="foo")
self.assertEqual(filtr.default_verb, "foo")
def test_repr(self):
model = self.app.model
filtr = self.make_filter(model.Setting.name, factory=mod.GridFilter)
self.assertEqual(repr(filtr), "GridFilter(key='name', active=False, verb=None, value=None)")
self.assertEqual(
repr(filtr), "GridFilter(key='name', active=False, verb=None, value=None)"
)
def test_get_verbs(self):
model = self.app.model
filtr = self.make_filter(model.Setting.name, factory=mod.AlchemyFilter)
self.assertFalse(hasattr(filtr, 'verbs'))
self.assertEqual(filtr.default_verbs, ['equal', 'not_equal'])
self.assertFalse(hasattr(filtr, "verbs"))
self.assertEqual(filtr.default_verbs, ["equal", "not_equal"])
# by default, returns default verbs (plus 'is_any')
self.assertEqual(filtr.get_verbs(), ['equal', 'not_equal', 'is_any'])
self.assertEqual(filtr.get_verbs(), ["equal", "not_equal", "is_any"])
# default verbs can be a callable
filtr.default_verbs = lambda: ['foo', 'bar']
self.assertEqual(filtr.get_verbs(), ['foo', 'bar', 'is_any'])
filtr.default_verbs = lambda: ["foo", "bar"]
self.assertEqual(filtr.get_verbs(), ["foo", "bar", "is_any"])
# uses filtr.verbs if set
filtr.verbs = ['is_true', 'is_false']
self.assertEqual(filtr.get_verbs(), ['is_true', 'is_false', 'is_any'])
filtr.verbs = ["is_true", "is_false"]
self.assertEqual(filtr.get_verbs(), ["is_true", "is_false", "is_any"])
# may add is/null verbs
filtr = self.make_filter(model.Setting.name, factory=mod.AlchemyFilter,
nullable=True)
self.assertEqual(filtr.get_verbs(), ['equal', 'not_equal',
'is_null', 'is_not_null',
'is_any'])
filtr = self.make_filter(
model.Setting.name, factory=mod.AlchemyFilter, nullable=True
)
self.assertEqual(
filtr.get_verbs(),
["equal", "not_equal", "is_null", "is_not_null", "is_any"],
)
# filtr.verbs can be a callable
filtr.nullable = False
filtr.verbs = lambda: ['baz', 'blarg']
self.assertEqual(filtr.get_verbs(), ['baz', 'blarg', 'is_any'])
filtr.verbs = lambda: ["baz", "blarg"]
self.assertEqual(filtr.get_verbs(), ["baz", "blarg", "is_any"])
def test_get_default_verb(self):
model = self.app.model
filtr = self.make_filter(model.Setting.name, factory=mod.AlchemyFilter)
self.assertFalse(hasattr(filtr, 'verbs'))
self.assertEqual(filtr.default_verbs, ['equal', 'not_equal'])
self.assertEqual(filtr.get_verbs(), ['equal', 'not_equal', 'is_any'])
self.assertFalse(hasattr(filtr, "verbs"))
self.assertEqual(filtr.default_verbs, ["equal", "not_equal"])
self.assertEqual(filtr.get_verbs(), ["equal", "not_equal", "is_any"])
# returns first verb by default
self.assertEqual(filtr.get_default_verb(), 'equal')
self.assertEqual(filtr.get_default_verb(), "equal")
# returns filtr.verb if set
filtr.verb = 'foo'
self.assertEqual(filtr.get_default_verb(), 'foo')
filtr.verb = "foo"
self.assertEqual(filtr.get_default_verb(), "foo")
# returns filtr.default_verb if set
# (nb. this overrides filtr.verb since the point of this
# method is to return the *default* verb)
filtr.default_verb = 'bar'
self.assertEqual(filtr.get_default_verb(), 'bar')
filtr.default_verb = "bar"
self.assertEqual(filtr.get_default_verb(), "bar")
def test_get_verb_labels(self):
model = self.app.model
filtr = self.make_filter(model.Setting.name, factory=mod.AlchemyFilter)
self.assertFalse(hasattr(filtr, 'verbs'))
self.assertEqual(filtr.get_verbs(), ['equal', 'not_equal', 'is_any'])
self.assertFalse(hasattr(filtr, "verbs"))
self.assertEqual(filtr.get_verbs(), ["equal", "not_equal", "is_any"])
labels = filtr.get_verb_labels()
self.assertIsInstance(labels, dict)
self.assertEqual(labels['equal'], "equal to")
self.assertEqual(labels['not_equal'], "not equal to")
self.assertEqual(labels['is_any'], "is any")
self.assertEqual(labels["equal"], "equal to")
self.assertEqual(labels["not_equal"], "not equal to")
self.assertEqual(labels["is_any"], "is any")
def test_get_valueless_verbs(self):
model = self.app.model
filtr = self.make_filter(model.Setting.name, factory=mod.AlchemyFilter)
self.assertFalse(hasattr(filtr, 'verbs'))
self.assertEqual(filtr.get_verbs(), ['equal', 'not_equal', 'is_any'])
self.assertFalse(hasattr(filtr, "verbs"))
self.assertEqual(filtr.get_verbs(), ["equal", "not_equal", "is_any"])
verbs = filtr.get_valueless_verbs()
self.assertIsInstance(verbs, list)
self.assertIn('is_any', verbs)
self.assertIn("is_any", verbs)
def test_set_choices(self):
model = self.app.model
filtr = self.make_filter(model.Setting.name, factory=mod.AlchemyFilter)
self.assertEqual(filtr.choices, {})
self.assertEqual(filtr.data_type, 'string')
self.assertEqual(filtr.data_type, "string")
class MockEnum(Enum):
FOO = 'foo'
BAR = 'bar'
FOO = "foo"
BAR = "bar"
filtr.set_choices(MockEnum)
self.assertEqual(filtr.choices, OrderedDict([
('FOO', 'foo'),
('BAR', 'bar'),
]))
self.assertEqual(filtr.data_type, 'choice')
self.assertEqual(
filtr.choices,
OrderedDict(
[
("FOO", "foo"),
("BAR", "bar"),
]
),
)
self.assertEqual(filtr.data_type, "choice")
filtr.set_choices(None)
self.assertEqual(filtr.choices, {})
self.assertEqual(filtr.data_type, 'string')
self.assertEqual(filtr.data_type, "string")
def test_normalize_choices(self):
model = self.app.model
filtr = self.make_filter(model.Setting.name, factory=mod.AlchemyFilter)
class MockEnum(Enum):
FOO = 'foo'
BAR = 'bar'
FOO = "foo"
BAR = "bar"
choices = filtr.normalize_choices(MockEnum)
self.assertEqual(choices, OrderedDict([
('FOO', 'foo'),
('BAR', 'bar'),
]))
self.assertEqual(
choices,
OrderedDict(
[
("FOO", "foo"),
("BAR", "bar"),
]
),
)
choices = filtr.normalize_choices(OrderedDict([
('first', '1'),
('second', '2'),
]))
self.assertEqual(choices, OrderedDict([
('first', '1'),
('second', '2'),
]))
choices = filtr.normalize_choices(
OrderedDict(
[
("first", "1"),
("second", "2"),
]
)
)
self.assertEqual(
choices,
OrderedDict(
[
("first", "1"),
("second", "2"),
]
),
)
choices = filtr.normalize_choices({
'bbb': 'b',
'aaa': 'a',
})
self.assertEqual(choices, OrderedDict([
('aaa', 'a'),
('bbb', 'b'),
]))
choices = filtr.normalize_choices(
{
"bbb": "b",
"aaa": "a",
}
)
self.assertEqual(
choices,
OrderedDict(
[
("aaa", "a"),
("bbb", "b"),
]
),
)
choices = filtr.normalize_choices(['one', 'two', 'three'])
self.assertEqual(choices, OrderedDict([
('one', 'one'),
('two', 'two'),
('three', 'three'),
]))
choices = filtr.normalize_choices(["one", "two", "three"])
self.assertEqual(
choices,
OrderedDict(
[
("one", "one"),
("two", "two"),
("three", "three"),
]
),
)
def test_apply_filter(self):
model = self.app.model
@ -205,32 +240,46 @@ class TestGridFilter(WebTestCase):
# default verb used as fallback
# self.assertEqual(filtr.default_verb, 'contains')
filtr.default_verb = 'contains'
filtr.default_verb = "contains"
filtr.verb = None
with patch.object(filtr, 'filter_contains', side_effect=lambda q, v: q) as filter_contains:
filtered_query = filtr.apply_filter(self.sample_query, value='foo')
filter_contains.assert_called_once_with(self.sample_query, 'foo')
with patch.object(
filtr, "filter_contains", side_effect=lambda q, v: q
) as filter_contains:
filtered_query = filtr.apply_filter(self.sample_query, value="foo")
filter_contains.assert_called_once_with(self.sample_query, "foo")
self.assertIsNone(filtr.verb)
# filter verb used as fallback
filtr.verb = 'equal'
with patch.object(filtr, 'filter_equal', create=True, side_effect=lambda q, v: q) as filter_equal:
filtered_query = filtr.apply_filter(self.sample_query, value='foo')
filter_equal.assert_called_once_with(self.sample_query, 'foo')
filtr.verb = "equal"
with patch.object(
filtr, "filter_equal", create=True, side_effect=lambda q, v: q
) as filter_equal:
filtered_query = filtr.apply_filter(self.sample_query, value="foo")
filter_equal.assert_called_once_with(self.sample_query, "foo")
# filter value used as fallback
filtr.verb = 'contains'
filtr.value = 'blarg'
with patch.object(filtr, 'filter_contains', side_effect=lambda q, v: q) as filter_contains:
filtr.verb = "contains"
filtr.value = "blarg"
with patch.object(
filtr, "filter_contains", side_effect=lambda q, v: q
) as filter_contains:
filtered_query = filtr.apply_filter(self.sample_query)
filter_contains.assert_called_once_with(self.sample_query, 'blarg')
filter_contains.assert_called_once_with(self.sample_query, "blarg")
# error if invalid verb
self.assertRaises(mod.VerbNotSupported, filtr.apply_filter,
self.sample_query, verb='doesnotexist')
filtr.verbs = ['doesnotexist']
self.assertRaises(mod.VerbNotSupported, filtr.apply_filter,
self.sample_query, verb='doesnotexist')
self.assertRaises(
mod.VerbNotSupported,
filtr.apply_filter,
self.sample_query,
verb="doesnotexist",
)
filtr.verbs = ["doesnotexist"]
self.assertRaises(
mod.VerbNotSupported,
filtr.apply_filter,
self.sample_query,
verb="doesnotexist",
)
def test_filter_is_any(self):
model = self.app.model
@ -250,24 +299,24 @@ class TestAlchemyFilter(WebTestCase):
model = self.app.model
self.sample_data = [
{'name': 'foo1', 'value': 'ONE'},
{'name': 'foo2', 'value': 'two'},
{'name': 'foo3', 'value': 'ggg'},
{'name': 'foo4', 'value': 'ggg'},
{'name': 'foo5', 'value': 'ggg'},
{'name': 'foo6', 'value': 'six'},
{'name': 'foo7', 'value': 'seven'},
{'name': 'foo8', 'value': 'eight'},
{'name': 'foo9', 'value': None},
{"name": "foo1", "value": "ONE"},
{"name": "foo2", "value": "two"},
{"name": "foo3", "value": "ggg"},
{"name": "foo4", "value": "ggg"},
{"name": "foo5", "value": "ggg"},
{"name": "foo6", "value": "six"},
{"name": "foo7", "value": "seven"},
{"name": "foo8", "value": "eight"},
{"name": "foo9", "value": None},
]
for setting in self.sample_data:
self.app.save_setting(self.session, setting['name'], setting['value'])
self.app.save_setting(self.session, setting["name"], setting["value"])
self.session.commit()
self.sample_query = self.session.query(model.Setting)
def make_filter(self, model_property, **kwargs):
factory = kwargs.pop('factory', mod.AlchemyFilter)
kwargs['model_property'] = model_property
factory = kwargs.pop("factory", mod.AlchemyFilter)
kwargs["model_property"] = model_property
return factory(self.request, model_property.key, **kwargs)
def test_filter_equal(self):
@ -280,12 +329,12 @@ class TestAlchemyFilter(WebTestCase):
self.assertIs(filtered_query, self.sample_query)
# nb. by default, *is filtered* by empty string
filtered_query = filtr.filter_equal(self.sample_query, '')
filtered_query = filtr.filter_equal(self.sample_query, "")
self.assertIsNot(filtered_query, self.sample_query)
self.assertEqual(filtered_query.count(), 0)
# filtered by value
filtered_query = filtr.filter_equal(self.sample_query, 'ggg')
filtered_query = filtr.filter_equal(self.sample_query, "ggg")
self.assertIsNot(filtered_query, self.sample_query)
self.assertEqual(filtered_query.count(), 3)
@ -299,12 +348,12 @@ class TestAlchemyFilter(WebTestCase):
self.assertIs(filtered_query, self.sample_query)
# nb. by default, *is filtered* by empty string
filtered_query = filtr.filter_not_equal(self.sample_query, '')
filtered_query = filtr.filter_not_equal(self.sample_query, "")
self.assertIsNot(filtered_query, self.sample_query)
self.assertEqual(filtered_query.count(), 9)
# filtered by value
filtered_query = filtr.filter_not_equal(self.sample_query, 'ggg')
filtered_query = filtr.filter_not_equal(self.sample_query, "ggg")
self.assertIsNot(filtered_query, self.sample_query)
self.assertEqual(filtered_query.count(), 6)
@ -336,24 +385,24 @@ class TestStringAlchemyFilter(WebTestCase):
model = self.app.model
self.sample_data = [
{'name': 'foo1', 'value': 'ONE'},
{'name': 'foo2', 'value': 'two'},
{'name': 'foo3', 'value': 'ggg'},
{'name': 'foo4', 'value': 'ggg'},
{'name': 'foo5', 'value': 'ggg'},
{'name': 'foo6', 'value': 'six'},
{'name': 'foo7', 'value': 'seven'},
{'name': 'foo8', 'value': 'eight'},
{'name': 'foo9', 'value': 'nine'},
{"name": "foo1", "value": "ONE"},
{"name": "foo2", "value": "two"},
{"name": "foo3", "value": "ggg"},
{"name": "foo4", "value": "ggg"},
{"name": "foo5", "value": "ggg"},
{"name": "foo6", "value": "six"},
{"name": "foo7", "value": "seven"},
{"name": "foo8", "value": "eight"},
{"name": "foo9", "value": "nine"},
]
for setting in self.sample_data:
self.app.save_setting(self.session, setting['name'], setting['value'])
self.app.save_setting(self.session, setting["name"], setting["value"])
self.session.commit()
self.sample_query = self.session.query(model.Setting)
def make_filter(self, model_property, **kwargs):
factory = kwargs.pop('factory', mod.StringAlchemyFilter)
kwargs['model_property'] = model_property
factory = kwargs.pop("factory", mod.StringAlchemyFilter)
kwargs["model_property"] = model_property
return factory(self.request, model_property.key, **kwargs)
def test_filter_contains(self):
@ -364,11 +413,11 @@ class TestStringAlchemyFilter(WebTestCase):
# not filtered for empty value
filtered_query = filtr.filter_contains(self.sample_query, None)
self.assertIs(filtered_query, self.sample_query)
filtered_query = filtr.filter_contains(self.sample_query, '')
filtered_query = filtr.filter_contains(self.sample_query, "")
self.assertIs(filtered_query, self.sample_query)
# filtered by value
filtered_query = filtr.filter_contains(self.sample_query, 'ggg')
filtered_query = filtr.filter_contains(self.sample_query, "ggg")
self.assertIsNot(filtered_query, self.sample_query)
self.assertEqual(filtered_query.count(), 3)
@ -380,11 +429,11 @@ class TestStringAlchemyFilter(WebTestCase):
# not filtered for empty value
filtered_query = filtr.filter_does_not_contain(self.sample_query, None)
self.assertIs(filtered_query, self.sample_query)
filtered_query = filtr.filter_does_not_contain(self.sample_query, '')
filtered_query = filtr.filter_does_not_contain(self.sample_query, "")
self.assertIs(filtered_query, self.sample_query)
# filtered by value
filtered_query = filtr.filter_does_not_contain(self.sample_query, 'ggg')
filtered_query = filtr.filter_does_not_contain(self.sample_query, "ggg")
self.assertIsNot(filtered_query, self.sample_query)
self.assertEqual(filtered_query.count(), 6)
@ -392,8 +441,8 @@ class TestStringAlchemyFilter(WebTestCase):
class TestIntegerAlchemyFilter(WebTestCase):
def make_filter(self, model_property, **kwargs):
factory = kwargs.pop('factory', mod.IntegerAlchemyFilter)
kwargs['model_property'] = model_property
factory = kwargs.pop("factory", mod.IntegerAlchemyFilter)
kwargs["model_property"] = model_property
return factory(self.request, model_property.key, **kwargs)
def test_coerce_value(self):
@ -402,15 +451,15 @@ class TestIntegerAlchemyFilter(WebTestCase):
# null
self.assertIsNone(filtr.coerce_value(None))
self.assertIsNone(filtr.coerce_value(''))
self.assertIsNone(filtr.coerce_value(""))
# typical
self.assertEqual(filtr.coerce_value('42'), 42)
self.assertEqual(filtr.coerce_value('-42'), -42)
self.assertEqual(filtr.coerce_value("42"), 42)
self.assertEqual(filtr.coerce_value("-42"), -42)
# invalid
self.assertIsNone(filtr.coerce_value('42.12'))
self.assertIsNone(filtr.coerce_value('bogus'))
self.assertIsNone(filtr.coerce_value("42.12"))
self.assertIsNone(filtr.coerce_value("bogus"))
class TestBooleanAlchemyFilter(WebTestCase):
@ -420,15 +469,9 @@ class TestBooleanAlchemyFilter(WebTestCase):
model = self.app.model
self.sample_data = [
{'username': 'alice',
'prevent_edit': False,
'active': True},
{'username': 'bob',
'prevent_edit': True,
'active': True},
{'username': 'charlie',
'active': False,
'prevent_edit': None},
{"username": "alice", "prevent_edit": False, "active": True},
{"username": "bob", "prevent_edit": True, "active": True},
{"username": "charlie", "active": False, "prevent_edit": None},
]
for user in self.sample_data:
user = model.User(**user)
@ -437,37 +480,46 @@ class TestBooleanAlchemyFilter(WebTestCase):
self.sample_query = self.session.query(model.User)
def make_filter(self, model_property, **kwargs):
factory = kwargs.pop('factory', mod.BooleanAlchemyFilter)
kwargs['model_property'] = model_property
factory = kwargs.pop("factory", mod.BooleanAlchemyFilter)
kwargs["model_property"] = model_property
return factory(self.request, model_property.key, **kwargs)
def test_get_verbs(self):
model = self.app.model
# bool field, not nullable
filtr = self.make_filter(model.User.active,
factory=mod.BooleanAlchemyFilter,
nullable=False)
self.assertFalse(hasattr(filtr, 'verbs'))
self.assertEqual(filtr.default_verbs, ['is_true', 'is_false'])
filtr = self.make_filter(
model.User.active, factory=mod.BooleanAlchemyFilter, nullable=False
)
self.assertFalse(hasattr(filtr, "verbs"))
self.assertEqual(filtr.default_verbs, ["is_true", "is_false"])
# by default, returns default verbs (plus 'is_any')
self.assertEqual(filtr.get_verbs(), ['is_true', 'is_false', 'is_any'])
self.assertEqual(filtr.get_verbs(), ["is_true", "is_false", "is_any"])
# default verbs can be a callable
filtr.default_verbs = lambda: ['foo', 'bar']
self.assertEqual(filtr.get_verbs(), ['foo', 'bar', 'is_any'])
filtr.default_verbs = lambda: ["foo", "bar"]
self.assertEqual(filtr.get_verbs(), ["foo", "bar", "is_any"])
# bool field, *nullable*
filtr = self.make_filter(model.User.active,
factory=mod.BooleanAlchemyFilter,
nullable=True)
self.assertFalse(hasattr(filtr, 'verbs'))
self.assertEqual(filtr.default_verbs, ['is_true', 'is_false'])
filtr = self.make_filter(
model.User.active, factory=mod.BooleanAlchemyFilter, nullable=True
)
self.assertFalse(hasattr(filtr, "verbs"))
self.assertEqual(filtr.default_verbs, ["is_true", "is_false"])
# effective verbs also include is_false_null
self.assertEqual(filtr.get_verbs(), ['is_true', 'is_false', 'is_false_null',
'is_null', 'is_not_null', 'is_any'])
self.assertEqual(
filtr.get_verbs(),
[
"is_true",
"is_false",
"is_false_null",
"is_null",
"is_not_null",
"is_any",
],
)
def test_coerce_value(self):
model = self.app.model
@ -477,11 +529,11 @@ class TestBooleanAlchemyFilter(WebTestCase):
self.assertTrue(filtr.coerce_value(True))
self.assertTrue(filtr.coerce_value(1))
self.assertTrue(filtr.coerce_value('1'))
self.assertTrue(filtr.coerce_value("1"))
self.assertFalse(filtr.coerce_value(False))
self.assertFalse(filtr.coerce_value(0))
self.assertFalse(filtr.coerce_value(''))
self.assertFalse(filtr.coerce_value(""))
def test_filter_is_true(self):
model = self.app.model
@ -515,7 +567,7 @@ class TestBooleanAlchemyFilter(WebTestCase):
class TheLocalThing(Base):
__tablename__ = 'the_local_thing'
__tablename__ = "the_local_thing"
id = sa.Column(sa.Integer(), primary_key=True, autoincrement=False)
date = sa.Column(sa.DateTime(timezone=True), nullable=True)
@ -530,12 +582,12 @@ class TestDateAlchemyFilter(WebTestCase):
model.Base.metadata.create_all(bind=self.session.bind)
self.sample_data = [
{'id': 1, 'date': datetime.date(2024, 1, 1)},
{'id': 2, 'date': datetime.date(2024, 1, 1)},
{'id': 3, 'date': datetime.date(2024, 3, 1)},
{'id': 4, 'date': datetime.date(2024, 3, 1)},
{'id': 5, 'date': None},
{'id': 6, 'date': None},
{"id": 1, "date": datetime.date(2024, 1, 1)},
{"id": 2, "date": datetime.date(2024, 1, 1)},
{"id": 3, "date": datetime.date(2024, 3, 1)},
{"id": 4, "date": datetime.date(2024, 3, 1)},
{"id": 5, "date": None},
{"id": 6, "date": None},
]
for thing in self.sample_data:
@ -546,8 +598,8 @@ class TestDateAlchemyFilter(WebTestCase):
self.sample_query = self.session.query(TheLocalThing)
def make_filter(self, model_property, **kwargs):
factory = kwargs.pop('factory', mod.DateAlchemyFilter)
kwargs['model_property'] = model_property
factory = kwargs.pop("factory", mod.DateAlchemyFilter)
kwargs["model_property"] = model_property
return factory(self.request, model_property.key, **kwargs)
def test_coerce_value(self):
@ -562,12 +614,12 @@ class TestDateAlchemyFilter(WebTestCase):
self.assertIs(value, result)
# value as string
result = filtr.coerce_value('2024-04-01')
result = filtr.coerce_value("2024-04-01")
self.assertIsInstance(result, datetime.date)
self.assertEqual(result, datetime.date(2024, 4, 1))
# invalid
result = filtr.coerce_value('thisinputisbad')
result = filtr.coerce_value("thisinputisbad")
self.assertIsNone(result)
def test_greater_than(self):
@ -582,12 +634,14 @@ class TestDateAlchemyFilter(WebTestCase):
self.assertEqual(filtered_query.count(), 6)
# value as date
filtered_query = filtr.filter_greater_than(self.sample_query, datetime.date(2024, 2, 1))
filtered_query = filtr.filter_greater_than(
self.sample_query, datetime.date(2024, 2, 1)
)
self.assertIsNot(filtered_query, self.sample_query)
self.assertEqual(filtered_query.count(), 2)
# value as string
filtered_query = filtr.filter_greater_than(self.sample_query, '2024-02-01')
filtered_query = filtr.filter_greater_than(self.sample_query, "2024-02-01")
self.assertIsNot(filtered_query, self.sample_query)
self.assertEqual(filtered_query.count(), 2)
@ -603,17 +657,21 @@ class TestDateAlchemyFilter(WebTestCase):
self.assertEqual(filtered_query.count(), 6)
# value as date (clear of boundary)
filtered_query = filtr.filter_greater_equal(self.sample_query, datetime.date(2024, 2, 1))
filtered_query = filtr.filter_greater_equal(
self.sample_query, datetime.date(2024, 2, 1)
)
self.assertIsNot(filtered_query, self.sample_query)
self.assertEqual(filtered_query.count(), 2)
# value as date (at boundary)
filtered_query = filtr.filter_greater_equal(self.sample_query, datetime.date(2024, 3, 1))
filtered_query = filtr.filter_greater_equal(
self.sample_query, datetime.date(2024, 3, 1)
)
self.assertIsNot(filtered_query, self.sample_query)
self.assertEqual(filtered_query.count(), 2)
# value as string
filtered_query = filtr.filter_greater_equal(self.sample_query, '2024-01-01')
filtered_query = filtr.filter_greater_equal(self.sample_query, "2024-01-01")
self.assertIsNot(filtered_query, self.sample_query)
self.assertEqual(filtered_query.count(), 4)
@ -629,12 +687,14 @@ class TestDateAlchemyFilter(WebTestCase):
self.assertEqual(filtered_query.count(), 6)
# value as date
filtered_query = filtr.filter_less_than(self.sample_query, datetime.date(2024, 2, 1))
filtered_query = filtr.filter_less_than(
self.sample_query, datetime.date(2024, 2, 1)
)
self.assertIsNot(filtered_query, self.sample_query)
self.assertEqual(filtered_query.count(), 2)
# value as string
filtered_query = filtr.filter_less_than(self.sample_query, '2024-04-01')
filtered_query = filtr.filter_less_than(self.sample_query, "2024-04-01")
self.assertIsNot(filtered_query, self.sample_query)
self.assertEqual(filtered_query.count(), 4)
@ -650,17 +710,21 @@ class TestDateAlchemyFilter(WebTestCase):
self.assertEqual(filtered_query.count(), 6)
# value as date (clear of boundary)
filtered_query = filtr.filter_less_equal(self.sample_query, datetime.date(2024, 2, 1))
filtered_query = filtr.filter_less_equal(
self.sample_query, datetime.date(2024, 2, 1)
)
self.assertIsNot(filtered_query, self.sample_query)
self.assertEqual(filtered_query.count(), 2)
# value as date (at boundary)
filtered_query = filtr.filter_less_equal(self.sample_query, datetime.date(2024, 3, 1))
filtered_query = filtr.filter_less_equal(
self.sample_query, datetime.date(2024, 3, 1)
)
self.assertIsNot(filtered_query, self.sample_query)
self.assertEqual(filtered_query.count(), 2)
# value as string
filtered_query = filtr.filter_less_equal(self.sample_query, '2024-04-01')
filtered_query = filtr.filter_less_equal(self.sample_query, "2024-04-01")
self.assertIsNot(filtered_query, self.sample_query)
self.assertEqual(filtered_query.count(), 4)
@ -668,5 +732,5 @@ class TestDateAlchemyFilter(WebTestCase):
class TestVerbNotSupported(TestCase):
def test_basic(self):
error = mod.VerbNotSupported('equal')
error = mod.VerbNotSupported("equal")
self.assertEqual(str(error), "unknown filter verb not supported: equal")

View file

@ -36,34 +36,34 @@ class TestMakeWuttaConfig(FileTestCase):
def test_basic(self):
# mock path to config file
myconf = self.write_file('my.conf', '')
settings = {'wutta.config': myconf}
myconf = self.write_file("my.conf", "")
settings = {"wutta.config": myconf}
# can make a config okay
config = mod.make_wutta_config(settings)
# and that config is also stored in settings
self.assertIn('wutta_config', settings)
self.assertIs(settings['wutta_config'], config)
self.assertIn("wutta_config", settings)
self.assertIs(settings["wutta_config"], config)
class TestMakePyramidConfig(DataTestCase):
def test_basic(self):
with patch.object(AppHandler, 'make_session', return_value=self.session):
settings = {'wutta_config': self.config}
with patch.object(AppHandler, "make_session", return_value=self.session):
settings = {"wutta_config": self.config}
config = mod.make_pyramid_config(settings)
self.assertIsInstance(config, Configurator)
self.assertEqual(settings['wuttaweb.theme'], 'default')
self.assertEqual(settings["wuttaweb.theme"], "default")
class TestMain(DataTestCase):
def test_basic(self):
with patch.object(AppHandler, 'make_session', return_value=self.session):
with patch.object(AppHandler, "make_session", return_value=self.session):
global_config = None
myconf = self.write_file('my.conf', '')
settings = {'wutta.config': myconf}
myconf = self.write_file("my.conf", "")
settings = {"wutta.config": myconf}
app = mod.main(global_config, **settings)
self.assertIsInstance(app, Router)
@ -73,9 +73,9 @@ def mock_main(global_config, **settings):
wutta_config = mod.make_wutta_config(settings)
pyramid_config = mod.make_pyramid_config(settings)
pyramid_config.include('wuttaweb.static')
pyramid_config.include('wuttaweb.subscribers')
pyramid_config.include('wuttaweb.views')
pyramid_config.include("wuttaweb.static")
pyramid_config.include("wuttaweb.subscribers")
pyramid_config.include("wuttaweb.views")
return pyramid_config.make_wsgi_app()
@ -83,26 +83,26 @@ def mock_main(global_config, **settings):
class TestMakeWsgiApp(DataTestCase):
def test_with_callable(self):
with patch.object(self.app, 'make_session', return_value=self.session):
with patch.object(self.app, "make_session", return_value=self.session):
# specify config
wsgi = mod.make_wsgi_app(mock_main, config=self.config)
self.assertIsInstance(wsgi, Router)
# auto config
with patch.object(mod, 'make_config', return_value=self.config):
with patch.object(mod, "make_config", return_value=self.config):
wsgi = mod.make_wsgi_app(mock_main)
self.assertIsInstance(wsgi, Router)
def test_with_spec(self):
# specify config
wsgi = mod.make_wsgi_app('tests.test_app:mock_main', config=self.config)
wsgi = mod.make_wsgi_app("tests.test_app:mock_main", config=self.config)
self.assertIsInstance(wsgi, Router)
# auto config
with patch.object(mod, 'make_config', return_value=self.config):
wsgi = mod.make_wsgi_app('tests.test_app:mock_main')
with patch.object(mod, "make_config", return_value=self.config):
wsgi = mod.make_wsgi_app("tests.test_app:mock_main")
self.assertIsInstance(wsgi, Router)
def test_invalid(self):
@ -112,27 +112,27 @@ class TestMakeWsgiApp(DataTestCase):
class TestMakeAsgiApp(DataTestCase):
def test_with_callable(self):
with patch.object(self.app, 'make_session', return_value=self.session):
with patch.object(self.app, "make_session", return_value=self.session):
# specify config
asgi = mod.make_asgi_app(mock_main, config=self.config)
self.assertIsInstance(asgi, WsgiToAsgi)
# auto config
with patch.object(mod, 'make_config', return_value=self.config):
with patch.object(mod, "make_config", return_value=self.config):
asgi = mod.make_asgi_app(mock_main)
self.assertIsInstance(asgi, WsgiToAsgi)
def test_with_spec(self):
with patch.object(self.app, 'make_session', return_value=self.session):
with patch.object(self.app, "make_session", return_value=self.session):
# specify config
asgi = mod.make_asgi_app('tests.test_app:mock_main', config=self.config)
asgi = mod.make_asgi_app("tests.test_app:mock_main", config=self.config)
self.assertIsInstance(asgi, WsgiToAsgi)
# auto config
with patch.object(mod, 'make_config', return_value=self.config):
asgi = mod.make_asgi_app('tests.test_app:mock_main')
with patch.object(mod, "make_config", return_value=self.config):
asgi = mod.make_asgi_app("tests.test_app:mock_main")
self.assertIsInstance(asgi, WsgiToAsgi)
def test_invalid(self):
@ -143,53 +143,65 @@ class TestEstablishTheme(DataTestCase):
def test_default(self):
settings = {
'wutta_config': self.config,
'mako.directories': ['wuttaweb:templates'],
"wutta_config": self.config,
"mako.directories": ["wuttaweb:templates"],
}
mod.establish_theme(settings)
self.assertEqual(settings['wuttaweb.theme'], 'default')
self.assertEqual(settings['mako.directories'], [
resource_path('wuttaweb:templates/themes/default'),
'wuttaweb:templates',
])
self.assertEqual(settings["wuttaweb.theme"], "default")
self.assertEqual(
settings["mako.directories"],
[
resource_path("wuttaweb:templates/themes/default"),
"wuttaweb:templates",
],
)
def test_mako_dirs_as_string(self):
settings = {
'wutta_config': self.config,
'mako.directories': 'wuttaweb:templates',
"wutta_config": self.config,
"mako.directories": "wuttaweb:templates",
}
mod.establish_theme(settings)
self.assertEqual(settings['wuttaweb.theme'], 'default')
self.assertEqual(settings['mako.directories'], [
resource_path('wuttaweb:templates/themes/default'),
'wuttaweb:templates',
])
self.assertEqual(settings["wuttaweb.theme"], "default")
self.assertEqual(
settings["mako.directories"],
[
resource_path("wuttaweb:templates/themes/default"),
"wuttaweb:templates",
],
)
def test_butterfly(self):
settings = {
'wutta_config': self.config,
'mako.directories': 'wuttaweb:templates',
"wutta_config": self.config,
"mako.directories": "wuttaweb:templates",
}
self.app.save_setting(self.session, 'wuttaweb.theme', 'butterfly')
self.app.save_setting(self.session, "wuttaweb.theme", "butterfly")
self.session.commit()
mod.establish_theme(settings)
self.assertEqual(settings['wuttaweb.theme'], 'butterfly')
self.assertEqual(settings['mako.directories'], [
resource_path('wuttaweb:templates/themes/butterfly'),
'wuttaweb:templates',
])
self.assertEqual(settings["wuttaweb.theme"], "butterfly")
self.assertEqual(
settings["mako.directories"],
[
resource_path("wuttaweb:templates/themes/butterfly"),
"wuttaweb:templates",
],
)
def test_custom(self):
settings = {
'wutta_config': self.config,
'mako.directories': 'wuttaweb:templates',
"wutta_config": self.config,
"mako.directories": "wuttaweb:templates",
}
self.config.setdefault('wuttaweb.themes.keys', 'anotherone')
self.app.save_setting(self.session, 'wuttaweb.theme', 'anotherone')
self.config.setdefault("wuttaweb.themes.keys", "anotherone")
self.app.save_setting(self.session, "wuttaweb.theme", "anotherone")
self.session.commit()
mod.establish_theme(settings)
self.assertEqual(settings['wuttaweb.theme'], 'anotherone')
self.assertEqual(settings['mako.directories'], [
resource_path('wuttaweb:templates/themes/anotherone'),
'wuttaweb:templates',
])
self.assertEqual(settings["wuttaweb.theme"], "anotherone")
self.assertEqual(
settings["mako.directories"],
[
resource_path("wuttaweb:templates/themes/anotherone"),
"wuttaweb:templates",
],
)

View file

@ -18,10 +18,11 @@ class TestLoginUser(TestCase):
app = config.get_app()
model = app.model
request = testing.DummyRequest(wutta_config=config)
user = model.User(username='barney')
user = model.User(username="barney")
headers = mod.login_user(request, user)
self.assertEqual(headers, [])
class TestLogoutUser(TestCase):
def test_basic(self):
@ -36,20 +37,25 @@ class TestLogoutUser(TestCase):
class TestWuttaSecurityPolicy(TestCase):
def setUp(self):
self.config = WuttaConfig(defaults={
'wutta.db.default.url': 'sqlite://',
})
self.config = WuttaConfig(
defaults={
"wutta.db.default.url": "sqlite://",
}
)
self.request = testing.DummyRequest()
self.pyramid_config = testing.setUp(request=self.request, settings={
'wutta_config': self.config,
})
self.pyramid_config = testing.setUp(
request=self.request,
settings={
"wutta_config": self.config,
},
)
self.app = self.config.get_app()
model = self.app.model
model.Base.metadata.create_all(bind=self.config.appdb_engine)
self.session = self.app.make_session()
self.user = model.User(username='barney')
self.user = model.User(username="barney")
self.session.add(self.user)
self.session.commit()
@ -66,12 +72,16 @@ class TestWuttaSecurityPolicy(TestCase):
self.assertIsNotNone(uuid)
self.assertIsNone(self.policy.session_helper.authenticated_userid(self.request))
self.policy.remember(self.request, uuid)
self.assertEqual(self.policy.session_helper.authenticated_userid(self.request), uuid)
self.assertEqual(
self.policy.session_helper.authenticated_userid(self.request), uuid
)
def test_forget(self):
uuid = self.user.uuid
self.policy.remember(self.request, uuid)
self.assertEqual(self.policy.session_helper.authenticated_userid(self.request), uuid)
self.assertEqual(
self.policy.session_helper.authenticated_userid(self.request), uuid
)
self.policy.forget(self.request)
self.assertIsNone(self.policy.session_helper.authenticated_userid(self.request))
@ -91,7 +101,7 @@ class TestWuttaSecurityPolicy(TestCase):
# invalid identity yields no user
self.policy = self.make_policy()
self.policy.remember(self.request, _uuid.uuid4()) # random uuid
self.policy.remember(self.request, _uuid.uuid4()) # random uuid
user = self.policy.identity(self.request)
self.assertIsNone(user)
@ -112,59 +122,59 @@ class TestWuttaSecurityPolicy(TestCase):
model = self.app.model
# anon has no perms
self.assertFalse(self.policy.permits(self.request, None, 'foo.bar'))
self.assertFalse(self.policy.permits(self.request, None, "foo.bar"))
# but we can grant it
anons = auth.get_role_anonymous(self.session)
self.user.roles.append(anons)
auth.grant_permission(anons, 'foo.bar')
auth.grant_permission(anons, "foo.bar")
self.session.commit()
# and then perm check is satisfied
self.assertTrue(self.policy.permits(self.request, None, 'foo.bar'))
self.assertTrue(self.policy.permits(self.request, None, "foo.bar"))
# now, create a separate role and grant another perm
# (but user does not yet belong to this role)
role = model.Role(name='whatever')
role = model.Role(name="whatever")
self.session.add(role)
auth.grant_permission(role, 'baz.edit')
auth.grant_permission(role, "baz.edit")
self.session.commit()
# so far then, user does not have the permission
self.policy = self.make_policy()
self.policy.remember(self.request, self.user.uuid)
self.assertFalse(self.policy.permits(self.request, None, 'baz.edit'))
self.assertFalse(self.policy.permits(self.request, None, "baz.edit"))
# but if we assign user to role, perm check should pass
self.user.roles.append(role)
self.session.commit()
self.assertTrue(self.policy.permits(self.request, None, 'baz.edit'))
self.assertTrue(self.policy.permits(self.request, None, "baz.edit"))
# now let's try another perm - we won't grant it, but will
# confirm user is denied access unless they become root
self.assertFalse(self.policy.permits(self.request, None, 'some-root-perm'))
self.assertFalse(self.policy.permits(self.request, None, "some-root-perm"))
self.request.is_root = True
self.assertTrue(self.policy.permits(self.request, None, 'some-root-perm'))
self.assertTrue(self.policy.permits(self.request, None, "some-root-perm"))
class TestAddPermissionGroup(WebTestCase):
def test_basic(self):
permissions = self.pyramid_config.get_settings().get('wutta_permissions', {})
self.assertNotIn('widgets', permissions)
self.pyramid_config.add_wutta_permission_group('widgets')
permissions = self.pyramid_config.get_settings().get('wutta_permissions', {})
self.assertIn('widgets', permissions)
self.assertEqual(permissions['widgets']['label'], "Widgets")
permissions = self.pyramid_config.get_settings().get("wutta_permissions", {})
self.assertNotIn("widgets", permissions)
self.pyramid_config.add_wutta_permission_group("widgets")
permissions = self.pyramid_config.get_settings().get("wutta_permissions", {})
self.assertIn("widgets", permissions)
self.assertEqual(permissions["widgets"]["label"], "Widgets")
class TestAddPermission(WebTestCase):
def test_basic(self):
permissions = self.pyramid_config.get_settings().get('wutta_permissions', {})
self.assertNotIn('widgets', permissions)
self.pyramid_config.add_wutta_permission('widgets', 'widgets.polish')
permissions = self.pyramid_config.get_settings().get('wutta_permissions', {})
self.assertIn('widgets', permissions)
self.assertEqual(permissions['widgets']['label'], "Widgets")
self.assertIn('widgets.polish', permissions['widgets']['perms'])
permissions = self.pyramid_config.get_settings().get("wutta_permissions", {})
self.assertNotIn("widgets", permissions)
self.pyramid_config.add_wutta_permission("widgets", "widgets.polish")
permissions = self.pyramid_config.get_settings().get("wutta_permissions", {})
self.assertIn("widgets", permissions)
self.assertEqual(permissions["widgets"]["label"], "Widgets")
self.assertIn("widgets.polish", permissions["widgets"]["perms"])

View file

@ -17,7 +17,9 @@ class TestAllSettings(DataTestCase):
def test_all(self):
for name in dir(mod):
obj = getattr(mod, name)
if (isinstance(obj, type)
if (
isinstance(obj, type)
and obj is not EmailSetting
and issubclass(obj, EmailSetting)):
and issubclass(obj, EmailSetting)
):
self.check_setting(obj)

View file

@ -12,9 +12,11 @@ from wuttaweb.testing import WebTestCase
class MockMenuHandler(MenuHandler):
pass
class LegacyMenuHandler(MenuHandler):
pass
class AnotherMenuHandler(MenuHandler):
pass
@ -29,48 +31,48 @@ class TestWebHandler(WebTestCase):
# default with / root path
url = handler.get_fanstatic_url(self.request, static.logo)
self.assertEqual(url, '/fanstatic/wuttaweb_img/logo.png')
self.assertEqual(url, "/fanstatic/wuttaweb_img/logo.png")
# what about a subpath
self.request.script_name = '/testing'
self.request.script_name = "/testing"
url = handler.get_fanstatic_url(self.request, static.logo)
self.assertEqual(url, '/testing/fanstatic/wuttaweb_img/logo.png')
self.assertEqual(url, "/testing/fanstatic/wuttaweb_img/logo.png")
def test_get_favicon_url(self):
handler = self.make_handler()
# default
url = handler.get_favicon_url(self.request)
self.assertEqual(url, '/fanstatic/wuttaweb_img/favicon.ico')
self.assertEqual(url, "/fanstatic/wuttaweb_img/favicon.ico")
# config override
self.config.setdefault('wuttaweb.favicon_url', '/testing/other.ico')
self.config.setdefault("wuttaweb.favicon_url", "/testing/other.ico")
url = handler.get_favicon_url(self.request)
self.assertEqual(url, '/testing/other.ico')
self.assertEqual(url, "/testing/other.ico")
def test_get_header_logo_url(self):
handler = self.make_handler()
# default
url = handler.get_header_logo_url(self.request)
self.assertEqual(url, '/fanstatic/wuttaweb_img/favicon.ico')
self.assertEqual(url, "/fanstatic/wuttaweb_img/favicon.ico")
# config override
self.config.setdefault('wuttaweb.header_logo_url', '/testing/header.png')
self.config.setdefault("wuttaweb.header_logo_url", "/testing/header.png")
url = handler.get_header_logo_url(self.request)
self.assertEqual(url, '/testing/header.png')
self.assertEqual(url, "/testing/header.png")
def test_get_main_logo_url(self):
handler = self.make_handler()
# default
url = handler.get_main_logo_url(self.request)
self.assertEqual(url, '/fanstatic/wuttaweb_img/logo.png')
self.assertEqual(url, "/fanstatic/wuttaweb_img/logo.png")
# config override
self.config.setdefault('wuttaweb.logo_url', '/testing/other.png')
self.config.setdefault("wuttaweb.logo_url", "/testing/other.png")
url = handler.get_main_logo_url(self.request)
self.assertEqual(url, '/testing/other.png')
self.assertEqual(url, "/testing/other.png")
def test_get_menu_handler(self):
handler = self.make_handler()
@ -81,20 +83,23 @@ class TestWebHandler(WebTestCase):
self.assertIs(type(menus), MenuHandler)
# configured default
self.config.setdefault('wutta.web.menus.handler.default_spec',
'tests.test_handler:MockMenuHandler')
self.config.setdefault(
"wutta.web.menus.handler.default_spec", "tests.test_handler:MockMenuHandler"
)
menus = handler.get_menu_handler()
self.assertIsInstance(menus, MockMenuHandler)
# configured handler (legacy)
self.config.setdefault('wutta.web.menus.handler_spec',
'tests.test_handler:LegacyMenuHandler')
self.config.setdefault(
"wutta.web.menus.handler_spec", "tests.test_handler:LegacyMenuHandler"
)
menus = handler.get_menu_handler()
self.assertIsInstance(menus, LegacyMenuHandler)
# configued handler (proper)
self.config.setdefault('wutta.web.menus.handler.spec',
'tests.test_handler:AnotherMenuHandler')
self.config.setdefault(
"wutta.web.menus.handler.spec", "tests.test_handler:AnotherMenuHandler"
)
menus = handler.get_menu_handler()
self.assertIsInstance(menus, AnotherMenuHandler)
@ -103,40 +108,51 @@ class TestWebHandler(WebTestCase):
# at least one spec by default
specs = handler.get_menu_handler_specs()
self.assertIn('wuttaweb.menus:MenuHandler', specs)
self.assertIn("wuttaweb.menus:MenuHandler", specs)
# caller can specify default as string
specs = handler.get_menu_handler_specs(default='tests.test_handler:MockMenuHandler')
self.assertIn('wuttaweb.menus:MenuHandler', specs)
self.assertIn('tests.test_handler:MockMenuHandler', specs)
self.assertNotIn('tests.test_handler:AnotherMenuHandler', specs)
specs = handler.get_menu_handler_specs(
default="tests.test_handler:MockMenuHandler"
)
self.assertIn("wuttaweb.menus:MenuHandler", specs)
self.assertIn("tests.test_handler:MockMenuHandler", specs)
self.assertNotIn("tests.test_handler:AnotherMenuHandler", specs)
# caller can specify default as list
specs = handler.get_menu_handler_specs(default=[
'tests.test_handler:MockMenuHandler',
'tests.test_handler:AnotherMenuHandler'])
self.assertIn('wuttaweb.menus:MenuHandler', specs)
self.assertIn('tests.test_handler:MockMenuHandler', specs)
self.assertIn('tests.test_handler:AnotherMenuHandler', specs)
specs = handler.get_menu_handler_specs(
default=[
"tests.test_handler:MockMenuHandler",
"tests.test_handler:AnotherMenuHandler",
]
)
self.assertIn("wuttaweb.menus:MenuHandler", specs)
self.assertIn("tests.test_handler:MockMenuHandler", specs)
self.assertIn("tests.test_handler:AnotherMenuHandler", specs)
# default can be configured
self.config.setdefault('wutta.web.menus.handler.default_spec',
'tests.test_handler:AnotherMenuHandler')
self.config.setdefault(
"wutta.web.menus.handler.default_spec",
"tests.test_handler:AnotherMenuHandler",
)
specs = handler.get_menu_handler_specs()
self.assertIn('wuttaweb.menus:MenuHandler', specs)
self.assertNotIn('tests.test_handler:MockMenuHandler', specs)
self.assertIn('tests.test_handler:AnotherMenuHandler', specs)
self.assertIn("wuttaweb.menus:MenuHandler", specs)
self.assertNotIn("tests.test_handler:MockMenuHandler", specs)
self.assertIn("tests.test_handler:AnotherMenuHandler", specs)
# the rest come from entry points
with patch.object(mod, 'load_entry_points', return_value={
'legacy': LegacyMenuHandler,
}):
with patch.object(
mod,
"load_entry_points",
return_value={
"legacy": LegacyMenuHandler,
},
):
specs = handler.get_menu_handler_specs()
self.assertNotIn('wuttaweb.menus:MenuHandler', specs)
self.assertNotIn('tests.test_handler:MockMenuHandler', specs)
self.assertIn('tests.test_handler:LegacyMenuHandler', specs)
self.assertNotIn("wuttaweb.menus:MenuHandler", specs)
self.assertNotIn("tests.test_handler:MockMenuHandler", specs)
self.assertIn("tests.test_handler:LegacyMenuHandler", specs)
# nb. this remains from previous config default
self.assertIn('tests.test_handler:AnotherMenuHandler', specs)
self.assertIn("tests.test_handler:AnotherMenuHandler", specs)
def test_make_form(self):
handler = self.make_handler()

View file

@ -18,13 +18,13 @@ class TestMenuHandler(WebTestCase):
# no people entry by default
menu = self.handler.make_admin_menu(self.request)
self.assertIsInstance(menu, dict)
routes = [item.get('route') for item in menu['items']]
self.assertNotIn('people', routes)
routes = [item.get("route") for item in menu["items"]]
self.assertNotIn("people", routes)
# but we can request it
menu = self.handler.make_admin_menu(self.request, include_people=True)
routes = [item.get('route') for item in menu['items']]
self.assertIn('people', routes)
routes = [item.get("route") for item in menu["items"]]
self.assertIn("people", routes)
def test_make_menus(self):
menus = self.handler.make_menus(self.request)
@ -35,20 +35,20 @@ class TestMenuHandler(WebTestCase):
auth = self.app.get_auth_handler()
# user with perms
barney = model.User(username='barney')
barney = model.User(username="barney")
self.session.add(barney)
blokes = model.Role(name="Blokes")
self.session.add(blokes)
barney.roles.append(blokes)
auth.grant_permission(blokes, 'appinfo.list')
auth.grant_permission(blokes, "appinfo.list")
self.request.user = barney
# perm not granted to user
item = {'perm': 'appinfo.configure'}
item = {"perm": "appinfo.configure"}
self.assertFalse(self.handler._is_allowed(self.request, item))
# perm *is* granted to user
item = {'perm': 'appinfo.list'}
item = {"perm": "appinfo.list"}
self.assertTrue(self.handler._is_allowed(self.request, item))
# perm not required
@ -60,49 +60,49 @@ class TestMenuHandler(WebTestCase):
def make_menus():
return [
{
'type': 'menu',
'items': [
{'title': "Foo", 'url': '#'},
{'title': "Bar", 'url': '#'},
"type": "menu",
"items": [
{"title": "Foo", "url": "#"},
{"title": "Bar", "url": "#"},
],
},
]
mock_is_allowed = MagicMock()
with patch.object(self.handler, '_is_allowed', new=mock_is_allowed):
with patch.object(self.handler, "_is_allowed", new=mock_is_allowed):
# all should be allowed
mock_is_allowed.return_value = True
menus = make_menus()
self.handler._mark_allowed(self.request, menus)
menu = menus[0]
self.assertTrue(menu['allowed'])
foo, bar = menu['items']
self.assertTrue(foo['allowed'])
self.assertTrue(bar['allowed'])
self.assertTrue(menu["allowed"])
foo, bar = menu["items"]
self.assertTrue(foo["allowed"])
self.assertTrue(bar["allowed"])
# none should be allowed
mock_is_allowed.return_value = False
menus = make_menus()
self.handler._mark_allowed(self.request, menus)
menu = menus[0]
self.assertFalse(menu['allowed'])
foo, bar = menu['items']
self.assertFalse(foo['allowed'])
self.assertFalse(bar['allowed'])
self.assertFalse(menu["allowed"])
foo, bar = menu["items"]
self.assertFalse(foo["allowed"])
self.assertFalse(bar["allowed"])
def test_mark_allowed_submenu(self):
def make_menus():
return [
{
'type': 'menu',
'items': [
{'title': "Foo", 'url': '#'},
"type": "menu",
"items": [
{"title": "Foo", "url": "#"},
{
'type': 'menu',
'items': [
{'title': "Bar", 'url': '#'},
"type": "menu",
"items": [
{"title": "Bar", "url": "#"},
],
},
],
@ -110,215 +110,215 @@ class TestMenuHandler(WebTestCase):
]
mock_is_allowed = MagicMock()
with patch.object(self.handler, '_is_allowed', new=mock_is_allowed):
with patch.object(self.handler, "_is_allowed", new=mock_is_allowed):
# all should be allowed
mock_is_allowed.return_value = True
menus = make_menus()
self.handler._mark_allowed(self.request, menus)
menu = menus[0]
self.assertTrue(menu['allowed'])
foo, submenu = menu['items']
self.assertTrue(foo['allowed'])
self.assertTrue(submenu['allowed'])
subitem = submenu['items'][0]
self.assertTrue(subitem['allowed'])
self.assertTrue(menu["allowed"])
foo, submenu = menu["items"]
self.assertTrue(foo["allowed"])
self.assertTrue(submenu["allowed"])
subitem = submenu["items"][0]
self.assertTrue(subitem["allowed"])
# none should be allowed
mock_is_allowed.return_value = False
menus = make_menus()
self.handler._mark_allowed(self.request, menus)
menu = menus[0]
self.assertFalse(menu['allowed'])
foo, submenu = menu['items']
self.assertFalse(foo['allowed'])
self.assertFalse(submenu['allowed'])
subitem = submenu['items'][0]
self.assertFalse(subitem['allowed'])
self.assertFalse(menu["allowed"])
foo, submenu = menu["items"]
self.assertFalse(foo["allowed"])
self.assertFalse(submenu["allowed"])
subitem = submenu["items"][0]
self.assertFalse(subitem["allowed"])
def test_make_menu_key(self):
self.assertEqual(self.handler._make_menu_key('foo'), 'foo')
self.assertEqual(self.handler._make_menu_key('FooBar'), 'foobar')
self.assertEqual(self.handler._make_menu_key('Foo - $#Bar'), 'foobar')
self.assertEqual(self.handler._make_menu_key('Foo__Bar'), 'foo__bar')
self.assertEqual(self.handler._make_menu_key("foo"), "foo")
self.assertEqual(self.handler._make_menu_key("FooBar"), "foobar")
self.assertEqual(self.handler._make_menu_key("Foo - $#Bar"), "foobar")
self.assertEqual(self.handler._make_menu_key("Foo__Bar"), "foo__bar")
def test_make_menu_entry_item(self):
item = {'title': "Foo", 'url': '#'}
item = {"title": "Foo", "url": "#"}
entry = self.handler._make_menu_entry(self.request, item)
self.assertEqual(entry['type'], 'item')
self.assertEqual(entry['title'], "Foo")
self.assertEqual(entry['url'], '#')
self.assertTrue(entry['is_link'])
self.assertEqual(entry["type"], "item")
self.assertEqual(entry["title"], "Foo")
self.assertEqual(entry["url"], "#")
self.assertTrue(entry["is_link"])
def test_make_menu_entry_item_with_no_url(self):
item = {'title': "Foo"}
item = {"title": "Foo"}
entry = self.handler._make_menu_entry(self.request, item)
self.assertEqual(entry['type'], 'item')
self.assertEqual(entry['title'], "Foo")
self.assertNotIn('url', entry)
self.assertEqual(entry["type"], "item")
self.assertEqual(entry["title"], "Foo")
self.assertNotIn("url", entry)
# nb. still sets is_link = True; basically it's <a> with no href
self.assertTrue(entry['is_link'])
self.assertTrue(entry["is_link"])
def test_make_menu_entry_item_with_known_route(self):
item = {'title': "Foo", 'route': 'home'}
with patch.object(self.request, 'route_url', return_value='/something'):
item = {"title": "Foo", "route": "home"}
with patch.object(self.request, "route_url", return_value="/something"):
entry = self.handler._make_menu_entry(self.request, item)
self.assertEqual(entry['type'], 'item')
self.assertEqual(entry['url'], '/something')
self.assertTrue(entry['is_link'])
self.assertEqual(entry["type"], "item")
self.assertEqual(entry["url"], "/something")
self.assertTrue(entry["is_link"])
def test_make_menu_entry_item_with_unknown_route(self):
item = {'title': "Foo", 'route': 'home'}
with patch.object(self.request, 'route_url', side_effect=KeyError):
item = {"title": "Foo", "route": "home"}
with patch.object(self.request, "route_url", side_effect=KeyError):
entry = self.handler._make_menu_entry(self.request, item)
self.assertEqual(entry['type'], 'item')
self.assertEqual(entry["type"], "item")
# nb. fake url is used, based on (bad) route name
self.assertEqual(entry['url'], 'home')
self.assertTrue(entry['is_link'])
self.assertEqual(entry["url"], "home")
self.assertTrue(entry["is_link"])
def test_make_menu_entry_sep(self):
item = {'type': 'sep'}
item = {"type": "sep"}
entry = self.handler._make_menu_entry(self.request, item)
self.assertEqual(entry['type'], 'sep')
self.assertTrue(entry['is_sep'])
self.assertFalse(entry['is_menu'])
self.assertEqual(entry["type"], "sep")
self.assertTrue(entry["is_sep"])
self.assertFalse(entry["is_menu"])
def test_make_raw_menus(self):
# minimal test to ensure it calls the other method
with patch.object(self.handler, 'make_menus') as make_menus:
self.handler._make_raw_menus(self.request, foo='bar')
make_menus.assert_called_once_with(self.request, foo='bar')
with patch.object(self.handler, "make_menus") as make_menus:
self.handler._make_raw_menus(self.request, foo="bar")
make_menus.assert_called_once_with(self.request, foo="bar")
def test_do_make_menus_prune_unallowed_item(self):
test_menus = [
{
'title': "First Menu",
'type': 'menu',
'items': [
{'title': "Foo", 'url': '#'},
{'title': "Bar", 'url': '#'},
"title": "First Menu",
"type": "menu",
"items": [
{"title": "Foo", "url": "#"},
{"title": "Bar", "url": "#"},
],
},
]
def is_allowed(request, item):
if item.get('title') == 'Bar':
if item.get("title") == "Bar":
return False
return True
with patch.object(self.handler, 'make_menus', return_value=test_menus):
with patch.object(self.handler, '_is_allowed', side_effect=is_allowed):
with patch.object(self.handler, "make_menus", return_value=test_menus):
with patch.object(self.handler, "_is_allowed", side_effect=is_allowed):
menus = self.handler.do_make_menus(self.request)
# Foo remains but Bar is pruned
menu = menus[0]
self.assertEqual(len(menu['items']), 1)
item = menu['items'][0]
self.assertEqual(item['title'], 'Foo')
self.assertEqual(len(menu["items"]), 1)
item = menu["items"][0]
self.assertEqual(item["title"], "Foo")
def test_do_make_menus_prune_unallowed_menu(self):
test_menus = [
{
'title': "First Menu",
'type': 'menu',
'items': [
{'title': "Foo", 'url': '#'},
{'title': "Bar", 'url': '#'},
"title": "First Menu",
"type": "menu",
"items": [
{"title": "Foo", "url": "#"},
{"title": "Bar", "url": "#"},
],
},
{
'title': "Second Menu",
'type': 'menu',
'items': [
{'title': "Baz", 'url': '#'},
"title": "Second Menu",
"type": "menu",
"items": [
{"title": "Baz", "url": "#"},
],
},
]
def is_allowed(request, item):
if item.get('title') == 'Baz':
if item.get("title") == "Baz":
return True
return False
with patch.object(self.handler, 'make_menus', return_value=test_menus):
with patch.object(self.handler, '_is_allowed', side_effect=is_allowed):
with patch.object(self.handler, "make_menus", return_value=test_menus):
with patch.object(self.handler, "_is_allowed", side_effect=is_allowed):
menus = self.handler.do_make_menus(self.request)
# Second/Baz remains but First/Foo/Bar are pruned
self.assertEqual(len(menus), 1)
menu = menus[0]
self.assertEqual(menu['title'], 'Second Menu')
self.assertEqual(len(menu['items']), 1)
item = menu['items'][0]
self.assertEqual(item['title'], 'Baz')
self.assertEqual(menu["title"], "Second Menu")
self.assertEqual(len(menu["items"]), 1)
item = menu["items"][0]
self.assertEqual(item["title"], "Baz")
def test_do_make_menus_with_top_link(self):
test_menus = [
{
'title': "First Menu",
'type': 'menu',
'items': [
{'title': "Foo", 'url': '#'},
{'title': "Bar", 'url': '#'},
"title": "First Menu",
"type": "menu",
"items": [
{"title": "Foo", "url": "#"},
{"title": "Bar", "url": "#"},
],
},
{
'title': "Second Link",
'type': 'link',
"title": "Second Link",
"type": "link",
},
]
with patch.object(self.handler, 'make_menus', return_value=test_menus):
with patch.object(self.handler, '_is_allowed', return_value=True):
with patch.object(self.handler, "make_menus", return_value=test_menus):
with patch.object(self.handler, "_is_allowed", return_value=True):
menus = self.handler.do_make_menus(self.request)
# ensure top link remains
self.assertEqual(len(menus), 2)
menu = menus[1]
self.assertEqual(menu['title'], "Second Link")
self.assertEqual(menu["title"], "Second Link")
def test_do_make_menus_with_trailing_sep(self):
test_menus = [
{
'title': "First Menu",
'type': 'menu',
'items': [
{'title': "Foo", 'url': '#'},
{'title': "Bar", 'url': '#'},
{'type': 'sep'},
"title": "First Menu",
"type": "menu",
"items": [
{"title": "Foo", "url": "#"},
{"title": "Bar", "url": "#"},
{"type": "sep"},
],
},
]
with patch.object(self.handler, 'make_menus', return_value=test_menus):
with patch.object(self.handler, '_is_allowed', return_value=True):
with patch.object(self.handler, "make_menus", return_value=test_menus):
with patch.object(self.handler, "_is_allowed", return_value=True):
menus = self.handler.do_make_menus(self.request)
# ensure trailing sep was pruned
menu = menus[0]
self.assertEqual(len(menu['items']), 2)
foo, bar = menu['items']
self.assertEqual(foo['title'], 'Foo')
self.assertEqual(bar['title'], 'Bar')
self.assertEqual(len(menu["items"]), 2)
foo, bar = menu["items"]
self.assertEqual(foo["title"], "Foo")
self.assertEqual(bar["title"], "Bar")
def test_do_make_menus_with_submenu(self):
test_menus = [
{
'title': "First Menu",
'type': 'menu',
'items': [
"title": "First Menu",
"type": "menu",
"items": [
{
'title': "First Submenu",
'type': 'menu',
'items': [
{'title': "Foo", 'url': '#'},
"title": "First Submenu",
"type": "menu",
"items": [
{"title": "Foo", "url": "#"},
],
},
{
'title': "Second Submenu",
'type': 'menu',
'items': [
{'title': "Bar", 'url': '#'},
"title": "Second Submenu",
"type": "menu",
"items": [
{"title": "Bar", "url": "#"},
],
},
],
@ -326,20 +326,20 @@ class TestMenuHandler(WebTestCase):
]
def is_allowed(request, item):
if item.get('title') == 'Bar':
if item.get("title") == "Bar":
return False
return True
with patch.object(self.handler, 'make_menus', return_value=test_menus):
with patch.object(self.handler, '_is_allowed', side_effect=is_allowed):
with patch.object(self.handler, "make_menus", return_value=test_menus):
with patch.object(self.handler, "_is_allowed", side_effect=is_allowed):
menus = self.handler.do_make_menus(self.request)
# first submenu remains, second is pruned
menu = menus[0]
self.assertEqual(len(menu['items']), 1)
submenu = menu['items'][0]
self.assertEqual(submenu['type'], 'submenu')
self.assertEqual(submenu['title'], 'First Submenu')
self.assertEqual(len(submenu['items']), 1)
item = submenu['items'][0]
self.assertEqual(item['title'], 'Foo')
self.assertEqual(len(menu["items"]), 1)
submenu = menu["items"][0]
self.assertEqual(submenu["type"], "submenu")
self.assertEqual(submenu["title"], "First Submenu")
self.assertEqual(len(submenu["items"]), 1)
item = submenu["items"][0]
self.assertEqual(item["title"], "Foo")

View file

@ -27,10 +27,10 @@ class TestGetProgressSession(TestCase):
self.request = testing.DummyRequest()
def test_basic(self):
self.request.session.id = 'mockid'
session = mod.get_progress_session(self.request, 'foo')
self.request.session.id = "mockid"
session = mod.get_progress_session(self.request, "foo")
self.assertIsInstance(session, BeakerSession)
self.assertEqual(session.id, 'mockid.progress.foo')
self.assertEqual(session.id, "mockid.progress.foo")
class TestSessionProgress(ConfigTestCase):
@ -38,16 +38,16 @@ class TestSessionProgress(ConfigTestCase):
def setUp(self):
self.setup_config()
self.request = testing.DummyRequest(wutta_config=self.config)
self.request.session.id = 'mockid'
self.request.session.id = "mockid"
def test_error_url(self):
factory = mod.SessionProgress(self.request, 'foo', success_url='/blart')
self.assertEqual(factory.error_url, '/blart')
factory = mod.SessionProgress(self.request, "foo", success_url="/blart")
self.assertEqual(factory.error_url, "/blart")
def test_basic(self):
# sanity / coverage check
factory = mod.SessionProgress(self.request, 'foo')
factory = mod.SessionProgress(self.request, "foo")
prog = factory("doing things", 2)
prog.update(1)
prog.update(2)
@ -56,10 +56,10 @@ class TestSessionProgress(ConfigTestCase):
def test_error(self):
# sanity / coverage check
factory = mod.SessionProgress(self.request, 'foo')
factory = mod.SessionProgress(self.request, "foo")
prog = factory("doing things", 2)
prog.update(1)
try:
raise RuntimeError('omg')
raise RuntimeError("omg")
except Exception as error:
prog.handle_error(error)

View file

@ -11,4 +11,4 @@ class TestIncludeMe(TestCase):
with testing.testConfig() as pyramid_config:
# just ensure no error happens when included..
pyramid_config.include('wuttaweb.static')
pyramid_config.include("wuttaweb.static")

View file

@ -19,9 +19,12 @@ class TestNewRequest(TestCase):
def setUp(self):
self.config = WuttaConfig()
self.request = self.make_request()
self.pyramid_config = testing.setUp(request=self.request, settings={
'wutta_config': self.config,
})
self.pyramid_config = testing.setUp(
request=self.request,
settings={
"wutta_config": self.config,
},
)
def tearDown(self):
testing.tearDown()
@ -35,15 +38,15 @@ class TestNewRequest(TestCase):
event = MagicMock(request=self.request)
# request gets a new attr
self.assertFalse(hasattr(self.request, 'wutta_config'))
self.assertFalse(hasattr(self.request, "wutta_config"))
subscribers.new_request(event)
self.assertTrue(hasattr(self.request, 'wutta_config'))
self.assertTrue(hasattr(self.request, "wutta_config"))
self.assertIs(self.request.wutta_config, self.config)
def test_use_oruga_default(self):
# request gets a new attr, false by default
self.assertFalse(hasattr(self.request, 'use_oruga'))
self.assertFalse(hasattr(self.request, "use_oruga"))
event = MagicMock(request=self.request)
subscribers.new_request(event)
self.assertFalse(self.request.use_oruga)
@ -51,17 +54,20 @@ class TestNewRequest(TestCase):
# nb. using 'butterfly' theme should cause the 'use_oruga'
# flag to be turned on by default
self.request = self.make_request()
self.request.registry.settings['wuttaweb.theme'] = 'butterfly'
self.request.registry.settings["wuttaweb.theme"] = "butterfly"
event = MagicMock(request=self.request)
subscribers.new_request(event)
self.assertTrue(self.request.use_oruga)
def test_use_oruga_custom(self):
self.config.setdefault('wuttaweb.oruga_detector.spec', 'tests.test_subscribers:custom_oruga_detector')
self.config.setdefault(
"wuttaweb.oruga_detector.spec",
"tests.test_subscribers:custom_oruga_detector",
)
event = MagicMock(request=self.request)
# request gets a new attr, which should be true
self.assertFalse(hasattr(self.request, 'use_oruga'))
self.assertFalse(hasattr(self.request, "use_oruga"))
subscribers.new_request(event)
self.assertTrue(self.request.use_oruga)
@ -70,20 +76,24 @@ class TestNewRequest(TestCase):
subscribers.new_request(event)
# component tracking dict is missing at first
self.assertFalse(hasattr(self.request, '_wuttaweb_registered_components'))
self.assertFalse(hasattr(self.request, "_wuttaweb_registered_components"))
# registering a component
self.request.register_component('foo-example', 'FooExample')
self.assertTrue(hasattr(self.request, '_wuttaweb_registered_components'))
self.request.register_component("foo-example", "FooExample")
self.assertTrue(hasattr(self.request, "_wuttaweb_registered_components"))
self.assertEqual(len(self.request._wuttaweb_registered_components), 1)
self.assertIn('foo-example', self.request._wuttaweb_registered_components)
self.assertEqual(self.request._wuttaweb_registered_components['foo-example'], 'FooExample')
self.assertIn("foo-example", self.request._wuttaweb_registered_components)
self.assertEqual(
self.request._wuttaweb_registered_components["foo-example"], "FooExample"
)
# re-registering same name
self.request.register_component('foo-example', 'FooExample')
self.request.register_component("foo-example", "FooExample")
self.assertEqual(len(self.request._wuttaweb_registered_components), 1)
self.assertIn('foo-example', self.request._wuttaweb_registered_components)
self.assertEqual(self.request._wuttaweb_registered_components['foo-example'], 'FooExample')
self.assertIn("foo-example", self.request._wuttaweb_registered_components)
self.assertEqual(
self.request._wuttaweb_registered_components["foo-example"], "FooExample"
)
def test_get_referrer(self):
event = MagicMock(request=self.request)
@ -91,33 +101,33 @@ class TestNewRequest(TestCase):
def home(request):
pass
self.pyramid_config.add_route('home', '/')
self.pyramid_config.add_view(home, route_name='home')
self.pyramid_config.add_route("home", "/")
self.pyramid_config.add_view(home, route_name="home")
self.assertFalse(hasattr(self.request, 'get_referrer'))
self.assertFalse(hasattr(self.request, "get_referrer"))
subscribers.new_request(event)
self.assertTrue(hasattr(self.request, 'get_referrer'))
self.assertTrue(hasattr(self.request, "get_referrer"))
# default if no referrer, is home route
url = self.request.get_referrer()
self.assertEqual(url, self.request.route_url('home'))
self.assertEqual(url, self.request.route_url("home"))
# can specify another default
url = self.request.get_referrer(default='https://wuttaproject.org')
self.assertEqual(url, 'https://wuttaproject.org')
url = self.request.get_referrer(default="https://wuttaproject.org")
self.assertEqual(url, "https://wuttaproject.org")
# or referrer can come from user session
self.request.session['referrer'] = 'https://rattailproject.org'
self.assertIn('referrer', self.request.session)
self.request.session["referrer"] = "https://rattailproject.org"
self.assertIn("referrer", self.request.session)
url = self.request.get_referrer()
self.assertEqual(url, 'https://rattailproject.org')
self.assertEqual(url, "https://rattailproject.org")
# nb. referrer should also have been removed from user session
self.assertNotIn('referrer', self.request.session)
self.assertNotIn("referrer", self.request.session)
# or referrer can come from request params
self.request.params['referrer'] = 'https://kernel.org'
self.request.params["referrer"] = "https://kernel.org"
url = self.request.get_referrer()
self.assertEqual(url, 'https://kernel.org')
self.assertEqual(url, "https://kernel.org")
def custom_oruga_detector(request):
@ -127,30 +137,37 @@ def custom_oruga_detector(request):
class TestNewRequestSetUser(TestCase):
def setUp(self):
self.config = WuttaConfig(defaults={
'wutta.db.default.url': 'sqlite://',
})
self.config = WuttaConfig(
defaults={
"wutta.db.default.url": "sqlite://",
}
)
self.request = testing.DummyRequest(wutta_config=self.config)
self.pyramid_config = testing.setUp(request=self.request, settings={
'wutta_config': self.config,
})
self.pyramid_config = testing.setUp(
request=self.request,
settings={
"wutta_config": self.config,
},
)
self.app = self.config.get_app()
model = self.app.model
model.Base.metadata.create_all(bind=self.config.appdb_engine)
self.session = self.app.make_session()
self.user = model.User(username='barney')
self.user = model.User(username="barney")
self.session.add(self.user)
self.session.commit()
self.pyramid_config.set_security_policy(WuttaSecurityPolicy(db_session=self.session))
self.pyramid_config.set_security_policy(
WuttaSecurityPolicy(db_session=self.session)
)
def tearDown(self):
testing.tearDown()
def test_anonymous(self):
self.assertFalse(hasattr(self.request, 'user'))
self.assertFalse(hasattr(self.request, "user"))
event = MagicMock(request=self.request)
subscribers.new_request_set_user(event)
self.assertIsNone(self.request.user)
@ -167,8 +184,8 @@ class TestNewRequestSetUser(TestCase):
event = MagicMock(request=self.request)
# anonymous user
self.assertFalse(hasattr(self.request, 'user'))
self.assertFalse(hasattr(self.request, 'is_admin'))
self.assertFalse(hasattr(self.request, "user"))
self.assertFalse(hasattr(self.request, "is_admin"))
subscribers.new_request_set_user(event, db_session=self.session)
self.assertIsNone(self.request.user)
self.assertFalse(self.request.is_admin)
@ -198,8 +215,8 @@ class TestNewRequestSetUser(TestCase):
event = MagicMock(request=self.request)
# anonymous user
self.assertFalse(hasattr(self.request, 'user'))
self.assertFalse(hasattr(self.request, 'is_root'))
self.assertFalse(hasattr(self.request, "user"))
self.assertFalse(hasattr(self.request, "is_root"))
subscribers.new_request_set_user(event, db_session=self.session)
self.assertIsNone(self.request.user)
self.assertFalse(self.request.is_root)
@ -233,7 +250,7 @@ class TestNewRequestSetUser(TestCase):
del self.request.is_root
# root status flag lives in user session
self.request.session['is_root'] = True
self.request.session["is_root"] = True
subscribers.new_request_set_user(event, db_session=self.session)
self.assertTrue(self.request.is_admin)
self.assertTrue(self.request.is_root)
@ -244,7 +261,7 @@ class TestNewRequestSetUser(TestCase):
event = MagicMock(request=self.request)
# anonymous user
self.assertFalse(hasattr(self.request, 'user_permissions'))
self.assertFalse(hasattr(self.request, "user_permissions"))
subscribers.new_request_set_user(event, db_session=self.session)
self.assertEqual(self.request.user_permissions, set())
@ -254,14 +271,14 @@ class TestNewRequestSetUser(TestCase):
# add user to role with perms
blokes = model.Role(name="Blokes")
self.session.add(blokes)
auth.grant_permission(blokes, 'appinfo.list')
auth.grant_permission(blokes, "appinfo.list")
self.user.roles.append(blokes)
self.session.commit()
# authenticated user, with perms
self.request.user = self.user
subscribers.new_request_set_user(event, db_session=self.session)
self.assertEqual(self.request.user_permissions, {'appinfo.list'})
self.assertEqual(self.request.user_permissions, {"appinfo.list"})
def test_has_perm(self):
model = self.app.model
@ -269,9 +286,9 @@ class TestNewRequestSetUser(TestCase):
event = MagicMock(request=self.request)
# anonymous user
self.assertFalse(hasattr(self.request, 'has_perm'))
self.assertFalse(hasattr(self.request, "has_perm"))
subscribers.new_request_set_user(event, db_session=self.session)
self.assertFalse(self.request.has_perm('appinfo.list'))
self.assertFalse(self.request.has_perm("appinfo.list"))
# reset
del self.request.user_permissions
@ -281,14 +298,14 @@ class TestNewRequestSetUser(TestCase):
# add user to role with perms
blokes = model.Role(name="Blokes")
self.session.add(blokes)
auth.grant_permission(blokes, 'appinfo.list')
auth.grant_permission(blokes, "appinfo.list")
self.user.roles.append(blokes)
self.session.commit()
# authenticated user, with perms
self.request.user = self.user
subscribers.new_request_set_user(event, db_session=self.session)
self.assertTrue(self.request.has_perm('appinfo.list'))
self.assertTrue(self.request.has_perm("appinfo.list"))
# reset
del self.request.user_permissions
@ -299,7 +316,7 @@ class TestNewRequestSetUser(TestCase):
self.user.roles.remove(blokes)
self.session.commit()
subscribers.new_request_set_user(event, db_session=self.session)
self.assertFalse(self.request.has_perm('appinfo.list'))
self.assertFalse(self.request.has_perm("appinfo.list"))
# reset
del self.request.user_permissions
@ -312,9 +329,9 @@ class TestNewRequestSetUser(TestCase):
admin = auth.get_role_administrator(self.session)
self.user.roles.append(admin)
self.session.commit()
self.request.session['is_root'] = True
self.request.session["is_root"] = True
subscribers.new_request_set_user(event, db_session=self.session)
self.assertTrue(self.request.has_perm('appinfo.list'))
self.assertTrue(self.request.has_perm("appinfo.list"))
def test_has_any_perm(self):
model = self.app.model
@ -322,9 +339,9 @@ class TestNewRequestSetUser(TestCase):
event = MagicMock(request=self.request)
# anonymous user
self.assertFalse(hasattr(self.request, 'has_any_perm'))
self.assertFalse(hasattr(self.request, "has_any_perm"))
subscribers.new_request_set_user(event, db_session=self.session)
self.assertFalse(self.request.has_any_perm('appinfo.list'))
self.assertFalse(self.request.has_any_perm("appinfo.list"))
# reset
del self.request.user_permissions
@ -334,14 +351,14 @@ class TestNewRequestSetUser(TestCase):
# add user to role with perms
blokes = model.Role(name="Blokes")
self.session.add(blokes)
auth.grant_permission(blokes, 'appinfo.list')
auth.grant_permission(blokes, "appinfo.list")
self.user.roles.append(blokes)
self.session.commit()
# authenticated user, with perms
self.request.user = self.user
subscribers.new_request_set_user(event, db_session=self.session)
self.assertTrue(self.request.has_any_perm('appinfo.list', 'appinfo.view'))
self.assertTrue(self.request.has_any_perm("appinfo.list", "appinfo.view"))
# reset
del self.request.user_permissions
@ -352,7 +369,7 @@ class TestNewRequestSetUser(TestCase):
self.user.roles.remove(blokes)
self.session.commit()
subscribers.new_request_set_user(event, db_session=self.session)
self.assertFalse(self.request.has_any_perm('appinfo.list'))
self.assertFalse(self.request.has_any_perm("appinfo.list"))
# reset
del self.request.user_permissions
@ -365,64 +382,66 @@ class TestNewRequestSetUser(TestCase):
admin = auth.get_role_administrator(self.session)
self.user.roles.append(admin)
self.session.commit()
self.request.session['is_root'] = True
self.request.session["is_root"] = True
subscribers.new_request_set_user(event, db_session=self.session)
self.assertTrue(self.request.has_any_perm('appinfo.list'))
self.assertTrue(self.request.has_any_perm("appinfo.list"))
class TestBeforeRender(TestCase):
def setUp(self):
self.config = WuttaConfig(defaults={
'wutta.web.menus.handler.spec': 'tests.util:NullMenuHandler',
})
self.config = WuttaConfig(
defaults={
"wutta.web.menus.handler.spec": "tests.util:NullMenuHandler",
}
)
def make_request(self):
request = testing.DummyRequest(use_oruga=False)
request.registry.settings = {'wutta_config': self.config}
request.registry.settings = {"wutta_config": self.config}
request.wutta_config = self.config
return request
def test_basic(self):
request = self.make_request()
event = {'request': request}
event = {"request": request}
# event dict will get populated with more context
subscribers.before_render(event)
self.assertIn('config', event)
self.assertIs(event['config'], self.config)
self.assertIn("config", event)
self.assertIs(event["config"], self.config)
self.assertIn('app', event)
self.assertIs(event['app'], self.config.get_app())
self.assertIn("app", event)
self.assertIs(event["app"], self.config.get_app())
self.assertIn('h', event)
self.assertIs(event['h'], helpers)
self.assertIn("h", event)
self.assertIs(event["h"], helpers)
self.assertIn('url', event)
self.assertIn("url", event)
# TODO: not sure how to test this?
# self.assertIs(event['url'], request.route_url)
self.assertIn('json', event)
self.assertIs(event['json'], json)
self.assertIn("json", event)
self.assertIs(event["json"], json)
# current theme should be 'default' and picker not exposed
self.assertEqual(event['theme'], 'default')
self.assertFalse(event['expose_theme_picker'])
self.assertNotIn('available_themes', event)
self.assertEqual(event["theme"], "default")
self.assertFalse(event["expose_theme_picker"])
self.assertNotIn("available_themes", event)
def test_custom_theme(self):
self.config.setdefault('wuttaweb.themes.expose_picker', 'true')
self.config.setdefault("wuttaweb.themes.expose_picker", "true")
request = self.make_request()
request.registry.settings['wuttaweb.theme'] = 'butterfly'
event = {'request': request}
request.registry.settings["wuttaweb.theme"] = "butterfly"
event = {"request": request}
# event dict will get populated with more context
subscribers.before_render(event)
self.assertEqual(event['theme'], 'butterfly')
self.assertTrue(event['expose_theme_picker'])
self.assertIn('available_themes', event)
self.assertEqual(event['available_themes'], ['default', 'butterfly'])
self.assertEqual(event["theme"], "butterfly")
self.assertTrue(event["expose_theme_picker"])
self.assertIn("available_themes", event)
self.assertEqual(event["available_themes"], ["default", "butterfly"])
class TestIncludeMe(TestCase):
@ -431,4 +450,4 @@ class TestIncludeMe(TestCase):
with testing.testConfig() as pyramid_config:
# just ensure no error happens when included..
pyramid_config.include('wuttaweb.subscribers')
pyramid_config.include("wuttaweb.subscribers")

View file

@ -22,40 +22,40 @@ from wuttaweb.testing import WebTestCase
class TestFieldList(TestCase):
def test_insert_before(self):
fields = mod.FieldList(['f1', 'f2'])
self.assertEqual(fields, ['f1', 'f2'])
fields = mod.FieldList(["f1", "f2"])
self.assertEqual(fields, ["f1", "f2"])
# typical
fields.insert_before('f1', 'XXX')
self.assertEqual(fields, ['XXX', 'f1', 'f2'])
fields.insert_before('f2', 'YYY')
self.assertEqual(fields, ['XXX', 'f1', 'YYY', 'f2'])
fields.insert_before("f1", "XXX")
self.assertEqual(fields, ["XXX", "f1", "f2"])
fields.insert_before("f2", "YYY")
self.assertEqual(fields, ["XXX", "f1", "YYY", "f2"])
# appends new field if reference field is invalid
fields.insert_before('f3', 'ZZZ')
self.assertEqual(fields, ['XXX', 'f1', 'YYY', 'f2', 'ZZZ'])
fields.insert_before("f3", "ZZZ")
self.assertEqual(fields, ["XXX", "f1", "YYY", "f2", "ZZZ"])
def test_insert_after(self):
fields = mod.FieldList(['f1', 'f2'])
self.assertEqual(fields, ['f1', 'f2'])
fields = mod.FieldList(["f1", "f2"])
self.assertEqual(fields, ["f1", "f2"])
# typical
fields.insert_after('f1', 'XXX')
self.assertEqual(fields, ['f1', 'XXX', 'f2'])
fields.insert_after('XXX', 'YYY')
self.assertEqual(fields, ['f1', 'XXX', 'YYY', 'f2'])
fields.insert_after("f1", "XXX")
self.assertEqual(fields, ["f1", "XXX", "f2"])
fields.insert_after("XXX", "YYY")
self.assertEqual(fields, ["f1", "XXX", "YYY", "f2"])
# appends new field if reference field is invalid
fields.insert_after('f3', 'ZZZ')
self.assertEqual(fields, ['f1', 'XXX', 'YYY', 'f2', 'ZZZ'])
fields.insert_after("f3", "ZZZ")
self.assertEqual(fields, ["f1", "XXX", "YYY", "f2", "ZZZ"])
def test_set_sequence(self):
fields = mod.FieldList(['f5', 'f1', 'f3', 'f4', 'f2'])
fields = mod.FieldList(["f5", "f1", "f3", "f4", "f2"])
# setting sequence will only "sort" for explicit fields.
# other fields remain in original order, but at the end.
fields.set_sequence(['f1', 'f2', 'f3'])
self.assertEqual(fields, ['f1', 'f2', 'f3', 'f5', 'f4'])
fields.set_sequence(["f1", "f2", "f3"])
self.assertEqual(fields, ["f1", "f2", "f3", "f5", "f4"])
class TestGetLibVer(TestCase):
@ -66,169 +66,169 @@ class TestGetLibVer(TestCase):
self.request.wutta_config = self.config
def test_buefy_default(self):
version = mod.get_libver(self.request, 'buefy')
self.assertEqual(version, '0.9.25')
version = mod.get_libver(self.request, "buefy")
self.assertEqual(version, "0.9.25")
def test_buefy_custom_old(self):
self.config.setdefault('wuttaweb.buefy_version', '0.9.29')
version = mod.get_libver(self.request, 'buefy')
self.assertEqual(version, '0.9.29')
self.config.setdefault("wuttaweb.buefy_version", "0.9.29")
version = mod.get_libver(self.request, "buefy")
self.assertEqual(version, "0.9.29")
def test_buefy_custom_old_tailbone(self):
self.config.setdefault('tailbone.libver.buefy', '0.9.28')
version = mod.get_libver(self.request, 'buefy', prefix='tailbone')
self.assertEqual(version, '0.9.28')
self.config.setdefault("tailbone.libver.buefy", "0.9.28")
version = mod.get_libver(self.request, "buefy", prefix="tailbone")
self.assertEqual(version, "0.9.28")
def test_buefy_custom_new(self):
self.config.setdefault('wuttaweb.libver.buefy', '0.9.29')
version = mod.get_libver(self.request, 'buefy')
self.assertEqual(version, '0.9.29')
self.config.setdefault("wuttaweb.libver.buefy", "0.9.29")
version = mod.get_libver(self.request, "buefy")
self.assertEqual(version, "0.9.29")
def test_buefy_configured_only(self):
version = mod.get_libver(self.request, 'buefy', configured_only=True)
version = mod.get_libver(self.request, "buefy", configured_only=True)
self.assertIsNone(version)
def test_buefy_default_only(self):
self.config.setdefault('wuttaweb.libver.buefy', '0.9.29')
version = mod.get_libver(self.request, 'buefy', default_only=True)
self.assertEqual(version, '0.9.25')
self.config.setdefault("wuttaweb.libver.buefy", "0.9.29")
version = mod.get_libver(self.request, "buefy", default_only=True)
self.assertEqual(version, "0.9.25")
def test_buefy_css_default(self):
version = mod.get_libver(self.request, 'buefy.css')
self.assertEqual(version, '0.9.25')
version = mod.get_libver(self.request, "buefy.css")
self.assertEqual(version, "0.9.25")
def test_buefy_css_custom_old(self):
# nb. this uses same setting as buefy (js)
self.config.setdefault('wuttaweb.buefy_version', '0.9.29')
version = mod.get_libver(self.request, 'buefy.css')
self.assertEqual(version, '0.9.29')
self.config.setdefault("wuttaweb.buefy_version", "0.9.29")
version = mod.get_libver(self.request, "buefy.css")
self.assertEqual(version, "0.9.29")
def test_buefy_css_custom_new(self):
# nb. this uses same setting as buefy (js)
self.config.setdefault('wuttaweb.libver.buefy', '0.9.29')
version = mod.get_libver(self.request, 'buefy.css')
self.assertEqual(version, '0.9.29')
self.config.setdefault("wuttaweb.libver.buefy", "0.9.29")
version = mod.get_libver(self.request, "buefy.css")
self.assertEqual(version, "0.9.29")
def test_buefy_css_configured_only(self):
version = mod.get_libver(self.request, 'buefy.css', configured_only=True)
version = mod.get_libver(self.request, "buefy.css", configured_only=True)
self.assertIsNone(version)
def test_buefy_css_default_only(self):
self.config.setdefault('wuttaweb.libver.buefy', '0.9.29')
version = mod.get_libver(self.request, 'buefy.css', default_only=True)
self.assertEqual(version, '0.9.25')
self.config.setdefault("wuttaweb.libver.buefy", "0.9.29")
version = mod.get_libver(self.request, "buefy.css", default_only=True)
self.assertEqual(version, "0.9.25")
def test_vue_default(self):
version = mod.get_libver(self.request, 'vue')
self.assertEqual(version, '2.6.14')
version = mod.get_libver(self.request, "vue")
self.assertEqual(version, "2.6.14")
def test_vue_custom_old(self):
self.config.setdefault('wuttaweb.vue_version', '3.4.31')
version = mod.get_libver(self.request, 'vue')
self.assertEqual(version, '3.4.31')
self.config.setdefault("wuttaweb.vue_version", "3.4.31")
version = mod.get_libver(self.request, "vue")
self.assertEqual(version, "3.4.31")
def test_vue_custom_new(self):
self.config.setdefault('wuttaweb.libver.vue', '3.4.31')
version = mod.get_libver(self.request, 'vue')
self.assertEqual(version, '3.4.31')
self.config.setdefault("wuttaweb.libver.vue", "3.4.31")
version = mod.get_libver(self.request, "vue")
self.assertEqual(version, "3.4.31")
def test_vue_configured_only(self):
version = mod.get_libver(self.request, 'vue', configured_only=True)
version = mod.get_libver(self.request, "vue", configured_only=True)
self.assertIsNone(version)
def test_vue_default_only(self):
self.config.setdefault('wuttaweb.libver.vue', '3.4.31')
version = mod.get_libver(self.request, 'vue', default_only=True)
self.assertEqual(version, '2.6.14')
self.config.setdefault("wuttaweb.libver.vue", "3.4.31")
version = mod.get_libver(self.request, "vue", default_only=True)
self.assertEqual(version, "2.6.14")
def test_vue_resource_default(self):
version = mod.get_libver(self.request, 'vue_resource')
self.assertEqual(version, '1.5.3')
version = mod.get_libver(self.request, "vue_resource")
self.assertEqual(version, "1.5.3")
def test_vue_resource_custom(self):
self.config.setdefault('wuttaweb.libver.vue_resource', '1.5.3')
version = mod.get_libver(self.request, 'vue_resource')
self.assertEqual(version, '1.5.3')
self.config.setdefault("wuttaweb.libver.vue_resource", "1.5.3")
version = mod.get_libver(self.request, "vue_resource")
self.assertEqual(version, "1.5.3")
def test_fontawesome_default(self):
version = mod.get_libver(self.request, 'fontawesome')
self.assertEqual(version, '5.3.1')
version = mod.get_libver(self.request, "fontawesome")
self.assertEqual(version, "5.3.1")
def test_fontawesome_custom(self):
self.config.setdefault('wuttaweb.libver.fontawesome', '5.6.3')
version = mod.get_libver(self.request, 'fontawesome')
self.assertEqual(version, '5.6.3')
self.config.setdefault("wuttaweb.libver.fontawesome", "5.6.3")
version = mod.get_libver(self.request, "fontawesome")
self.assertEqual(version, "5.6.3")
def test_bb_vue_default(self):
version = mod.get_libver(self.request, 'bb_vue')
self.assertEqual(version, '3.5.18')
version = mod.get_libver(self.request, "bb_vue")
self.assertEqual(version, "3.5.18")
def test_bb_vue_custom(self):
self.config.setdefault('wuttaweb.libver.bb_vue', '3.4.30')
version = mod.get_libver(self.request, 'bb_vue')
self.assertEqual(version, '3.4.30')
self.config.setdefault("wuttaweb.libver.bb_vue", "3.4.30")
version = mod.get_libver(self.request, "bb_vue")
self.assertEqual(version, "3.4.30")
def test_bb_oruga_default(self):
version = mod.get_libver(self.request, 'bb_oruga')
self.assertEqual(version, '0.11.4')
version = mod.get_libver(self.request, "bb_oruga")
self.assertEqual(version, "0.11.4")
def test_bb_oruga_custom(self):
self.config.setdefault('wuttaweb.libver.bb_oruga', '0.8.11')
version = mod.get_libver(self.request, 'bb_oruga')
self.assertEqual(version, '0.8.11')
self.config.setdefault("wuttaweb.libver.bb_oruga", "0.8.11")
version = mod.get_libver(self.request, "bb_oruga")
self.assertEqual(version, "0.8.11")
def test_bb_oruga_bulma_default(self):
version = mod.get_libver(self.request, 'bb_oruga_bulma')
self.assertEqual(version, '0.7.3')
version = mod.get_libver(self.request, 'bb_oruga_bulma_css')
self.assertEqual(version, '0.7.3')
version = mod.get_libver(self.request, "bb_oruga_bulma")
self.assertEqual(version, "0.7.3")
version = mod.get_libver(self.request, "bb_oruga_bulma_css")
self.assertEqual(version, "0.7.3")
def test_bb_oruga_bulma_custom(self):
self.config.setdefault('wuttaweb.libver.bb_oruga_bulma', '0.2.11')
version = mod.get_libver(self.request, 'bb_oruga_bulma')
self.assertEqual(version, '0.2.11')
self.config.setdefault("wuttaweb.libver.bb_oruga_bulma", "0.2.11")
version = mod.get_libver(self.request, "bb_oruga_bulma")
self.assertEqual(version, "0.2.11")
def test_bb_fontawesome_svg_core_default(self):
version = mod.get_libver(self.request, 'bb_fontawesome_svg_core')
self.assertEqual(version, '7.0.0')
version = mod.get_libver(self.request, "bb_fontawesome_svg_core")
self.assertEqual(version, "7.0.0")
def test_bb_fontawesome_svg_core_custom(self):
self.config.setdefault('wuttaweb.libver.bb_fontawesome_svg_core', '6.5.1')
version = mod.get_libver(self.request, 'bb_fontawesome_svg_core')
self.assertEqual(version, '6.5.1')
self.config.setdefault("wuttaweb.libver.bb_fontawesome_svg_core", "6.5.1")
version = mod.get_libver(self.request, "bb_fontawesome_svg_core")
self.assertEqual(version, "6.5.1")
def test_bb_free_solid_svg_icons_default(self):
version = mod.get_libver(self.request, 'bb_free_solid_svg_icons')
self.assertEqual(version, '7.0.0')
version = mod.get_libver(self.request, "bb_free_solid_svg_icons")
self.assertEqual(version, "7.0.0")
def test_bb_free_solid_svg_icons_custom(self):
self.config.setdefault('wuttaweb.libver.bb_free_solid_svg_icons', '6.5.1')
version = mod.get_libver(self.request, 'bb_free_solid_svg_icons')
self.assertEqual(version, '6.5.1')
self.config.setdefault("wuttaweb.libver.bb_free_solid_svg_icons", "6.5.1")
version = mod.get_libver(self.request, "bb_free_solid_svg_icons")
self.assertEqual(version, "6.5.1")
def test_bb_vue_fontawesome_default(self):
version = mod.get_libver(self.request, 'bb_vue_fontawesome')
self.assertEqual(version, '3.1.1')
version = mod.get_libver(self.request, "bb_vue_fontawesome")
self.assertEqual(version, "3.1.1")
def test_bb_vue_fontawesome_custom(self):
self.config.setdefault('wuttaweb.libver.bb_vue_fontawesome', '3.0.8')
version = mod.get_libver(self.request, 'bb_vue_fontawesome')
self.assertEqual(version, '3.0.8')
self.config.setdefault("wuttaweb.libver.bb_vue_fontawesome", "3.0.8")
version = mod.get_libver(self.request, "bb_vue_fontawesome")
self.assertEqual(version, "3.0.8")
libcache = Library('testing', 'libcache')
vue_js = Resource(libcache, 'vue.js')
vue_resource_js = Resource(libcache, 'vue_resource.js')
buefy_js = Resource(libcache, 'buefy.js')
buefy_css = Resource(libcache, 'buefy.css')
fontawesome_js = Resource(libcache, 'fontawesome.js')
bb_vue_js = Resource(libcache, 'bb_vue.js')
bb_oruga_js = Resource(libcache, 'bb_oruga.js')
bb_oruga_bulma_js = Resource(libcache, 'bb_oruga_bulma.js')
bb_oruga_bulma_css = Resource(libcache, 'bb_oruga_bulma.css')
bb_fontawesome_svg_core_js = Resource(libcache, 'bb_fontawesome_svg_core.js')
bb_free_solid_svg_icons_js = Resource(libcache, 'bb_free_solid_svg_icons.js')
bb_vue_fontawesome_js = Resource(libcache, 'bb_vue_fontawesome.js')
libcache = Library("testing", "libcache")
vue_js = Resource(libcache, "vue.js")
vue_resource_js = Resource(libcache, "vue_resource.js")
buefy_js = Resource(libcache, "buefy.js")
buefy_css = Resource(libcache, "buefy.css")
fontawesome_js = Resource(libcache, "fontawesome.js")
bb_vue_js = Resource(libcache, "bb_vue.js")
bb_oruga_js = Resource(libcache, "bb_oruga.js")
bb_oruga_bulma_js = Resource(libcache, "bb_oruga_bulma.js")
bb_oruga_bulma_css = Resource(libcache, "bb_oruga_bulma.css")
bb_fontawesome_svg_core_js = Resource(libcache, "bb_fontawesome_svg_core.js")
bb_free_solid_svg_icons_js = Resource(libcache, "bb_free_solid_svg_icons.js")
bb_vue_fontawesome_js = Resource(libcache, "bb_vue_fontawesome.js")
class TestGetLibUrl(TestCase):
@ -242,203 +242,226 @@ class TestGetLibUrl(TestCase):
testing.tearDown()
def setup_fanstatic(self, register=True):
self.pyramid_config.include('pyramid_fanstatic')
self.pyramid_config.include("pyramid_fanstatic")
if register:
self.config.setdefault('wuttaweb.static_libcache.module',
'tests.test_util')
self.config.setdefault("wuttaweb.static_libcache.module", "tests.test_util")
needed = MagicMock()
needed.library_url = MagicMock(return_value='/fanstatic')
self.request.environ['fanstatic.needed'] = needed
self.request.script_name = '/wutta'
needed.library_url = MagicMock(return_value="/fanstatic")
self.request.environ["fanstatic.needed"] = needed
self.request.script_name = "/wutta"
def test_buefy_default(self):
url = mod.get_liburl(self.request, 'buefy')
self.assertEqual(url, 'https://unpkg.com/buefy@0.9.25/dist/buefy.min.js')
url = mod.get_liburl(self.request, "buefy")
self.assertEqual(url, "https://unpkg.com/buefy@0.9.25/dist/buefy.min.js")
def test_buefy_custom(self):
self.config.setdefault('wuttaweb.liburl.buefy', '/lib/buefy.js')
url = mod.get_liburl(self.request, 'buefy')
self.assertEqual(url, '/lib/buefy.js')
self.config.setdefault("wuttaweb.liburl.buefy", "/lib/buefy.js")
url = mod.get_liburl(self.request, "buefy")
self.assertEqual(url, "/lib/buefy.js")
def test_buefy_custom_tailbone(self):
self.config.setdefault('tailbone.liburl.buefy', '/tailbone/buefy.js')
url = mod.get_liburl(self.request, 'buefy', prefix='tailbone')
self.assertEqual(url, '/tailbone/buefy.js')
self.config.setdefault("tailbone.liburl.buefy", "/tailbone/buefy.js")
url = mod.get_liburl(self.request, "buefy", prefix="tailbone")
self.assertEqual(url, "/tailbone/buefy.js")
def test_buefy_default_only(self):
self.config.setdefault('wuttaweb.liburl.buefy', '/lib/buefy.js')
url = mod.get_liburl(self.request, 'buefy', default_only=True)
self.assertEqual(url, 'https://unpkg.com/buefy@0.9.25/dist/buefy.min.js')
self.config.setdefault("wuttaweb.liburl.buefy", "/lib/buefy.js")
url = mod.get_liburl(self.request, "buefy", default_only=True)
self.assertEqual(url, "https://unpkg.com/buefy@0.9.25/dist/buefy.min.js")
def test_buefy_configured_only(self):
url = mod.get_liburl(self.request, 'buefy', configured_only=True)
url = mod.get_liburl(self.request, "buefy", configured_only=True)
self.assertIsNone(url)
def test_buefy_fanstatic(self):
self.setup_fanstatic()
url = mod.get_liburl(self.request, 'buefy')
self.assertEqual(url, '/wutta/fanstatic/buefy.js')
url = mod.get_liburl(self.request, "buefy")
self.assertEqual(url, "/wutta/fanstatic/buefy.js")
def test_buefy_fanstatic_tailbone(self):
self.setup_fanstatic(register=False)
self.config.setdefault('tailbone.static_libcache.module', 'tests.test_util')
url = mod.get_liburl(self.request, 'buefy', prefix='tailbone')
self.assertEqual(url, '/wutta/fanstatic/buefy.js')
self.config.setdefault("tailbone.static_libcache.module", "tests.test_util")
url = mod.get_liburl(self.request, "buefy", prefix="tailbone")
self.assertEqual(url, "/wutta/fanstatic/buefy.js")
def test_buefy_css_default(self):
url = mod.get_liburl(self.request, 'buefy.css')
self.assertEqual(url, 'https://unpkg.com/buefy@0.9.25/dist/buefy.min.css')
url = mod.get_liburl(self.request, "buefy.css")
self.assertEqual(url, "https://unpkg.com/buefy@0.9.25/dist/buefy.min.css")
def test_buefy_css_custom(self):
self.config.setdefault('wuttaweb.liburl.buefy.css', '/lib/buefy.css')
url = mod.get_liburl(self.request, 'buefy.css')
self.assertEqual(url, '/lib/buefy.css')
self.config.setdefault("wuttaweb.liburl.buefy.css", "/lib/buefy.css")
url = mod.get_liburl(self.request, "buefy.css")
self.assertEqual(url, "/lib/buefy.css")
def test_buefy_css_fanstatic(self):
self.setup_fanstatic()
url = mod.get_liburl(self.request, 'buefy.css')
self.assertEqual(url, '/wutta/fanstatic/buefy.css')
url = mod.get_liburl(self.request, "buefy.css")
self.assertEqual(url, "/wutta/fanstatic/buefy.css")
def test_vue_default(self):
url = mod.get_liburl(self.request, 'vue')
self.assertEqual(url, 'https://unpkg.com/vue@2.6.14/dist/vue.min.js')
url = mod.get_liburl(self.request, "vue")
self.assertEqual(url, "https://unpkg.com/vue@2.6.14/dist/vue.min.js")
def test_vue_custom(self):
self.config.setdefault('wuttaweb.liburl.vue', '/lib/vue.js')
url = mod.get_liburl(self.request, 'vue')
self.assertEqual(url, '/lib/vue.js')
self.config.setdefault("wuttaweb.liburl.vue", "/lib/vue.js")
url = mod.get_liburl(self.request, "vue")
self.assertEqual(url, "/lib/vue.js")
def test_vue_fanstatic(self):
self.setup_fanstatic()
url = mod.get_liburl(self.request, 'vue')
self.assertEqual(url, '/wutta/fanstatic/vue.js')
url = mod.get_liburl(self.request, "vue")
self.assertEqual(url, "/wutta/fanstatic/vue.js")
def test_vue_resource_default(self):
url = mod.get_liburl(self.request, 'vue_resource')
self.assertEqual(url, 'https://cdn.jsdelivr.net/npm/vue-resource@1.5.3')
url = mod.get_liburl(self.request, "vue_resource")
self.assertEqual(url, "https://cdn.jsdelivr.net/npm/vue-resource@1.5.3")
def test_vue_resource_custom(self):
self.config.setdefault('wuttaweb.liburl.vue_resource', '/lib/vue-resource.js')
url = mod.get_liburl(self.request, 'vue_resource')
self.assertEqual(url, '/lib/vue-resource.js')
self.config.setdefault("wuttaweb.liburl.vue_resource", "/lib/vue-resource.js")
url = mod.get_liburl(self.request, "vue_resource")
self.assertEqual(url, "/lib/vue-resource.js")
def test_vue_resource_fanstatic(self):
self.setup_fanstatic()
url = mod.get_liburl(self.request, 'vue_resource')
self.assertEqual(url, '/wutta/fanstatic/vue_resource.js')
url = mod.get_liburl(self.request, "vue_resource")
self.assertEqual(url, "/wutta/fanstatic/vue_resource.js")
def test_fontawesome_default(self):
url = mod.get_liburl(self.request, 'fontawesome')
self.assertEqual(url, 'https://use.fontawesome.com/releases/v5.3.1/js/all.js')
url = mod.get_liburl(self.request, "fontawesome")
self.assertEqual(url, "https://use.fontawesome.com/releases/v5.3.1/js/all.js")
def test_fontawesome_custom(self):
self.config.setdefault('wuttaweb.liburl.fontawesome', '/lib/fontawesome.js')
url = mod.get_liburl(self.request, 'fontawesome')
self.assertEqual(url, '/lib/fontawesome.js')
self.config.setdefault("wuttaweb.liburl.fontawesome", "/lib/fontawesome.js")
url = mod.get_liburl(self.request, "fontawesome")
self.assertEqual(url, "/lib/fontawesome.js")
def test_fontawesome_fanstatic(self):
self.setup_fanstatic()
url = mod.get_liburl(self.request, 'fontawesome')
self.assertEqual(url, '/wutta/fanstatic/fontawesome.js')
url = mod.get_liburl(self.request, "fontawesome")
self.assertEqual(url, "/wutta/fanstatic/fontawesome.js")
def test_bb_vue_default(self):
url = mod.get_liburl(self.request, 'bb_vue')
self.assertEqual(url, 'https://unpkg.com/vue@3.5.18/dist/vue.esm-browser.prod.js')
url = mod.get_liburl(self.request, "bb_vue")
self.assertEqual(
url, "https://unpkg.com/vue@3.5.18/dist/vue.esm-browser.prod.js"
)
def test_bb_vue_custom(self):
self.config.setdefault('wuttaweb.liburl.bb_vue', '/lib/vue.js')
url = mod.get_liburl(self.request, 'bb_vue')
self.assertEqual(url, '/lib/vue.js')
self.config.setdefault("wuttaweb.liburl.bb_vue", "/lib/vue.js")
url = mod.get_liburl(self.request, "bb_vue")
self.assertEqual(url, "/lib/vue.js")
def test_bb_vue_fanstatic(self):
self.setup_fanstatic()
url = mod.get_liburl(self.request, 'bb_vue')
self.assertEqual(url, '/wutta/fanstatic/bb_vue.js')
url = mod.get_liburl(self.request, "bb_vue")
self.assertEqual(url, "/wutta/fanstatic/bb_vue.js")
def test_bb_oruga_default(self):
url = mod.get_liburl(self.request, 'bb_oruga')
self.assertEqual(url, 'https://unpkg.com/@oruga-ui/oruga-next@0.11.4/dist/oruga.mjs')
url = mod.get_liburl(self.request, "bb_oruga")
self.assertEqual(
url, "https://unpkg.com/@oruga-ui/oruga-next@0.11.4/dist/oruga.mjs"
)
def test_bb_oruga_custom(self):
self.config.setdefault('wuttaweb.liburl.bb_oruga', '/lib/oruga.js')
url = mod.get_liburl(self.request, 'bb_oruga')
self.assertEqual(url, '/lib/oruga.js')
self.config.setdefault("wuttaweb.liburl.bb_oruga", "/lib/oruga.js")
url = mod.get_liburl(self.request, "bb_oruga")
self.assertEqual(url, "/lib/oruga.js")
def test_bb_oruga_fanstatic(self):
self.setup_fanstatic()
url = mod.get_liburl(self.request, 'bb_oruga')
self.assertEqual(url, '/wutta/fanstatic/bb_oruga.js')
url = mod.get_liburl(self.request, "bb_oruga")
self.assertEqual(url, "/wutta/fanstatic/bb_oruga.js")
def test_bb_oruga_bulma_default(self):
url = mod.get_liburl(self.request, 'bb_oruga_bulma')
self.assertEqual(url, 'https://unpkg.com/@oruga-ui/theme-bulma@0.7.3/dist/bulma.js')
url = mod.get_liburl(self.request, "bb_oruga_bulma")
self.assertEqual(
url, "https://unpkg.com/@oruga-ui/theme-bulma@0.7.3/dist/bulma.js"
)
def test_bb_oruga_bulma_custom(self):
self.config.setdefault('wuttaweb.liburl.bb_oruga_bulma', '/lib/oruga_bulma.js')
url = mod.get_liburl(self.request, 'bb_oruga_bulma')
self.assertEqual(url, '/lib/oruga_bulma.js')
self.config.setdefault("wuttaweb.liburl.bb_oruga_bulma", "/lib/oruga_bulma.js")
url = mod.get_liburl(self.request, "bb_oruga_bulma")
self.assertEqual(url, "/lib/oruga_bulma.js")
def test_bb_oruga_bulma_fanstatic(self):
self.setup_fanstatic()
url = mod.get_liburl(self.request, 'bb_oruga_bulma')
self.assertEqual(url, '/wutta/fanstatic/bb_oruga_bulma.js')
url = mod.get_liburl(self.request, "bb_oruga_bulma")
self.assertEqual(url, "/wutta/fanstatic/bb_oruga_bulma.js")
def test_bb_oruga_bulma_css_default(self):
url = mod.get_liburl(self.request, 'bb_oruga_bulma_css')
self.assertEqual(url, 'https://unpkg.com/@oruga-ui/theme-bulma@0.7.3/dist/bulma.css')
url = mod.get_liburl(self.request, "bb_oruga_bulma_css")
self.assertEqual(
url, "https://unpkg.com/@oruga-ui/theme-bulma@0.7.3/dist/bulma.css"
)
def test_bb_oruga_bulma_css_custom(self):
self.config.setdefault('wuttaweb.liburl.bb_oruga_bulma_css', '/lib/oruga-bulma.css')
url = mod.get_liburl(self.request, 'bb_oruga_bulma_css')
self.assertEqual(url, '/lib/oruga-bulma.css')
self.config.setdefault(
"wuttaweb.liburl.bb_oruga_bulma_css", "/lib/oruga-bulma.css"
)
url = mod.get_liburl(self.request, "bb_oruga_bulma_css")
self.assertEqual(url, "/lib/oruga-bulma.css")
def test_bb_oruga_bulma_css_fanstatic(self):
self.setup_fanstatic()
url = mod.get_liburl(self.request, 'bb_oruga_bulma_css')
self.assertEqual(url, '/wutta/fanstatic/bb_oruga_bulma.css')
url = mod.get_liburl(self.request, "bb_oruga_bulma_css")
self.assertEqual(url, "/wutta/fanstatic/bb_oruga_bulma.css")
def test_bb_fontawesome_svg_core_default(self):
url = mod.get_liburl(self.request, 'bb_fontawesome_svg_core')
self.assertEqual(url, 'https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-svg-core@7.0.0/+esm')
url = mod.get_liburl(self.request, "bb_fontawesome_svg_core")
self.assertEqual(
url,
"https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-svg-core@7.0.0/+esm",
)
def test_bb_fontawesome_svg_core_custom(self):
self.config.setdefault('wuttaweb.liburl.bb_fontawesome_svg_core', '/lib/fontawesome-svg-core.js')
url = mod.get_liburl(self.request, 'bb_fontawesome_svg_core')
self.assertEqual(url, '/lib/fontawesome-svg-core.js')
self.config.setdefault(
"wuttaweb.liburl.bb_fontawesome_svg_core", "/lib/fontawesome-svg-core.js"
)
url = mod.get_liburl(self.request, "bb_fontawesome_svg_core")
self.assertEqual(url, "/lib/fontawesome-svg-core.js")
def test_bb_fontawesome_svg_core_fanstatic(self):
self.setup_fanstatic()
url = mod.get_liburl(self.request, 'bb_fontawesome_svg_core')
self.assertEqual(url, '/wutta/fanstatic/bb_fontawesome_svg_core.js')
url = mod.get_liburl(self.request, "bb_fontawesome_svg_core")
self.assertEqual(url, "/wutta/fanstatic/bb_fontawesome_svg_core.js")
def test_bb_free_solid_svg_icons_default(self):
url = mod.get_liburl(self.request, 'bb_free_solid_svg_icons')
self.assertEqual(url, 'https://cdn.jsdelivr.net/npm/@fortawesome/free-solid-svg-icons@7.0.0/+esm')
url = mod.get_liburl(self.request, "bb_free_solid_svg_icons")
self.assertEqual(
url,
"https://cdn.jsdelivr.net/npm/@fortawesome/free-solid-svg-icons@7.0.0/+esm",
)
def test_bb_free_solid_svg_icons_custom(self):
self.config.setdefault('wuttaweb.liburl.bb_free_solid_svg_icons', '/lib/free-solid-svg-icons.js')
url = mod.get_liburl(self.request, 'bb_free_solid_svg_icons')
self.assertEqual(url, '/lib/free-solid-svg-icons.js')
self.config.setdefault(
"wuttaweb.liburl.bb_free_solid_svg_icons", "/lib/free-solid-svg-icons.js"
)
url = mod.get_liburl(self.request, "bb_free_solid_svg_icons")
self.assertEqual(url, "/lib/free-solid-svg-icons.js")
def test_bb_free_solid_svg_icons_fanstatic(self):
self.setup_fanstatic()
url = mod.get_liburl(self.request, 'bb_free_solid_svg_icons')
self.assertEqual(url, '/wutta/fanstatic/bb_free_solid_svg_icons.js')
url = mod.get_liburl(self.request, "bb_free_solid_svg_icons")
self.assertEqual(url, "/wutta/fanstatic/bb_free_solid_svg_icons.js")
def test_bb_vue_fontawesome_default(self):
url = mod.get_liburl(self.request, 'bb_vue_fontawesome')
self.assertEqual(url, 'https://cdn.jsdelivr.net/npm/@fortawesome/vue-fontawesome@3.1.1/+esm')
url = mod.get_liburl(self.request, "bb_vue_fontawesome")
self.assertEqual(
url, "https://cdn.jsdelivr.net/npm/@fortawesome/vue-fontawesome@3.1.1/+esm"
)
def test_bb_vue_fontawesome_custom(self):
self.config.setdefault('wuttaweb.liburl.bb_vue_fontawesome', '/lib/vue-fontawesome.js')
url = mod.get_liburl(self.request, 'bb_vue_fontawesome')
self.assertEqual(url, '/lib/vue-fontawesome.js')
self.config.setdefault(
"wuttaweb.liburl.bb_vue_fontawesome", "/lib/vue-fontawesome.js"
)
url = mod.get_liburl(self.request, "bb_vue_fontawesome")
self.assertEqual(url, "/lib/vue-fontawesome.js")
def test_bb_vue_fontawesome_fanstatic(self):
self.setup_fanstatic()
url = mod.get_liburl(self.request, 'bb_vue_fontawesome')
self.assertEqual(url, '/wutta/fanstatic/bb_vue_fontawesome.js')
url = mod.get_liburl(self.request, "bb_vue_fontawesome")
self.assertEqual(url, "/wutta/fanstatic/bb_vue_fontawesome.js")
class TestGetFormData(TestCase):
@ -447,25 +470,25 @@ class TestGetFormData(TestCase):
self.config = WuttaConfig()
def make_request(self, **kwargs):
kwargs.setdefault('wutta_config', self.config)
kwargs.setdefault('POST', {'foo1': 'bar'})
kwargs.setdefault('json_body', {'foo2': 'baz'})
kwargs.setdefault("wutta_config", self.config)
kwargs.setdefault("POST", {"foo1": "bar"})
kwargs.setdefault("json_body", {"foo2": "baz"})
return testing.DummyRequest(**kwargs)
def test_default(self):
request = self.make_request()
data = mod.get_form_data(request)
self.assertEqual(data, {'foo1': 'bar'})
self.assertEqual(data, {"foo1": "bar"})
def test_is_xhr(self):
request = self.make_request(POST=None, is_xhr=True)
data = mod.get_form_data(request)
self.assertEqual(data, {'foo2': 'baz'})
self.assertEqual(data, {"foo2": "baz"})
def test_content_type(self):
request = self.make_request(POST=None, content_type='application/json')
request = self.make_request(POST=None, content_type="application/json")
data = mod.get_form_data(request)
self.assertEqual(data, {'foo2': 'baz'})
self.assertEqual(data, {"foo2": "baz"})
class TestGetModelFields(ConfigTestCase):
@ -481,41 +504,47 @@ class TestGetModelFields(ConfigTestCase):
def test_basic(self):
model = self.app.model
fields = mod.get_model_fields(self.config, model.Setting)
self.assertEqual(fields, ['name', 'value'])
self.assertEqual(fields, ["name", "value"])
def test_include_fk(self):
model = self.app.model
# fk excluded by default
fields = mod.get_model_fields(self.config, model.User)
self.assertNotIn('person_uuid', fields)
self.assertIn('person', fields)
self.assertNotIn("person_uuid", fields)
self.assertIn("person", fields)
# fk can be included
fields = mod.get_model_fields(self.config, model.User, include_fk=True)
self.assertIn('person_uuid', fields)
self.assertIn('person', fields)
self.assertIn("person_uuid", fields)
self.assertIn("person", fields)
def test_avoid_versions(self):
model = self.app.model
mapper = MagicMock(iterate_properties = [
MagicMock(key='uuid'),
MagicMock(key='full_name'),
MagicMock(key='first_name'),
MagicMock(key='middle_name'),
MagicMock(key='last_name'),
MagicMock(key='versions'),
])
mapper = MagicMock(
iterate_properties=[
MagicMock(key="uuid"),
MagicMock(key="full_name"),
MagicMock(key="first_name"),
MagicMock(key="middle_name"),
MagicMock(key="last_name"),
MagicMock(key="versions"),
]
)
with patch.object(mod, 'sa') as sa:
with patch.object(mod, "sa") as sa:
sa.inspect.return_value = mapper
with patch.object(self.app, 'continuum_is_enabled', return_value=True):
with patch.object(self.app, "continuum_is_enabled", return_value=True):
fields = mod.get_model_fields(self.config, model.Person)
# nb. no versions field
self.assertEqual(set(fields), set(['uuid', 'full_name', 'first_name',
'middle_name', 'last_name']))
self.assertEqual(
set(fields),
set(
["uuid", "full_name", "first_name", "middle_name", "last_name"]
),
)
class TestGetCsrfToken(TestCase):
@ -541,7 +570,7 @@ class TestGetCsrfToken(TestCase):
# nb. dummy request always returns same token, so must
# trick it into thinking it doesn't have one yet
with patch.object(self.request.session, 'get_csrf_token', return_value=None):
with patch.object(self.request.session, "get_csrf_token", return_value=None):
token = mod.get_csrf_token(self.request)
self.assertIsNotNone(token)
@ -577,7 +606,7 @@ class TestMakeJsonSafe(TestCase):
model = self.app.model
person = model.Person(full_name="Betty Boop")
self.assertRaises(TypeError, json.dumps, person)
value = mod.make_json_safe(person, key='person')
value = mod.make_json_safe(person, key="person")
self.assertEqual(value, "Betty Boop")
def test_uuid(self):
@ -586,7 +615,7 @@ class TestMakeJsonSafe(TestCase):
self.assertEqual(value, uuid.hex)
def test_decimal(self):
value = decimal.Decimal('42.42')
value = decimal.Decimal("42.42")
self.assertNotEqual(value, 42.42)
result = mod.make_json_safe(value)
self.assertEqual(result, 42.42)
@ -596,34 +625,40 @@ class TestMakeJsonSafe(TestCase):
person = model.Person(full_name="Betty Boop")
data = {
'foo': 'bar',
'person': person,
"foo": "bar",
"person": person,
}
self.assertRaises(TypeError, json.dumps, data)
value = mod.make_json_safe(data)
self.assertEqual(value, {
'foo': 'bar',
'person': "Betty Boop",
})
self.assertEqual(
value,
{
"foo": "bar",
"person": "Betty Boop",
},
)
def test_list(self):
model = self.app.model
person = model.Person(full_name="Betty Boop")
data = [
'foo',
'bar',
"foo",
"bar",
person,
]
self.assertRaises(TypeError, json.dumps, data)
value = mod.make_json_safe(data)
self.assertEqual(value, [
'foo',
'bar',
"Betty Boop",
])
self.assertEqual(
value,
[
"foo",
"bar",
"Betty Boop",
],
)
class TestGetAvailableThemes(TestCase):
@ -634,63 +669,65 @@ class TestGetAvailableThemes(TestCase):
def test_defaults(self):
themes = mod.get_available_themes(self.config)
self.assertEqual(themes, ['default', 'butterfly'])
self.assertEqual(themes, ["default", "butterfly"])
def test_sorting(self):
self.config.setdefault('wuttaweb.themes.keys', 'default, foo2, foo4, foo1')
self.config.setdefault("wuttaweb.themes.keys", "default, foo2, foo4, foo1")
themes = mod.get_available_themes(self.config)
self.assertEqual(themes, ['default', 'foo1', 'foo2', 'foo4'])
self.assertEqual(themes, ["default", "foo1", "foo2", "foo4"])
def test_default_omitted(self):
self.config.setdefault('wuttaweb.themes.keys', 'butterfly, foo')
self.config.setdefault("wuttaweb.themes.keys", "butterfly, foo")
themes = mod.get_available_themes(self.config)
self.assertEqual(themes, ['default', 'butterfly', 'foo'])
self.assertEqual(themes, ["default", "butterfly", "foo"])
def test_default_notfirst(self):
self.config.setdefault('wuttaweb.themes.keys', 'butterfly, foo, default')
self.config.setdefault("wuttaweb.themes.keys", "butterfly, foo, default")
themes = mod.get_available_themes(self.config)
self.assertEqual(themes, ['default', 'butterfly', 'foo'])
self.assertEqual(themes, ["default", "butterfly", "foo"])
class TestGetEffectiveTheme(DataTestCase):
def test_default(self):
theme = mod.get_effective_theme(self.config)
self.assertEqual(theme, 'default')
self.assertEqual(theme, "default")
def test_override_config(self):
self.app.save_setting(self.session, 'wuttaweb.theme', 'butterfly')
self.app.save_setting(self.session, "wuttaweb.theme", "butterfly")
self.session.commit()
theme = mod.get_effective_theme(self.config)
self.assertEqual(theme, 'butterfly')
self.assertEqual(theme, "butterfly")
def test_override_param(self):
theme = mod.get_effective_theme(self.config, theme='butterfly')
self.assertEqual(theme, 'butterfly')
theme = mod.get_effective_theme(self.config, theme="butterfly")
self.assertEqual(theme, "butterfly")
def test_invalid(self):
self.assertRaises(ValueError, mod.get_effective_theme, self.config, theme='invalid')
self.assertRaises(
ValueError, mod.get_effective_theme, self.config, theme="invalid"
)
class TestThemeTemplatePath(DataTestCase):
def test_default(self):
path = mod.get_theme_template_path(self.config, theme='default')
path = mod.get_theme_template_path(self.config, theme="default")
# nb. even though the path does not exist, we still want to
# pretend like it does, hence prev call should return this:
expected = resource_path('wuttaweb:templates/themes/default')
expected = resource_path("wuttaweb:templates/themes/default")
self.assertEqual(path, expected)
def test_default(self):
path = mod.get_theme_template_path(self.config, theme='butterfly')
expected = resource_path('wuttaweb:templates/themes/butterfly')
path = mod.get_theme_template_path(self.config, theme="butterfly")
expected = resource_path("wuttaweb:templates/themes/butterfly")
self.assertEqual(path, expected)
def test_custom(self):
self.config.setdefault('wuttaweb.themes.keys', 'default, butterfly, poser')
self.config.setdefault('wuttaweb.theme.poser', '/tmp/poser-theme')
path = mod.get_theme_template_path(self.config, theme='poser')
self.assertEqual(path, '/tmp/poser-theme')
self.config.setdefault("wuttaweb.themes.keys", "default, butterfly, poser")
self.config.setdefault("wuttaweb.theme.poser", "/tmp/poser-theme")
path = mod.get_theme_template_path(self.config, theme="poser")
self.assertEqual(path, "/tmp/poser-theme")
class TestSetAppTheme(WebTestCase):
@ -699,14 +736,14 @@ class TestSetAppTheme(WebTestCase):
# establish default
settings = self.request.registry.settings
self.assertNotIn('wuttaweb.theme', settings)
self.assertNotIn("wuttaweb.theme", settings)
establish_theme(settings)
self.assertEqual(settings['wuttaweb.theme'], 'default')
self.assertEqual(settings["wuttaweb.theme"], "default")
# set to butterfly
mod.set_app_theme(self.request, 'butterfly', session=self.session)
self.assertEqual(settings['wuttaweb.theme'], 'butterfly')
mod.set_app_theme(self.request, "butterfly", session=self.session)
self.assertEqual(settings["wuttaweb.theme"], "butterfly")
# set back to default
mod.set_app_theme(self.request, 'default', session=self.session)
self.assertEqual(settings['wuttaweb.theme'], 'default')
mod.set_app_theme(self.request, "default", session=self.session)
self.assertEqual(settings["wuttaweb.theme"], "default")

View file

@ -15,9 +15,11 @@ class DataTestCase(FileConfigTestCase):
def setup_db(self):
self.setup_files()
self.config = WuttaConfig(defaults={
'wutta.db.default.url': 'sqlite://',
})
self.config = WuttaConfig(
defaults={
"wutta.db.default.url": "sqlite://",
}
)
self.app = self.config.get_app()
# init db
@ -36,5 +38,6 @@ class NullMenuHandler(MenuHandler):
"""
Dummy menu handler for testing.
"""
def make_menus(self, request, **kwargs):
return []

View file

@ -7,4 +7,4 @@ class TestIncludeMe(WebTestCase):
def test_basic(self):
# just ensure no error happens when included..
self.pyramid_config.include('wuttaweb.views')
self.pyramid_config.include("wuttaweb.views")

View file

@ -12,13 +12,13 @@ class TestAuthView(WebTestCase):
def setUp(self):
self.setup_web()
self.pyramid_config.include('wuttaweb.views.common')
self.pyramid_config.include("wuttaweb.views.common")
def make_view(self):
return mod.AuthView(self.request)
def test_includeme(self):
self.pyramid_config.include('wuttaweb.views.auth')
self.pyramid_config.include("wuttaweb.views.auth")
def test_login(self):
model = self.app.model
@ -31,37 +31,37 @@ class TestAuthView(WebTestCase):
self.assertEqual(response.status_code, 302)
# make a user
barney = model.User(username='barney')
auth.set_user_password(barney, 'testpass')
barney = model.User(username="barney")
auth.set_user_password(barney, "testpass")
self.session.add(barney)
self.session.commit()
# now since user exists, form will display
context = view.login(session=self.session)
self.assertIn('form', context)
self.assertIn("form", context)
# redirect if user already logged in
with patch.object(self.request, 'user', new=barney):
with patch.object(self.request, "user", new=barney):
view = self.make_view()
response = view.login(session=self.session)
self.assertEqual(response.status_code, 302)
# login fails w/ wrong password
self.request.method = 'POST'
self.request.POST = {'username': 'barney', 'password': 'WRONG'}
self.request.method = "POST"
self.request.POST = {"username": "barney", "password": "WRONG"}
view = self.make_view()
context = view.login(session=self.session)
self.assertIn('form', context)
self.assertIn("form", context)
# redirect if login succeeds
self.request.method = 'POST'
self.request.POST = {'username': 'barney', 'password': 'testpass'}
self.request.method = "POST"
self.request.POST = {"username": "barney", "password": "testpass"}
view = self.make_view()
response = view.login(session=self.session)
self.assertEqual(response.status_code, 302)
def test_logout(self):
self.pyramid_config.add_route('login', '/login')
self.pyramid_config.add_route("login", "/login")
view = self.make_view()
self.request.session.delete = MagicMock()
response = view.logout()
@ -71,7 +71,7 @@ class TestAuthView(WebTestCase):
def test_change_password(self):
model = self.app.model
auth = self.app.get_auth_handler()
barney = model.User(username='barney')
barney = model.User(username="barney")
self.session.add(barney)
self.session.commit()
view = self.make_view()
@ -81,7 +81,7 @@ class TestAuthView(WebTestCase):
self.assertIsInstance(redirect, HTTPFound)
# set initial password
auth.set_user_password(barney, 'foo')
auth.set_user_password(barney, "foo")
self.session.commit()
# forbidden if prevent_edit is set for user
@ -94,24 +94,24 @@ class TestAuthView(WebTestCase):
# view should now return context w/ form
context = view.change_password()
self.assertIn('form', context)
self.assertIn("form", context)
# submit valid form, ensure password is changed
# (nb. this also would redirect user to home page)
self.request.method = 'POST'
self.request.method = "POST"
self.request.POST = {
'current_password': 'foo',
"current_password": "foo",
# nb. new_password requires colander mapping structure
'__start__': 'new_password:mapping',
'new_password': 'bar',
'new_password-confirm': 'bar',
'__end__': 'new_password:mapping',
"__start__": "new_password:mapping",
"new_password": "bar",
"new_password-confirm": "bar",
"__end__": "new_password:mapping",
}
redirect = view.change_password()
self.assertIsInstance(redirect, HTTPFound)
self.session.commit()
self.assertFalse(auth.check_user_password(barney, 'foo'))
self.assertTrue(auth.check_user_password(barney, 'bar'))
self.assertFalse(auth.check_user_password(barney, "foo"))
self.assertTrue(auth.check_user_password(barney, "bar"))
# at this point 'foo' is the password, now let's submit some
# invalid forms and make sure we get back a context w/ form
@ -119,72 +119,77 @@ class TestAuthView(WebTestCase):
# first try empty data
self.request.POST = {}
context = view.change_password()
self.assertIn('form', context)
dform = context['form'].get_deform()
self.assertEqual(dform['current_password'].errormsg, "Required")
self.assertEqual(dform['new_password'].errormsg, "Required")
self.assertIn("form", context)
dform = context["form"].get_deform()
self.assertEqual(dform["current_password"].errormsg, "Required")
self.assertEqual(dform["new_password"].errormsg, "Required")
# now try bad current password
self.request.POST = {
'current_password': 'blahblah',
'__start__': 'new_password:mapping',
'new_password': 'baz',
'new_password-confirm': 'baz',
'__end__': 'new_password:mapping',
"current_password": "blahblah",
"__start__": "new_password:mapping",
"new_password": "baz",
"new_password-confirm": "baz",
"__end__": "new_password:mapping",
}
context = view.change_password()
self.assertIn('form', context)
dform = context['form'].get_deform()
self.assertEqual(dform['current_password'].errormsg, "Current password is incorrect.")
self.assertIn("form", context)
dform = context["form"].get_deform()
self.assertEqual(
dform["current_password"].errormsg, "Current password is incorrect."
)
# now try bad new password
self.request.POST = {
'current_password': 'bar',
'__start__': 'new_password:mapping',
'new_password': 'bar',
'new_password-confirm': 'bar',
'__end__': 'new_password:mapping',
"current_password": "bar",
"__start__": "new_password:mapping",
"new_password": "bar",
"new_password-confirm": "bar",
"__end__": "new_password:mapping",
}
context = view.change_password()
self.assertIn('form', context)
dform = context['form'].get_deform()
self.assertEqual(dform['new_password'].errormsg, "New password must be different from old password.")
self.assertIn("form", context)
dform = context["form"].get_deform()
self.assertEqual(
dform["new_password"].errormsg,
"New password must be different from old password.",
)
def test_become_root(self):
view = mod.AuthView(self.request)
# GET not allowed
self.request.method = 'GET'
self.request.method = "GET"
self.assertRaises(HTTPForbidden, view.become_root)
# non-admin users also not allowed
self.request.method = 'POST'
self.request.method = "POST"
self.request.is_admin = False
self.assertRaises(HTTPForbidden, view.become_root)
# but admin users can become root
self.request.is_admin = True
self.assertNotIn('is_root', self.request.session)
self.assertNotIn("is_root", self.request.session)
redirect = view.become_root()
self.assertIsInstance(redirect, HTTPFound)
self.assertTrue(self.request.session['is_root'])
self.assertTrue(self.request.session["is_root"])
def test_stop_root(self):
view = mod.AuthView(self.request)
# GET not allowed
self.request.method = 'GET'
self.request.method = "GET"
self.assertRaises(HTTPForbidden, view.stop_root)
# non-admin users also not allowed
self.request.method = 'POST'
self.request.method = "POST"
self.request.is_admin = False
self.assertRaises(HTTPForbidden, view.stop_root)
# but admin users can stop being root
# (nb. there is no check whether user is currently root)
self.request.is_admin = True
self.assertNotIn('is_root', self.request.session)
self.assertNotIn("is_root", self.request.session)
redirect = view.stop_root()
self.assertIsInstance(redirect, HTTPFound)
self.assertFalse(self.request.session['is_root'])
self.assertFalse(self.request.session["is_root"])

View file

@ -36,7 +36,7 @@ class TestView(WebTestCase):
def test_make_grid_action(self):
view = self.make_view()
action = view.make_grid_action('view')
action = view.make_grid_action("view")
self.assertIsInstance(action, GridAction)
def test_notfound(self):
@ -46,31 +46,31 @@ class TestView(WebTestCase):
def test_redirect(self):
view = self.make_view()
error = view.redirect('/')
error = view.redirect("/")
self.assertIsInstance(error, HTTPFound)
self.assertEqual(error.location, '/')
self.assertEqual(error.location, "/")
def test_file_response(self):
view = self.make_view()
# default uses attachment behavior
datfile = self.write_file('dat.txt', 'hello')
datfile = self.write_file("dat.txt", "hello")
response = view.file_response(datfile)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content_disposition, 'attachment; filename="dat.txt"')
# but can disable attachment behavior
datfile = self.write_file('dat.txt', 'hello')
datfile = self.write_file("dat.txt", "hello")
response = view.file_response(datfile, attachment=False)
self.assertEqual(response.status_code, 200)
self.assertIsNone(response.content_disposition)
# path not found
crapfile = '/does/not/exist'
crapfile = "/does/not/exist"
response = view.file_response(crapfile)
self.assertEqual(response.status_code, 404)
def test_json_response(self):
view = self.make_view()
response = view.json_response({'foo': 'bar'})
response = view.json_response({"foo": "bar"})
self.assertEqual(response.status_code, 200)

View file

@ -14,14 +14,17 @@ from wuttaweb.testing import WebTestCase
class MockBatch(model.BatchMixin, model.Base):
__tablename__ = 'testing_batch_mock'
__tablename__ = "testing_batch_mock"
class MockBatchRow(model.BatchRowMixin, model.Base):
__tablename__ = 'testing_batch_mock_row'
__tablename__ = "testing_batch_mock_row"
__batch_class__ = MockBatch
MockBatch.__row_class__ = MockBatchRow
class MockBatchHandler(BatchHandler):
model_class = MockBatch
@ -43,43 +46,52 @@ class TestBatchMasterView(WebTestCase):
def test_get_batch_handler(self):
self.assertRaises(NotImplementedError, mod.BatchMasterView, self.request)
with patch.object(mod.BatchMasterView, 'get_batch_handler', return_value=42):
with patch.object(mod.BatchMasterView, "get_batch_handler", return_value=42):
view = mod.BatchMasterView(self.request)
self.assertEqual(view.batch_handler, 42)
def test_get_fallback_templates(self):
handler = MockBatchHandler(self.config)
with patch.object(mod.BatchMasterView, 'get_batch_handler', return_value=handler):
with patch.object(
mod.BatchMasterView, "get_batch_handler", return_value=handler
):
view = self.make_view()
templates = view.get_fallback_templates('view')
self.assertEqual(templates, [
'/batch/view.mako',
'/master/view.mako',
])
templates = view.get_fallback_templates("view")
self.assertEqual(
templates,
[
"/batch/view.mako",
"/master/view.mako",
],
)
def test_render_to_response(self):
model = self.app.model
handler = MockBatchHandler(self.config)
user = model.User(username='barney')
user = model.User(username="barney")
self.session.add(user)
batch = handler.make_batch(self.session, created_by=user)
self.session.add(batch)
self.session.flush()
with patch.object(mod.BatchMasterView, 'get_batch_handler', return_value=handler):
with patch.object(MasterView, 'render_to_response') as render_to_response:
with patch.object(
mod.BatchMasterView, "get_batch_handler", return_value=handler
):
with patch.object(MasterView, "render_to_response") as render_to_response:
view = self.make_view()
response = view.render_to_response('view', {'instance': batch})
response = view.render_to_response("view", {"instance": batch})
self.assertTrue(render_to_response.called)
context = render_to_response.call_args[0][1]
self.assertIs(context['batch'], batch)
self.assertIs(context['batch_handler'], handler)
self.assertIs(context["batch"], batch)
self.assertIs(context["batch_handler"], handler)
def test_configure_grid(self):
handler = MockBatchHandler(self.config)
with patch.multiple(mod.BatchMasterView, create=True, model_class=MockBatch):
with patch.object(mod.BatchMasterView, 'get_batch_handler', return_value=handler):
with patch.object(
mod.BatchMasterView, "get_batch_handler", return_value=handler
):
view = mod.BatchMasterView(self.request)
grid = view.make_model_grid()
# nb. coverage only; tests nothing
@ -87,19 +99,23 @@ class TestBatchMasterView(WebTestCase):
def test_render_batch_id(self):
handler = MockBatchHandler(self.config)
with patch.object(mod.BatchMasterView, 'get_batch_handler', return_value=handler):
with patch.object(
mod.BatchMasterView, "get_batch_handler", return_value=handler
):
view = mod.BatchMasterView(self.request)
batch = MockBatch(id=42)
result = view.render_batch_id(batch, 'id', 42)
self.assertEqual(result, '00000042')
result = view.render_batch_id(batch, "id", 42)
self.assertEqual(result, "00000042")
result = view.render_batch_id(batch, 'id', None)
result = view.render_batch_id(batch, "id", None)
self.assertIsNone(result)
def test_get_instance_title(self):
handler = MockBatchHandler(self.config)
with patch.object(mod.BatchMasterView, 'get_batch_handler', return_value=handler):
with patch.object(
mod.BatchMasterView, "get_batch_handler", return_value=handler
):
view = mod.BatchMasterView(self.request)
batch = MockBatch(id=42)
@ -113,46 +129,52 @@ class TestBatchMasterView(WebTestCase):
def test_configure_form(self):
handler = MockBatchHandler(self.config)
with patch.multiple(mod.BatchMasterView, create=True, model_class=MockBatch):
with patch.object(mod.BatchMasterView, 'get_batch_handler', return_value=handler):
with patch.object(
mod.BatchMasterView, "get_batch_handler", return_value=handler
):
view = mod.BatchMasterView(self.request)
# creating
with patch.object(view, 'creating', new=True):
with patch.object(view, "creating", new=True):
form = view.make_model_form(model_instance=None)
view.configure_form(form)
batch = MockBatch(id=42)
# viewing
with patch.object(view, 'viewing', new=True):
with patch.object(view, "viewing", new=True):
form = view.make_model_form(model_instance=batch)
view.configure_form(form)
# editing
with patch.object(view, 'editing', new=True):
with patch.object(view, "editing", new=True):
form = view.make_model_form(model_instance=batch)
view.configure_form(form)
# deleting
with patch.object(view, 'deleting', new=True):
with patch.object(view, "deleting", new=True):
form = view.make_model_form(model_instance=batch)
view.configure_form(form)
# viewing (executed)
batch.executed = datetime.datetime.now()
with patch.object(view, 'viewing', new=True):
with patch.object(view, "viewing", new=True):
form = view.make_model_form(model_instance=batch)
view.configure_form(form)
def test_objectify(self):
handler = MockBatchHandler(self.config)
with patch.multiple(mod.BatchMasterView, create=True, model_class=MockBatch):
with patch.object(mod.BatchMasterView, 'get_batch_handler', return_value=handler):
with patch.object(mod.BatchMasterView, 'Session', return_value=self.session):
with patch.object(
mod.BatchMasterView, "get_batch_handler", return_value=handler
):
with patch.object(
mod.BatchMasterView, "Session", return_value=self.session
):
view = mod.BatchMasterView(self.request)
# create batch
with patch.object(view, 'creating', new=True):
with patch.object(view, "creating", new=True):
form = view.make_model_form(model_instance=None)
form.validated = {}
batch = view.objectify(form)
@ -160,21 +182,28 @@ class TestBatchMasterView(WebTestCase):
self.assertTrue(batch.id > 0)
# edit batch
with patch.object(view, 'editing', new=True):
with patch.object(view.batch_handler, 'make_batch') as make_batch:
with patch.object(view, "editing", new=True):
with patch.object(
view.batch_handler, "make_batch"
) as make_batch:
form = view.make_model_form(model_instance=batch)
form.validated = {'description': 'foo'}
form.validated = {"description": "foo"}
self.assertIsNone(batch.description)
batch = view.objectify(form)
self.assertEqual(batch.description, 'foo')
self.assertEqual(batch.description, "foo")
def test_redirect_after_create(self):
self.pyramid_config.add_route('mock_batches.view', '/batch/mock/{uuid}')
self.pyramid_config.add_route("mock_batches.view", "/batch/mock/{uuid}")
handler = MockBatchHandler(self.config)
with patch.object(mod.BatchMasterView, 'get_batch_handler', return_value=handler):
with patch.multiple(mod.BatchMasterView, create=True,
model_class=MockBatch,
route_prefix='mock_batches'):
with patch.object(
mod.BatchMasterView, "get_batch_handler", return_value=handler
):
with patch.multiple(
mod.BatchMasterView,
create=True,
model_class=MockBatch,
route_prefix="mock_batches",
):
view = mod.BatchMasterView(self.request)
batch = MockBatch(id=42)
@ -183,12 +212,14 @@ class TestBatchMasterView(WebTestCase):
self.assertIsInstance(result, HTTPFound)
# unless populating in which case thread is launched
self.request.session.id = 'abcdefghijk'
with patch.object(mod, 'threading') as threading:
self.request.session.id = "abcdefghijk"
with patch.object(mod, "threading") as threading:
thread = MagicMock()
threading.Thread.return_value = thread
with patch.object(view.batch_handler, 'should_populate', return_value=True):
with patch.object(view, 'render_progress') as render_progress:
with patch.object(
view.batch_handler, "should_populate", return_value=True
):
with patch.object(view, "render_progress") as render_progress:
view.redirect_after_create(batch)
self.assertTrue(threading.Thread.called)
thread.start.assert_called_once_with()
@ -198,14 +229,16 @@ class TestBatchMasterView(WebTestCase):
model = self.app.model
handler = self.make_handler()
user = model.User(username='barney')
user = model.User(username="barney")
self.session.add(user)
batch = handler.make_batch(self.session, created_by=user)
self.session.add(batch)
self.session.flush()
with patch.object(mod.BatchMasterView, 'get_batch_handler', return_value=handler):
with patch.object(
mod.BatchMasterView, "get_batch_handler", return_value=handler
):
view = self.make_view()
self.assertEqual(self.session.query(MockBatch).count(), 1)
@ -215,20 +248,24 @@ class TestBatchMasterView(WebTestCase):
def test_populate_thread(self):
model = self.app.model
handler = MockBatchHandler(self.config)
with patch.object(mod.BatchMasterView, 'get_batch_handler', return_value=handler):
with patch.multiple(mod.BatchMasterView, create=True, model_class=MockBatch):
with patch.object(
mod.BatchMasterView, "get_batch_handler", return_value=handler
):
with patch.multiple(
mod.BatchMasterView, create=True, model_class=MockBatch
):
view = mod.BatchMasterView(self.request)
user = model.User(username='barney')
user = model.User(username="barney")
self.session.add(user)
batch = MockBatch(id=42, created_by=user)
self.session.add(batch)
self.session.commit()
# nb. use our session within thread method
with patch.object(self.app, 'make_session', return_value=self.session):
with patch.object(self.app, "make_session", return_value=self.session):
# nb. prevent closing our session
with patch.object(self.session, 'close') as close:
with patch.object(self.session, "close") as close:
# without progress
view.populate_thread(batch.uuid)
@ -236,24 +273,34 @@ class TestBatchMasterView(WebTestCase):
close.reset_mock()
# with progress
self.request.session.id = 'abcdefghijk'
view.populate_thread(batch.uuid,
progress=SessionProgress(self.request,
'populate_mock_batch'))
self.request.session.id = "abcdefghijk"
view.populate_thread(
batch.uuid,
progress=SessionProgress(
self.request, "populate_mock_batch"
),
)
close.assert_called_once_with()
close.reset_mock()
# failure to populate, without progress
with patch.object(view.batch_handler, 'do_populate', side_effect=RuntimeError):
with patch.object(
view.batch_handler, "do_populate", side_effect=RuntimeError
):
view.populate_thread(batch.uuid)
close.assert_called_once_with()
close.reset_mock()
# failure to populate, with progress
with patch.object(view.batch_handler, 'do_populate', side_effect=RuntimeError):
view.populate_thread(batch.uuid,
progress=SessionProgress(self.request,
'populate_mock_batch'))
with patch.object(
view.batch_handler, "do_populate", side_effect=RuntimeError
):
view.populate_thread(
batch.uuid,
progress=SessionProgress(
self.request, "populate_mock_batch"
),
)
close.assert_called_once_with()
close.reset_mock()
@ -261,53 +308,64 @@ class TestBatchMasterView(WebTestCase):
self.session.delete(batch)
self.session.commit()
# nb. should give up waiting after 1 second
self.assertRaises(RuntimeError, view.populate_thread, batch.uuid)
self.assertRaises(
RuntimeError, view.populate_thread, batch.uuid
)
def test_execute(self):
self.pyramid_config.add_route('mock_batches.view', '/batch/mock/{uuid}')
self.pyramid_config.add_route("mock_batches.view", "/batch/mock/{uuid}")
model = self.app.model
handler = MockBatchHandler(self.config)
user = model.User(username='barney')
user = model.User(username="barney")
self.session.add(user)
batch = handler.make_batch(self.session, created_by=user)
self.session.add(batch)
self.session.commit()
with patch.multiple(mod.BatchMasterView, create=True,
model_class=MockBatch,
route_prefix='mock_batches',
get_batch_handler=MagicMock(return_value=handler),
get_instance=MagicMock(return_value=batch)):
with patch.multiple(
mod.BatchMasterView,
create=True,
model_class=MockBatch,
route_prefix="mock_batches",
get_batch_handler=MagicMock(return_value=handler),
get_instance=MagicMock(return_value=batch),
):
view = self.make_view()
# batch executes okay
response = view.execute()
self.assertEqual(response.status_code, 302) # redirect to "view batch"
self.assertFalse(self.request.session.peek_flash('error'))
self.assertEqual(response.status_code, 302) # redirect to "view batch"
self.assertFalse(self.request.session.peek_flash("error"))
# but cannot be executed again
response = view.execute()
self.assertEqual(response.status_code, 302) # redirect to "view batch"
self.assertEqual(response.status_code, 302) # redirect to "view batch"
# nb. flash has error this time
self.assertTrue(self.request.session.peek_flash('error'))
self.assertTrue(self.request.session.peek_flash("error"))
def test_get_row_model_class(self):
handler = MockBatchHandler(self.config)
with patch.object(mod.BatchMasterView, 'get_batch_handler', return_value=handler):
with patch.object(
mod.BatchMasterView, "get_batch_handler", return_value=handler
):
view = self.make_view()
self.assertRaises(AttributeError, view.get_row_model_class)
# row class determined from batch class
with patch.object(mod.BatchMasterView, 'model_class', new=MockBatch, create=True):
with patch.object(
mod.BatchMasterView, "model_class", new=MockBatch, create=True
):
cls = view.get_row_model_class()
self.assertIs(cls, MockBatchRow)
self.assertRaises(AttributeError, view.get_row_model_class)
# view may specify row class
with patch.object(mod.BatchMasterView, 'row_model_class', new=MockBatchRow, create=True):
with patch.object(
mod.BatchMasterView, "row_model_class", new=MockBatchRow, create=True
):
cls = view.get_row_model_class()
self.assertIs(cls, MockBatchRow)
@ -315,7 +373,7 @@ class TestBatchMasterView(WebTestCase):
handler = MockBatchHandler(self.config)
model = self.app.model
user = model.User(username='barney')
user = model.User(username="barney")
self.session.add(user)
batch = handler.make_batch(self.session, created_by=user)
@ -324,16 +382,18 @@ class TestBatchMasterView(WebTestCase):
handler.add_row(batch, row)
self.session.flush()
with patch.object(mod.BatchMasterView, 'get_batch_handler', return_value=handler):
with patch.object(
mod.BatchMasterView, "get_batch_handler", return_value=handler
):
view = self.make_view()
self.assertRaises(AttributeError, view.get_row_grid_data, batch)
Session = MagicMock(return_value=self.session)
Session.query.side_effect = lambda m: self.session.query(m)
with patch.multiple(mod.BatchMasterView, create=True,
Session=Session,
model_class=MockBatch):
with patch.multiple(
mod.BatchMasterView, create=True, Session=Session, model_class=MockBatch
):
view = self.make_view()
data = view.get_row_grid_data(batch)
@ -344,7 +404,7 @@ class TestBatchMasterView(WebTestCase):
handler = MockBatchHandler(self.config)
model = self.app.model
user = model.User(username='barney')
user = model.User(username="barney")
self.session.add(user)
batch = handler.make_batch(self.session, created_by=user)
@ -353,27 +413,31 @@ class TestBatchMasterView(WebTestCase):
handler.add_row(batch, row)
self.session.flush()
with patch.object(mod.BatchMasterView, 'get_batch_handler', return_value=handler):
with patch.object(
mod.BatchMasterView, "get_batch_handler", return_value=handler
):
Session = MagicMock(return_value=self.session)
Session.query.side_effect = lambda m: self.session.query(m)
with patch.multiple(mod.BatchMasterView, create=True,
Session=Session,
model_class=MockBatch):
with patch.multiple(
mod.BatchMasterView, create=True, Session=Session, model_class=MockBatch
):
with patch.object(self.request, 'matchdict', new={'uuid': batch.uuid}):
with patch.object(self.request, "matchdict", new={"uuid": batch.uuid}):
view = self.make_view()
grid = view.make_row_model_grid(batch)
self.assertIn('sequence', grid.labels)
self.assertEqual(grid.labels['sequence'], "Seq.")
self.assertIn("sequence", grid.labels)
self.assertEqual(grid.labels["sequence"], "Seq.")
def test_render_row_status(self):
with patch.object(mod.BatchMasterView, 'get_batch_handler', return_value=None):
with patch.object(mod.BatchMasterView, "get_batch_handler", return_value=None):
view = self.make_view()
row = MagicMock(foo=1, STATUS={1: 'bar'})
self.assertEqual(view.render_row_status(row, 'foo', 1), 'bar')
row = MagicMock(foo=1, STATUS={1: "bar"})
self.assertEqual(view.render_row_status(row, "foo", 1), "bar")
def test_defaults(self):
# nb. coverage only
with patch.object(mod.BatchMasterView, 'model_class', new=MockBatch, create=True):
with patch.object(
mod.BatchMasterView, "model_class", new=MockBatch, create=True
):
mod.BatchMasterView.defaults(self.pyramid_config)

View file

@ -15,21 +15,21 @@ class TestCommonView(WebTestCase):
return mod.CommonView(self.request)
def test_includeme(self):
self.pyramid_config.include('wuttaweb.views.common')
self.pyramid_config.include("wuttaweb.views.common")
def test_forbidden_view(self):
view = self.make_view()
context = view.forbidden_view()
self.assertEqual(context['index_title'], self.app.get_title())
self.assertEqual(context["index_title"], self.app.get_title())
def test_notfound_view(self):
view = self.make_view()
context = view.notfound_view()
self.assertEqual(context['index_title'], self.app.get_title())
self.assertEqual(context["index_title"], self.app.get_title())
def test_home(self):
self.pyramid_config.add_route('setup', '/setup')
self.pyramid_config.add_route('login', '/login')
self.pyramid_config.add_route("setup", "/setup")
self.pyramid_config.add_route("login", "/login")
model = self.app.model
view = self.make_view()
@ -38,50 +38,50 @@ class TestCommonView(WebTestCase):
self.assertEqual(response.status_code, 302)
# so add a user
user = model.User(username='foo')
user = model.User(username="foo")
self.session.add(user)
self.session.commit()
# now we see the home page
context = view.home(session=self.session)
self.assertEqual(context['index_title'], self.app.get_title())
self.assertEqual(context["index_title"], self.app.get_title())
# but if configured, anons will be redirected to login
self.config.setdefault('wuttaweb.home_redirect_to_login', 'true')
self.config.setdefault("wuttaweb.home_redirect_to_login", "true")
response = view.home(session=self.session)
self.assertEqual(response.status_code, 302)
# now only an auth'ed user can see home page
self.request.user = user
context = view.home(session=self.session)
self.assertEqual(context['index_title'], self.app.get_title())
self.assertEqual(context["index_title"], self.app.get_title())
def test_feedback_make_schema(self):
view = self.make_view()
schema = view.feedback_make_schema()
self.assertIsInstance(schema, colander.Schema)
self.assertIn('message', schema)
self.assertIn("message", schema)
def test_feedback(self):
self.pyramid_config.add_route('users.view', '/users/{uuid}')
self.pyramid_config.add_route("users.view", "/users/{uuid}")
model = self.app.model
user = model.User(username='barney')
user = model.User(username="barney")
self.session.add(user)
self.session.commit()
view = self.make_view()
with patch.object(view, 'feedback_send') as feedback_send:
with patch.object(view, "feedback_send") as feedback_send:
# basic send, no user
self.request.client_addr = '127.0.0.1'
self.request.method = 'POST'
self.request.client_addr = "127.0.0.1"
self.request.method = "POST"
self.request.POST = {
'referrer': '/foo',
'user_name': "Barney Rubble",
'message': "hello world",
"referrer": "/foo",
"user_name": "Barney Rubble",
"message": "hello world",
}
context = view.feedback()
self.assertEqual(context, {'ok': True})
self.assertEqual(context, {"ok": True})
feedback_send.assert_called_once()
# reset
@ -89,10 +89,10 @@ class TestCommonView(WebTestCase):
# basic send, with user
self.request.user = user
self.request.POST['user_uuid'] = str(user.uuid)
with patch.object(mod, 'Session', return_value=self.session):
self.request.POST["user_uuid"] = str(user.uuid)
with patch.object(mod, "Session", return_value=self.session):
context = view.feedback()
self.assertEqual(context, {'ok': True})
self.assertEqual(context, {"ok": True})
feedback_send.assert_called_once()
# reset
@ -100,37 +100,35 @@ class TestCommonView(WebTestCase):
feedback_send.reset_mock()
# invalid form data
self.request.POST = {'message': 'hello world'}
self.request.POST = {"message": "hello world"}
context = view.feedback()
self.assertEqual(list(context), ['error'])
self.assertIn('Required', context['error'])
self.assertEqual(list(context), ["error"])
self.assertIn("Required", context["error"])
feedback_send.assert_not_called()
# error on send
self.request.POST = {
'referrer': '/foo',
'user_name': "Barney Rubble",
'message': "hello world",
"referrer": "/foo",
"user_name": "Barney Rubble",
"message": "hello world",
}
feedback_send.side_effect = RuntimeError
context = view.feedback()
feedback_send.assert_called_once()
self.assertEqual(list(context), ['error'])
self.assertIn('RuntimeError', context['error'])
self.assertEqual(list(context), ["error"])
self.assertIn("RuntimeError", context["error"])
def test_feedback_send(self):
view = self.make_view()
with patch.object(self.app, 'send_email') as send_email:
view.feedback_send({'user_name': "Barney",
'message': "hello world"})
send_email.assert_called_once_with('feedback', {
'user_name': "Barney",
'message': "hello world"
})
with patch.object(self.app, "send_email") as send_email:
view.feedback_send({"user_name": "Barney", "message": "hello world"})
send_email.assert_called_once_with(
"feedback", {"user_name": "Barney", "message": "hello world"}
)
def test_setup(self):
self.pyramid_config.add_route('home', '/')
self.pyramid_config.add_route('login', '/login')
self.pyramid_config.add_route("home", "/")
self.pyramid_config.add_route("login", "/login")
model = self.app.model
auth = self.app.get_auth_handler()
view = self.make_view()
@ -138,10 +136,10 @@ class TestCommonView(WebTestCase):
# at first, can see the setup page
self.assertEqual(self.session.query(model.User).count(), 0)
context = view.setup(session=self.session)
self.assertEqual(context['index_title'], self.app.get_title())
self.assertEqual(context["index_title"], self.app.get_title())
# so add a user
user = model.User(username='foo')
user = model.User(username="foo")
self.session.add(user)
self.session.commit()
@ -155,25 +153,25 @@ class TestCommonView(WebTestCase):
# so we can see the setup page again
context = view.setup(session=self.session)
self.assertEqual(context['index_title'], self.app.get_title())
self.assertEqual(context["index_title"], self.app.get_title())
# and finally, post data to create admin user
self.request.method = 'POST'
self.request.method = "POST"
self.request.POST = {
'username': 'barney',
'__start__': 'password:mapping',
'password': 'testpass',
'password-confirm': 'testpass',
'__end__': 'password:mapping',
'first_name': "Barney",
'last_name': "Rubble",
"username": "barney",
"__start__": "password:mapping",
"password": "testpass",
"password-confirm": "testpass",
"__end__": "password:mapping",
"first_name": "Barney",
"last_name": "Rubble",
}
response = view.setup(session=self.session)
# nb. redirects on success
self.assertEqual(response.status_code, 302)
barney = self.session.query(model.User).one()
self.assertEqual(barney.username, 'barney')
self.assertTrue(auth.check_user_password(barney, 'testpass'))
self.assertEqual(barney.username, "barney")
self.assertTrue(auth.check_user_password(barney, "testpass"))
admin = auth.get_role_administrator(self.session)
self.assertIn(admin, barney.roles)
self.assertIsNotNone(barney.person)
@ -183,30 +181,30 @@ class TestCommonView(WebTestCase):
self.assertEqual(person.full_name, "Barney Rubble")
def test_change_theme(self):
self.pyramid_config.add_route('home', '/')
self.pyramid_config.add_route("home", "/")
settings = self.request.registry.settings
establish_theme(settings)
view = self.make_view()
# theme is not changed if not provided by caller
self.assertEqual(settings['wuttaweb.theme'], 'default')
with patch.object(mod, 'set_app_theme') as set_app_theme:
self.assertEqual(settings["wuttaweb.theme"], "default")
with patch.object(mod, "set_app_theme") as set_app_theme:
view.change_theme()
set_app_theme.assert_not_called()
self.assertEqual(settings['wuttaweb.theme'], 'default')
self.assertEqual(settings["wuttaweb.theme"], "default")
# but theme will change if provided
with patch.object(self.request, 'params', new={'theme': 'butterfly'}):
with patch.object(mod, 'Session', return_value=self.session):
with patch.object(self.request, "params", new={"theme": "butterfly"}):
with patch.object(mod, "Session", return_value=self.session):
view.change_theme()
self.assertEqual(settings['wuttaweb.theme'], 'butterfly')
self.assertEqual(settings["wuttaweb.theme"], "butterfly")
# flash error if invalid theme is provided
self.assertFalse(self.request.session.peek_flash('error'))
with patch.object(self.request, 'params', new={'theme': 'anotherone'}):
with patch.object(mod, 'Session', return_value=self.session):
self.assertFalse(self.request.session.peek_flash("error"))
with patch.object(self.request, "params", new={"theme": "anotherone"}):
with patch.object(mod, "Session", return_value=self.session):
view.change_theme()
self.assertEqual(settings['wuttaweb.theme'], 'butterfly')
self.assertTrue(self.request.session.peek_flash('error'))
messages = self.request.session.pop_flash('error')
self.assertIn('Failed to set theme', messages[0])
self.assertEqual(settings["wuttaweb.theme"], "butterfly")
self.assertTrue(self.request.session.peek_flash("error"))
messages = self.request.session.pop_flash("error")
self.assertIn("Failed to set theme", messages[0])

View file

@ -18,194 +18,232 @@ class TestEmailSettingViews(WebTestCase):
return mod.EmailSettingView(self.request)
def test_includeme(self):
self.pyramid_config.include('wuttaweb.views.email')
self.pyramid_config.include("wuttaweb.views.email")
def test_get_grid_data(self):
self.config.setdefault('wutta.email.default.sender', 'test@example.com')
self.config.setdefault("wutta.email.default.sender", "test@example.com")
view = self.make_view()
data = view.get_grid_data()
self.assertIsInstance(data, list)
self.assertTrue(data) # 1+ items
self.assertTrue(data) # 1+ items
setting = data[0]
self.assertIn('key', setting)
self.assertIn('subject', setting)
self.assertIn('sender', setting)
self.assertIn('to', setting)
self.assertIn('cc', setting)
self.assertIn('notes', setting)
self.assertIn("key", setting)
self.assertIn("subject", setting)
self.assertIn("sender", setting)
self.assertIn("to", setting)
self.assertIn("cc", setting)
self.assertIn("notes", setting)
def test_configure_grid(self):
self.config.setdefault('wutta.email.default.sender', 'test@example.com')
self.config.setdefault("wutta.email.default.sender", "test@example.com")
view = self.make_view()
grid = view.make_model_grid()
self.assertIn('key', grid.searchable_columns)
self.assertIn('subject', grid.searchable_columns)
self.assertIn("key", grid.searchable_columns)
self.assertIn("subject", grid.searchable_columns)
def test_render_to_short(self):
view = self.make_view()
setting = EmailSetting(self.config)
# more than 2 recips
result = view.render_to_short(setting, 'to', [
'alice@example.com',
'bob@example.com',
'charlie@example.com',
'diana@example.com',
])
self.assertEqual(result, 'alice@example.com, bob@example.com, ...')
result = view.render_to_short(
setting,
"to",
[
"alice@example.com",
"bob@example.com",
"charlie@example.com",
"diana@example.com",
],
)
self.assertEqual(result, "alice@example.com, bob@example.com, ...")
# just 2 recips
result = view.render_to_short(setting, 'to', [
'alice@example.com',
'bob@example.com',
])
self.assertEqual(result, 'alice@example.com, bob@example.com')
result = view.render_to_short(
setting,
"to",
[
"alice@example.com",
"bob@example.com",
],
)
self.assertEqual(result, "alice@example.com, bob@example.com")
# just 1 recip
result = view.render_to_short(setting, 'to', ['alice@example.com'])
self.assertEqual(result, 'alice@example.com')
result = view.render_to_short(setting, "to", ["alice@example.com"])
self.assertEqual(result, "alice@example.com")
# no recips
result = view.render_to_short(setting, 'to', [])
result = view.render_to_short(setting, "to", [])
self.assertIsNone(result)
def test_get_instance(self):
self.config.setdefault('wutta.email.default.sender', 'test@example.com')
self.config.setdefault("wutta.email.default.sender", "test@example.com")
view = self.make_view()
# normal
with patch.object(self.request, 'matchdict', new={'key': 'feedback'}):
with patch.object(self.request, "matchdict", new={"key": "feedback"}):
setting = view.get_instance()
self.assertIsInstance(setting, dict)
self.assertIn('key', setting)
self.assertIn('sender', setting)
self.assertIn('subject', setting)
self.assertIn('to', setting)
self.assertIn('cc', setting)
self.assertIn('notes', setting)
self.assertIn('enabled', setting)
self.assertIn("key", setting)
self.assertIn("sender", setting)
self.assertIn("subject", setting)
self.assertIn("to", setting)
self.assertIn("cc", setting)
self.assertIn("notes", setting)
self.assertIn("enabled", setting)
# not found
with patch.object(self.request, 'matchdict', new={'key': 'this-should_notEXIST'}):
with patch.object(
self.request, "matchdict", new={"key": "this-should_notEXIST"}
):
self.assertRaises(HTTPNotFound, view.get_instance)
def test_get_instance_title(self):
view = self.make_view()
result = view.get_instance_title({'subject': 'whatever'})
self.assertEqual(result, 'whatever')
result = view.get_instance_title({"subject": "whatever"})
self.assertEqual(result, "whatever")
def test_configure_form(self):
self.config.setdefault('wutta.email.default.sender', 'test@example.com')
self.config.setdefault("wutta.email.default.sender", "test@example.com")
view = self.make_view()
with patch.object(self.request, 'matchdict', new={'key': 'feedback'}):
with patch.object(self.request, "matchdict", new={"key": "feedback"}):
setting = view.get_instance()
form = view.make_model_form(setting)
self.assertIn('description', form.readonly_fields)
self.assertFalse(form.required_fields['replyto'])
self.assertIn("description", form.readonly_fields)
self.assertFalse(form.required_fields["replyto"])
def test_persist(self):
model = self.app.model
self.config.setdefault('wutta.email.default.sender', 'test@example.com')
self.config.setdefault("wutta.email.default.sender", "test@example.com")
view = self.make_view()
# start w/ no settings in db
self.assertEqual(self.session.query(model.Setting).count(), 0)
# "edit" settings for feedback email
with patch.object(self.request, 'matchdict', new={'key': 'feedback'}):
with patch.object(self.request, "matchdict", new={"key": "feedback"}):
setting = view.get_instance()
setting['subject'] = 'Testing Feedback'
setting['sender'] = 'feedback@example.com'
setting['replyto'] = 'feedback4@example.com'
setting['to'] = 'feedback@example.com'
setting['cc'] = 'feedback2@example.com'
setting['bcc'] = 'feedback3@example.com'
setting['notes'] = "did this work?"
setting['enabled'] = True
setting["subject"] = "Testing Feedback"
setting["sender"] = "feedback@example.com"
setting["replyto"] = "feedback4@example.com"
setting["to"] = "feedback@example.com"
setting["cc"] = "feedback2@example.com"
setting["bcc"] = "feedback3@example.com"
setting["notes"] = "did this work?"
setting["enabled"] = True
# persist email settings
with patch.object(view, 'Session', return_value=self.session):
with patch.object(view, "Session", return_value=self.session):
view.persist(setting)
self.session.commit()
# check settings in db
self.assertEqual(self.session.query(model.Setting).count(), 8)
self.assertEqual(self.app.get_setting(self.session, 'wutta.email.feedback.subject'),
"Testing Feedback")
self.assertEqual(self.app.get_setting(self.session, 'wutta.email.feedback.sender'),
'feedback@example.com')
self.assertEqual(self.app.get_setting(self.session, 'wutta.email.feedback.replyto'),
'feedback4@example.com')
self.assertEqual(self.app.get_setting(self.session, 'wutta.email.feedback.to'),
'feedback@example.com')
self.assertEqual(self.app.get_setting(self.session, 'wutta.email.feedback.cc'),
'feedback2@example.com')
self.assertEqual(self.app.get_setting(self.session, 'wutta.email.feedback.bcc'),
'feedback3@example.com')
self.assertEqual(self.app.get_setting(self.session, 'wutta.email.feedback.notes'),
"did this work?")
self.assertEqual(self.app.get_setting(self.session, 'wutta.email.feedback.enabled'),
'true')
self.assertEqual(
self.app.get_setting(self.session, "wutta.email.feedback.subject"),
"Testing Feedback",
)
self.assertEqual(
self.app.get_setting(self.session, "wutta.email.feedback.sender"),
"feedback@example.com",
)
self.assertEqual(
self.app.get_setting(self.session, "wutta.email.feedback.replyto"),
"feedback4@example.com",
)
self.assertEqual(
self.app.get_setting(self.session, "wutta.email.feedback.to"),
"feedback@example.com",
)
self.assertEqual(
self.app.get_setting(self.session, "wutta.email.feedback.cc"),
"feedback2@example.com",
)
self.assertEqual(
self.app.get_setting(self.session, "wutta.email.feedback.bcc"),
"feedback3@example.com",
)
self.assertEqual(
self.app.get_setting(self.session, "wutta.email.feedback.notes"),
"did this work?",
)
self.assertEqual(
self.app.get_setting(self.session, "wutta.email.feedback.enabled"), "true"
)
# "edit" settings for feedback email
with patch.object(self.request, 'matchdict', new={'key': 'feedback'}):
with patch.object(self.request, "matchdict", new={"key": "feedback"}):
setting = view.get_instance()
setting['subject'] = None
setting['sender'] = None
setting['replyto'] = None
setting['to'] = None
setting['cc'] = None
setting['bcc'] = None
setting['notes'] = None
setting['enabled'] = False
setting["subject"] = None
setting["sender"] = None
setting["replyto"] = None
setting["to"] = None
setting["cc"] = None
setting["bcc"] = None
setting["notes"] = None
setting["enabled"] = False
# persist email settings
with patch.object(view, 'Session', return_value=self.session):
with patch.object(view, "Session", return_value=self.session):
view.persist(setting)
self.session.commit()
# check settings in db
self.assertEqual(self.session.query(model.Setting).count(), 1)
self.assertIsNone(self.app.get_setting(self.session, 'wutta.email.feedback.subject'))
self.assertIsNone(self.app.get_setting(self.session, 'wutta.email.feedback.sender'))
self.assertIsNone(self.app.get_setting(self.session, 'wutta.email.feedback.replyto'))
self.assertIsNone(self.app.get_setting(self.session, 'wutta.email.feedback.to'))
self.assertIsNone(self.app.get_setting(self.session, 'wutta.email.feedback.cc'))
self.assertIsNone(self.app.get_setting(self.session, 'wutta.email.feedback.bcc'))
self.assertIsNone(self.app.get_setting(self.session, 'wutta.email.feedback.notes'))
self.assertEqual(self.app.get_setting(self.session, 'wutta.email.feedback.enabled'),
'false')
self.assertIsNone(
self.app.get_setting(self.session, "wutta.email.feedback.subject")
)
self.assertIsNone(
self.app.get_setting(self.session, "wutta.email.feedback.sender")
)
self.assertIsNone(
self.app.get_setting(self.session, "wutta.email.feedback.replyto")
)
self.assertIsNone(self.app.get_setting(self.session, "wutta.email.feedback.to"))
self.assertIsNone(self.app.get_setting(self.session, "wutta.email.feedback.cc"))
self.assertIsNone(
self.app.get_setting(self.session, "wutta.email.feedback.bcc")
)
self.assertIsNone(
self.app.get_setting(self.session, "wutta.email.feedback.notes")
)
self.assertEqual(
self.app.get_setting(self.session, "wutta.email.feedback.enabled"), "false"
)
def test_render_to_response(self):
self.config.setdefault('wutta.email.default.sender', 'test@example.com')
self.pyramid_config.add_route('home', '/')
self.pyramid_config.add_route('login', '/auth/login')
self.pyramid_config.add_route('email_settings', '/email/settings')
self.pyramid_config.add_route('email_settings.preview', '/email/settings/{key}/preview')
self.config.setdefault("wutta.email.default.sender", "test@example.com")
self.pyramid_config.add_route("home", "/")
self.pyramid_config.add_route("login", "/auth/login")
self.pyramid_config.add_route("email_settings", "/email/settings")
self.pyramid_config.add_route(
"email_settings.preview", "/email/settings/{key}/preview"
)
view = self.make_view()
# nb. this gives coverage, but tests nothing..
with patch.object(self.request, 'matchdict', new={'key': 'feedback'}):
with patch.object(self.request, "matchdict", new={"key": "feedback"}):
setting = view.get_instance()
with patch.object(view, 'viewing', new=True):
context = {'instance': setting}
response = view.render_to_response('view', context)
with patch.object(view, "viewing", new=True):
context = {"instance": setting}
response = view.render_to_response("view", context)
self.assertIsInstance(response, Response)
def test_preview(self):
self.config.setdefault('wutta.email.default.sender', 'test@example.com')
self.config.setdefault("wutta.email.default.sender", "test@example.com")
view = self.make_view()
# nb. this gives coverage, but tests nothing..
with patch.object(self.request, 'matchdict', new={'key': 'feedback'}):
with patch.object(self.request, "matchdict", new={"key": "feedback"}):
# html
with patch.object(self.request, 'params', new={'mode': 'html'}):
with patch.object(self.request, "params", new={"mode": "html"}):
response = view.preview()
self.assertEqual(response.content_type, 'text/html')
self.assertEqual(response.content_type, "text/html")
# txt
with patch.object(self.request, 'params', new={'mode': 'txt'}):
with patch.object(self.request, "params", new={"mode": "txt"}):
response = view.preview()
self.assertEqual(response.content_type, 'text/plain')
self.assertEqual(response.content_type, "text/plain")

View file

@ -7,4 +7,4 @@ from wuttaweb.testing import WebTestCase
class TestEssentialViews(WebTestCase):
def test_includeme(self):
self.pyramid_config.include('wuttaweb.views.essential')
self.pyramid_config.include("wuttaweb.views.essential")

File diff suppressed because it is too large Load diff

View file

@ -18,7 +18,7 @@ class TestPersonView(WebTestCase):
return people.PersonView(self.request)
def test_includeme(self):
self.pyramid_config.include('wuttaweb.views.people')
self.pyramid_config.include("wuttaweb.views.people")
def test_get_query(self):
view = self.make_view()
@ -31,7 +31,7 @@ class TestPersonView(WebTestCase):
grid = view.make_grid(model_class=model.Setting)
self.assertEqual(grid.linked_columns, [])
view.configure_grid(grid)
self.assertIn('full_name', grid.linked_columns)
self.assertIn("full_name", grid.linked_columns)
def test_configure_form(self):
model = self.app.model
@ -39,19 +39,19 @@ class TestPersonView(WebTestCase):
# full_name
form = view.make_form(model_class=model.Person)
self.assertIn('full_name', form)
with patch.object(view, 'creating', new=True):
self.assertIn("full_name", form)
with patch.object(view, "creating", new=True):
view.configure_form(form)
self.assertNotIn('full_name', form)
self.assertNotIn("full_name", form)
# users
person = model.Person()
form = view.make_form(model_instance=person)
self.assertNotIn('users', form.widgets)
with patch.object(view, 'viewing', new=True):
self.assertNotIn("users", form.widgets)
with patch.object(view, "viewing", new=True):
view.configure_form(form)
self.assertIn('users', form.widgets)
self.assertIsInstance(form.widgets['users'], GridWidget)
self.assertIn("users", form.widgets)
self.assertIsInstance(form.widgets["users"], GridWidget)
def test_make_users_grid(self):
model = self.app.model
@ -65,13 +65,13 @@ class TestPersonView(WebTestCase):
self.assertFalse(grid.actions)
# view + edit actions
with patch.object(self.request, 'is_root', new=True):
with patch.object(self.request, "is_root", new=True):
grid = view.make_users_grid(person)
self.assertIsInstance(grid, Grid)
self.assertIn('username', grid.linked_columns)
self.assertIn("username", grid.linked_columns)
self.assertEqual(len(grid.actions), 2)
self.assertEqual(grid.actions[0].key, 'view')
self.assertEqual(grid.actions[1].key, 'edit')
self.assertEqual(grid.actions[0].key, "view")
self.assertEqual(grid.actions[1].key, "edit")
def test_objectify(self):
model = self.app.model
@ -79,15 +79,15 @@ class TestPersonView(WebTestCase):
# creating
form = view.make_model_form()
form.validated = {'first_name': 'Barney', 'last_name': 'Rubble'}
form.validated = {"first_name": "Barney", "last_name": "Rubble"}
person = view.objectify(form)
self.assertEqual(person.full_name, 'Barney Rubble')
self.assertEqual(person.full_name, "Barney Rubble")
# editing
form = view.make_model_form(model_instance=person)
form.validated = {'first_name': 'Betty', 'last_name': 'Rubble'}
form.validated = {"first_name": "Betty", "last_name": "Rubble"}
person2 = view.objectify(form)
self.assertEqual(person2.full_name, 'Betty Rubble')
self.assertEqual(person2.full_name, "Betty Rubble")
self.assertIs(person2, person)
def test_autocomplete_query(self):
@ -100,24 +100,24 @@ class TestPersonView(WebTestCase):
self.session.commit()
view = self.make_view()
with patch.object(view, 'Session', return_value=self.session):
with patch.object(view, "Session", return_value=self.session):
# both people match
query = view.autocomplete_query('george')
query = view.autocomplete_query("george")
self.assertEqual(query.count(), 2)
# just 1 match
query = view.autocomplete_query('jones')
query = view.autocomplete_query("jones")
self.assertEqual(query.count(), 1)
# no matches
query = view.autocomplete_query('sally')
query = view.autocomplete_query("sally")
self.assertEqual(query.count(), 0)
def test_view_profile(self):
self.pyramid_config.include('wuttaweb.views.common')
self.pyramid_config.include('wuttaweb.views.auth')
self.pyramid_config.add_route('people', '/people/')
self.pyramid_config.include("wuttaweb.views.common")
self.pyramid_config.include("wuttaweb.views.auth")
self.pyramid_config.add_route("people", "/people/")
model = self.app.model
person = model.Person(full_name="Barney Rubble")
@ -126,12 +126,12 @@ class TestPersonView(WebTestCase):
# sanity check
view = self.make_view()
self.request.matchdict = {'uuid': person.uuid}
self.request.matchdict = {"uuid": person.uuid}
response = view.view_profile(session=self.session)
self.assertEqual(response.status_code, 200)
def test_make_user(self):
self.pyramid_config.include('wuttaweb.views.common')
self.pyramid_config.include("wuttaweb.views.common")
model = self.app.model
person = model.Person(full_name="Barney Rubble")
@ -140,7 +140,7 @@ class TestPersonView(WebTestCase):
# sanity check
view = self.make_view()
self.request.matchdict = {'uuid': person.uuid}
self.request.matchdict = {"uuid": person.uuid}
response = view.make_user()
# nb. this always redirects for now
self.assertEqual(response.status_code, 302)

View file

@ -10,11 +10,11 @@ from wuttaweb.testing import WebTestCase
class TestProgressView(WebTestCase):
def test_includeme(self):
self.pyramid_config.include('wuttaweb.views.progress')
self.pyramid_config.include("wuttaweb.views.progress")
def test_basic(self):
self.request.session.id = 'mockid'
self.request.matchdict = {'key': 'foo'}
self.request.session.id = "mockid"
self.request.matchdict = {"key": "foo"}
# first call with no setup, will create the progress session
# but it should be "empty" - except not really since beaker
@ -23,40 +23,40 @@ class TestProgressView(WebTestCase):
self.assertIsInstance(context, dict)
# now let's establish a progress session of our own
progsess = get_progress_session(self.request, 'bar')
progsess['maximum'] = 2
progsess['value'] = 1
progsess = get_progress_session(self.request, "bar")
progsess["maximum"] = 2
progsess["value"] = 1
progsess.save()
# then call view, check results
self.request.matchdict = {'key': 'bar'}
self.request.matchdict = {"key": "bar"}
context = mod.progress(self.request)
self.assertEqual(context['maximum'], 2)
self.assertEqual(context['value'], 1)
self.assertNotIn('complete', context)
self.assertEqual(context["maximum"], 2)
self.assertEqual(context["value"], 1)
self.assertNotIn("complete", context)
# now mark it as complete, check results
progsess['complete'] = True
progsess['success_msg'] = "yay!"
progsess["complete"] = True
progsess["success_msg"] = "yay!"
progsess.save()
context = mod.progress(self.request)
self.assertTrue(context['complete'])
self.assertEqual(context['success_msg'], "yay!")
self.assertTrue(context["complete"])
self.assertEqual(context["success_msg"], "yay!")
# now do that all again, with error
progsess = get_progress_session(self.request, 'baz')
progsess['maximum'] = 2
progsess['value'] = 1
progsess = get_progress_session(self.request, "baz")
progsess["maximum"] = 2
progsess["value"] = 1
progsess.save()
self.request.matchdict = {'key': 'baz'}
self.request.matchdict = {"key": "baz"}
context = mod.progress(self.request)
self.assertEqual(context['maximum'], 2)
self.assertEqual(context['value'], 1)
self.assertNotIn('complete', context)
self.assertNotIn('error', context)
progsess['error'] = True
progsess['error_msg'] = "omg!"
self.assertEqual(context["maximum"], 2)
self.assertEqual(context["value"], 1)
self.assertNotIn("complete", context)
self.assertNotIn("error", context)
progsess["error"] = True
progsess["error_msg"] = "omg!"
progsess.save()
context = mod.progress(self.request)
self.assertTrue(context['error'])
self.assertEqual(context['error_msg'], "omg!")
self.assertTrue(context["error"])
self.assertEqual(context["error_msg"], "omg!")

View file

@ -16,28 +16,29 @@ class SomeRandomReport(Report):
"""
This report shows something random.
"""
report_key = 'testing_some_random'
report_key = "testing_some_random"
report_title = "Random Test Report"
def add_params(self, schema):
schema.add(colander.SchemaNode(
colander.String(),
name='foo',
missing=colander.null))
schema.add(
colander.SchemaNode(colander.String(), name="foo", missing=colander.null)
)
schema.add(colander.SchemaNode(
colander.Date(),
name='start_date',
missing=colander.null))
schema.add(
colander.SchemaNode(
colander.Date(), name="start_date", missing=colander.null
)
)
def get_output_columns(self):
return ['foo']
return ["foo"]
def make_data(self, params, **kwargs):
return {
'output_title': "Testing Output",
'data': [{'foo': 'bar'}],
"output_title": "Testing Output",
"data": [{"foo": "bar"}],
}
@ -47,68 +48,77 @@ class TestReportViews(WebTestCase):
return mod.ReportView(self.request)
def test_includeme(self):
self.pyramid_config.include('wuttaweb.views.reports')
self.pyramid_config.include("wuttaweb.views.reports")
def test_get_grid_data(self):
view = self.make_view()
providers = dict(self.app.providers)
providers['wuttatest'] = MagicMock(report_modules=['tests.views.test_reports'])
with patch.object(self.app, 'providers', new=providers):
providers["wuttatest"] = MagicMock(report_modules=["tests.views.test_reports"])
with patch.object(self.app, "providers", new=providers):
data = view.get_grid_data()
self.assertIsInstance(data, list)
self.assertTrue(data) # 1+ reports
self.assertTrue(data) # 1+ reports
def test_normalize_report(self):
view = self.make_view()
report = SomeRandomReport(self.config)
normal = view.normalize_report(report)
help_text = normal.pop('help_text').strip()
help_text = normal.pop("help_text").strip()
self.assertEqual(help_text, "This report shows something random.")
self.assertEqual(normal, {
'report_key': 'testing_some_random',
'report_title': "Random Test Report",
})
self.assertEqual(
normal,
{
"report_key": "testing_some_random",
"report_title": "Random Test Report",
},
)
def test_configure_grid(self):
view = self.make_view()
grid = view.make_model_grid()
self.assertIn('report_title', grid.searchable_columns)
self.assertIn('help_text', grid.searchable_columns)
self.assertIn("report_title", grid.searchable_columns)
self.assertIn("help_text", grid.searchable_columns)
def test_get_instance(self):
view = self.make_view()
providers = {
'wuttatest': MagicMock(report_modules=['tests.views.test_reports']),
"wuttatest": MagicMock(report_modules=["tests.views.test_reports"]),
}
with patch.object(self.app, 'providers', new=providers):
with patch.object(self.app, "providers", new=providers):
# normal
with patch.object(self.request, 'matchdict', new={'report_key': 'testing_some_random'}):
with patch.object(
self.request, "matchdict", new={"report_key": "testing_some_random"}
):
report = view.get_instance()
self.assertIsInstance(report, dict)
self.assertEqual(report['report_key'], 'testing_some_random')
self.assertEqual(report['report_title'], "Random Test Report")
self.assertEqual(report["report_key"], "testing_some_random")
self.assertEqual(report["report_title"], "Random Test Report")
# not found
with patch.object(self.request, 'matchdict', new={'report_key': 'this-should_notEXIST'}):
with patch.object(
self.request, "matchdict", new={"report_key": "this-should_notEXIST"}
):
self.assertRaises(HTTPNotFound, view.get_instance)
def test_get_instance_title(self):
view = self.make_view()
result = view.get_instance_title({'report_title': 'whatever'})
self.assertEqual(result, 'whatever')
result = view.get_instance_title({"report_title": "whatever"})
self.assertEqual(result, "whatever")
def test_view(self):
self.pyramid_config.add_route('home', '/')
self.pyramid_config.add_route('login', '/auth/login')
self.pyramid_config.add_route('reports', '/reports/')
self.pyramid_config.add_route('reports.view', '/reports/{report_key}')
self.pyramid_config.add_route("home", "/")
self.pyramid_config.add_route("login", "/auth/login")
self.pyramid_config.add_route("reports", "/reports/")
self.pyramid_config.add_route("reports.view", "/reports/{report_key}")
view = self.make_view()
providers = dict(self.app.providers)
providers['wuttatest'] = MagicMock(report_modules=['tests.views.test_reports'])
with patch.object(self.app, 'providers', new=providers):
with patch.object(self.request, 'matchdict', new={'report_key': 'testing_some_random'}):
providers["wuttatest"] = MagicMock(report_modules=["tests.views.test_reports"])
with patch.object(self.app, "providers", new=providers):
with patch.object(
self.request, "matchdict", new={"report_key": "testing_some_random"}
):
# initial view
response = view.view()
@ -118,11 +128,15 @@ class TestReportViews(WebTestCase):
self.assertNotIn("Testing Output", response.text)
# run the report
with patch.object(self.request, 'GET', new={
'__start__': 'start_date:mapping',
'date': '2025-01-11',
'__end__': 'start_date',
}):
with patch.object(
self.request,
"GET",
new={
"__start__": "start_date:mapping",
"date": "2025-01-11",
"__end__": "start_date",
},
):
response = view.view()
self.assertEqual(response.status_code, 200)
# nb. there's a button in there somewhere, *and* an output title
@ -132,105 +146,142 @@ class TestReportViews(WebTestCase):
def test_configure_form(self):
view = self.make_view()
providers = dict(self.app.providers)
providers['wuttatest'] = MagicMock(report_modules=['tests.views.test_reports'])
with patch.object(self.app, 'providers', new=providers):
providers["wuttatest"] = MagicMock(report_modules=["tests.views.test_reports"])
with patch.object(self.app, "providers", new=providers):
with patch.object(self.request, 'matchdict', new={'report_key': 'testing_some_random'}):
with patch.object(
self.request, "matchdict", new={"report_key": "testing_some_random"}
):
report = view.get_instance()
form = view.make_model_form(report)
self.assertIn('help_text', form.readonly_fields)
self.assertIn('foo', form)
self.assertIn("help_text", form.readonly_fields)
self.assertIn("foo", form)
def test_normalize_columns(self):
view = self.make_view()
columns = view.normalize_columns(['foo'])
self.assertEqual(columns, [
{'name': 'foo', 'label': 'foo'},
])
columns = view.normalize_columns(["foo"])
self.assertEqual(
columns,
[
{"name": "foo", "label": "foo"},
],
)
columns = view.normalize_columns([{'name': 'foo'}])
self.assertEqual(columns, [
{'name': 'foo', 'label': 'foo'},
])
columns = view.normalize_columns([{"name": "foo"}])
self.assertEqual(
columns,
[
{"name": "foo", "label": "foo"},
],
)
columns = view.normalize_columns([{'name': 'foo', 'label': "FOO"}])
self.assertEqual(columns, [
{'name': 'foo', 'label': 'FOO'},
])
columns = view.normalize_columns([{"name": "foo", "label": "FOO"}])
self.assertEqual(
columns,
[
{"name": "foo", "label": "FOO"},
],
)
columns = view.normalize_columns([{'name': 'foo', 'label': "FOO", 'numeric': True}])
self.assertEqual(columns, [
{'name': 'foo', 'label': 'FOO', 'numeric': True},
])
columns = view.normalize_columns(
[{"name": "foo", "label": "FOO", "numeric": True}]
)
self.assertEqual(
columns,
[
{"name": "foo", "label": "FOO", "numeric": True},
],
)
def test_run_report(self):
view = self.make_view()
providers = dict(self.app.providers)
providers['wuttatest'] = MagicMock(report_modules=['tests.views.test_reports'])
with patch.object(self.app, 'providers', new=providers):
providers["wuttatest"] = MagicMock(report_modules=["tests.views.test_reports"])
with patch.object(self.app, "providers", new=providers):
with patch.object(self.request, 'matchdict', new={'report_key': 'testing_some_random'}):
report = view.report_handler.get_report('testing_some_random')
with patch.object(
self.request, "matchdict", new={"report_key": "testing_some_random"}
):
report = view.report_handler.get_report("testing_some_random")
normal = view.normalize_report(report)
form = view.make_model_form(normal)
# typical
context = view.run_report(report, {'form': form})
self.assertEqual(sorted(context['report_params']), ['foo', 'start_date'])
self.assertEqual(context['report_data'], {
'output_title': "Testing Output",
'data': [{'foo': 'bar'}],
})
self.assertIn('report_generated', context)
context = view.run_report(report, {"form": form})
self.assertEqual(
sorted(context["report_params"]), ["foo", "start_date"]
)
self.assertEqual(
context["report_data"],
{
"output_title": "Testing Output",
"data": [{"foo": "bar"}],
},
)
self.assertIn("report_generated", context)
# invalid params
with patch.object(self.request, 'GET', new={'start_date': 'NOT_GOOD'}):
context = view.run_report(report, {'form': form})
self.assertNotIn('report_params', context)
self.assertNotIn('report_data', context)
self.assertNotIn('report_generated', context)
with patch.object(self.request, "GET", new={"start_date": "NOT_GOOD"}):
context = view.run_report(report, {"form": form})
self.assertNotIn("report_params", context)
self.assertNotIn("report_data", context)
self.assertNotIn("report_generated", context)
# custom formatter
with patch.object(report, 'get_output_columns') as get_output_columns:
with patch.object(report, "get_output_columns") as get_output_columns:
get_output_columns.return_value = [
'foo',
{'name': 'start_date',
'formatter': lambda val: "FORMATTED VALUE"},
"foo",
{
"name": "start_date",
"formatter": lambda val: "FORMATTED VALUE",
},
]
with patch.object(report, 'make_data') as make_data:
with patch.object(report, "make_data") as make_data:
make_data.return_value = [
{'foo': 'bar', 'start_date': datetime.date(2025, 1, 11)},
{"foo": "bar", "start_date": datetime.date(2025, 1, 11)},
]
context = view.run_report(report, {'form': form})
context = view.run_report(report, {"form": form})
get_output_columns.assert_called_once_with()
self.assertEqual(len(context['report_columns']), 2)
self.assertEqual(context['report_columns'][0]['name'], 'foo')
self.assertEqual(context['report_columns'][1]['name'], 'start_date')
self.assertEqual(context['report_data'], {
'output_title': "Random Test Report",
'data': [{'foo': 'bar', 'start_date': 'FORMATTED VALUE'}],
})
self.assertEqual(len(context["report_columns"]), 2)
self.assertEqual(context["report_columns"][0]["name"], "foo")
self.assertEqual(
context["report_columns"][1]["name"], "start_date"
)
self.assertEqual(
context["report_data"],
{
"output_title": "Random Test Report",
"data": [
{"foo": "bar", "start_date": "FORMATTED VALUE"}
],
},
)
def test_download_data(self):
view = self.make_view()
providers = dict(self.app.providers)
providers['wuttatest'] = MagicMock(report_modules=['tests.views.test_reports'])
with patch.object(self.app, 'providers', new=providers):
with patch.object(self.request, 'matchdict', new={'report_key': 'testing_some_random'}):
providers["wuttatest"] = MagicMock(report_modules=["tests.views.test_reports"])
with patch.object(self.app, "providers", new=providers):
with patch.object(
self.request, "matchdict", new={"report_key": "testing_some_random"}
):
params, columns, data = view.get_download_data()
self.assertEqual(params, {})
self.assertEqual(columns, [{'name': 'foo', 'label': 'foo'}])
self.assertEqual(data, {
'output_title': "Testing Output",
'data': [{'foo': 'bar'}],
})
self.assertEqual(columns, [{"name": "foo", "label": "foo"}])
self.assertEqual(
data,
{
"output_title": "Testing Output",
"data": [{"foo": "bar"}],
},
)
def test_download_path(self):
view = self.make_view()
data = {'output_title': "My Report"}
path = view.get_download_path(data, 'csv')
self.assertTrue(path.endswith('My Report.csv'))
data = {"output_title": "My Report"}
path = view.get_download_path(data, "csv")
self.assertTrue(path.endswith("My Report.csv"))

View file

@ -18,7 +18,7 @@ class TestRoleView(WebTestCase):
return mod.RoleView(self.request)
def test_includeme(self):
self.pyramid_config.include('wuttaweb.views.roles')
self.pyramid_config.include("wuttaweb.views.roles")
def test_get_query(self):
view = self.make_view()
@ -29,9 +29,9 @@ class TestRoleView(WebTestCase):
model = self.app.model
view = self.make_view()
grid = view.make_grid(model_class=model.Role)
self.assertFalse(grid.is_linked('name'))
self.assertFalse(grid.is_linked("name"))
view.configure_grid(grid)
self.assertTrue(grid.is_linked('name'))
self.assertTrue(grid.is_linked("name"))
def test_is_editable(self):
model = self.app.model
@ -56,10 +56,10 @@ class TestRoleView(WebTestCase):
# reset
del self.request.user_permissions
barney = model.User(username='barney')
barney = model.User(username="barney")
self.session.add(barney)
barney.roles.append(blokes)
auth.grant_permission(blokes, 'roles.edit_builtin')
auth.grant_permission(blokes, "roles.edit_builtin")
self.session.commit()
# user with perms can edit *some* built-in
@ -90,9 +90,9 @@ class TestRoleView(WebTestCase):
role = model.Role(name="Foo")
view = self.make_view()
form = view.make_form(model_instance=role)
self.assertNotIn('name', form.validators)
self.assertNotIn("name", form.validators)
view.configure_form(form)
self.assertIsNotNone(form.validators['name'])
self.assertIsNotNone(form.validators["name"])
def test_make_users_grid(self):
model = self.app.model
@ -106,48 +106,48 @@ class TestRoleView(WebTestCase):
self.assertFalse(grid.actions)
# view + edit actions
with patch.object(self.request, 'is_root', new=True):
with patch.object(self.request, "is_root", new=True):
grid = view.make_users_grid(role)
self.assertIsInstance(grid, Grid)
self.assertIn('person', grid.linked_columns)
self.assertIn('username', grid.linked_columns)
self.assertIn("person", grid.linked_columns)
self.assertIn("username", grid.linked_columns)
self.assertEqual(len(grid.actions), 2)
self.assertEqual(grid.actions[0].key, 'view')
self.assertEqual(grid.actions[1].key, 'edit')
self.assertEqual(grid.actions[0].key, "view")
self.assertEqual(grid.actions[1].key, "edit")
def test_unique_name(self):
model = self.app.model
view = self.make_view()
role = model.Role(name='Foo')
role = model.Role(name="Foo")
self.session.add(role)
self.session.commit()
with patch.object(mod, 'Session', return_value=self.session):
with patch.object(mod, "Session", return_value=self.session):
# invalid if same name in data
node = colander.SchemaNode(colander.String(), name='name')
self.assertRaises(colander.Invalid, view.unique_name, node, 'Foo')
node = colander.SchemaNode(colander.String(), name="name")
self.assertRaises(colander.Invalid, view.unique_name, node, "Foo")
# but not if name belongs to current role
view.editing = True
self.request.matchdict = {'uuid': role.uuid}
node = colander.SchemaNode(colander.String(), name='name')
self.assertIsNone(view.unique_name(node, 'Foo'))
self.request.matchdict = {"uuid": role.uuid}
node = colander.SchemaNode(colander.String(), name="name")
self.assertIsNone(view.unique_name(node, "Foo"))
def get_permissions(self):
return {
'widgets': {
'label': "Widgets",
'perms': {
'widgets.list': {
'label': "List widgets",
"widgets": {
"label": "Widgets",
"perms": {
"widgets.list": {
"label": "List widgets",
},
'widgets.polish': {
'label': "Polish the widgets",
"widgets.polish": {
"label": "Polish the widgets",
},
'widgets.view': {
'label': "View widget",
"widgets.view": {
"label": "View widget",
},
},
},
@ -157,55 +157,59 @@ class TestRoleView(WebTestCase):
model = self.app.model
auth = self.app.get_auth_handler()
blokes = model.Role(name="Blokes")
auth.grant_permission(blokes, 'widgets.list')
auth.grant_permission(blokes, "widgets.list")
self.session.add(blokes)
barney = model.User(username='barney')
barney = model.User(username="barney")
barney.roles.append(blokes)
self.session.add(barney)
self.session.commit()
view = self.make_view()
all_perms = self.get_permissions()
self.request.registry.settings['wutta_permissions'] = all_perms
self.request.registry.settings["wutta_permissions"] = all_perms
def has_perm(perm):
if perm == 'widgets.list':
if perm == "widgets.list":
return True
return False
with patch.object(self.request, 'has_perm', new=has_perm, create=True):
with patch.object(self.request, "has_perm", new=has_perm, create=True):
# sanity check; current request has 1 perm
self.assertTrue(self.request.has_perm('widgets.list'))
self.assertFalse(self.request.has_perm('widgets.polish'))
self.assertFalse(self.request.has_perm('widgets.view'))
self.assertTrue(self.request.has_perm("widgets.list"))
self.assertFalse(self.request.has_perm("widgets.polish"))
self.assertFalse(self.request.has_perm("widgets.view"))
# when editing, user sees only the 1 perm
with patch.object(view, 'editing', new=True):
with patch.object(view, "editing", new=True):
perms = view.get_available_permissions()
self.assertEqual(list(perms), ['widgets'])
self.assertEqual(list(perms['widgets']['perms']), ['widgets.list'])
self.assertEqual(list(perms), ["widgets"])
self.assertEqual(list(perms["widgets"]["perms"]), ["widgets.list"])
# but when viewing, same user sees all perms
with patch.object(view, 'viewing', new=True):
with patch.object(view, "viewing", new=True):
perms = view.get_available_permissions()
self.assertEqual(list(perms), ['widgets'])
self.assertEqual(list(perms['widgets']['perms']),
['widgets.list', 'widgets.polish', 'widgets.view'])
self.assertEqual(list(perms), ["widgets"])
self.assertEqual(
list(perms["widgets"]["perms"]),
["widgets.list", "widgets.polish", "widgets.view"],
)
# also, when admin user is editing, sees all perms
self.request.is_admin = True
with patch.object(view, 'editing', new=True):
with patch.object(view, "editing", new=True):
perms = view.get_available_permissions()
self.assertEqual(list(perms), ['widgets'])
self.assertEqual(list(perms['widgets']['perms']),
['widgets.list', 'widgets.polish', 'widgets.view'])
self.assertEqual(list(perms), ["widgets"])
self.assertEqual(
list(perms["widgets"]["perms"]),
["widgets.list", "widgets.polish", "widgets.view"],
)
def test_objectify(self):
model = self.app.model
auth = self.app.get_auth_handler()
blokes = model.Role(name="Blokes")
self.session.add(blokes)
barney = model.User(username='barney')
barney = model.User(username="barney")
barney.roles.append(blokes)
self.session.add(barney)
self.session.commit()
@ -213,56 +217,62 @@ class TestRoleView(WebTestCase):
permissions = self.get_permissions()
# sanity check, role has just 1 perm
auth.grant_permission(blokes, 'widgets.list')
auth.grant_permission(blokes, "widgets.list")
self.session.commit()
self.assertEqual(blokes.permissions, ['widgets.list'])
self.assertEqual(blokes.permissions, ["widgets.list"])
# form can update role perms
view.editing = True
self.request.matchdict = {'uuid': blokes.uuid}
with patch.object(view, 'get_available_permissions', return_value=permissions):
self.request.matchdict = {"uuid": blokes.uuid}
with patch.object(view, "get_available_permissions", return_value=permissions):
form = view.make_model_form(model_instance=blokes)
form.validated = {'name': 'Blokes',
'permissions': {'widgets.list', 'widgets.polish', 'widgets.view'}}
form.validated = {
"name": "Blokes",
"permissions": {"widgets.list", "widgets.polish", "widgets.view"},
}
role = view.objectify(form)
self.session.commit()
self.assertIs(role, blokes)
self.assertEqual(blokes.permissions, ['widgets.list', 'widgets.polish', 'widgets.view'])
self.assertEqual(
blokes.permissions, ["widgets.list", "widgets.polish", "widgets.view"]
)
def test_update_permissions(self):
model = self.app.model
auth = self.app.get_auth_handler()
blokes = model.Role(name="Blokes")
auth.grant_permission(blokes, 'widgets.list')
auth.grant_permission(blokes, "widgets.list")
self.session.add(blokes)
barney = model.User(username='barney')
barney = model.User(username="barney")
barney.roles.append(blokes)
self.session.add(barney)
self.session.commit()
view = self.make_view()
permissions = self.get_permissions()
with patch.object(view, 'get_available_permissions', return_value=permissions):
with patch.object(view, "get_available_permissions", return_value=permissions):
# no error if data is missing perms
form = view.make_model_form(model_instance=blokes)
form.validated = {'name': 'BloX'}
form.validated = {"name": "BloX"}
role = view.objectify(form)
self.session.commit()
self.assertIs(role, blokes)
self.assertEqual(blokes.name, 'BloX')
self.assertEqual(blokes.name, "BloX")
# sanity check, role has just 1 perm
self.assertEqual(blokes.permissions, ['widgets.list'])
self.assertEqual(blokes.permissions, ["widgets.list"])
# role perms are updated
form = view.make_model_form(model_instance=blokes)
form.validated = {'name': 'Blokes',
'permissions': {'widgets.polish', 'widgets.view'}}
form.validated = {
"name": "Blokes",
"permissions": {"widgets.polish", "widgets.view"},
}
role = view.objectify(form)
self.session.commit()
self.assertIs(role, blokes)
self.assertEqual(blokes.permissions, ['widgets.polish', 'widgets.view'])
self.assertEqual(blokes.permissions, ["widgets.polish", "widgets.view"])
class TestPermissionView(WebTestCase):
@ -279,17 +289,17 @@ class TestPermissionView(WebTestCase):
model = self.app.model
view = self.make_view()
grid = view.make_grid(model_class=model.Permission)
self.assertFalse(grid.is_linked('role'))
self.assertFalse(grid.is_linked("role"))
view.configure_grid(grid)
self.assertTrue(grid.is_linked('role'))
self.assertTrue(grid.is_linked("role"))
def test_configure_form(self):
model = self.app.model
role = model.Role(name="Foo")
perm = model.Permission(role=role, permission='whatever')
perm = model.Permission(role=role, permission="whatever")
view = self.make_view()
form = view.make_form(model_instance=perm)
self.assertIsNone(form.schema)
view.configure_form(form)
schema = form.get_schema()
self.assertIsInstance(schema['role'].typ, RoleRef)
self.assertIsInstance(schema["role"].typ, RoleRef)

View file

@ -13,7 +13,7 @@ class TestAppInfoView(WebTestCase):
def setUp(self):
self.setup_web()
self.pyramid_config.include('wuttaweb.views.essential')
self.pyramid_config.include("wuttaweb.views.essential")
def make_view(self):
return mod.AppInfoView(self.request)
@ -26,7 +26,7 @@ class TestAppInfoView(WebTestCase):
self.assertEqual(data, [])
# 'partial' request returns data
self.request.GET = {'partial': '1'}
self.request.GET = {"partial": "1"}
data = view.get_grid_data()
self.assertIsInstance(data, list)
self.assertTrue(data)
@ -61,7 +61,7 @@ class TestSettingView(WebTestCase):
self.assertEqual(len(data), 0)
# unless we save some settings
self.app.save_setting(self.session, 'foo', 'bar')
self.app.save_setting(self.session, "foo", "bar")
self.session.commit()
query = view.get_grid_data(session=self.session)
data = query.all()
@ -71,34 +71,34 @@ class TestSettingView(WebTestCase):
model = self.app.model
view = self.make_view()
grid = view.make_grid(model_class=model.Setting)
self.assertFalse(grid.is_linked('name'))
self.assertFalse(grid.is_linked("name"))
view.configure_grid(grid)
self.assertTrue(grid.is_linked('name'))
self.assertTrue(grid.is_linked("name"))
def test_configure_form(self):
view = self.make_view()
form = view.make_form(fields=view.get_form_fields())
self.assertNotIn('value', form.required_fields)
self.assertNotIn("value", form.required_fields)
view.configure_form(form)
self.assertIn('value', form.required_fields)
self.assertFalse(form.required_fields['value'])
self.assertIn("value", form.required_fields)
self.assertFalse(form.required_fields["value"])
def test_unique_name(self):
model = self.app.model
view = self.make_view()
setting = model.Setting(name='foo')
setting = model.Setting(name="foo")
self.session.add(setting)
self.session.commit()
with patch.object(view, 'Session', return_value=self.session):
with patch.object(view, "Session", return_value=self.session):
# invalid if same name in data
node = colander.SchemaNode(colander.String(), name='name')
self.assertRaises(colander.Invalid, view.unique_name, node, 'foo')
node = colander.SchemaNode(colander.String(), name="name")
self.assertRaises(colander.Invalid, view.unique_name, node, "foo")
# but not if name belongs to current setting
view.editing = True
self.request.matchdict = {'name': 'foo'}
node = colander.SchemaNode(colander.String(), name='name')
self.assertIsNone(view.unique_name(node, 'foo'))
self.request.matchdict = {"name": "foo"}
node = colander.SchemaNode(colander.String(), name="name")
self.assertIsNone(view.unique_name(node, "foo"))

View file

@ -17,7 +17,7 @@ class TestUpgradeView(WebTestCase):
return mod.UpgradeView(self.request)
def test_includeme(self):
self.pyramid_config.include('wuttaweb.views.upgrades')
self.pyramid_config.include("wuttaweb.views.upgrades")
def test_configure_grid(self):
model = self.app.model
@ -37,74 +37,79 @@ class TestUpgradeView(WebTestCase):
self.assertIsNone(view.grid_row_class(upgrade, data, 1))
upgrade.status = enum.UpgradeStatus.EXECUTING
self.assertEqual(view.grid_row_class(upgrade, data, 1), 'has-background-warning')
self.assertEqual(
view.grid_row_class(upgrade, data, 1), "has-background-warning"
)
upgrade.status = enum.UpgradeStatus.SUCCESS
self.assertIsNone(view.grid_row_class(upgrade, data, 1))
upgrade.status = enum.UpgradeStatus.FAILURE
self.assertEqual(view.grid_row_class(upgrade, data, 1), 'has-background-warning')
self.assertEqual(
view.grid_row_class(upgrade, data, 1), "has-background-warning"
)
def test_configure_form(self):
self.pyramid_config.add_route('upgrades.download', '/upgrades/{uuid}/download')
self.pyramid_config.add_route("upgrades.download", "/upgrades/{uuid}/download")
model = self.app.model
enum = self.app.enum
user = model.User(username='barney')
user = model.User(username="barney")
self.session.add(user)
upgrade = model.Upgrade(description='test', created_by=user,
status=enum.UpgradeStatus.PENDING)
upgrade = model.Upgrade(
description="test", created_by=user, status=enum.UpgradeStatus.PENDING
)
self.session.add(upgrade)
self.session.commit()
view = self.make_view()
# some fields exist when viewing
with patch.object(view, 'viewing', new=True):
with patch.object(view, "viewing", new=True):
form = view.make_form(model_class=model.Upgrade, model_instance=upgrade)
self.assertIn('created', form)
self.assertIn("created", form)
view.configure_form(form)
self.assertIn('created', form)
self.assertIn("created", form)
# but then are removed when creating
with patch.object(view, 'creating', new=True):
with patch.object(view, "creating", new=True):
form = view.make_form(model_class=model.Upgrade)
self.assertIn('created', form)
self.assertIn("created", form)
view.configure_form(form)
self.assertNotIn('created', form)
self.assertNotIn("created", form)
# test executed, stdout/stderr when viewing
with patch.object(view, 'viewing', new=True):
with patch.object(view, "viewing", new=True):
# executed is *not* shown by default
form = view.make_form(model_class=model.Upgrade, model_instance=upgrade)
self.assertIn('executed', form)
self.assertIn("executed", form)
view.configure_form(form)
self.assertNotIn('executed', form)
self.assertNotIn('stdout_file', form)
self.assertNotIn('stderr_file', form)
self.assertNotIn("executed", form)
self.assertNotIn("stdout_file", form)
self.assertNotIn("stderr_file", form)
# but it *is* shown if upgrade is executed
upgrade.executed = datetime.datetime.now()
upgrade.status = enum.UpgradeStatus.SUCCESS
form = view.make_form(model_class=model.Upgrade, model_instance=upgrade)
self.assertIn('executed', form)
self.assertIn("executed", form)
view.configure_form(form)
self.assertIn('executed', form)
self.assertIn('stdout_file', form)
self.assertIn('stderr_file', form)
self.assertIn("executed", form)
self.assertIn("stdout_file", form)
self.assertIn("stderr_file", form)
def test_objectify(self):
model = self.app.model
enum = self.app.enum
user = model.User(username='barney')
user = model.User(username="barney")
self.session.add(user)
self.session.commit()
view = self.make_view()
# user and status are auto-set when creating
self.request.user = user
self.request.method = 'POST'
self.request.POST = {'description': "new one"}
with patch.object(view, 'creating', new=True):
self.request.method = "POST"
self.request.POST = {"description": "new one"}
with patch.object(view, "creating", new=True):
form = view.make_model_form()
self.assertTrue(form.validate())
upgrade = view.objectify(form)
@ -116,13 +121,14 @@ class TestUpgradeView(WebTestCase):
model = self.app.model
enum = self.app.enum
appdir = self.mkdir('app')
self.config.setdefault('wutta.appdir', appdir)
appdir = self.mkdir("app")
self.config.setdefault("wutta.appdir", appdir)
self.assertEqual(self.app.get_appdir(), appdir)
user = model.User(username='barney')
upgrade = model.Upgrade(description='test', created_by=user,
status=enum.UpgradeStatus.PENDING)
user = model.User(username="barney")
upgrade = model.Upgrade(
description="test", created_by=user, status=enum.UpgradeStatus.PENDING
)
self.session.add(upgrade)
self.session.commit()
@ -134,21 +140,24 @@ class TestUpgradeView(WebTestCase):
self.assertIsNone(path)
# with filename
path = view.download_path(upgrade, 'foo.txt')
self.assertEqual(path, os.path.join(appdir, 'data', 'upgrades',
uuid[:2], uuid[2:], 'foo.txt'))
path = view.download_path(upgrade, "foo.txt")
self.assertEqual(
path,
os.path.join(appdir, "data", "upgrades", uuid[:2], uuid[2:], "foo.txt"),
)
def test_get_upgrade_filepath(self):
model = self.app.model
enum = self.app.enum
appdir = self.mkdir('app')
self.config.setdefault('wutta.appdir', appdir)
appdir = self.mkdir("app")
self.config.setdefault("wutta.appdir", appdir)
self.assertEqual(self.app.get_appdir(), appdir)
user = model.User(username='barney')
upgrade = model.Upgrade(description='test', created_by=user,
status=enum.UpgradeStatus.PENDING)
user = model.User(username="barney")
upgrade = model.Upgrade(
description="test", created_by=user, status=enum.UpgradeStatus.PENDING
)
self.session.add(upgrade)
self.session.commit()
@ -157,25 +166,29 @@ class TestUpgradeView(WebTestCase):
# no filename
path = view.get_upgrade_filepath(upgrade)
self.assertEqual(path, os.path.join(appdir, 'data', 'upgrades',
uuid[:2], uuid[2:]))
self.assertEqual(
path, os.path.join(appdir, "data", "upgrades", uuid[:2], uuid[2:])
)
# with filename
path = view.get_upgrade_filepath(upgrade, 'foo.txt')
self.assertEqual(path, os.path.join(appdir, 'data', 'upgrades',
uuid[:2], uuid[2:], 'foo.txt'))
path = view.get_upgrade_filepath(upgrade, "foo.txt")
self.assertEqual(
path,
os.path.join(appdir, "data", "upgrades", uuid[:2], uuid[2:], "foo.txt"),
)
def test_delete_instance(self):
model = self.app.model
enum = self.app.enum
appdir = self.mkdir('app')
self.config.setdefault('wutta.appdir', appdir)
appdir = self.mkdir("app")
self.config.setdefault("wutta.appdir", appdir)
self.assertEqual(self.app.get_appdir(), appdir)
user = model.User(username='barney')
upgrade = model.Upgrade(description='test', created_by=user,
status=enum.UpgradeStatus.PENDING)
user = model.User(username="barney")
upgrade = model.Upgrade(
description="test", created_by=user, status=enum.UpgradeStatus.PENDING
)
self.session.add(upgrade)
self.session.commit()
@ -183,19 +196,19 @@ class TestUpgradeView(WebTestCase):
# mock stdout/stderr files
upgrade_dir = view.get_upgrade_filepath(upgrade)
stdout = view.get_upgrade_filepath(upgrade, 'stdout.log')
with open(stdout, 'w') as f:
f.write('stdout')
stderr = view.get_upgrade_filepath(upgrade, 'stderr.log')
with open(stderr, 'w') as f:
f.write('stderr')
stdout = view.get_upgrade_filepath(upgrade, "stdout.log")
with open(stdout, "w") as f:
f.write("stdout")
stderr = view.get_upgrade_filepath(upgrade, "stderr.log")
with open(stderr, "w") as f:
f.write("stderr")
# both upgrade and files are deleted
self.assertTrue(os.path.exists(upgrade_dir))
self.assertTrue(os.path.exists(stdout))
self.assertTrue(os.path.exists(stderr))
self.assertEqual(self.session.query(model.Upgrade).count(), 1)
with patch.object(view, 'Session', return_value=self.session):
with patch.object(view, "Session", return_value=self.session):
view.delete_instance(upgrade)
self.assertFalse(os.path.exists(upgrade_dir))
self.assertFalse(os.path.exists(stdout))
@ -206,13 +219,14 @@ class TestUpgradeView(WebTestCase):
model = self.app.model
enum = self.app.enum
appdir = self.mkdir('app')
self.config.setdefault('wutta.appdir', appdir)
appdir = self.mkdir("app")
self.config.setdefault("wutta.appdir", appdir)
self.assertEqual(self.app.get_appdir(), appdir)
user = model.User(username='barney')
upgrade = model.Upgrade(description='test', created_by=user,
status=enum.UpgradeStatus.PENDING)
user = model.User(username="barney")
upgrade = model.Upgrade(
description="test", created_by=user, status=enum.UpgradeStatus.PENDING
)
self.session.add(upgrade)
self.session.commit()
@ -224,139 +238,153 @@ class TestUpgradeView(WebTestCase):
self.assertRaises(ConfigurationError, view.execute_instance, upgrade, user)
# script w/ success
goodpy = self.write_file('good.py', """
goodpy = self.write_file(
"good.py",
"""
import sys
sys.stdout.write('hello from good.py')
sys.exit(0)
""")
self.app.save_setting(self.session, 'wutta.upgrades.command', f'{python} {goodpy}')
""",
)
self.app.save_setting(
self.session, "wutta.upgrades.command", f"{python} {goodpy}"
)
self.assertIsNone(upgrade.executed)
self.assertIsNone(upgrade.executed_by)
self.assertEqual(upgrade.status, enum.UpgradeStatus.PENDING)
with patch.object(view, 'Session', return_value=self.session):
with patch.object(self.config, 'usedb', new=True):
with patch.object(view, "Session", return_value=self.session):
with patch.object(self.config, "usedb", new=True):
view.execute_instance(upgrade, user)
self.assertIsNotNone(upgrade.executed)
self.assertIs(upgrade.executed_by, user)
self.assertEqual(upgrade.status, enum.UpgradeStatus.SUCCESS)
with open(view.get_upgrade_filepath(upgrade, 'stdout.log')) as f:
self.assertEqual(f.read(), 'hello from good.py')
with open(view.get_upgrade_filepath(upgrade, 'stderr.log')) as f:
self.assertEqual(f.read(), '')
with open(view.get_upgrade_filepath(upgrade, "stdout.log")) as f:
self.assertEqual(f.read(), "hello from good.py")
with open(view.get_upgrade_filepath(upgrade, "stderr.log")) as f:
self.assertEqual(f.read(), "")
# need a new record for next test
upgrade = model.Upgrade(description='test', created_by=user,
status=enum.UpgradeStatus.PENDING)
upgrade = model.Upgrade(
description="test", created_by=user, status=enum.UpgradeStatus.PENDING
)
self.session.add(upgrade)
self.session.commit()
# script w/ failure
badpy = self.write_file('bad.py', """
badpy = self.write_file(
"bad.py",
"""
import sys
sys.stderr.write('hello from bad.py')
sys.exit(42)
""")
self.app.save_setting(self.session, 'wutta.upgrades.command', f'{python} {badpy}')
""",
)
self.app.save_setting(
self.session, "wutta.upgrades.command", f"{python} {badpy}"
)
self.assertIsNone(upgrade.executed)
self.assertIsNone(upgrade.executed_by)
self.assertEqual(upgrade.status, enum.UpgradeStatus.PENDING)
with patch.object(view, 'Session', return_value=self.session):
with patch.object(self.config, 'usedb', new=True):
with patch.object(view, "Session", return_value=self.session):
with patch.object(self.config, "usedb", new=True):
view.execute_instance(upgrade, user)
self.assertIsNotNone(upgrade.executed)
self.assertIs(upgrade.executed_by, user)
self.assertEqual(upgrade.status, enum.UpgradeStatus.FAILURE)
with open(view.get_upgrade_filepath(upgrade, 'stdout.log')) as f:
self.assertEqual(f.read(), '')
with open(view.get_upgrade_filepath(upgrade, 'stderr.log')) as f:
self.assertEqual(f.read(), 'hello from bad.py')
with open(view.get_upgrade_filepath(upgrade, "stdout.log")) as f:
self.assertEqual(f.read(), "")
with open(view.get_upgrade_filepath(upgrade, "stderr.log")) as f:
self.assertEqual(f.read(), "hello from bad.py")
def test_execute_progress(self):
model = self.app.model
enum = self.app.enum
view = self.make_view()
user = model.User(username='barney')
user = model.User(username="barney")
self.session.add(user)
upgrade = model.Upgrade(description='test', created_by=user,
status=enum.UpgradeStatus.PENDING)
upgrade = model.Upgrade(
description="test", created_by=user, status=enum.UpgradeStatus.PENDING
)
self.session.add(upgrade)
self.session.commit()
stdout = self.write_file('stdout.log', 'hello 001\n')
stdout = self.write_file("stdout.log", "hello 001\n")
self.request.matchdict = {'uuid': upgrade.uuid}
with patch.multiple(mod.UpgradeView,
Session=MagicMock(return_value=self.session),
get_upgrade_filepath=MagicMock(return_value=stdout)):
self.request.matchdict = {"uuid": upgrade.uuid}
with patch.multiple(
mod.UpgradeView,
Session=MagicMock(return_value=self.session),
get_upgrade_filepath=MagicMock(return_value=stdout),
):
# nb. this is used to identify progress tracker
self.request.session.id = 'mockid#1'
self.request.session.id = "mockid#1"
# first call should get the full contents
context = view.execute_progress()
self.assertFalse(context.get('complete'))
self.assertFalse(context.get('error'))
self.assertFalse(context.get("complete"))
self.assertFalse(context.get("error"))
# nb. newline is converted to <br>
self.assertEqual(context['stdout'], 'hello 001<br />')
self.assertEqual(context["stdout"], "hello 001<br />")
# next call should get any new contents
with open(stdout, 'a') as f:
f.write('hello 002\n')
with open(stdout, "a") as f:
f.write("hello 002\n")
context = view.execute_progress()
self.assertFalse(context.get('complete'))
self.assertFalse(context.get('error'))
self.assertEqual(context['stdout'], 'hello 002<br />')
self.assertFalse(context.get("complete"))
self.assertFalse(context.get("error"))
self.assertEqual(context["stdout"], "hello 002<br />")
# nb. switch to a different progress tracker
self.request.session.id = 'mockid#2'
self.request.session.id = "mockid#2"
# first call should get the full contents
context = view.execute_progress()
self.assertFalse(context.get('complete'))
self.assertFalse(context.get('error'))
self.assertEqual(context['stdout'], 'hello 001<br />hello 002<br />')
self.assertFalse(context.get("complete"))
self.assertFalse(context.get("error"))
self.assertEqual(context["stdout"], "hello 001<br />hello 002<br />")
# mark progress complete
session = get_progress_session(self.request, 'upgrades.execute')
session = get_progress_session(self.request, "upgrades.execute")
session.load()
session['complete'] = True
session['success_msg'] = 'yay!'
session["complete"] = True
session["success_msg"] = "yay!"
session.save()
# next call should reflect that
self.assertEqual(self.request.session.pop_flash(), [])
context = view.execute_progress()
self.assertTrue(context.get('complete'))
self.assertFalse(context.get('error'))
self.assertTrue(context.get("complete"))
self.assertFalse(context.get("error"))
# nb. this is missing b/c we already got all contents
self.assertNotIn('stdout', context)
self.assertEqual(self.request.session.pop_flash(), ['yay!'])
self.assertNotIn("stdout", context)
self.assertEqual(self.request.session.pop_flash(), ["yay!"])
# nb. switch to a different progress tracker
self.request.session.id = 'mockid#3'
self.request.session.id = "mockid#3"
# first call should get the full contents
context = view.execute_progress()
self.assertFalse(context.get('complete'))
self.assertFalse(context.get('error'))
self.assertEqual(context['stdout'], 'hello 001<br />hello 002<br />')
self.assertFalse(context.get("complete"))
self.assertFalse(context.get("error"))
self.assertEqual(context["stdout"], "hello 001<br />hello 002<br />")
# mark progress error
session = get_progress_session(self.request, 'upgrades.execute')
session = get_progress_session(self.request, "upgrades.execute")
session.load()
session['error'] = True
session['error_msg'] = 'omg!'
session["error"] = True
session["error_msg"] = "omg!"
session.save()
# next call should reflect that
self.assertEqual(self.request.session.pop_flash('error'), [])
self.assertEqual(self.request.session.pop_flash("error"), [])
context = view.execute_progress()
self.assertFalse(context.get('complete'))
self.assertTrue(context.get('error'))
self.assertFalse(context.get("complete"))
self.assertTrue(context.get("error"))
# nb. this is missing b/c we already got all contents
self.assertNotIn('stdout', context)
self.assertEqual(self.request.session.pop_flash('error'), ['omg!'])
self.assertNotIn("stdout", context)
self.assertEqual(self.request.session.pop_flash("error"), ["omg!"])
def test_configure_get_simple_settings(self):
# sanity/coverage check

View file

@ -17,7 +17,7 @@ class TestUserView(WebTestCase):
return mod.UserView(self.request)
def test_includeme(self):
self.pyramid_config.include('wuttaweb.views.users')
self.pyramid_config.include("wuttaweb.views.users")
def test_get_query(self):
view = self.make_view()
@ -28,35 +28,35 @@ class TestUserView(WebTestCase):
model = self.app.model
view = self.make_view()
grid = view.make_grid(model_class=model.User)
self.assertFalse(grid.is_linked('person'))
self.assertFalse(grid.is_linked("person"))
view.configure_grid(grid)
self.assertTrue(grid.is_linked('person'))
self.assertTrue(grid.is_linked("person"))
def test_grid_row_class(self):
model = self.app.model
user = model.User(username='barney', active=True)
user = model.User(username="barney", active=True)
data = dict(user)
view = self.make_view()
self.assertIsNone(view.grid_row_class(user, data, 1))
user.active = False
self.assertEqual(view.grid_row_class(user, data, 1), 'has-background-warning')
self.assertEqual(view.grid_row_class(user, data, 1), "has-background-warning")
def test_is_editable(self):
model = self.app.model
view = self.make_view()
# active user is editable
user = model.User(username='barney', active=True)
user = model.User(username="barney", active=True)
self.assertTrue(view.is_editable(user))
# inactive also editable
user = model.User(username='barney', active=False)
user = model.User(username="barney", active=False)
self.assertTrue(view.is_editable(user))
# but not if prevent_edit flag is set
user = model.User(username='barney', prevent_edit=True)
user = model.User(username="barney", prevent_edit=True)
self.assertFalse(view.is_editable(user))
# unless request user is root
@ -65,95 +65,97 @@ class TestUserView(WebTestCase):
def test_configure_form(self):
model = self.app.model
person = model.Person(first_name='Barney', last_name='Rubble', full_name='Barney Rubble')
barney = model.User(username='barney', person=person)
person = model.Person(
first_name="Barney", last_name="Rubble", full_name="Barney Rubble"
)
barney = model.User(username="barney", person=person)
self.session.add(barney)
self.session.commit()
view = self.make_view()
# person replaced with first/last name when creating or editing
with patch.object(view, 'viewing', new=True):
with patch.object(view, "viewing", new=True):
form = view.make_form(model_instance=barney)
self.assertIn('person', form)
self.assertNotIn('first_name', form)
self.assertNotIn('last_name', form)
self.assertIn("person", form)
self.assertNotIn("first_name", form)
self.assertNotIn("last_name", form)
view.configure_form(form)
self.assertIn('person', form)
self.assertNotIn('first_name', form)
self.assertNotIn('last_name', form)
with patch.object(view, 'creating', new=True):
self.assertIn("person", form)
self.assertNotIn("first_name", form)
self.assertNotIn("last_name", form)
with patch.object(view, "creating", new=True):
form = view.make_form(model_instance=barney)
self.assertIn('person', form)
self.assertNotIn('first_name', form)
self.assertNotIn('last_name', form)
self.assertIn("person", form)
self.assertNotIn("first_name", form)
self.assertNotIn("last_name", form)
view.configure_form(form)
self.assertNotIn('person', form)
self.assertIn('first_name', form)
self.assertIn('last_name', form)
with patch.object(view, 'editing', new=True):
self.assertNotIn("person", form)
self.assertIn("first_name", form)
self.assertIn("last_name", form)
with patch.object(view, "editing", new=True):
form = view.make_form(model_instance=barney)
self.assertIn('person', form)
self.assertNotIn('first_name', form)
self.assertNotIn('last_name', form)
self.assertIn("person", form)
self.assertNotIn("first_name", form)
self.assertNotIn("last_name", form)
view.configure_form(form)
self.assertNotIn('person', form)
self.assertIn('first_name', form)
self.assertIn('last_name', form)
self.assertNotIn("person", form)
self.assertIn("first_name", form)
self.assertIn("last_name", form)
# first/last name have default values when editing
with patch.object(view, 'editing', new=True):
with patch.object(view, "editing", new=True):
form = view.make_form(model_instance=barney)
self.assertNotIn('first_name', form.defaults)
self.assertNotIn('last_name', form.defaults)
self.assertNotIn("first_name", form.defaults)
self.assertNotIn("last_name", form.defaults)
view.configure_form(form)
self.assertIn('first_name', form.defaults)
self.assertEqual(form.defaults['first_name'], 'Barney')
self.assertIn('last_name', form.defaults)
self.assertEqual(form.defaults['last_name'], 'Rubble')
self.assertIn("first_name", form.defaults)
self.assertEqual(form.defaults["first_name"], "Barney")
self.assertIn("last_name", form.defaults)
self.assertEqual(form.defaults["last_name"], "Rubble")
# password removed (always, for now)
with patch.object(view, 'viewing', new=True):
with patch.object(view, "viewing", new=True):
form = view.make_form(model_instance=barney)
self.assertIn('password', form)
self.assertIn("password", form)
view.configure_form(form)
self.assertNotIn('password', form)
with patch.object(view, 'editing', new=True):
self.assertNotIn("password", form)
with patch.object(view, "editing", new=True):
form = view.make_form(model_instance=barney)
self.assertIn('password', form)
self.assertIn("password", form)
view.configure_form(form)
self.assertNotIn('password', form)
self.assertNotIn("password", form)
# api tokens grid shown only if current user has perm
with patch.object(view, 'viewing', new=True):
with patch.object(view, "viewing", new=True):
form = view.make_form(model_instance=barney)
self.assertIn('api_tokens', form)
self.assertIn("api_tokens", form)
view.configure_form(form)
self.assertNotIn('api_tokens', form)
with patch.object(self.request, 'is_root', new=True):
self.assertNotIn("api_tokens", form)
with patch.object(self.request, "is_root", new=True):
form = view.make_form(model_instance=barney)
self.assertIn('api_tokens', form)
self.assertIn("api_tokens", form)
view.configure_form(form)
self.assertIn('api_tokens', form)
self.assertIn("api_tokens", form)
def test_unique_username(self):
model = self.app.model
view = self.make_view()
user = model.User(username='foo')
user = model.User(username="foo")
self.session.add(user)
self.session.commit()
with patch.object(view, 'Session', return_value=self.session):
with patch.object(view, "Session", return_value=self.session):
# invalid if same username in data
node = colander.SchemaNode(colander.String(), name='username')
self.assertRaises(colander.Invalid, view.unique_username, node, 'foo')
node = colander.SchemaNode(colander.String(), name="username")
self.assertRaises(colander.Invalid, view.unique_username, node, "foo")
# but not if username belongs to current user
view.editing = True
self.request.matchdict = {'uuid': user.uuid}
node = colander.SchemaNode(colander.String(), name='username')
self.assertIsNone(view.unique_username(node, 'foo'))
self.request.matchdict = {"uuid": user.uuid}
node = colander.SchemaNode(colander.String(), name="username")
self.assertIsNone(view.unique_username(node, "foo"))
def test_objectify(self):
model = self.app.model
@ -164,14 +166,14 @@ class TestUserView(WebTestCase):
self.session.add(blokes)
others = model.Role(name="Others")
self.session.add(others)
barney = model.User(username='barney')
auth.set_user_password(barney, 'testpass')
barney = model.User(username="barney")
auth.set_user_password(barney, "testpass")
barney.roles.append(blokes)
self.session.add(barney)
self.session.commit()
with patch.object(self.request, 'matchdict', new={'uuid': barney.uuid}):
with patch.object(view, 'editing', new=True):
with patch.object(self.request, "matchdict", new={"uuid": barney.uuid}):
with patch.object(view, "editing", new=True):
# sanity check, user is just in 'blokes' role
self.session.refresh(barney)
@ -179,18 +181,18 @@ class TestUserView(WebTestCase):
self.assertEqual(barney.roles[0].name, "Blokes")
# form can update user password
self.assertTrue(auth.check_user_password(barney, 'testpass'))
self.assertTrue(auth.check_user_password(barney, "testpass"))
form = view.make_model_form(model_instance=barney)
form.validated = {'username': 'barney', 'set_password': 'testpass2'}
with patch.object(view, 'Session', return_value=self.session):
form.validated = {"username": "barney", "set_password": "testpass2"}
with patch.object(view, "Session", return_value=self.session):
user = view.objectify(form)
self.assertIs(user, barney)
self.assertTrue(auth.check_user_password(barney, 'testpass2'))
self.assertTrue(auth.check_user_password(barney, "testpass2"))
# form can update user roles
form = view.make_model_form(model_instance=barney)
form.validated = {'username': 'barney', 'roles': {others.uuid}}
with patch.object(view, 'Session', return_value=self.session):
form.validated = {"username": "barney", "roles": {others.uuid}}
with patch.object(view, "Session", return_value=self.session):
user = view.objectify(form)
self.assertIs(user, barney)
self.assertEqual(len(user.roles), 1)
@ -199,19 +201,27 @@ class TestUserView(WebTestCase):
# person is auto-created
self.assertIsNone(barney.person)
form = view.make_model_form(model_instance=barney)
form.validated = {'username': 'barney', 'first_name': 'Barney', 'last_name': 'Rubble'}
with patch.object(view, 'Session', return_value=self.session):
form.validated = {
"username": "barney",
"first_name": "Barney",
"last_name": "Rubble",
}
with patch.object(view, "Session", return_value=self.session):
user = view.objectify(form)
self.assertIsNotNone(barney.person)
self.assertEqual(barney.person.first_name, 'Barney')
self.assertEqual(barney.person.last_name, 'Rubble')
self.assertEqual(barney.person.full_name, 'Barney Rubble')
self.assertEqual(barney.person.first_name, "Barney")
self.assertEqual(barney.person.last_name, "Rubble")
self.assertEqual(barney.person.full_name, "Barney Rubble")
# person is auto-removed
self.assertIsNotNone(barney.person)
form = view.make_model_form(model_instance=barney)
form.validated = {'username': 'barney', 'first_name': '', 'last_name': ''}
with patch.object(view, 'Session', return_value=self.session):
form.validated = {
"username": "barney",
"first_name": "",
"last_name": "",
}
with patch.object(view, "Session", return_value=self.session):
user = view.objectify(form)
self.assertIsNone(barney.person)
@ -219,37 +229,45 @@ class TestUserView(WebTestCase):
barney.person = self.session.query(model.Person).one()
# person name is updated
self.assertEqual(barney.person.first_name, 'Barney')
self.assertEqual(barney.person.last_name, 'Rubble')
self.assertEqual(barney.person.full_name, 'Barney Rubble')
self.assertEqual(barney.person.first_name, "Barney")
self.assertEqual(barney.person.last_name, "Rubble")
self.assertEqual(barney.person.full_name, "Barney Rubble")
form = view.make_model_form(model_instance=barney)
form.validated = {'username': 'barney', 'first_name': 'Fred', 'last_name': 'Flintstone'}
with patch.object(view, 'Session', return_value=self.session):
form.validated = {
"username": "barney",
"first_name": "Fred",
"last_name": "Flintstone",
}
with patch.object(view, "Session", return_value=self.session):
user = view.objectify(form)
self.assertIsNotNone(barney.person)
self.assertEqual(barney.person.first_name, 'Fred')
self.assertEqual(barney.person.last_name, 'Flintstone')
self.assertEqual(barney.person.full_name, 'Fred Flintstone')
self.assertEqual(barney.person.first_name, "Fred")
self.assertEqual(barney.person.last_name, "Flintstone")
self.assertEqual(barney.person.full_name, "Fred Flintstone")
with patch.object(view, 'creating', new=True):
with patch.object(view, "creating", new=True):
# person is auto-created when making new user
form = view.make_model_form()
form.validated = {'username': 'betty', 'first_name': 'Betty', 'last_name': 'Boop'}
with patch.object(view, 'Session', return_value=self.session):
form.validated = {
"username": "betty",
"first_name": "Betty",
"last_name": "Boop",
}
with patch.object(view, "Session", return_value=self.session):
user = view.objectify(form)
self.assertIsNotNone(user.person)
self.assertEqual(user.person.first_name, 'Betty')
self.assertEqual(user.person.last_name, 'Boop')
self.assertEqual(user.person.full_name, 'Betty Boop')
self.assertEqual(user.person.first_name, "Betty")
self.assertEqual(user.person.last_name, "Boop")
self.assertEqual(user.person.full_name, "Betty Boop")
# nb. keep ref to last user
last_user = user
# person is *not* auto-created if no name provided
form = view.make_model_form()
form.validated = {'username': 'betty', 'first_name': '', 'last_name': ''}
with patch.object(view, 'Session', return_value=self.session):
form.validated = {"username": "betty", "first_name": "", "last_name": ""}
with patch.object(view, "Session", return_value=self.session):
user = view.objectify(form)
self.assertIsNone(user.person)
self.assertIsNot(user, last_user)
@ -264,20 +282,20 @@ class TestUserView(WebTestCase):
self.session.add(blokes)
others = model.Role(name="Others")
self.session.add(others)
barney = model.User(username='barney')
barney = model.User(username="barney")
barney.roles.append(blokes)
self.session.add(barney)
self.session.commit()
view = self.make_view()
view.editing = True
self.request.matchdict = {'uuid': barney.uuid}
self.request.matchdict = {"uuid": barney.uuid}
# no error if data is missing roles
form = view.make_model_form(model_instance=barney)
form.validated = {'username': 'barneyx'}
form.validated = {"username": "barneyx"}
user = view.objectify(form)
self.assertIs(user, barney)
self.assertEqual(barney.username, 'barneyx')
self.assertEqual(barney.username, "barneyx")
# sanity check, user is just in 'blokes' role
self.session.refresh(barney)
@ -289,9 +307,11 @@ class TestUserView(WebTestCase):
# - authed / anon roles are not added
# - admin role not added if current user is not root
form = view.make_model_form(model_instance=barney)
form.validated = {'username': 'barney',
'roles': {admin.uuid, authed.uuid, anon.uuid, others.uuid}}
with patch.object(view, 'Session', return_value=self.session):
form.validated = {
"username": "barney",
"roles": {admin.uuid, authed.uuid, anon.uuid, others.uuid},
}
with patch.object(view, "Session", return_value=self.session):
user = view.objectify(form)
self.assertIs(user, barney)
self.assertEqual(len(user.roles), 1)
@ -302,9 +322,11 @@ class TestUserView(WebTestCase):
# - admin role is added if current user is root
self.request.is_root = True
form = view.make_model_form(model_instance=barney)
form.validated = {'username': 'barney',
'roles': {admin.uuid, blokes.uuid, others.uuid}}
with patch.object(view, 'Session', return_value=self.session):
form.validated = {
"username": "barney",
"roles": {admin.uuid, blokes.uuid, others.uuid},
}
with patch.object(view, "Session", return_value=self.session):
user = view.objectify(form)
self.assertIs(user, barney)
self.assertEqual(len(user.roles), 3)
@ -314,9 +336,8 @@ class TestUserView(WebTestCase):
# admin role not removed if current user is not root
self.request.is_root = False
form = view.make_model_form(model_instance=barney)
form.validated = {'username': 'barney',
'roles': {blokes.uuid, others.uuid}}
with patch.object(view, 'Session', return_value=self.session):
form.validated = {"username": "barney", "roles": {blokes.uuid, others.uuid}}
with patch.object(view, "Session", return_value=self.session):
user = view.objectify(form)
self.assertIs(user, barney)
self.assertEqual(len(user.roles), 3)
@ -324,9 +345,8 @@ class TestUserView(WebTestCase):
# admin role is removed if current user is root
self.request.is_root = True
form = view.make_model_form(model_instance=barney)
form.validated = {'username': 'barney',
'roles': {blokes.uuid, others.uuid}}
with patch.object(view, 'Session', return_value=self.session):
form.validated = {"username": "barney", "roles": {blokes.uuid, others.uuid}}
with patch.object(view, "Session", return_value=self.session):
user = view.objectify(form)
self.assertIs(user, barney)
self.assertEqual(len(user.roles), 2)
@ -336,27 +356,27 @@ class TestUserView(WebTestCase):
auth = self.app.get_auth_handler()
view = self.make_view()
user = model.User(username='foo')
user = model.User(username="foo")
self.session.add(user)
token = auth.add_api_token(user, 'test token')
token = auth.add_api_token(user, "test token")
self.session.commit()
normal = view.normalize_api_token(token)
self.assertIn('uuid', normal)
self.assertEqual(normal['uuid'], token.uuid.hex)
self.assertIn('description', normal)
self.assertEqual(normal['description'], 'test token')
self.assertIn('created', normal)
self.assertIn("uuid", normal)
self.assertEqual(normal["uuid"], token.uuid.hex)
self.assertIn("description", normal)
self.assertEqual(normal["description"], "test token")
self.assertIn("created", normal)
def test_make_api_tokens_grid(self):
model = self.app.model
auth = self.app.get_auth_handler()
view = self.make_view()
user = model.User(username='foo')
user = model.User(username="foo")
self.session.add(user)
token1 = auth.add_api_token(user, 'test1')
token2 = auth.add_api_token(user, 'test2')
token1 = auth.add_api_token(user, "test1")
token2 = auth.add_api_token(user, "test2")
self.session.commit()
# grid should have 2 records but no tools/actions
@ -367,52 +387,60 @@ class TestUserView(WebTestCase):
self.assertEqual(len(grid.actions), 0)
# create + delete allowed
with patch.object(self.request, 'is_root', new=True):
with patch.object(self.request, "is_root", new=True):
grid = view.make_api_tokens_grid(user)
self.assertEqual(len(grid.tools), 1)
self.assertIn('create', grid.tools)
self.assertIn("create", grid.tools)
self.assertEqual(len(grid.actions), 1)
self.assertEqual(grid.actions[0].key, 'delete')
self.assertEqual(grid.actions[0].key, "delete")
def test_add_api_token(self):
model = self.app.model
view = self.make_view()
user = model.User(username='foo')
user = model.User(username="foo")
self.session.add(user)
self.session.commit()
self.session.refresh(user)
self.assertEqual(len(user.api_tokens), 0)
with patch.object(view, 'Session', return_value=self.session):
with patch.object(self.request, 'matchdict', new={'uuid': user.uuid}):
with patch.object(self.request, 'json_body', create=True,
new={'description': 'testing'}):
with patch.object(view, "Session", return_value=self.session):
with patch.object(self.request, "matchdict", new={"uuid": user.uuid}):
with patch.object(
self.request,
"json_body",
create=True,
new={"description": "testing"},
):
result = view.add_api_token()
self.assertEqual(len(user.api_tokens), 1)
token = user.api_tokens[0]
self.assertEqual(result['token_string'], token.token_string)
self.assertEqual(result['description'], 'testing')
self.assertEqual(result["token_string"], token.token_string)
self.assertEqual(result["description"], "testing")
def test_delete_api_token(self):
model = self.app.model
auth = self.app.get_auth_handler()
view = self.make_view()
user = model.User(username='foo')
user = model.User(username="foo")
self.session.add(user)
token1 = auth.add_api_token(user, 'test1')
token2 = auth.add_api_token(user, 'test2')
token1 = auth.add_api_token(user, "test1")
token2 = auth.add_api_token(user, "test2")
self.session.commit()
self.session.refresh(user)
self.assertEqual(len(user.api_tokens), 2)
with patch.object(view, 'Session', return_value=self.session):
with patch.object(self.request, 'matchdict', new={'uuid': user.uuid}):
with patch.object(view, "Session", return_value=self.session):
with patch.object(self.request, "matchdict", new={"uuid": user.uuid}):
# normal behavior
with patch.object(self.request, 'json_body', create=True,
new={'uuid': token1.uuid.hex}):
with patch.object(
self.request,
"json_body",
create=True,
new={"uuid": token1.uuid.hex},
):
result = view.delete_api_token()
self.assertEqual(result, {})
self.session.refresh(user)
@ -421,17 +449,25 @@ class TestUserView(WebTestCase):
self.assertIs(token, token2)
# token for wrong user
user2 = model.User(username='bar')
user2 = model.User(username="bar")
self.session.add(user2)
token3 = auth.add_api_token(user2, 'test3')
token3 = auth.add_api_token(user2, "test3")
self.session.commit()
with patch.object(self.request, 'json_body', create=True,
new={'uuid': token3.uuid.hex}):
with patch.object(
self.request,
"json_body",
create=True,
new={"uuid": token3.uuid.hex},
):
result = view.delete_api_token()
self.assertEqual(result, {'error': "API token not found"})
self.assertEqual(result, {"error": "API token not found"})
# token not found
with patch.object(self.request, 'json_body', create=True,
new={'uuid': self.app.make_true_uuid().hex}):
with patch.object(
self.request,
"json_body",
create=True,
new={"uuid": self.app.make_true_uuid().hex},
):
result = view.delete_api_token()
self.assertEqual(result, {'error': "API token not found"})
self.assertEqual(result, {"error": "API token not found"})