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;
- }
-
%def>
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>
-
-<%def name="tool_panels()">
- ${parent.tool_panels()}
- ${self.tool_panel_execution()}
-%def>
-
-<%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
-
-
-
-
-
-
- Execute ${model_title}
-
-
- ## TODO: forcing black text b/c of b-notification
- ## wrapping button, which has white text
-
-
- What will happen when this batch is executed?
-
-
- ${execution_described|n}
-
- ${h.form(master.get_action_url('execute', batch), ref='executeForm')}
- ${h.csrf_token(request)}
- ${h.end_form()}
-
-
-
-
-
-
-
-
- % else:
-
-
- Batch may be executed,
- but you do not have permission.
-
-
- % endif
- % endif
-
-%def>
-
-<%def name="modify_vue_vars()">
- ${parent.modify_vue_vars()}
- % if not batch.executed and not why_not_execute and master.has_perm('execute'):
-
- % endif
-%def>
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()">
-
@@ -22,14 +9,6 @@
% endif
%def>
-<%def name="tool_panels_wrapper()">
-
- ${self.tool_panels()}
-
-%def>
-
-<%def name="tool_panels()">%def>
-
<%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>
-<%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>
-
-<%def name="render_vue_templates()">
- ${parent.render_vue_templates()}
- % if master.has_rows:
- ${self.render_vue_template_rows_grid()}
- % endif
-%def>
-
-<%def name="render_vue_template_rows_grid()">
- ${rows_grid.render_vue_template()}
-%def>
-
-<%def name="make_vue_components()">
- ${parent.make_vue_components()}
- % if master.has_rows:
- ${rows_grid.render_vue_finalize()}
- % endif
-%def>
+${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>
-
<%def name="page_content()">%def>
<%def name="render_vue_templates()">
@@ -16,7 +12,7 @@
<%def name="render_vue_template_this_page()">
%def>
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>
<%def name="make_wutta_request_mixin()">
@@ -478,28 +477,3 @@
%def>
-
-<%def name="make_wutta_tool_panel_component()">
-
-
-%def>
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")