diff --git a/CHANGELOG.md b/CHANGELOG.md index 4500527..63497e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,29 +5,6 @@ All notable changes to wuttaweb will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## v0.17.0 (2024-12-15) - -### Feat - -- add basic support for batch execution -- add basic support for rows grid for master, batch views -- add basic master view class for batches - -### Fix - -- add handling for decimal values and lists, in `make_json_safe()` -- fix behavior when editing Roles for a User -- add basic views for raw Permissions -- improve support for date, datetime fields in grids, forms -- add way to set field widgets using pseudo-type -- add support for date, datetime form fields -- make dropdown widgets as wide as other text fields in main form -- add fallback instance title -- display "global" errors at top of form, if present -- add `make_form()` and `make_grid()` methods on web handler -- correct "empty option" behavior for `ObjectRef` schema type -- use fanstatic to serve built-in images by default - ## v0.16.2 (2024-12-10) ### Fix diff --git a/docs/api/wuttaweb.views.batch.rst b/docs/api/wuttaweb.views.batch.rst deleted file mode 100644 index 8adc64b..0000000 --- a/docs/api/wuttaweb.views.batch.rst +++ /dev/null @@ -1,6 +0,0 @@ - -``wuttaweb.views.batch`` -======================== - -.. automodule:: wuttaweb.views.batch - :members: diff --git a/docs/index.rst b/docs/index.rst index ce74ae6..7ece535 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -49,7 +49,6 @@ the narrative docs are pretty scant. That will eventually change. api/wuttaweb.views api/wuttaweb.views.auth api/wuttaweb.views.base - api/wuttaweb.views.batch api/wuttaweb.views.common api/wuttaweb.views.essential api/wuttaweb.views.master diff --git a/pyproject.toml b/pyproject.toml index 5aed35f..0a0435c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "WuttaWeb" -version = "0.17.0" +version = "0.16.2" description = "Web App for Wutta Framework" readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}] @@ -32,7 +32,6 @@ requires-python = ">= 3.8" dependencies = [ "ColanderAlchemy", "humanize", - "markdown", "paginate", "paginate_sqlalchemy", "pyramid>=2", @@ -43,7 +42,7 @@ dependencies = [ "pyramid_tm", "waitress", "WebHelpers2", - "WuttJamaican[db]>=0.18.0", + "WuttJamaican[db]>=0.17.1", "zope.sqlalchemy>=1.5", ] diff --git a/src/wuttaweb/forms/widgets.py b/src/wuttaweb/forms/widgets.py index f90768a..2c8a944 100644 --- a/src/wuttaweb/forms/widgets.py +++ b/src/wuttaweb/forms/widgets.py @@ -104,7 +104,7 @@ class ObjectRefWidget(SelectWidget): # add url, only if rendering readonly readonly = kw.get('readonly', self.readonly) if readonly: - if 'url' not in values and self.url and getattr(field.schema, 'model_instance', None): + if 'url' not in values and self.url and hasattr(field.schema, 'model_instance'): values['url'] = self.url(field.schema.model_instance) return values @@ -421,22 +421,3 @@ class PermissionsWidget(WuttaCheckboxChoiceWidget): kw['values'] = values return super().serialize(field, cstruct, **kw) - - -class BatchIdWidget(Widget): - """ - Widget for use with the - :attr:`~wuttjamaican:wuttjamaican.db.model.batch.BatchMixin.id` - field of a :term:`batch` model. - - This widget is "always" read-only and renders the Batch ID as - zero-padded 8-char string - """ - - def serialize(self, field, cstruct, **kw): - """ """ - if cstruct is colander.null: - return colander.null - - batch_id = int(cstruct) - return f'{batch_id:08d}' diff --git a/src/wuttaweb/templates/base.mako b/src/wuttaweb/templates/base.mako index df05cbb..a33d3ca 100644 --- a/src/wuttaweb/templates/base.mako +++ b/src/wuttaweb/templates/base.mako @@ -201,10 +201,6 @@ width: 100%; } - .tool-panels-wrapper { - padding: 1rem; - } - diff --git a/src/wuttaweb/templates/batch/view.mako b/src/wuttaweb/templates/batch/view.mako deleted file mode 100644 index 569af5b..0000000 --- a/src/wuttaweb/templates/batch/view.mako +++ /dev/null @@ -1,124 +0,0 @@ -## -*- coding: utf-8; -*- -<%inherit file="/master/view.mako" /> - -<%def name="extra_styles()"> - ${parent.extra_styles()} - - - -<%def name="tool_panels()"> - ${parent.tool_panels()} - ${self.tool_panel_execution()} - - -<%def name="tool_panel_execution()"> - - % if batch.executed: - -

- Batch was executed
- ${app.render_time_ago(batch.executed)} - by ${batch.executed_by}. -

-
- % elif why_not_execute: - -

- Batch cannot be executed: -

-

- ${why_not_execute} -

-
- % else: - % if master.has_perm('execute'): - -

- Batch can be executed. -

- - Execute Batch - - - - - -
- - % else: - -

- Batch may be executed,
- but you do not have permission. -

-
- % endif - % endif -
- - -<%def name="modify_vue_vars()"> - ${parent.modify_vue_vars()} - % if not batch.executed and not why_not_execute and master.has_perm('execute'): - - % endif - diff --git a/src/wuttaweb/templates/form.mako b/src/wuttaweb/templates/form.mako index 1a4fe2d..de7209a 100644 --- a/src/wuttaweb/templates/form.mako +++ b/src/wuttaweb/templates/form.mako @@ -1,19 +1,6 @@ ## -*- coding: utf-8; -*- <%inherit file="/page.mako" /> -<%def name="page_layout()"> -
- - ## main form -
- ${self.page_content()} -
- - ## tool panels - ${self.tool_panels_wrapper()} -
- - <%def name="page_content()"> % if form is not Undefined:
@@ -22,14 +9,6 @@ % endif -<%def name="tool_panels_wrapper()"> -
- ${self.tool_panels()} -
- - -<%def name="tool_panels()"> - <%def name="render_vue_template_form()"> % if form is not Undefined: ${form.render_vue_template()} diff --git a/src/wuttaweb/templates/master/view.mako b/src/wuttaweb/templates/master/view.mako index b4db013..b84ebc1 100644 --- a/src/wuttaweb/templates/master/view.mako +++ b/src/wuttaweb/templates/master/view.mako @@ -5,48 +5,5 @@ <%def name="content_title()">${instance_title} -<%def name="page_layout()"> - % if master.has_rows: -
-
- - ## main form -
- ${self.page_content()} -
- - ## tool panels - ${self.tool_panels_wrapper()} - -
- - ## rows grid -
-

${master.get_rows_title() or ''}

- ${rows_grid.render_vue_tag()} -
- - % else: - ## no rows, just main form + tool panels - ${parent.page_layout()} - % endif - - -<%def name="render_vue_templates()"> - ${parent.render_vue_templates()} - % if master.has_rows: - ${self.render_vue_template_rows_grid()} - % endif - - -<%def name="render_vue_template_rows_grid()"> - ${rows_grid.render_vue_template()} - - -<%def name="make_vue_components()"> - ${parent.make_vue_components()} - % if master.has_rows: - ${rows_grid.render_vue_finalize()} - % endif - +${parent.body()} diff --git a/src/wuttaweb/templates/page.mako b/src/wuttaweb/templates/page.mako index c23ce90..218e9f4 100644 --- a/src/wuttaweb/templates/page.mako +++ b/src/wuttaweb/templates/page.mako @@ -1,10 +1,6 @@ ## -*- coding: utf-8; -*- <%inherit file="/base.mako" /> -<%def name="page_layout()"> - ${self.page_content()} - - <%def name="page_content()"> <%def name="render_vue_templates()"> @@ -16,7 +12,7 @@ <%def name="render_vue_template_this_page()"> diff --git a/src/wuttaweb/templates/wutta-components.mako b/src/wuttaweb/templates/wutta-components.mako index 5664933..6030840 100644 --- a/src/wuttaweb/templates/wutta-components.mako +++ b/src/wuttaweb/templates/wutta-components.mako @@ -6,7 +6,6 @@ ${self.make_wutta_timepicker_component()} ${self.make_wutta_filter_component()} ${self.make_wutta_filter_value_component()} - ${self.make_wutta_tool_panel_component()} <%def name="make_wutta_request_mixin()"> @@ -478,28 +477,3 @@ - -<%def name="make_wutta_tool_panel_component()"> - - - diff --git a/src/wuttaweb/util.py b/src/wuttaweb/util.py index 0697f03..c0069d4 100644 --- a/src/wuttaweb/util.py +++ b/src/wuttaweb/util.py @@ -24,7 +24,6 @@ Web Utilities """ -import decimal import importlib import json import logging @@ -526,28 +525,17 @@ def make_json_safe(value, key=None, warn=True): if value is colander.null: return None - elif isinstance(value, dict): - # recursively convert dict + # recursively convert dict + if isinstance(value, dict): parent = dict(value) for key, value in parent.items(): parent[key] = make_json_safe(value, key=key, warn=warn) value = parent - elif isinstance(value, list): - # recursively convert list - parent = list(value) - for i, value in enumerate(parent): - parent[i] = make_json_safe(value, key=key, warn=warn) - value = parent - - elif isinstance(value, _uuid.UUID): - # convert UUID to str + # convert UUID to str + if isinstance(value, _uuid.UUID): value = value.hex - elif isinstance(value, decimal.Decimal): - # convert decimal to float - value = float(value) - # ensure JSON-compatibility, warn if problems try: json.dumps(value) diff --git a/src/wuttaweb/views/batch.py b/src/wuttaweb/views/batch.py deleted file mode 100644 index 1645455..0000000 --- a/src/wuttaweb/views/batch.py +++ /dev/null @@ -1,404 +0,0 @@ -# -*- coding: utf-8; -*- -################################################################################ -# -# wuttaweb -- Web App for Wutta Framework -# Copyright © 2024 Lance Edgar -# -# This file is part of Wutta Framework. -# -# Wutta Framework is free software: you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) any -# later version. -# -# Wutta Framework is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along with -# Wutta Framework. If not, see . -# -################################################################################ -""" -Base logic for Batch Master views -""" - -import logging -import threading -import time - -import markdown -from sqlalchemy import orm - -from wuttaweb.views import MasterView -from wuttaweb.forms.schema import UserRef -from wuttaweb.forms.widgets import BatchIdWidget - - -log = logging.getLogger(__name__) - - -class BatchMasterView(MasterView): - """ - Base class for all "batch master" views. - - .. attribute:: batch_handler - - Reference to the :term:`batch handler` for use with the view. - - This is set when the view is first created, using return value - from :meth:`get_batch_handler()`. - """ - - labels = { - 'id': "Batch ID", - 'status_code': "Batch Status", - } - - sort_defaults = ('id', 'desc') - - has_rows = True - rows_title = "Batch Rows" - rows_sort_defaults = 'sequence' - - def __init__(self, request, context=None): - super().__init__(request, context=context) - self.batch_handler = self.get_batch_handler() - - def get_batch_handler(self): - """ - Must return the :term:`batch handler` for use with this view. - - There is no default logic; subclass must override. - """ - raise NotImplementedError - - def get_fallback_templates(self, template): - """ - We override the default logic here, to prefer "batch" - templates over the "master" templates. - - So for instance the "view batch" page will by default use the - ``/batch/view.mako`` template - which does inherit from - ``/master/view.mako`` but adds extra features specific to - batches. - """ - templates = super().get_fallback_templates(template) - templates.insert(0, f'/batch/{template}.mako') - return templates - - def render_to_response(self, template, context): - """ - We override the default logic here, to inject batch-related - context for the - :meth:`~wuttaweb.views.master.MasterView.view()` template - specifically. These values are used in the template file, - ``/batch/view.mako``. - - * ``batch`` - reference to the current :term:`batch` - * ``batch_handler`` reference to :attr:`batch_handler` - * ``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) - - 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) - - def configure_grid(self, g): - """ """ - super().configure_grid(g) - model = self.app.model - - # 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_filter('created_by', CreatedBy.username, label="Created By Username") - - # id - g.set_renderer('id', self.render_batch_id) - g.set_link('id') - - # description - g.set_link('description') - - def render_batch_id(self, batch, key, value): - """ """ - if value: - batch_id = int(value) - return f'{batch_id:08d}' - - def get_instance_title(self, batch): - """ """ - if batch.description: - return f"{batch.id_str} {batch.description}" - return batch.id_str - - def configure_form(self, f): - """ """ - super().configure_form(f) - batch = f.model_instance - - # id - if self.creating: - f.remove('id') - else: - f.set_readonly('id') - f.set_widget('id', BatchIdWidget()) - - # notes - f.set_widget('notes', 'notes') - - # rows - f.remove('rows') - if self.creating: - f.remove('row_count') - else: - f.set_readonly('row_count') - - # status - f.remove('status_text') - if self.creating: - f.remove('status_code') - else: - f.set_readonly('status_code') - - # created - if self.creating: - f.remove('created') - else: - f.set_readonly('created') - - # created_by - f.remove('created_by_uuid') - if self.creating: - f.remove('created_by') - else: - f.set_node('created_by', UserRef(self.request)) - f.set_readonly('created_by') - - # executed - if self.creating or not batch.executed: - f.remove('executed') - else: - f.set_readonly('executed') - - # executed_by - f.remove('executed_by_uuid') - if self.creating or not batch.executed: - f.remove('executed_by') - else: - f.set_node('executed_by', UserRef(self.request)) - f.set_readonly('executed_by') - - def objectify(self, form): - """ - We override the default logic here, to invoke - :meth:`~wuttjamaican:wuttjamaican.batch.BatchHandler.make_batch()` - on the batch handler - when creating. Parent/default logic is - used when updating. - """ - if self.creating: - - # first get the "normal" objectified batch. this will have - # all attributes set correctly per the form data, but will - # not yet belong to the db session. we ultimately discard it. - schema = form.get_schema() - batch = schema.objectify(form.validated, context=form.model_instance) - - # then we collect attributes from the new batch - kwargs = dict([(key, getattr(batch, key)) - for key in form.validated - if hasattr(batch, key)]) - - # and set attribute for user creating the batch - kwargs['created_by'] = self.request.user - - # finally let batch handler make the "real" batch - return self.batch_handler.make_batch(self.Session(), **kwargs) - - # when not creating, normal logic is fine - return super().objectify(form) - - def redirect_after_create(self, batch): - """ - If the new batch requires initial population, we launch a - thread for that and show the "progress" page. - - Otherwise this will do the normal thing of redirecting to the - "view" page for the new batch. - """ - # just view batch if should not populate - if not self.batch_handler.should_populate(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)) - - # start thread and show progress page - thread.start() - return self.render_progress(progress) - - def delete_instance(self, batch): - """ - Delete the given batch instance. - - This calls - :meth:`~wuttjamaican:wuttjamaican.batch.BatchHandler.do_delete()` - on the :attr:`batch_handler`. - """ - self.batch_handler.do_delete(batch, self.request.user) - - ############################## - # populate methods - ############################## - - def populate_thread(self, batch_uuid, progress=None): - """ - Thread target for populating new object with progress indicator. - - When a new batch is created, and the batch handler says it - should also be populated, then this thread is launched to do - so outside of the main request/response cycle. Progress bar - is then shown to the user until it completes. - - This method mostly just calls - :meth:`~wuttjamaican:wuttjamaican.batch.BatchHandler.do_populate()` - on the :term:`batch handler`. - """ - # nb. must use our own session in separate thread - session = self.app.make_session() - - # nb. main web request which created the batch, must complete - # before that session is committed. until that happens we - # will not be able to see the new batch. hence this loop, - # where we wait for the batch to appear. - batch = None - tries = 0 - while not batch: - batch = session.get(self.model_class, batch_uuid) - tries += 1 - if tries > 10: - raise RuntimeError("can't find the batch") - time.sleep(0.1) - - try: - # populate the batch - self.batch_handler.do_populate(batch, progress=progress) - session.flush() - - except Exception as error: - session.rollback() - log.warning("failed to populate %s: %s", - self.get_model_title(), batch, - exc_info=True) - if progress: - progress.handle_error(error) - - else: - session.commit() - if progress: - progress.handle_success() - - finally: - session.close() - - ############################## - # execute methods - ############################## - - def execute(self): - """ - View to execute the current :term:`batch`. - - Eventually this should show a progress indicator etc., but for - now it simply calls - :meth:`~wuttjamaican:wuttjamaican.batch.BatchHandler.do_execute()` - on the :attr:`batch_handler` and waits for it to complete, - then redirects user back to the "view batch" page. - """ - self.executing = True - batch = self.get_instance() - - try: - 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') - - return self.redirect(self.get_action_url('view', batch)) - - ############################## - # row methods - ############################## - - @classmethod - def get_row_model_class(cls): - """ """ - if hasattr(cls, 'row_model_class'): - return cls.row_model_class - - Batch = cls.get_model_class() - return Batch.__row_class__ - - def get_row_grid_data(self, batch): - """ - Returns the base query for the batch - :attr:`~wuttjamaican:wuttjamaican.db.model.batch.BatchMixin.rows` - data. - """ - BatchRow = self.get_row_model_class() - 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) - - ############################## - # configuration - ############################## - - @classmethod - def defaults(cls, config): - """ """ - cls._defaults(config) - cls._batch_defaults(config) - - @classmethod - def _batch_defaults(cls, config): - route_prefix = cls.get_route_prefix() - permission_prefix = cls.get_permission_prefix() - model_title = cls.get_model_title() - 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}") diff --git a/src/wuttaweb/views/master.py b/src/wuttaweb/views/master.py index 0030859..4ce9d2d 100644 --- a/src/wuttaweb/views/master.py +++ b/src/wuttaweb/views/master.py @@ -73,12 +73,12 @@ class MasterView(View): .. attribute:: model_class - Optional reference to a :term:`data model` class. While not - strictly required, most views will set this to a SQLAlchemy - mapped class, + Optional reference to a data model class. While not strictly + required, most views will set this to a SQLAlchemy mapped + class, e.g. :class:`~wuttjamaican:wuttjamaican.db.model.base.Person`. - The base logic should not access this directly but instead call + Code should not access this directly but instead call :meth:`get_model_class()`. .. attribute:: model_name @@ -340,38 +340,6 @@ class MasterView(View): Boolean indicating whether the master view supports "configuring" - i.e. it should have a :meth:`configure()` view. Default value is ``False``. - - **ROW FEATURES** - - .. attribute:: has_rows - - Whether the model has "rows" which should also be displayed - when viewing model records. - - This the "master switch" for all row features; if this is turned - on then many other things kick in. - - See also :attr:`row_model_class`. - - .. attribute:: row_model_class - - Reference to a :term:`data model` class for the rows. - - The base logic should not access this directly but instead call - :meth:`get_row_model_class()`. - - .. attribute:: rows_title - - Display title for the rows grid. - - The base logic should not access this directly but instead call - :meth:`get_rows_title()`. - - .. attribute:: row_grid_columns - - List of columns for the row grid. - - This is optional; see also :meth:`get_row_grid_columns()`. """ ############################## @@ -400,16 +368,6 @@ class MasterView(View): execute_progress_template = None configurable = False - # row features - has_rows = False - rows_filterable = True - rows_filter_defaults = None - rows_sortable = True - rows_sort_on_backend = True - rows_sort_defaults = None - rows_paginated = True - rows_paginate_on_backend = True - # current action listing = False creating = False @@ -500,7 +458,6 @@ class MasterView(View): * :meth:`make_model_form()` * :meth:`configure_form()` * :meth:`create_save_form()` - * :meth:`redirect_after_create()` """ self.creating = True form = self.make_model_form(cancel_url_fallback=self.get_index_url()) @@ -508,7 +465,7 @@ class MasterView(View): if form.validate(): obj = self.create_save_form(form) self.Session.flush() - return self.redirect_after_create(obj) + return self.redirect(self.get_action_url('view', obj)) context = { 'form': form, @@ -534,16 +491,6 @@ class MasterView(View): self.persist(obj) return obj - def redirect_after_create(self, obj): - """ - Usually, this returns a redirect to which we send the user, - after a new model record has been created. By default this - sends them to the "view" page for the record. - - It is called automatically by :meth:`create()`. - """ - return self.redirect(self.get_action_url('view', obj)) - ############################## # view methods ############################## @@ -567,40 +514,15 @@ class MasterView(View): * :meth:`make_model_form()` * :meth:`configure_form()` - * :meth:`make_row_model_grid()` - if :attr:`has_rows` is true """ self.viewing = True - obj = self.get_instance() - form = self.make_model_form(obj, readonly=True) + instance = self.get_instance() + form = self.make_model_form(instance, readonly=True) + context = { - 'instance': obj, + 'instance': instance, 'form': form, } - - if self.has_rows: - - # always make the grid first. note that it already knows - # to "reset" its params when that is requested. - grid = self.make_row_model_grid(obj) - - # but if user did request a "reset" then we want to - # redirect so the query string gets cleared out - if self.request.GET.get('reset-view'): - - # nb. we want to preserve url hash if applicable - kw = {'_query': None, - '_anchor': self.request.GET.get('hash')} - return self.redirect(self.request.current_route_url(**kw)) - - # so-called 'partial' requests get just the grid data - if self.request.params.get('partial'): - context = grid.get_vue_context() - if grid.paginated and grid.paginate_on_backend: - context['pager_stats'] = grid.get_vue_pager_stats() - return self.json_response(context) - - context['rows_grid'] = grid - return self.render_to_response('view', context) ############################## @@ -1974,8 +1896,8 @@ class MasterView(View): This is called by :meth:`make_model_grid()`. - There is minimal default logic here; subclass should override - as needed. The ``grid`` param will already be "complete" and + There is no default logic here; subclass should override as + needed. The ``grid`` param will already be "complete" and ready to use as-is, but this method can further modify it based on request details etc. """ @@ -2308,182 +2230,6 @@ class MasterView(View): session = session or self.Session() session.add(obj) - ############################## - # row methods - ############################## - - def get_rows_title(self): - """ - Returns the display title for model **rows** grid, if - applicable/desired. Only relevant if :attr:`has_rows` is - true. - - There is no default here, but subclass may override by - assigning :attr:`rows_title`. - """ - if hasattr(self, 'rows_title'): - return self.rows_title - - def make_row_model_grid(self, obj, **kwargs): - """ - Create and return a grid for a record's **rows** data, for use - in :meth:`view()`. Only applicable if :attr:`has_rows` is - true. - - :param obj: Current model instance for which rows data is - being displayed. - - :returns: :class:`~wuttaweb.grids.base.Grid` instance for the - rows data. - - See also related methods, which are called by this one: - - * :meth:`get_row_grid_key()` - * :meth:`get_row_grid_columns()` - * :meth:`get_row_grid_data()` - * :meth:`configure_row_grid()` - """ - if 'key' not in kwargs: - kwargs['key'] = self.get_row_grid_key() - - if 'model_class' not in kwargs: - model_class = self.get_row_model_class() - if model_class: - kwargs['model_class'] = model_class - - if 'columns' not in kwargs: - kwargs['columns'] = self.get_row_grid_columns() - - if 'data' not in kwargs: - kwargs['data'] = self.get_row_grid_data(obj) - - kwargs.setdefault('filterable', self.rows_filterable) - kwargs.setdefault('filter_defaults', self.rows_filter_defaults) - kwargs.setdefault('sortable', self.rows_sortable) - kwargs.setdefault('sort_multiple', not self.request.use_oruga) - kwargs.setdefault('sort_on_backend', self.rows_sort_on_backend) - kwargs.setdefault('sort_defaults', self.rows_sort_defaults) - kwargs.setdefault('paginated', self.rows_paginated) - kwargs.setdefault('paginate_on_backend', self.rows_paginate_on_backend) - - grid = self.make_grid(**kwargs) - self.configure_row_grid(grid) - grid.load_settings() - return grid - - def get_row_grid_key(self): - """ - Returns the (presumably) unique key to be used for the - **rows** grid in :meth:`view()`. Only relevant if - :attr:`has_rows` is true. - - This is called from :meth:`make_row_model_grid()`; in the - resulting grid, this becomes - :attr:`~wuttaweb.grids.base.Grid.key`. - - Whereas you can define :attr:`grid_key` for the main grid, the - row grid key is always generated dynamically. This - incorporates the current record key (whose rows are in the - grid) so that the rows grid for each record is unique. - """ - parts = [self.get_grid_key()] - for key in self.get_model_key(): - parts.append(str(self.request.matchdict[key])) - return '.'.join(parts) - - def get_row_grid_columns(self): - """ - Returns the default list of column names for the **rows** - grid, for use in :meth:`view()`. Only relevant if - :attr:`has_rows` is true. - - This is called by :meth:`make_row_model_grid()`; in the - resulting grid, this becomes - :attr:`~wuttaweb.grids.base.Grid.columns`. - - This method may return ``None``, in which case the grid may - (try to) generate its own default list. - - Subclass may define :attr:`row_grid_columns` for simple cases, - or can override this method if needed. - - Also note that :meth:`configure_row_grid()` may be used to - further modify the final column set, regardless of what this - method returns. So a common pattern is to declare all - "supported" columns by setting :attr:`row_grid_columns` but - then optionally remove or replace some of those within - :meth:`configure_row_grid()`. - """ - if hasattr(self, 'row_grid_columns'): - return self.row_grid_columns - - def get_row_grid_data(self, obj): - """ - Returns the data for the **rows** grid, for use in - :meth:`view()`. Only relevant if :attr:`has_rows` is true. - - This is called by :meth:`make_row_model_grid()`; in the - resulting grid, this becomes - :attr:`~wuttaweb.grids.base.Grid.data`. - - Default logic not implemented; subclass must define this. - """ - raise NotImplementedError - - def configure_row_grid(self, grid): - """ - Configure the **rows** grid for use in :meth:`view()`. Only - relevant if :attr:`has_rows` is true. - - This is called by :meth:`make_row_model_grid()`. - - There is minimal default logic here; subclass should override - as needed. The ``grid`` param will already be "complete" and - ready to use as-is, but this method can further modify it - based on request details etc. - """ - grid.remove('uuid') - self.set_row_labels(grid) - - def set_row_labels(self, obj): - """ - Set label overrides on a **row** form or grid, based on what - is defined by the view class and its parent class(es). - - This is called automatically from - :meth:`configure_row_grid()` and - :meth:`configure_row_form()`. - - This calls :meth:`collect_row_labels()` to find everything, - then it assigns the labels using one of (based on ``obj`` - type): - - * :func:`wuttaweb.forms.base.Form.set_label()` - * :func:`wuttaweb.grids.base.Grid.set_label()` - - :param obj: Either a :class:`~wuttaweb.grids.base.Grid` or a - :class:`~wuttaweb.forms.base.Form` instance. - """ - labels = self.collect_row_labels() - for key, label in labels.items(): - obj.set_label(key, label) - - def collect_row_labels(self): - """ - Collect all **row** labels defined within the view class - hierarchy. - - This is called by :meth:`set_row_labels()`. - - :returns: Dict of all labels found. - """ - labels = {} - hierarchy = self.get_class_hierarchy() - for cls in hierarchy: - if hasattr(cls, 'row_labels'): - labels.update(cls.row_labels) - return labels - ############################## # class methods ############################## @@ -2769,18 +2515,6 @@ class MasterView(View): return cls.get_model_title_plural() - @classmethod - def get_row_model_class(cls): - """ - Returns the **row** model class for the view, if defined. - Only relevant if :attr:`has_rows` is true. - - There is no default here, but a subclass may override by - assigning :attr:`row_model_class`. - """ - if hasattr(cls, 'row_model_class'): - return cls.row_model_class - ############################## # configuration ############################## diff --git a/tests/forms/test_widgets.py b/tests/forms/test_widgets.py index cd3c7c4..a49bdf5 100644 --- a/tests/forms/test_widgets.py +++ b/tests/forms/test_widgets.py @@ -302,23 +302,3 @@ class TestPermissionsWidget(WebTestCase): # editable output always includes the perm html = widget.serialize(field, set()) self.assertIn("Polish the widgets", html) - - -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) - return deform.Field(node, **kwargs) - - def test_serialize(self): - node = colander.SchemaNode(colander.Integer()) - field = self.make_field(node) - widget = mod.BatchIdWidget() - - result = widget.serialize(field, colander.null) - self.assertIs(result, colander.null) - - result = widget.serialize(field, 42) - self.assertEqual(result, '00000042') diff --git a/tests/test_util.py b/tests/test_util.py index 6946d65..21de3a4 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -1,6 +1,5 @@ # -*- coding: utf-8; -*- -import decimal import json import uuid as _uuid from unittest import TestCase @@ -571,12 +570,6 @@ class TestMakeJsonSafe(TestCase): value = mod.make_json_safe(uuid) self.assertEqual(value, uuid.hex) - def test_decimal(self): - value = decimal.Decimal('42.42') - self.assertNotEqual(value, 42.42) - result = mod.make_json_safe(value) - self.assertEqual(result, 42.42) - def test_dict(self): model = self.app.model person = model.Person(full_name="Betty Boop") @@ -592,21 +585,3 @@ class TestMakeJsonSafe(TestCase): 'foo': 'bar', 'person': "Betty Boop", }) - - def test_list(self): - model = self.app.model - person = model.Person(full_name="Betty Boop") - - data = [ - 'foo', - 'bar', - person, - ] - - self.assertRaises(TypeError, json.dumps, data) - value = mod.make_json_safe(data) - self.assertEqual(value, [ - 'foo', - 'bar', - "Betty Boop", - ]) diff --git a/tests/views/test_batch.py b/tests/views/test_batch.py deleted file mode 100644 index a3a34fe..0000000 --- a/tests/views/test_batch.py +++ /dev/null @@ -1,373 +0,0 @@ -# -*- coding: utf-8; -*- - -import datetime -from unittest.mock import patch, MagicMock - -from sqlalchemy import orm -from pyramid.httpexceptions import HTTPFound - -from wuttjamaican.db import model -from wuttjamaican.batch import BatchHandler -from wuttaweb.views import MasterView, batch as mod -from wuttaweb.progress import SessionProgress -from tests.util import WebTestCase - - -class MockBatch(model.BatchMixin, model.Base): - __tablename__ = 'testing_batch_mock' - -class MockBatchRow(model.BatchRowMixin, model.Base): - __tablename__ = 'testing_batch_mock_row' - __batch_class__ = MockBatch - -MockBatch.__row_class__ = MockBatchRow - -class MockBatchHandler(BatchHandler): - model_class = MockBatch - - -class TestBatchMasterView(WebTestCase): - - def setUp(self): - self.setup_web() - - # nb. create MockBatch, MockBatchRow - model.Base.metadata.create_all(bind=self.session.bind) - - def make_handler(self): - return MockBatchHandler(self.config) - - def make_view(self): - return mod.BatchMasterView(self.request) - - def test_get_batch_handler(self): - self.assertRaises(NotImplementedError, mod.BatchMasterView, self.request) - - 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): - view = self.make_view() - 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') - 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: - view = self.make_view() - 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) - - 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): - view = mod.BatchMasterView(self.request) - grid = view.make_model_grid() - # nb. coverage only; tests nothing - view.configure_grid(grid) - - def test_render_batch_id(self): - handler = MockBatchHandler(self.config) - 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', 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): - view = mod.BatchMasterView(self.request) - - batch = MockBatch(id=42) - result = view.get_instance_title(batch) - self.assertEqual(result, "00000042") - - batch = MockBatch(id=43, description="runnin some numbers") - result = view.get_instance_title(batch) - self.assertEqual(result, "00000043 runnin some numbers") - - 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): - view = mod.BatchMasterView(self.request) - - # creating - 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): - form = view.make_model_form(model_instance=batch) - view.configure_form(form) - - # editing - 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): - 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): - 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): - view = mod.BatchMasterView(self.request) - - # create batch - with patch.object(view, 'creating', new=True): - form = view.make_model_form(model_instance=None) - form.validated = {} - batch = view.objectify(form) - self.assertIsInstance(batch.id, int) - 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: - form = view.make_model_form(model_instance=batch) - form.validated = {'description': 'foo'} - self.assertIsNone(batch.description) - batch = view.objectify(form) - self.assertEqual(batch.description, 'foo') - - def test_redirect_after_create(self): - 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'): - view = mod.BatchMasterView(self.request) - batch = MockBatch(id=42) - - # typically redirect to view batch - result = view.redirect_after_create(batch) - self.assertIsInstance(result, HTTPFound) - - # unless populating in which case thread is launched - 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: - view.redirect_after_create(batch) - self.assertTrue(threading.Thread.called) - thread.start.assert_called_once_with() - self.assertTrue(render_progress.called) - - def test_delete_instance(self): - model = self.app.model - handler = self.make_handler() - - 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): - view = self.make_view() - - self.assertEqual(self.session.query(MockBatch).count(), 1) - view.delete_instance(batch) - self.assertEqual(self.session.query(MockBatch).count(), 0) - - 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): - view = mod.BatchMasterView(self.request) - 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): - - # nb. prevent closing our session - with patch.object(self.session, 'close') as close: - - # without progress - view.populate_thread(batch.uuid) - close.assert_called_once_with() - close.reset_mock() - - # with progress - 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): - 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')) - close.assert_called_once_with() - close.reset_mock() - - # failure for batch to appear - self.session.delete(batch) - self.session.commit() - # nb. should give up waiting after 1 second - self.assertRaises(RuntimeError, view.populate_thread, batch.uuid) - - def test_execute(self): - self.pyramid_config.add_route('mock_batches.view', '/batch/mock/{uuid}') - model = self.app.model - handler = MockBatchHandler(self.config) - - 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)): - 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')) - - # but cannot be executed again - response = view.execute() - self.assertEqual(response.status_code, 302) # redirect to "view batch" - # nb. flash has error this time - 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): - 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): - 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): - cls = view.get_row_model_class() - self.assertIs(cls, MockBatchRow) - - def test_get_row_grid_data(self): - handler = MockBatchHandler(self.config) - model = self.app.model - - user = model.User(username='barney') - self.session.add(user) - - batch = handler.make_batch(self.session, created_by=user) - self.session.add(batch) - row = handler.make_row() - handler.add_row(batch, row) - self.session.flush() - - 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): - - view = self.make_view() - data = view.get_row_grid_data(batch) - self.assertIsInstance(data, orm.Query) - self.assertEqual(data.count(), 1) - - def test_configure_row_grid(self): - handler = MockBatchHandler(self.config) - model = self.app.model - - user = model.User(username='barney') - self.session.add(user) - - batch = handler.make_batch(self.session, created_by=user) - self.session.add(batch) - row = handler.make_row() - handler.add_row(batch, row) - self.session.flush() - - 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.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.") - - def test_defaults(self): - # nb. coverage only - with patch.object(mod.BatchMasterView, 'model_class', new=MockBatch, create=True): - mod.BatchMasterView.defaults(self.pyramid_config) diff --git a/tests/views/test_master.py b/tests/views/test_master.py index 06d8ed1..8e451ee 100644 --- a/tests/views/test_master.py +++ b/tests/views/test_master.py @@ -334,16 +334,6 @@ class TestMasterView(WebTestCase): model_class=MyModel): self.assertEqual(mod.MasterView.get_config_title(), "Dinosaurs") - def test_get_row_model_class(self): - model = self.app.model - - # no default - self.assertIsNone(mod.MasterView.get_row_model_class()) - - # class may specify - with patch.object(mod.MasterView, 'row_model_class', create=True, new=model.User): - self.assertIs(mod.MasterView.get_row_model_class(), model.User) - ############################## # support methods ############################## @@ -1027,53 +1017,6 @@ class TestMasterView(WebTestCase): with patch.object(view, 'get_instance', return_value=setting): response = view.view() - def test_view_with_rows(self): - 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="Whitney Houston") - self.session.add(person) - user = model.User(username='whitney', person=person) - self.session.add(user) - self.session.commit() - - get_row_grid_data = MagicMock() - with patch.multiple(mod.MasterView, create=True, - Session=MagicMock(return_value=self.session), - model_class=model.Person, - route_prefix='people', - has_rows=True, - row_model_class=model.User, - get_row_grid_data=get_row_grid_data): - with patch.object(self.request, 'matchdict', new={'uuid': person.uuid}): - view = self.make_view() - - # just for coverage - get_row_grid_data.return_value = [] - response = view.view() - self.assertEqual(response.status_code, 200) - self.assertEqual(response.content_type, 'text/html') - - # now with data... - get_row_grid_data.return_value = [user] - response = view.view() - self.assertEqual(response.status_code, 200) - self.assertEqual(response.content_type, 'text/html') - - # then once more as 'partial' - aka. data only - with patch.dict(self.request.GET, {'partial': 1}): - response = view.view() - self.assertEqual(response.status_code, 200) - self.assertEqual(response.content_type, 'application/json') - - # redirects when view is reset - with patch.dict(self.request.GET, {'reset-view': '1', 'hash': 'foo'}): - # nb. mock current route - with patch.object(self.request, 'current_route_url'): - response = view.view() - self.assertEqual(response.status_code, 302) - def test_edit(self): self.pyramid_config.include('wuttaweb.views.common') self.pyramid_config.include('wuttaweb.views.auth') @@ -1558,103 +1501,3 @@ class TestMasterView(WebTestCase): # should now have 0 settings count = self.session.query(model.Setting).count() self.assertEqual(count, 0) - - ############################## - # row methods - ############################## - - def test_collect_row_labels(self): - - # default labels - view = self.make_view() - labels = view.collect_row_labels() - self.assertEqual(labels, {}) - - # labels come from all classes; subclass wins - with patch.object(View, 'row_labels', create=True, new={'foo': "Foo", 'bar': "Bar"}): - with patch.object(mod.MasterView, 'row_labels', create=True, new={'foo': "FOO FIGHTERS"}): - view = self.make_view() - labels = view.collect_row_labels() - self.assertEqual(labels, {'foo': "FOO FIGHTERS", 'bar': "Bar"}) - - def test_set_row_labels(self): - model = self.app.model - person = model.Person(full_name="Fred Flintstone") - self.session.add(person) - - with patch.multiple(mod.MasterView, create=True, - model_class=model.Person, - has_rows=True, - row_model_class=model.User): - - # no labels by default - view = self.make_view() - grid = view.make_row_model_grid(person, key='person.users', data=[]) - view.set_row_labels(grid) - self.assertEqual(grid.labels, {}) - - # labels come from all classes; subclass wins - with patch.object(View, 'row_labels', create=True, new={'username': "USERNAME"}): - with patch.object(mod.MasterView, 'row_labels', create=True, new={'username': "UserName"}): - view = self.make_view() - grid = view.make_row_model_grid(person, key='person.users', data=[]) - view.set_row_labels(grid) - self.assertEqual(grid.labels, {'username': "UserName"}) - - def test_get_row_grid_data(self): - model = self.app.model - person = model.Person(full_name="Fred Flintstone") - self.session.add(person) - view = self.make_view() - self.assertRaises(NotImplementedError, view.get_row_grid_data, person) - - def test_get_row_grid_columns(self): - - # no default - view = self.make_view() - self.assertIsNone(view.get_row_grid_columns()) - - # class may specify - with patch.object(view, 'row_grid_columns', create=True, new=['foo', 'bar']): - self.assertEqual(view.get_row_grid_columns(), ['foo', 'bar']) - - def test_get_row_grid_key(self): - view = self.make_view() - with patch.multiple(mod.MasterView, create=True, - model_key='id', - grid_key='widgets'): - - self.request.matchdict = {'id': 42} - self.assertEqual(view.get_row_grid_key(), 'widgets.42') - - def test_make_row_model_grid(self): - model = self.app.model - person = model.Person(full_name="Barney Rubble") - self.session.add(person) - self.session.commit() - - self.request.matchdict = {'uuid': person.uuid} - with patch.multiple(mod.MasterView, create=True, - model_class=model.Person): - view = self.make_view() - - # specify data - grid = view.make_row_model_grid(person, data=[]) - self.assertIsNone(grid.model_class) - self.assertEqual(grid.data, []) - - # fetch data - with patch.object(view, 'get_row_grid_data', return_value=[]): - grid = view.make_row_model_grid(person) - self.assertIsNone(grid.model_class) - self.assertEqual(grid.data, []) - - def test_get_rows_title(self): - view = self.make_view() - - # no default - self.assertIsNone(view.get_rows_title()) - - # class may specify - with patch.object(view, 'rows_title', create=True, new="Mock Rows"): - self.assertEqual(view.get_rows_title(), "Mock Rows")