From 2219315ccc0be3379c0854eca7ee19481d1df37e Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 5 Feb 2018 21:23:23 -0600 Subject: [PATCH] Collapse all master4 views back to just 'master' --- tailbone/views/__init__.py | 3 - tailbone/views/batch/__init__.py | 3 - tailbone/views/batch/core.py | 316 ++++++++-- tailbone/views/batch/core2.py | 129 ---- tailbone/views/batch/core3.py | 205 ------ tailbone/views/batch/core4.py | 127 ---- tailbone/views/batch/importer.py | 2 +- tailbone/views/batch/pricing.py | 2 +- tailbone/views/bouncer.py | 2 +- tailbone/views/brands.py | 2 +- tailbone/views/categories.py | 2 +- tailbone/views/customergroups.py | 2 +- tailbone/views/customers.py | 2 +- tailbone/views/custorders/items.py | 2 +- tailbone/views/custorders/orders.py | 2 +- tailbone/views/datasync.py | 2 +- tailbone/views/departments.py | 2 +- tailbone/views/depositlinks.py | 2 +- tailbone/views/email.py | 2 +- tailbone/views/employees.py | 2 +- tailbone/views/exports.py | 2 +- tailbone/views/families.py | 2 +- tailbone/views/handheld.py | 2 +- tailbone/views/inventory.py | 4 +- tailbone/views/labels/batch.py | 2 +- tailbone/views/labels/profiles.py | 2 +- tailbone/views/master.py | 923 +++++++++++++++++++++++----- tailbone/views/master2.py | 422 ------------- tailbone/views/master3.py | 178 ------ tailbone/views/master4.py | 319 ---------- tailbone/views/messages.py | 2 +- tailbone/views/people.py | 2 +- tailbone/views/principal.py | 2 +- tailbone/views/products.py | 2 +- tailbone/views/purchases/core.py | 2 +- tailbone/views/purchases/credits.py | 2 +- tailbone/views/purchasing/batch.py | 2 +- tailbone/views/reportcodes.py | 2 +- tailbone/views/settings.py | 2 +- tailbone/views/shifts/core.py | 2 +- tailbone/views/stores.py | 2 +- tailbone/views/subdepartments.py | 2 +- tailbone/views/tables.py | 2 +- tailbone/views/taxes.py | 2 +- tailbone/views/tempmon/core.py | 2 +- tailbone/views/trainwreck.py | 2 +- tailbone/views/upgrades.py | 2 +- tailbone/views/users.py | 2 +- tailbone/views/vendors/catalogs.py | 2 +- tailbone/views/vendors/core.py | 2 +- tailbone/views/vendors/invoices.py | 2 +- 51 files changed, 1096 insertions(+), 1613 deletions(-) delete mode 100644 tailbone/views/batch/core2.py delete mode 100644 tailbone/views/batch/core3.py delete mode 100644 tailbone/views/batch/core4.py delete mode 100644 tailbone/views/master2.py delete mode 100644 tailbone/views/master3.py delete mode 100644 tailbone/views/master4.py diff --git a/tailbone/views/__init__.py b/tailbone/views/__init__.py index f2b4fcdc..8d5c680b 100644 --- a/tailbone/views/__init__.py +++ b/tailbone/views/__init__.py @@ -28,9 +28,6 @@ from __future__ import unicode_literals, absolute_import from .core import View from .master import MasterView -from .master2 import MasterView2 -from .master3 import MasterView3 -from .master4 import MasterView4 # TODO: deprecate / remove some of this from .autocomplete import AutocompleteView diff --git a/tailbone/views/batch/__init__.py b/tailbone/views/batch/__init__.py index 9a053ca2..e4b61802 100644 --- a/tailbone/views/batch/__init__.py +++ b/tailbone/views/batch/__init__.py @@ -27,6 +27,3 @@ Views for batches from __future__ import unicode_literals, absolute_import from .core import BatchMasterView, FileBatchMasterView -from .core2 import BatchMasterView2, FileBatchMasterView2 -from .core3 import BatchMasterView3, FileBatchMasterView3 -from .core4 import BatchMasterView4, FileBatchMasterView4 diff --git a/tailbone/views/batch/core.py b/tailbone/views/batch/core.py index ab28c429..2d066ae8 100644 --- a/tailbone/views/batch/core.py +++ b/tailbone/views/batch/core.py @@ -73,6 +73,33 @@ class BatchMasterView(MasterView): mobile_rows_viewable = True has_worksheet = False + grid_columns = [ + 'id', + 'description', + 'created', + 'created_by', + 'rowcount', + # 'status_code', + # 'complete', + 'executed', + 'executed_by', + ] + + form_fields = [ + 'id', + 'created', + 'created_by', + 'rowcount', + 'status_code', + 'executed', + 'executed_by', + 'purge', + ] + + row_labels = { + 'status_code': "Status", + } + def __init__(self, request): super(BatchMasterView, self).__init__(request) self.handler = self.get_handler() @@ -114,6 +141,44 @@ class BatchMasterView(MasterView): def allow_worksheet(self, batch): return not batch.executed and not batch.complete + def configure_grid(self, g): + super(BatchMasterView, self).configure_grid(g) + + g.joiners['created_by'] = lambda q: q.join(model.User, model.User.uuid == self.model_class.created_by_uuid) + g.joiners['executed_by'] = lambda q: q.outerjoin(model.User, model.User.uuid == self.model_class.executed_by_uuid) + + g.filters['executed'].default_active = True + g.filters['executed'].default_verb = 'is_null' + + # TODO: not sure this todo is still relevant? + # TODO: in some cases grid has no sorters yet..e.g. when building query for bulk-delete + # if hasattr(g, 'sorters'): + g.sorters['created_by'] = g.make_sorter(model.User.username) + g.sorters['executed_by'] = g.make_sorter(model.User.username) + + g.set_sort_defaults('id', 'desc') + + g.set_enum('status_code', self.model_class.STATUS) + + g.set_type('created', 'datetime') + g.set_type('executed', 'datetime') + + g.set_renderer('id', self.render_batch_id) + + g.set_link('id') + g.set_link('description') + g.set_link('created') + g.set_link('executed') + + g.set_label('id', "Batch ID") + g.set_label('created_by', "Created by") + g.set_label('rowcount', "Rows") + g.set_label('status_code', "Status") + g.set_label('executed_by', "Executed by") + + def render_batch_id(self, batch, column): + return batch.id_str + def template_kwargs_index(self, **kwargs): kwargs['execute_enabled'] = self.instance_executable(None) if kwargs['execute_enabled'] and self.has_execution_options(): @@ -151,6 +216,82 @@ class BatchMasterView(MasterView): filters['status'] = MobileBatchStatusFilter(self.model_class, 'status', default_value='pending') return filters + def configure_form(self, f): + super(BatchMasterView, self).configure_form(f) + + # id + f.set_readonly('id') + f.set_renderer('id', self.render_batch_id) + f.set_label('id', "Batch ID") + + # created + f.set_readonly('created') + f.set_readonly('created_by') + f.set_renderer('created_by', self.render_user) + f.set_label('created_by', "Created by") + + # cognized + f.set_renderer('cognized_by', self.render_user) + f.set_label('cognized_by', "Cognized by") + + # row count + f.set_readonly('rowcount') + f.set_label('rowcount', "Row Count") + + # status_code + f.set_readonly('status_code') + f.set_renderer('status_code', self.make_status_renderer(self.model_class.STATUS)) + f.set_label('status_code', "Status") + + # executed + f.set_readonly('executed') + f.set_readonly('executed_by') + f.set_renderer('executed_by', self.render_user) + f.set_label('executed_by', "Executed by") + + # notes + f.set_type('notes', 'text') + + # if self.creating and self.request.user: + # batch = fs.model + # batch.created_by_uuid = self.request.user.uuid + + if self.creating: + f.remove_fields('id', + 'rowcount', + 'created', + 'created_by', + 'cognized', + 'cognized_by', + 'executed', + 'executed_by', + 'purge') + + else: # not creating + batch = self.get_instance() + if not batch.executed: + f.remove_fields('executed', + 'executed_by') + + def make_status_renderer(self, enum): + def render_status(batch, field): + value = batch.status_code + if value is None: + return "" + status_code_text = enum.get(value, six.text_type(value)) + if batch.status_text: + return HTML.tag('span', title=batch.status_text, c=status_code_text) + return status_code_text + return render_status + + def render_user(self, batch, field): + user = getattr(batch, field) + if not user: + return "" + title = six.text_type(user) + url = self.request.route_url('users.view', uuid=user.uuid) + return tags.link_to(title, url) + def _preconfigure_fieldset(self, fs): """ Apply some commonly-useful pre-configuration to the main batch @@ -240,67 +381,93 @@ class BatchMasterView(MasterView): download_url=download_url) fs.append(fa.Field(name, **kwargs)) + def configure_mobile_form(self, f): + super(BatchMasterView, self).configure_mobile_form(f) + batch = f.model_instance + + if self.creating: + f.remove_fields('id', + 'rowcount', + 'created', + 'created_by', + 'cognized', + 'cognized_by', + 'executed', + 'executed_by', + 'purge') + + else: # not creating + if not batch.executed: + f.remove_fields('executed', + 'executed_by') + if not batch.complete: + f.remove_field('complete') + def save_create_form(self, form): self.before_create(form) - with Session.no_autoflush: + session = self.Session() + with session.no_autoflush: # transfer form data to batch instance - form.fieldset.sync() - batch = form.fieldset.model + batch = self.objectify(form, self.form_deserialized) # current user is batch creator batch.created_by = self.request.user or self.late_login_user() - # destroy initial batch and re-make using handler + # obtain kwargs for making batch via handler, below kwargs = self.get_batch_kwargs(batch) - Session.expunge(batch) - batch = self.handler.make_batch(Session(), **kwargs) - Session.flush() + # TODO: this needs work yet surely... + if 'filename' in form.schema: + filedict = kwargs.pop('filename', None) + filepath = None + if filedict: + kwargs['filename'] = '' # null not allowed + tempdir = tempfile.mkdtemp() + filepath = os.path.join(tempdir, filedict['filename']) + tmpinfo = form.deform_form['filename'].widget.tmpstore.get(filedict['uid']) + tmpdata = tmpinfo['fp'].read() + with open(filepath, 'wb') as f: + f.write(tmpdata) + + # TODO: is this still necessary with colander? + # destroy initial batch and re-make using handler + # if batch in self.Session: + # self.Session.expunge(batch) + batch = self.handler.make_batch(session, **kwargs) + + self.Session.flush() # TODO: this needs work yet surely... # if batch has input data file, let handler properly establish that - filename = getattr(batch, 'filename', None) - if filename: - path = os.path.join(self.upload_dir, filename) - if os.path.exists(path): - self.handler.set_input_file(batch, path) - os.remove(path) + if 'filename' in form.schema: + if filedict: + self.handler.set_input_file(batch, filepath) + os.remove(filepath) + os.rmdir(tempdir) - # return this object to replace the original return batch - # TODO: this is a totaly copy of save_create_form() def save_mobile_create_form(self, form): self.before_create(form) - - with Session.no_autoflush: + session = self.Session() + with session.no_autoflush: # transfer form data to batch instance - form.fieldset.sync() - batch = form.fieldset.model + batch = self.objectify(form, self.form_deserialized) # current user is batch creator - batch.created_by = self.request.user or self.late_login_user() + batch.created_by = self.request.user + # TODO: is this still necessary with colander? # destroy initial batch and re-make using handler kwargs = self.get_batch_kwargs(batch) - Session.expunge(batch) - batch = self.handler.make_batch(Session(), **kwargs) + if batch in session: + session.expunge(batch) + batch = self.handler.make_batch(session, **kwargs) - Session.flush() - - # TODO: this needs work yet surely... - # if batch has input data file, let handler properly establish that - filename = getattr(batch, 'filename', None) - if filename: - path = os.path.join(self.upload_dir, filename) - if os.path.exists(path): - self.handler.set_input_file(batch, path) - os.remove(path) - - # return this object to replace the original + session.flush() return batch def get_batch_kwargs(self, batch, mobile=False): @@ -367,6 +534,35 @@ class BatchMasterView(MasterView): """ return not batch.executed + def configure_row_grid(self, g): + super(BatchMasterView, self).configure_row_grid(g) + + if 'status_code' in g.filters: + g.filters['status_code'].set_value_renderer(grids.filters.EnumValueRenderer(self.model_row_class.STATUS)) + + g.set_sort_defaults('sequence') + + if self.model_row_class: + g.set_enum('status_code', self.model_row_class.STATUS) + + g.set_renderer('status_code', self.render_row_status) + + g.set_label('sequence', "Seq.") + g.set_label('status_code', "Status") + g.set_label('item_id', "Item ID") + + def get_row_status_enum(self): + return self.model_row_class.STATUS + + def render_row_status(self, row, column): + code = row.status_code + if code is None: + return "" + text = self.get_row_status_enum().get(code, six.text_type(code)) + if row.status_text: + return HTML.tag('span', title=row.status_text, c=text) + return text + def create_row(self): """ Only allow creating a new row if the batch hasn't yet been executed. @@ -387,14 +583,42 @@ class BatchMasterView(MasterView): return self.redirect(self.get_action_url('view', batch, mobile=True)) return super(BatchMasterView, self).mobile_create_row() - def before_create_row(self, form): + def save_create_row_form(self, form): batch = self.get_instance() - row = form.fieldset.model + row = self.objectify(form, self.form_deserialized) self.handler.add_row(batch, row) + self.Session.flush() + return row def after_create_row(self, row): self.handler.refresh_row(row) + def configure_row_form(self, f): + super(BatchMasterView, self).configure_row_form(f) + + # sequence + f.set_readonly('sequence') + + # status_code + if self.model_row_class: + f.set_enum('status_code', self.model_row_class.STATUS) + f.set_renderer('status_code', self.render_row_status) + f.set_readonly('status_code') + f.set_label('status_code', "Status") + + def configure_mobile_row_form(self, f): + super(BatchMasterView, self).configure_mobile_row_form(f) + + # sequence + f.set_readonly('sequence') + + # status_code + if self.model_row_class: + f.set_enum('status_code', self.model_row_class.STATUS) + f.set_renderer('status_code', self.render_row_status) + f.set_readonly('status_code') + f.set_label('status_code', "Status") + def make_default_row_grid_tools(self, batch): if self.rows_creatable and not batch.executed: permission_prefix = self.get_permission_prefix() @@ -1007,6 +1231,26 @@ class FileBatchMasterView(BatchMasterView): os.makedirs(uploads) return uploads + def configure_form(self, f): + super(FileBatchMasterView, self).configure_form(f) + + # filename + f.set_renderer('filename', self.render_filename) + f.set_label('filename', "Data File") + if self.editing: + f.set_readonly('filename') + + if self.creating: + if 'filename' not in f.fields: + f.fields.insert(0, 'filename') + tmpstore = SessionFileUploadTempStore(self.request) + f.set_node('filename', colander.SchemaNode(deform.FileData(), widget=dfwidget.FileUploadWidget(tmpstore))) + + def render_filename(self, batch, field): + path = batch.filepath(self.rattail_config, filename=batch.filename) + url = self.get_action_url('download', batch) + return self.render_file_field(path, url) + def _preconfigure_fieldset(self, fs): super(FileBatchMasterView, self)._preconfigure_fieldset(fs) fs.filename.set(label="Data File", renderer=FileFieldRenderer.new(self)) diff --git a/tailbone/views/batch/core2.py b/tailbone/views/batch/core2.py deleted file mode 100644 index 3a70366b..00000000 --- a/tailbone/views/batch/core2.py +++ /dev/null @@ -1,129 +0,0 @@ -# -*- coding: utf-8; -*- -################################################################################ -# -# Rattail -- Retail Software Framework -# Copyright © 2010-2017 Lance Edgar -# -# This file is part of Rattail. -# -# Rattail 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. -# -# Rattail 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 -# Rattail. If not, see . -# -################################################################################ -""" -Base views for maintaining batches -""" - -from __future__ import unicode_literals, absolute_import - -import six - -from rattail.db import model - -from webhelpers2.html import HTML - -from tailbone import grids -from tailbone.views import MasterView2 -from tailbone.views.batch import BatchMasterView, FileBatchMasterView -from tailbone.views.batch.core import MobileBatchStatusFilter - - -class BatchMasterView2(MasterView2, BatchMasterView): - """ - Base class for all "batch master" views - """ - - grid_columns = [ - 'id', - 'description', - 'created', - 'created_by', - 'rowcount', - # 'status_code', - # 'complete', - 'executed', - 'executed_by', - ] - - def configure_grid(self, g): - super(BatchMasterView2, self).configure_grid(g) - - g.joiners['created_by'] = lambda q: q.join(model.User, model.User.uuid == self.model_class.created_by_uuid) - g.joiners['executed_by'] = lambda q: q.outerjoin(model.User, model.User.uuid == self.model_class.executed_by_uuid) - - g.filters['executed'].default_active = True - g.filters['executed'].default_verb = 'is_null' - - # TODO: not sure this todo is still relevant? - # TODO: in some cases grid has no sorters yet..e.g. when building query for bulk-delete - # if hasattr(g, 'sorters'): - g.sorters['created_by'] = g.make_sorter(model.User.username) - g.sorters['executed_by'] = g.make_sorter(model.User.username) - - g.set_sort_defaults('id', 'desc') - - g.set_enum('status_code', self.model_class.STATUS) - - g.set_type('created', 'datetime') - g.set_type('executed', 'datetime') - - g.set_renderer('id', self.render_batch_id) - - g.set_link('id') - g.set_link('description') - g.set_link('created') - g.set_link('executed') - - g.set_label('id', "Batch ID") - g.set_label('created_by', "Created by") - g.set_label('rowcount', "Rows") - g.set_label('status_code', "Status") - g.set_label('executed_by', "Executed by") - - def render_batch_id(self, batch, column): - return batch.id_str - - def configure_row_grid(self, g): - super(BatchMasterView2, self).configure_row_grid(g) - - if 'status_code' in g.filters: - g.filters['status_code'].set_value_renderer(grids.filters.EnumValueRenderer(self.model_row_class.STATUS)) - - g.set_sort_defaults('sequence') - - if self.model_row_class: - g.set_enum('status_code', self.model_row_class.STATUS) - - g.set_renderer('status_code', self.render_row_status) - - g.set_label('sequence', "Seq.") - g.set_label('status_code', "Status") - g.set_label('item_id', "Item ID") - - def get_row_status_enum(self): - return self.model_row_class.STATUS - - def render_row_status(self, row, column): - code = row.status_code - if code is None: - return "" - text = self.get_row_status_enum().get(code, six.text_type(code)) - if row.status_text: - return HTML.tag('span', title=row.status_text, c=text) - return text - - -class FileBatchMasterView2(BatchMasterView2, FileBatchMasterView): - """ - Base class for all file-based "batch master" views - """ diff --git a/tailbone/views/batch/core3.py b/tailbone/views/batch/core3.py deleted file mode 100644 index 92dedf26..00000000 --- a/tailbone/views/batch/core3.py +++ /dev/null @@ -1,205 +0,0 @@ -# -*- coding: utf-8; -*- -################################################################################ -# -# Rattail -- Retail Software Framework -# Copyright © 2010-2018 Lance Edgar -# -# This file is part of Rattail. -# -# Rattail 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. -# -# Rattail 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 -# Rattail. If not, see . -# -################################################################################ -""" -Base views for maintaining batches -""" - -from __future__ import unicode_literals, absolute_import - -import os -import tempfile - -import six -import colander -import deform -from deform import widget as dfwidget -from pyramid_deform import SessionFileUploadTempStore -from webhelpers2.html import tags - -from tailbone.views import MasterView3 -from tailbone.views.batch import BatchMasterView2, FileBatchMasterView2 - - -class BatchMasterView3(MasterView3, BatchMasterView2): - """ - Base class for all "batch master" views - """ - - form_fields = [ - 'id', - 'created', - 'created_by', - 'rowcount', - 'status_code', - 'executed', - 'executed_by', - 'purge', - ] - - def configure_form(self, f): - super(BatchMasterView3, self).configure_form(f) - - # id - f.set_readonly('id') - f.set_renderer('id', self.render_batch_id) - f.set_label('id', "Batch ID") - - # created - f.set_readonly('created') - f.set_readonly('created_by') - f.set_renderer('created_by', self.render_user) - f.set_label('created_by', "Created by") - - # cognized - f.set_renderer('cognized_by', self.render_user) - f.set_label('cognized_by', "Cognized by") - - # row count - f.set_readonly('rowcount') - f.set_label('rowcount', "Row Count") - - # status_code - f.set_readonly('status_code') - f.set_renderer('status_code', self.make_status_renderer(self.model_class.STATUS)) - f.set_label('status_code', "Status") - - # executed - f.set_readonly('executed') - f.set_readonly('executed_by') - f.set_renderer('executed_by', self.render_user) - f.set_label('executed_by', "Executed by") - - # notes - f.set_type('notes', 'text') - - # if self.creating and self.request.user: - # batch = fs.model - # batch.created_by_uuid = self.request.user.uuid - - if self.creating: - f.remove_fields('id', - 'rowcount', - 'created', - 'created_by', - 'cognized', - 'cognized_by', - 'executed', - 'executed_by', - 'purge') - - else: # not creating - batch = self.get_instance() - if not batch.executed: - f.remove_fields('executed', - 'executed_by') - - def save_create_form(self, form): - self.before_create(form) - - session = self.Session() - with session.no_autoflush: - - # transfer form data to batch instance - batch = self.objectify(form, self.form_deserialized) - - # current user is batch creator - batch.created_by = self.request.user or self.late_login_user() - - # obtain kwargs for making batch via handler, below - kwargs = self.get_batch_kwargs(batch) - - # TODO: this needs work yet surely... - if 'filename' in form.schema: - filedict = kwargs.pop('filename', None) - filepath = None - if filedict: - kwargs['filename'] = '' # null not allowed - tempdir = tempfile.mkdtemp() - filepath = os.path.join(tempdir, filedict['filename']) - tmpinfo = form.deform_form['filename'].widget.tmpstore.get(filedict['uid']) - tmpdata = tmpinfo['fp'].read() - with open(filepath, 'wb') as f: - f.write(tmpdata) - - # TODO: is this still necessary with colander? - # destroy initial batch and re-make using handler - # if batch in self.Session: - # self.Session.expunge(batch) - batch = self.handler.make_batch(session, **kwargs) - - self.Session.flush() - - # TODO: this needs work yet surely... - # if batch has input data file, let handler properly establish that - if 'filename' in form.schema: - if filedict: - self.handler.set_input_file(batch, filepath) - os.remove(filepath) - os.rmdir(tempdir) - - return batch - - def make_status_renderer(self, enum): - def render_status(batch, field): - value = batch.status_code - if value is None: - return "" - status_code_text = enum.get(value, six.text_type(value)) - if batch.status_text: - return HTML.tag('span', title=batch.status_text, c=status_code_text) - return status_code_text - return render_status - - def render_user(self, batch, field): - user = getattr(batch, field) - if not user: - return "" - title = six.text_type(user) - url = self.request.route_url('users.view', uuid=user.uuid) - return tags.link_to(title, url) - - -class FileBatchMasterView3(BatchMasterView3, FileBatchMasterView2): - """ - Base class for all file-based "batch master" views - """ - - def configure_form(self, f): - super(FileBatchMasterView3, self).configure_form(f) - - # filename - f.set_renderer('filename', self.render_filename) - f.set_label('filename', "Data File") - if self.editing: - f.set_readonly('filename') - - if self.creating: - if 'filename' not in f.fields: - f.fields.insert(0, 'filename') - tmpstore = SessionFileUploadTempStore(self.request) - f.set_node('filename', colander.SchemaNode(deform.FileData(), widget=dfwidget.FileUploadWidget(tmpstore))) - - def render_filename(self, batch, field): - path = batch.filepath(self.rattail_config, filename=batch.filename) - url = self.get_action_url('download', batch) - return self.render_file_field(path, url) diff --git a/tailbone/views/batch/core4.py b/tailbone/views/batch/core4.py deleted file mode 100644 index 6d9ac792..00000000 --- a/tailbone/views/batch/core4.py +++ /dev/null @@ -1,127 +0,0 @@ -# -*- coding: utf-8; -*- -################################################################################ -# -# Rattail -- Retail Software Framework -# Copyright © 2010-2018 Lance Edgar -# -# This file is part of Rattail. -# -# Rattail 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. -# -# Rattail 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 -# Rattail. If not, see . -# -################################################################################ -""" -Base views for maintaining batches -""" - -from __future__ import unicode_literals, absolute_import - -from tailbone.views import MasterView4 -from tailbone.views.batch import BatchMasterView3, FileBatchMasterView3 - - -class BatchMasterView4(MasterView4, BatchMasterView3): - """ - Base class for all "batch master" views - """ - - row_labels = { - 'status_code': "Status", - } - - def configure_mobile_form(self, f): - super(BatchMasterView4, self).configure_mobile_form(f) - batch = f.model_instance - - if self.creating: - f.remove_fields('id', - 'rowcount', - 'created', - 'created_by', - 'cognized', - 'cognized_by', - 'executed', - 'executed_by', - 'purge') - - else: # not creating - if not batch.executed: - f.remove_fields('executed', - 'executed_by') - if not batch.complete: - f.remove_field('complete') - - def save_mobile_create_form(self, form): - self.before_create(form) - session = self.Session() - with session.no_autoflush: - - # transfer form data to batch instance - batch = self.objectify(form, self.form_deserialized) - - # current user is batch creator - batch.created_by = self.request.user - - # TODO: is this still necessary with colander? - # destroy initial batch and re-make using handler - kwargs = self.get_batch_kwargs(batch) - if batch in session: - session.expunge(batch) - batch = self.handler.make_batch(session, **kwargs) - - session.flush() - return batch - - def configure_row_form(self, f): - super(BatchMasterView4, self).configure_row_form(f) - - # sequence - f.set_readonly('sequence') - - # status_code - if self.model_row_class: - f.set_enum('status_code', self.model_row_class.STATUS) - f.set_renderer('status_code', self.render_row_status) - f.set_readonly('status_code') - f.set_label('status_code', "Status") - - def configure_mobile_row_form(self, f): - super(BatchMasterView4, self).configure_mobile_row_form(f) - - # sequence - f.set_readonly('sequence') - - # status_code - if self.model_row_class: - f.set_enum('status_code', self.model_row_class.STATUS) - f.set_renderer('status_code', self.render_row_status) - f.set_readonly('status_code') - f.set_label('status_code', "Status") - - # NOTE: must override default logic here, by doing nothing - def before_create_row(self, form): - pass - - def save_create_row_form(self, form): - batch = self.get_instance() - row = self.objectify(form, self.form_deserialized) - self.handler.add_row(batch, row) - self.Session.flush() - return row - - -class FileBatchMasterView4(BatchMasterView4, FileBatchMasterView3): - """ - Base class for all file-based "batch master" views - """ - diff --git a/tailbone/views/batch/importer.py b/tailbone/views/batch/importer.py index 9e966f66..22e4323a 100644 --- a/tailbone/views/batch/importer.py +++ b/tailbone/views/batch/importer.py @@ -30,7 +30,7 @@ import sqlalchemy as sa from rattail.db import model -from tailbone.views.batch import BatchMasterView4 as BatchMasterView +from tailbone.views.batch import BatchMasterView class ImporterBatchView(BatchMasterView): diff --git a/tailbone/views/batch/pricing.py b/tailbone/views/batch/pricing.py index a378614c..e871c3df 100644 --- a/tailbone/views/batch/pricing.py +++ b/tailbone/views/batch/pricing.py @@ -30,7 +30,7 @@ from rattail.db import model from webhelpers2.html import tags -from tailbone.views.batch import BatchMasterView4 as BatchMasterView +from tailbone.views.batch import BatchMasterView class PricingBatchView(BatchMasterView): diff --git a/tailbone/views/bouncer.py b/tailbone/views/bouncer.py index 861f6a86..680bdef8 100644 --- a/tailbone/views/bouncer.py +++ b/tailbone/views/bouncer.py @@ -37,7 +37,7 @@ from pyramid.response import FileResponse from webhelpers2.html import HTML, tags from tailbone import grids -from tailbone.views import MasterView4 as MasterView +from tailbone.views import MasterView class EmailBouncesView(MasterView): diff --git a/tailbone/views/brands.py b/tailbone/views/brands.py index 3a0b177f..c3796af8 100644 --- a/tailbone/views/brands.py +++ b/tailbone/views/brands.py @@ -28,7 +28,7 @@ from __future__ import unicode_literals, absolute_import from rattail.db import model -from tailbone.views import MasterView4 as MasterView, AutocompleteView +from tailbone.views import MasterView, AutocompleteView class BrandsView(MasterView): diff --git a/tailbone/views/categories.py b/tailbone/views/categories.py index f5a7731e..057e6d6a 100644 --- a/tailbone/views/categories.py +++ b/tailbone/views/categories.py @@ -28,7 +28,7 @@ from __future__ import unicode_literals, absolute_import from rattail.db import model -from tailbone.views import MasterView4 as MasterView +from tailbone.views import MasterView class CategoriesView(MasterView): diff --git a/tailbone/views/customergroups.py b/tailbone/views/customergroups.py index 0ee806b2..5c6892d5 100644 --- a/tailbone/views/customergroups.py +++ b/tailbone/views/customergroups.py @@ -29,7 +29,7 @@ from __future__ import unicode_literals, absolute_import from rattail.db import model from tailbone.db import Session -from tailbone.views import MasterView4 as MasterView +from tailbone.views import MasterView class CustomerGroupsView(MasterView): diff --git a/tailbone/views/customers.py b/tailbone/views/customers.py index a5305033..c6b64379 100644 --- a/tailbone/views/customers.py +++ b/tailbone/views/customers.py @@ -39,7 +39,7 @@ from webhelpers2.html import HTML, tags from tailbone import grids from tailbone.db import Session -from tailbone.views import MasterView4 as MasterView, AutocompleteView +from tailbone.views import MasterView, AutocompleteView from rattail.db import model diff --git a/tailbone/views/custorders/items.py b/tailbone/views/custorders/items.py index e075e1d8..f4c540f0 100644 --- a/tailbone/views/custorders/items.py +++ b/tailbone/views/custorders/items.py @@ -33,7 +33,7 @@ from sqlalchemy import orm from rattail.db import model from rattail.time import localtime -from tailbone.views import MasterView4 as MasterView +from tailbone.views import MasterView from tailbone.util import raw_datetime diff --git a/tailbone/views/custorders/orders.py b/tailbone/views/custorders/orders.py index 8dfcae28..dee21f15 100644 --- a/tailbone/views/custorders/orders.py +++ b/tailbone/views/custorders/orders.py @@ -34,7 +34,7 @@ from rattail.db import model from webhelpers2.html import tags from tailbone.db import Session -from tailbone.views import MasterView4 as MasterView +from tailbone.views import MasterView class CustomerOrdersView(MasterView): diff --git a/tailbone/views/datasync.py b/tailbone/views/datasync.py index 6d602666..def5ee5f 100644 --- a/tailbone/views/datasync.py +++ b/tailbone/views/datasync.py @@ -31,7 +31,7 @@ import logging from rattail.db import model -from tailbone.views import MasterView4 as MasterView +from tailbone.views import MasterView log = logging.getLogger(__name__) diff --git a/tailbone/views/departments.py b/tailbone/views/departments.py index 29c6ea4e..b0eba5be 100644 --- a/tailbone/views/departments.py +++ b/tailbone/views/departments.py @@ -33,7 +33,7 @@ from rattail.db import model from deform import widget as dfwidget from tailbone import grids -from tailbone.views import MasterView4 as MasterView, AutocompleteView +from tailbone.views import MasterView, AutocompleteView class DepartmentsView(MasterView): diff --git a/tailbone/views/depositlinks.py b/tailbone/views/depositlinks.py index 9be6260c..db28e3f6 100644 --- a/tailbone/views/depositlinks.py +++ b/tailbone/views/depositlinks.py @@ -28,7 +28,7 @@ from __future__ import unicode_literals, absolute_import from rattail.db import model -from tailbone.views import MasterView4 as MasterView +from tailbone.views import MasterView class DepositLinksView(MasterView): diff --git a/tailbone/views/email.py b/tailbone/views/email.py index 932840b4..de700260 100644 --- a/tailbone/views/email.py +++ b/tailbone/views/email.py @@ -37,7 +37,7 @@ from deform import widget as dfwidget from webhelpers2.html import HTML from tailbone.db import Session -from tailbone.views import View, MasterView4 as MasterView +from tailbone.views import View, MasterView class ProfilesView(MasterView): diff --git a/tailbone/views/employees.py b/tailbone/views/employees.py index 42025eba..89d7e014 100644 --- a/tailbone/views/employees.py +++ b/tailbone/views/employees.py @@ -37,7 +37,7 @@ from webhelpers2.html import tags, HTML from tailbone import grids from tailbone.db import Session -from tailbone.views import MasterView4 as MasterView, AutocompleteView +from tailbone.views import MasterView, AutocompleteView class EmployeesView(MasterView): diff --git a/tailbone/views/exports.py b/tailbone/views/exports.py index 8dc7c1ae..9e842616 100644 --- a/tailbone/views/exports.py +++ b/tailbone/views/exports.py @@ -36,7 +36,7 @@ from pyramid.response import FileResponse from webhelpers2.html import HTML, tags from tailbone import forms2 as forms -from tailbone.views import MasterView4 as MasterView +from tailbone.views import MasterView class ExportMasterView(MasterView): diff --git a/tailbone/views/families.py b/tailbone/views/families.py index 72feba3b..997255b3 100644 --- a/tailbone/views/families.py +++ b/tailbone/views/families.py @@ -28,7 +28,7 @@ from __future__ import unicode_literals, absolute_import from rattail.db import model -from tailbone.views import MasterView4 as MasterView +from tailbone.views import MasterView class FamiliesView(MasterView): diff --git a/tailbone/views/handheld.py b/tailbone/views/handheld.py index 015c7e41..13a74c54 100644 --- a/tailbone/views/handheld.py +++ b/tailbone/views/handheld.py @@ -35,7 +35,7 @@ import formencode as fe from webhelpers2.html import tags from tailbone.db import Session -from tailbone.views.batch import FileBatchMasterView4 as FileBatchMasterView +from tailbone.views.batch import FileBatchMasterView ACTION_OPTIONS = OrderedDict([ diff --git a/tailbone/views/inventory.py b/tailbone/views/inventory.py index e0e0976d..418515b0 100644 --- a/tailbone/views/inventory.py +++ b/tailbone/views/inventory.py @@ -43,8 +43,8 @@ from deform import widget as dfwidget from webhelpers2.html import HTML, tags from tailbone import forms, forms2, grids -from tailbone.views import MasterView4 as MasterView -from tailbone.views.batch import BatchMasterView4 as BatchMasterView +from tailbone.views import MasterView +from tailbone.views.batch import BatchMasterView class InventoryAdjustmentReasonsView(MasterView): diff --git a/tailbone/views/labels/batch.py b/tailbone/views/labels/batch.py index 663d3416..acf33d9e 100644 --- a/tailbone/views/labels/batch.py +++ b/tailbone/views/labels/batch.py @@ -30,7 +30,7 @@ from rattail.db import model from webhelpers2.html import HTML, tags -from tailbone.views.batch import BatchMasterView4 as BatchMasterView +from tailbone.views.batch import BatchMasterView class LabelBatchView(BatchMasterView): diff --git a/tailbone/views/labels/profiles.py b/tailbone/views/labels/profiles.py index d2b821dc..25ecfa0e 100644 --- a/tailbone/views/labels/profiles.py +++ b/tailbone/views/labels/profiles.py @@ -31,7 +31,7 @@ from rattail.db import model from pyramid.httpexceptions import HTTPFound from tailbone.db import Session -from tailbone.views import MasterView4 as MasterView +from tailbone.views import MasterView class ProfilesView(MasterView): diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 4a9bc50e..6c6080d1 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -38,19 +38,19 @@ import sqlalchemy_continuum as continuum from rattail.db import model, Session as RattailSession from rattail.db.continuum import model_transaction_query from rattail.util import prettify -from rattail.time import localtime #, make_utc +from rattail.time import localtime from rattail.threads import Thread from rattail.csvutil import UnicodeDictWriter from rattail.files import temp_path from rattail.excel import ExcelWriter -import formalchemy as fa +import deform from pyramid import httpexceptions from pyramid.renderers import get_renderer, render_to_response, render from pyramid.response import FileResponse from webhelpers2.html import HTML, tags -from tailbone import forms, grids, diffs +from tailbone import forms2 as forms, grids, diffs from tailbone.views import View from tailbone.progress import SessionProgress @@ -67,6 +67,7 @@ class MasterView(View): checkboxes = False listable = True + sortable = True results_downloadable_csv = False results_downloadable_xlsx = False creatable = True @@ -85,6 +86,7 @@ class MasterView(View): supports_mobile = False mobile_creatable = False + mobile_pageable = True mobile_filterable = False listing = False @@ -103,12 +105,15 @@ class MasterView(View): has_versions = False + labels = {'uuid': "UUID"} + # ROW-RELATED ATTRS FOLLOW: has_rows = False model_row_class = None - rows_filterable = True + rows_pageable = True rows_sortable = True + rows_filterable = True rows_viewable = True rows_creatable = False rows_editable = False @@ -123,6 +128,8 @@ class MasterView(View): mobile_rows_viewable = False mobile_rows_editable = False + row_labels = {} + @property def Session(self): """ @@ -132,6 +139,56 @@ class MasterView(View): from tailbone.db import Session return Session + @classmethod + def get_grid_factory(cls): + """ + Returns the grid factory or class which is to be used when creating new + grid instances. + """ + return getattr(cls, 'grid_factory', grids.Grid) + + @classmethod + def get_row_grid_factory(cls): + """ + Returns the grid factory or class which is to be used when creating new + row grid instances. + """ + return getattr(cls, 'row_grid_factory', grids.Grid) + + @classmethod + def get_version_grid_factory(cls): + """ + Returns the grid factory or class which is to be used when creating new + version grid instances. + """ + return getattr(cls, 'version_grid_factory', grids.Grid) + + @classmethod + def get_mobile_grid_factory(cls): + """ + Must return a callable to be used when creating new mobile grid + instances. Instead of overriding this, you can set + :attr:`mobile_grid_factory`. Default factory is :class:`MobileGrid`. + """ + return getattr(cls, 'mobile_grid_factory', grids.MobileGrid) + + @classmethod + def get_mobile_row_grid_factory(cls): + """ + Must return a callable to be used when creating new mobile row grid + instances. Instead of overriding this, you can set + :attr:`mobile_row_grid_factory`. Default factory is :class:`MobileGrid`. + """ + return getattr(cls, 'mobile_row_grid_factory', grids.MobileGrid) + + def set_labels(self, obj): + for key, label in self.labels.items(): + obj.set_label(key, label) + + def set_row_labels(self, obj): + for key, label in self.row_labels.items(): + obj.set_label(key, label) + ############################## # Available Views ############################## @@ -164,6 +221,223 @@ class MasterView(View): return self.render_to_response('index', {'grid': grid}) + def make_grid(self, factory=None, key=None, data=None, columns=None, **kwargs): + """ + Creates a new grid instance + """ + if factory is None: + factory = self.get_grid_factory() + if key is None: + key = self.get_grid_key() + if data is None: + data = self.get_data(session=kwargs.get('session')) + if columns is None: + columns = self.get_grid_columns() + + kwargs.setdefault('request', self.request) + kwargs = self.make_grid_kwargs(**kwargs) + grid = factory(key, data, columns, **kwargs) + self.configure_grid(grid) + grid.load_settings() + return grid + + def get_effective_data(self, session=None, **kwargs): + """ + Convenience method which returns the "effective" data for the master + grid, filtered and sorted to match what would show on the UI, but not + paged etc. + """ + if session is None: + session = self.Session() + kwargs.setdefault('pageable', False) + grid = self.make_grid(session=session, **kwargs) + return grid.make_visible_data() + + def get_grid_columns(self): + if hasattr(self, 'grid_columns'): + return self.grid_columns + # TODO + raise NotImplementedError + + def make_grid_kwargs(self, **kwargs): + """ + Return a dictionary of kwargs to be passed to the factory when creating + new grid instances. + """ + defaults = { + 'model_class': getattr(self, 'model_class', None), + 'width': 'full', + 'filterable': self.filterable, + 'sortable': self.sortable, + 'pageable': self.pageable, + 'extra_row_class': self.grid_extra_class, + 'url': lambda obj: self.get_action_url('view', obj), + 'checkboxes': self.checkboxes or ( + self.mergeable and self.request.has_perm('{}.merge'.format(self.get_permission_prefix()))), + 'checked': self.checked, + } + if 'main_actions' not in kwargs and 'more_actions' not in kwargs: + main, more = self.get_grid_actions() + defaults['main_actions'] = main + defaults['more_actions'] = more + defaults.update(kwargs) + return defaults + + def configure_grid(self, grid): + self.set_labels(grid) + + def grid_extra_class(self, obj, i): + """ + Returns string of extra class(es) for the table row corresponding to + the given object, or ``None``. + """ + + def make_row_grid(self, factory=None, key=None, data=None, columns=None, **kwargs): + """ + Make and return a new (configured) rows grid instance. + """ + instance = kwargs.pop('instance', None) + if not instance: + instance = self.get_instance() + + if factory is None: + factory = self.get_row_grid_factory() + if key is None: + key = self.get_row_grid_key() + if data is None: + data = self.get_row_data(instance) + if columns is None: + columns = self.get_row_grid_columns() + + kwargs.setdefault('request', self.request) + kwargs = self.make_row_grid_kwargs(**kwargs) + grid = factory(key, data, columns, **kwargs) + self.configure_row_grid(grid) + grid.load_settings() + return grid + + def get_row_grid_columns(self): + if hasattr(self, 'row_grid_columns'): + return self.row_grid_columns + # TODO + raise NotImplementedError + + def make_row_grid_kwargs(self, **kwargs): + """ + Return a dict of kwargs to be used when constructing a new rows grid. + """ + permission_prefix = self.get_permission_prefix() + + defaults = { + 'model_class': self.model_row_class, + 'width': 'full', + 'filterable': self.rows_filterable, + 'sortable': self.rows_sortable, + 'pageable': self.rows_pageable, + 'default_pagesize': self.rows_default_pagesize, + 'extra_row_class': self.row_grid_extra_class, + 'url': lambda obj: self.get_row_action_url('view', obj), + } + + if self.has_rows and 'main_actions' not in defaults: + actions = [] + + # view action + if self.rows_viewable: + view = lambda r, i: self.get_row_action_url('view', r) + actions.append(grids.GridAction('view', icon='zoomin', url=view)) + + # edit action + if self.rows_editable: + actions.append(grids.GridAction('edit', icon='pencil', url=self.row_edit_action_url)) + + # delete action + if self.rows_deletable and self.request.has_perm('{}.delete_row'.format(permission_prefix)): + actions.append(grids.GridAction('delete', icon='trash', url=self.row_delete_action_url)) + defaults['delete_speedbump'] = self.rows_deletable_speedbump + + defaults['main_actions'] = actions + + defaults.update(kwargs) + return defaults + + def configure_row_grid(self, grid): + # super(MasterView, self).configure_row_grid(grid) + self.set_row_labels(grid) + + def row_grid_extra_class(self, obj, i): + """ + Returns string of extra class(es) for the table row corresponding to + the given row object, or ``None``. + """ + + def make_version_grid(self, factory=None, key=None, data=None, columns=None, **kwargs): + """ + Creates a new version grid instance + """ + instance = kwargs.pop('instance', None) + if not instance: + instance = self.get_instance() + + if factory is None: + factory = self.get_version_grid_factory() + if key is None: + key = self.get_version_grid_key() + if data is None: + data = self.get_version_data(instance) + if columns is None: + columns = self.get_version_grid_columns() + + kwargs.setdefault('request', self.request) + kwargs = self.make_version_grid_kwargs(**kwargs) + grid = factory(key, data, columns, **kwargs) + self.configure_version_grid(grid) + grid.load_settings() + return grid + + def get_version_grid_columns(self): + if hasattr(self, 'version_grid_columns'): + return self.version_grid_columns + # TODO + return [ + 'issued_at', + 'user', + 'remote_addr', + 'comment', + ] + + def make_version_grid_kwargs(self, **kwargs): + """ + Return a dictionary of kwargs to be passed to the factory when + constructing a new version grid. + """ + defaults = { + 'model_class': continuum.transaction_class(self.get_model_class()), + 'width': 'full', + 'pageable': True, + } + if 'main_actions' not in kwargs: + route = '{}.version'.format(self.get_route_prefix()) + instance = kwargs.get('instance') or self.get_instance() + url = lambda txn, i: self.request.route_url(route, uuid=instance.uuid, txnid=txn.id) + defaults['main_actions'] = [ + self.make_action('view', icon='zoomin', url=url), + ] + defaults.update(kwargs) + return defaults + + def configure_version_grid(self, g): + g.set_sort_defaults('issued_at', 'desc') + g.set_renderer('comment', self.render_version_comment) + g.set_label('issued_at', "Changed") + g.set_label('user', "Changed by") + g.set_label('remote_addr', "IP Address") + # TODO: why does this render '#' as url? + # g.set_link('issued_at') + + def render_version_comment(self, transaction, column): + return transaction.meta.get('comment', "") + def mobile_index(self): """ Mobile "home" page for the data model @@ -184,6 +458,33 @@ class MasterView(View): return cls.mobile_grid_key return 'mobile.{}'.format(cls.get_route_prefix()) + def make_mobile_grid(self, factory=None, key=None, data=None, columns=None, **kwargs): + """ + Creates a new mobile grid instance + """ + if factory is None: + factory = self.get_mobile_grid_factory() + if key is None: + key = self.get_mobile_grid_key() + if data is None: + data = self.get_mobile_data(session=kwargs.get('session')) + if columns is None: + columns = self.get_mobile_grid_columns() + + kwargs.setdefault('request', self.request) + kwargs.setdefault('mobile', True) + kwargs = self.make_mobile_grid_kwargs(**kwargs) + grid = factory(key, data, columns, **kwargs) + self.configure_mobile_grid(grid) + grid.load_settings() + return grid + + def get_mobile_grid_columns(self): + if hasattr(self, 'mobile_grid_columns'): + return self.mobile_grid_columns + # TODO + return ['listitem'] + def get_mobile_data(self, session=None): """ Must return the "raw" / full data set for the mobile grid. This data @@ -193,6 +494,93 @@ class MasterView(View): """ return self.get_data(session=session) + def make_mobile_grid_kwargs(self, **kwargs): + """ + Must return a dictionary of kwargs to be passed to the factory when + creating new mobile grid instances. + """ + defaults = { + 'model_class': getattr(self, 'model_class', None), + 'pageable': self.mobile_pageable, + 'sortable': False, + 'filterable': self.mobile_filterable, + 'renderers': self.make_mobile_grid_renderers(), + 'url': lambda obj: self.get_action_url('view', obj, mobile=True), + } + # TODO: this seems wrong.. + if self.mobile_filterable: + defaults['filters'] = self.make_mobile_filters() + defaults.update(kwargs) + return defaults + + def make_mobile_grid_renderers(self): + return { + 'listitem': self.render_mobile_listitem, + } + + def render_mobile_listitem(self, obj, i): + return obj + + def configure_mobile_grid(self, grid): + pass + + def make_mobile_row_grid(self, factory=None, key=None, data=None, columns=None, **kwargs): + """ + Make a new (configured) rows grid instance for mobile. + """ + instance = kwargs.pop('instance', self.get_instance()) + + if factory is None: + factory = self.get_mobile_row_grid_factory() + if key is None: + key = 'mobile.{}.{}'.format(self.get_grid_key(), self.request.matchdict[self.get_model_key()]) + if data is None: + data = self.get_mobile_row_data(instance) + if columns is None: + columns = self.get_mobile_row_grid_columns() + + kwargs.setdefault('request', self.request) + kwargs.setdefault('mobile', True) + kwargs = self.make_mobile_row_grid_kwargs(**kwargs) + grid = factory(key, data, columns, **kwargs) + self.configure_mobile_row_grid(grid) + grid.load_settings() + return grid + + def get_mobile_row_grid_columns(self): + if hasattr(self, 'mobile_row_grid_columns'): + return self.mobile_row_grid_columns + # TODO + return ['listitem'] + + def make_mobile_row_grid_kwargs(self, **kwargs): + """ + Must return a dictionary of kwargs to be passed to the factory when + creating new mobile *row* grid instances. + """ + defaults = { + 'model_class': self.model_row_class, + # TODO + 'pageable': self.pageable, + 'sortable': False, + 'filterable': self.mobile_rows_filterable, + 'renderers': self.make_mobile_row_grid_renderers(), + 'url': lambda obj: self.get_row_action_url('view', obj, mobile=True), + } + # TODO: this seems wrong.. + if self.mobile_rows_filterable: + defaults['filters'] = self.make_mobile_row_filters() + defaults.update(kwargs) + return defaults + + def make_mobile_row_grid_renderers(self): + return { + 'listitem': self.render_mobile_row_listitem, + } + + def configure_mobile_row_grid(self, grid): + pass + def make_mobile_filters(self): """ Returns a set of filters for the mobile grid, if applicable. @@ -203,45 +591,8 @@ class MasterView(View): Returns a set of filters for the mobile row grid, if applicable. """ - def mobile_listitem_field(self): - """ - Must return a FormAlchemy field to be appended to grid, or ``None`` if - none is desired. - """ - return fa.Field('listitem', value=lambda obj: obj, - renderer=self.mobile_listitem_renderer()) - - def mobile_listitem_renderer(self): - """ - Must return a FormAlchemy field renderer callable for the mobile grid's - list item field. - """ - master = self - - class ListItemRenderer(fa.FieldRenderer): - - def render_readonly(self, **kwargs): - obj = self.raw_value - if obj is None: - return '' - title = master.get_instance_title(obj) - url = master.get_action_url('view', obj, mobile=True) - return tags.link_to(title, url) - - return ListItemRenderer - - def mobile_row_listitem_renderer(self): - """ - Must return a FormAlchemy field renderer callable for the mobile row - grid's list item field. - """ - master = self - - class ListItemRenderer(fa.FieldRenderer): - def render_readonly(self, **kwargs): - return master.render_mobile_row_listitem(self.raw_value, **kwargs) - - return ListItemRenderer + def render_mobile_row_listitem(self, obj, i): + return obj def create(self): """ @@ -276,17 +627,30 @@ class MasterView(View): return self.redirect_after_create(obj, mobile=True) return self.render_to_response('create', {'form': form}, mobile=True) + def save_create_form(self, form): + self.before_create(form) + with self.Session().no_autoflush: + obj = self.objectify(form, self.form_deserialized) + self.before_create_flush(obj, form) + self.Session.add(obj) + self.Session.flush() + return obj + + def before_create_flush(self, obj, form): + pass + def flash_after_create(self, obj): self.request.session.flash("{} has been created: {}".format( self.get_model_title(), self.get_instance_title(obj))) - def save_create_form(self, form): - self.before_create(form) - form.save() - def save_mobile_create_form(self, form): self.before_create(form) - form.save() + with self.Session.no_autoflush: + obj = self.objectify(form, self.form_deserialized) + self.before_create_flush(obj, form) + self.Session.add(obj) + self.Session.flush() + return obj def redirect_after_create(self, instance, mobile=False): if self.populatable and self.should_populate(instance): @@ -563,50 +927,160 @@ class MasterView(View): context['grid'] = self.make_mobile_row_grid(instance=instance) return self.render_to_response('view', context, mobile=True) - def make_mobile_form(self, instance, **kwargs): + def make_mobile_form(self, instance=None, factory=None, fields=None, schema=None, **kwargs): """ - Make a FormAlchemy-based form for use with mobile CRUD views + Creates a new mobile form for the given model class/instance. """ - fieldset = self.make_fieldset(instance) - self.preconfigure_mobile_fieldset(fieldset) - self.configure_mobile_fieldset(fieldset) - kwargs.setdefault('creating', self.creating) - kwargs.setdefault('editing', self.editing) - kwargs.setdefault('action_url', self.request.current_route_url(_query=None)) - if self.creating: - kwargs.setdefault('cancel_url', self.get_index_url(mobile=True)) - else: - kwargs.setdefault('cancel_url', self.get_action_url('view', instance, mobile=True)) - factory = kwargs.pop('factory', forms.AlchemyForm) - kwargs.setdefault('session', self.Session()) - form = factory(self.request, fieldset, **kwargs) - form.readonly = self.viewing + if factory is None: + factory = self.get_mobile_form_factory() + if fields is None: + fields = self.get_mobile_form_fields() + if schema is None: + schema = self.make_mobile_form_schema() + + if not self.creating: + kwargs['model_instance'] = instance + kwargs = self.make_mobile_form_kwargs(**kwargs) + form = factory(fields, schema, **kwargs) + self.configure_mobile_form(form) return form + def get_mobile_form_fields(self): + if hasattr(self, 'mobile_form_fields'): + return self.mobile_form_fields + # TODO + # raise NotImplementedError + + def make_mobile_form_schema(self): + if not self.model_class: + # TODO + raise NotImplementedError + + def make_mobile_form_kwargs(self, **kwargs): + """ + Return a dictionary of kwargs to be passed to the factory when creating + new mobile forms. + """ + defaults = { + 'request': self.request, + 'readonly': self.viewing, + 'model_class': getattr(self, 'model_class', None), + 'action_url': self.request.current_route_url(_query=None), + } + if self.creating: + defaults['cancel_url'] = self.get_index_url(mobile=True) + else: + instance = kwargs['model_instance'] + defaults['cancel_url'] = self.get_action_url('view', instance, mobile=True) + defaults.update(kwargs) + return defaults + + def configure_mobile_form(self, form): + """ + Configure the primary mobile form. + """ + # TODO: is any of this stuff from configure_form() needed? + # if self.editing: + # model_class = self.get_model_class(error=False) + # if model_class: + # mapper = orm.class_mapper(model_class) + # for key in mapper.primary_key: + # for field in form.fields: + # if field == key.name: + # form.set_readonly(field) + # break + # form.remove_field('uuid') + + self.set_labels(form) + def preconfigure_mobile_row_fieldset(self, fieldset): self._preconfigure_row_fieldset(fieldset) def configure_mobile_row_fieldset(self, fieldset): self.configure_row_fieldset(fieldset) - def make_mobile_row_form(self, row, **kwargs): + def validate_mobile_form(self, form): + controls = self.request.POST.items() + try: + self.form_deserialized = form.validate(controls) + except deform.ValidationFailure: + return False + return True + + def make_mobile_row_form(self, instance=None, factory=None, fields=None, schema=None, **kwargs): """ - Make a form for use with mobile CRUD views, for the given row object. + Creates a new mobile form for the given model class/instance. """ - fieldset = self.make_fieldset(row) - self.preconfigure_mobile_row_fieldset(fieldset) - self.configure_mobile_row_fieldset(fieldset) - kwargs.setdefault('session', self.Session()) - kwargs.setdefault('creating', self.creating) - kwargs.setdefault('editing', self.editing) - kwargs.setdefault('action_url', self.request.current_route_url(_query=None)) - if 'cancel_url' not in kwargs: - kwargs['cancel_url'] = self.get_action_url('view', self.get_parent(row), mobile=True) - factory = kwargs.pop('factory', forms.AlchemyForm) - form = factory(self.request, fieldset, **kwargs) - form.readonly = self.viewing + if factory is None: + factory = self.get_mobile_row_form_factory() + if fields is None: + fields = self.get_mobile_row_form_fields() + if schema is None: + schema = self.make_mobile_row_form_schema() + + if not self.creating: + kwargs['model_instance'] = instance + kwargs = self.make_mobile_row_form_kwargs(**kwargs) + form = factory(fields, schema, **kwargs) + self.configure_mobile_row_form(form) return form + def get_mobile_row_form_fields(self): + if hasattr(self, 'mobile_row_form_fields'): + return self.mobile_row_form_fields + # TODO + # raise NotImplementedError + + def make_mobile_row_form_schema(self): + if not self.model_row_class: + # TODO + raise NotImplementedError + + def make_mobile_row_form_kwargs(self, **kwargs): + """ + Return a dictionary of kwargs to be passed to the factory when creating + new mobile row forms. + """ + defaults = { + 'request': self.request, + 'readonly': self.viewing, + 'model_class': getattr(self, 'model_row_class', None), + 'action_url': self.request.current_route_url(_query=None), + } + if self.creating: + defaults['cancel_url'] = self.request.get_referrer() + else: + instance = kwargs['model_instance'] + defaults['cancel_url'] = self.get_row_action_url('view', instance, mobile=True) + defaults.update(kwargs) + return defaults + + def configure_mobile_row_form(self, form): + """ + Configure the mobile row form. + """ + # TODO: is any of this stuff from configure_form() needed? + # if self.editing: + # model_class = self.get_model_class(error=False) + # if model_class: + # mapper = orm.class_mapper(model_class) + # for key in mapper.primary_key: + # for field in form.fields: + # if field == key.name: + # form.set_readonly(field) + # break + # form.remove_field('uuid') + + self.set_row_labels(form) + + def validate_mobile_row_form(self, form): + controls = self.request.POST.items() + try: + self.form_deserialized = form.validate(controls) + except deform.ValidationFailure: + return False + return True + def preconfigure_mobile_fieldset(self, fieldset): self._preconfigure_fieldset(fieldset) @@ -619,14 +1093,6 @@ class MasterView(View): def get_mobile_row_data(self, parent): return self.get_row_data(parent) - def mobile_row_listitem_field(self): - """ - Must return a FormAlchemy field to be appended to row grid, or ``None`` - if none is desired. - """ - return fa.Field('listitem', value=lambda obj: obj, - renderer=self.mobile_row_listitem_renderer()) - def mobile_row_route_url(self, route_name, **kwargs): route_name = 'mobile.{}.{}_row'.format(self.get_route_prefix(), route_name) return self.request.route_url(route_name, **kwargs) @@ -818,18 +1284,10 @@ class MasterView(View): context['dform'] = form.make_deform_form() return self.render_to_response('edit', context) - def validate_form(self, form): - return form.validate() - - def validate_mobile_form(self, form): - return form.validate() - - def validate_row_form(self, form): - return form.validate() - def save_edit_form(self, form): - self.save_form(form) - self.after_edit(form.fieldset.model) + obj = self.objectify(form, self.form_deserialized) + self.after_edit(obj) + self.Session.flush() def redirect_after_edit(self, instance): return self.redirect(self.get_action_url('view', instance)) @@ -1623,45 +2081,151 @@ class MasterView(View): """ return six.text_type(instance) - def make_form(self, instance, **kwargs): + @classmethod + def get_form_factory(cls): """ - Make a FormAlchemy-based form for use with CRUD views. + Returns the grid factory or class which is to be used when creating new + grid instances. """ - # TODO: Some hacky stuff here, to accommodate old form cruft. Probably - # should refactor forms soon too, but trying to avoid it for the moment. + return getattr(cls, 'form_factory', forms.Form) - kwargs.setdefault('creating', self.creating) - kwargs.setdefault('editing', self.editing) + @classmethod + def get_mobile_form_factory(cls): + """ + Returns the factory or class which is to be used when creating new + mobile forms. + """ + return getattr(cls, 'mobile_form_factory', forms.Form) - fieldset = self.make_fieldset(instance) - self._preconfigure_fieldset(fieldset) - self.configure_fieldset(fieldset) - self._postconfigure_fieldset(fieldset) + @classmethod + def get_row_form_factory(cls): + """ + Returns the factory or class which is to be used when creating new row + forms. + """ + return getattr(cls, 'row_form_factory', forms.Form) - kwargs.setdefault('action_url', self.request.current_route_url(_query=None)) + @classmethod + def get_mobile_row_form_factory(cls): + """ + Returns the factory or class which is to be used when creating new + mobile row forms. + """ + return getattr(cls, 'mobile_row_form_factory', forms.Form) + + def render_file_field(self, path, url=None, filename=None): + """ + Convenience for rendering a file with optional download link + """ + if not filename: + filename = os.path.basename(path) + content = "{} ({})".format(filename, self.readable_size(path)) + if url: + return tags.link_to(content, url) + return content + + def readable_size(self, path): + # TODO: this was shamelessly copied from FormAlchemy ... + length = self.get_size(path) + if length == 0: + return '0 KB' + if length <= 1024: + return '1 KB' + if length > 1048576: + return '%0.02f MB' % (length / 1048576.0) + return '%0.02f KB' % (length / 1024.0) + + def get_size(self, path): + try: + return os.path.getsize(path) + except os.error: + return 0 + + def make_form(self, instance=None, factory=None, fields=None, schema=None, **kwargs): + """ + Creates a new form for the given model class/instance + """ + if factory is None: + factory = self.get_form_factory() + if fields is None: + fields = self.get_form_fields() + if schema is None: + schema = self.make_form_schema() + + # TODO: SQLAlchemy class instance is assumed *unless* we get a dict + # (seems like we should be smarter about this somehow) + # if not self.creating and not isinstance(instance, dict): + if not self.creating: + kwargs['model_instance'] = instance + kwargs = self.make_form_kwargs(**kwargs) + form = factory(fields, schema, **kwargs) + self.configure_form(form) + return form + + def get_form_fields(self): + if hasattr(self, 'form_fields'): + return self.form_fields + # TODO + # raise NotImplementedError + + def make_form_schema(self): + if not self.model_class: + # TODO + raise NotImplementedError + + def make_form_kwargs(self, **kwargs): + """ + Return a dictionary of kwargs to be passed to the factory when creating + new form instances. + """ + defaults = { + 'request': self.request, + 'readonly': self.viewing, + 'model_class': getattr(self, 'model_class', None), + 'action_url': self.request.current_route_url(_query=None), + } if self.creating: kwargs.setdefault('cancel_url', self.get_index_url()) else: + instance = kwargs['model_instance'] kwargs.setdefault('cancel_url', self.get_action_url('view', instance)) - factory = kwargs.pop('factory', forms.AlchemyForm) - kwargs.setdefault('session', self.Session()) - form = factory(self.request, fieldset, **kwargs) - form.readonly = self.viewing - return form + defaults.update(kwargs) + return defaults + + def configure_form(self, form): + """ + Configure the primary form. By default this just sets any primary key + fields to be readonly (if we have a :attr:`model_class`). + """ + if self.editing: + model_class = self.get_model_class(error=False) + if model_class: + mapper = orm.class_mapper(model_class) + for key in mapper.primary_key: + for field in form.fields: + if field == key.name: + form.set_readonly(field) + break + + form.remove_field('uuid') + + self.set_labels(form) + + def validate_form(self, form): + controls = self.request.POST.items() + try: + self.form_deserialized = form.validate(controls) + except deform.ValidationFailure: + return False + return True + + def objectify(self, form, data): + obj = form.schema.objectify(data, context=form.model_instance) + return obj def save_form(self, form): form.save() - def make_fieldset(self, instance, **kwargs): - """ - Make a FormAlchemy fieldset for the given model instance. - """ - kwargs.setdefault('session', self.Session()) - kwargs.setdefault('request', self.request) - fieldset = fa.FieldSet(instance, **kwargs) - fieldset.prettify = prettify - return fieldset - def _preconfigure_fieldset(self, fieldset): pass @@ -1756,8 +2320,19 @@ class MasterView(View): self.get_instance_title(parent)), 'form': form}) + # TODO: still need to verify this logic def save_create_row_form(self, form): - self.save_row_form(form) + # self.before_create(form) + # with self.Session().no_autoflush: + # obj = self.objectify(form, self.form_deserialized) + # self.before_create_flush(obj, form) + obj = self.objectify(form, self.form_deserialized) + self.Session.add(obj) + self.Session.flush() + return obj + + # def save_create_row_form(self, form): + # self.save_row_form(form) def before_create_row(self, form): pass @@ -1879,11 +2454,13 @@ class MasterView(View): mobile=True) def save_edit_row_form(self, form): - self.save_row_form(form) - self.after_edit_row(form.fieldset.model) + obj = self.objectify(form, self.form_deserialized) + self.after_edit_row(obj) + self.Session.flush() + return obj - def save_row_form(self, form): - form.save() + # def save_row_form(self, form): + # form.save() def after_edit_row(self, row): """ @@ -1926,38 +2503,86 @@ class MasterView(View): raise httpexceptions.HTTPNotFound() return instance - def make_row_form(self, instance, **kwargs): + def make_row_form(self, instance=None, factory=None, fields=None, schema=None, **kwargs): """ - Make a FormAlchemy form for use with CRUD views for a data *row*. + Creates a new row form for the given model class/instance. """ - # TODO: Some hacky stuff here, to accommodate old form cruft. Probably - # should refactor forms soon too, but trying to avoid it for the moment. + if factory is None: + factory = self.get_row_form_factory() + if fields is None: + fields = self.get_row_form_fields() + if schema is None: + schema = self.make_row_form_schema() - kwargs.setdefault('creating', self.creating) - kwargs.setdefault('editing', self.editing) - - fieldset = self.make_fieldset(instance) - self._preconfigure_row_fieldset(fieldset) - self.configure_row_fieldset(fieldset) - - kwargs.setdefault('action_url', self.request.current_route_url(_query=None)) - if 'cancel_url' not in kwargs: - if self.creating: - kwargs['cancel_url'] = self.get_action_url('view', self.get_parent(instance)) - else: - kwargs['cancel_url'] = self.get_row_action_url('view', instance) - - kwargs.setdefault('session', self.Session()) - form = forms.AlchemyForm(self.request, fieldset, **kwargs) - form.readonly = self.viewing + if not self.creating: + kwargs['model_instance'] = instance + kwargs = self.make_row_form_kwargs(**kwargs) + form = factory(fields, schema, **kwargs) + self.configure_row_form(form) return form + def get_row_form_fields(self): + if hasattr(self, 'row_form_fields'): + return self.row_form_fields + # TODO + # raise NotImplementedError + + def make_row_form_schema(self): + if not self.model_row_class: + # TODO + raise NotImplementedError + + def make_row_form_kwargs(self, **kwargs): + """ + Return a dictionary of kwargs to be passed to the factory when creating + new row forms. + """ + defaults = { + 'request': self.request, + 'readonly': self.viewing, + 'model_class': getattr(self, 'model_row_class', None), + 'action_url': self.request.current_route_url(_query=None), + } + if self.creating: + kwargs.setdefault('cancel_url', self.request.get_referrer()) + else: + instance = kwargs['model_instance'] + kwargs.setdefault('cancel_url', self.get_row_action_url('view', instance)) + defaults.update(kwargs) + return defaults + + def configure_row_form(self, form): + """ + Configure a row form. + """ + # TODO: is any of this stuff from configure_form() needed? + # if self.editing: + # model_class = self.get_model_class(error=False) + # if model_class: + # mapper = orm.class_mapper(model_class) + # for key in mapper.primary_key: + # for field in form.fields: + # if field == key.name: + # form.set_readonly(field) + # break + # form.remove_field('uuid') + + self.set_row_labels(form) + def _preconfigure_row_fieldset(self, fs): pass def configure_row_fieldset(self, fs): fs.configure() + def validate_row_form(self, form): + controls = self.request.POST.items() + try: + self.form_deserialized = form.validate(controls) + except deform.ValidationFailure: + return False + return True + def get_row_action_url(self, action, row, mobile=False): """ Generate a URL for the given action on the given row. diff --git a/tailbone/views/master2.py b/tailbone/views/master2.py deleted file mode 100644 index 233f5271..00000000 --- a/tailbone/views/master2.py +++ /dev/null @@ -1,422 +0,0 @@ -# -*- coding: utf-8; -*- -################################################################################ -# -# Rattail -- Retail Software Framework -# Copyright © 2010-2017 Lance Edgar -# -# This file is part of Rattail. -# -# Rattail 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. -# -# Rattail 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 -# Rattail. If not, see . -# -################################################################################ -""" -Master View -""" - -from __future__ import unicode_literals, absolute_import - -import sqlalchemy_continuum as continuum - -from tailbone import grids -from tailbone.views import MasterView - - -class MasterView2(MasterView): - """ - Base "master" view class. All model master views should derive from this. - """ - sortable = True - rows_pageable = True - mobile_pageable = True - labels = {'uuid': "UUID"} - - @classmethod - def get_grid_factory(cls): - """ - Returns the grid factory or class which is to be used when creating new - grid instances. - """ - return getattr(cls, 'grid_factory', grids.Grid) - - @classmethod - def get_row_grid_factory(cls): - """ - Returns the grid factory or class which is to be used when creating new - row grid instances. - """ - return getattr(cls, 'row_grid_factory', grids.Grid) - - @classmethod - def get_version_grid_factory(cls): - """ - Returns the grid factory or class which is to be used when creating new - version grid instances. - """ - return getattr(cls, 'version_grid_factory', grids.Grid) - - @classmethod - def get_mobile_grid_factory(cls): - """ - Must return a callable to be used when creating new mobile grid - instances. Instead of overriding this, you can set - :attr:`mobile_grid_factory`. Default factory is :class:`MobileGrid`. - """ - return getattr(cls, 'mobile_grid_factory', grids.MobileGrid) - - @classmethod - def get_mobile_row_grid_factory(cls): - """ - Must return a callable to be used when creating new mobile row grid - instances. Instead of overriding this, you can set - :attr:`mobile_row_grid_factory`. Default factory is :class:`MobileGrid`. - """ - return getattr(cls, 'mobile_row_grid_factory', grids.MobileGrid) - - def get_effective_data(self, session=None, **kwargs): - """ - Convenience method which returns the "effective" data for the master - grid, filtered and sorted to match what would show on the UI, but not - paged etc. - """ - if session is None: - session = self.Session() - kwargs.setdefault('pageable', False) - grid = self.make_grid(session=session, **kwargs) - return grid.make_visible_data() - - def make_grid(self, factory=None, key=None, data=None, columns=None, **kwargs): - """ - Creates a new grid instance - """ - if factory is None: - factory = self.get_grid_factory() - if key is None: - key = self.get_grid_key() - if data is None: - data = self.get_data(session=kwargs.get('session')) - if columns is None: - columns = self.get_grid_columns() - - kwargs.setdefault('request', self.request) - kwargs = self.make_grid_kwargs(**kwargs) - grid = factory(key, data, columns, **kwargs) - self.configure_grid(grid) - grid.load_settings() - return grid - - def make_row_grid(self, factory=None, key=None, data=None, columns=None, **kwargs): - """ - Make and return a new (configured) rows grid instance. - """ - instance = kwargs.pop('instance', None) - if not instance: - instance = self.get_instance() - - if factory is None: - factory = self.get_row_grid_factory() - if key is None: - key = self.get_row_grid_key() - if data is None: - data = self.get_row_data(instance) - if columns is None: - columns = self.get_row_grid_columns() - - kwargs.setdefault('request', self.request) - kwargs = self.make_row_grid_kwargs(**kwargs) - - grid = factory(key, data, columns, **kwargs) - self.configure_row_grid(grid) - grid.load_settings() - return grid - - def make_version_grid(self, factory=None, key=None, data=None, columns=None, **kwargs): - """ - Creates a new version grid instance - """ - instance = kwargs.pop('instance', None) - if not instance: - instance = self.get_instance() - - if factory is None: - factory = self.get_version_grid_factory() - if key is None: - key = self.get_version_grid_key() - if data is None: - data = self.get_version_data(instance) - if columns is None: - columns = self.get_version_grid_columns() - - kwargs.setdefault('request', self.request) - kwargs = self.make_version_grid_kwargs(**kwargs) - grid = factory(key, data, columns, **kwargs) - self.configure_version_grid(grid) - grid.load_settings() - return grid - - def make_mobile_grid(self, factory=None, key=None, data=None, columns=None, **kwargs): - """ - Creates a new mobile grid instance - """ - if factory is None: - factory = self.get_mobile_grid_factory() - if key is None: - key = self.get_mobile_grid_key() - if data is None: - data = self.get_mobile_data(session=kwargs.get('session')) - if columns is None: - columns = self.get_mobile_grid_columns() - - kwargs.setdefault('request', self.request) - kwargs.setdefault('mobile', True) - kwargs = self.make_mobile_grid_kwargs(**kwargs) - grid = factory(key, data, columns, **kwargs) - self.configure_mobile_grid(grid) - grid.load_settings() - return grid - - def make_mobile_row_grid(self, factory=None, key=None, data=None, columns=None, **kwargs): - """ - Make a new (configured) rows grid instance for mobile. - """ - instance = kwargs.pop('instance', self.get_instance()) - - if factory is None: - factory = self.get_mobile_row_grid_factory() - if key is None: - key = 'mobile.{}.{}'.format(self.get_grid_key(), self.request.matchdict[self.get_model_key()]) - if data is None: - data = self.get_mobile_row_data(instance) - if columns is None: - columns = self.get_mobile_row_grid_columns() - - kwargs.setdefault('request', self.request) - kwargs.setdefault('mobile', True) - kwargs = self.make_mobile_row_grid_kwargs(**kwargs) - grid = factory(key, data, columns, **kwargs) - self.configure_mobile_row_grid(grid) - grid.load_settings() - return grid - - def get_grid_columns(self): - if hasattr(self, 'grid_columns'): - return self.grid_columns - # TODO - raise NotImplementedError - - def get_row_grid_columns(self): - if hasattr(self, 'row_grid_columns'): - return self.row_grid_columns - # TODO - raise NotImplementedError - - def get_version_grid_columns(self): - if hasattr(self, 'version_grid_columns'): - return self.version_grid_columns - # TODO - return [ - 'issued_at', - 'user', - 'remote_addr', - 'comment', - ] - - def get_mobile_grid_columns(self): - if hasattr(self, 'mobile_grid_columns'): - return self.mobile_grid_columns - # TODO - return ['listitem'] - - def get_mobile_row_grid_columns(self): - if hasattr(self, 'mobile_row_grid_columns'): - return self.mobile_row_grid_columns - # TODO - return ['listitem'] - - def make_grid_kwargs(self, **kwargs): - """ - Return a dictionary of kwargs to be passed to the factory when creating - new grid instances. - """ - defaults = { - 'model_class': getattr(self, 'model_class', None), - 'width': 'full', - 'filterable': self.filterable, - 'sortable': self.sortable, - 'pageable': self.pageable, - 'extra_row_class': self.grid_extra_class, - 'url': lambda obj: self.get_action_url('view', obj), - 'checkboxes': self.checkboxes or ( - self.mergeable and self.request.has_perm('{}.merge'.format(self.get_permission_prefix()))), - 'checked': self.checked, - } - if 'main_actions' not in kwargs and 'more_actions' not in kwargs: - main, more = self.get_grid_actions() - defaults['main_actions'] = main - defaults['more_actions'] = more - defaults.update(kwargs) - return defaults - - def make_row_grid_kwargs(self, **kwargs): - """ - Return a dict of kwargs to be used when constructing a new rows grid. - """ - permission_prefix = self.get_permission_prefix() - - defaults = { - 'model_class': self.model_row_class, - 'width': 'full', - 'filterable': self.rows_filterable, - 'sortable': self.rows_sortable, - 'pageable': self.rows_pageable, - 'default_pagesize': self.rows_default_pagesize, - 'extra_row_class': self.row_grid_extra_class, - 'url': lambda obj: self.get_row_action_url('view', obj), - } - - if self.has_rows and 'main_actions' not in defaults: - actions = [] - - # view action - if self.rows_viewable: - view = lambda r, i: self.get_row_action_url('view', r) - actions.append(grids.GridAction('view', icon='zoomin', url=view)) - - # edit action - if self.rows_editable: - actions.append(grids.GridAction('edit', icon='pencil', url=self.row_edit_action_url)) - - # delete action - if self.rows_deletable and self.request.has_perm('{}.delete_row'.format(permission_prefix)): - actions.append(grids.GridAction('delete', icon='trash', url=self.row_delete_action_url)) - defaults['delete_speedbump'] = self.rows_deletable_speedbump - - defaults['main_actions'] = actions - - defaults.update(kwargs) - return defaults - - def make_version_grid_kwargs(self, **kwargs): - """ - Return a dictionary of kwargs to be passed to the factory when - constructing a new version grid. - """ - defaults = { - 'model_class': continuum.transaction_class(self.get_model_class()), - 'width': 'full', - 'pageable': True, - } - if 'main_actions' not in kwargs: - route = '{}.version'.format(self.get_route_prefix()) - instance = kwargs.get('instance') or self.get_instance() - url = lambda txn, i: self.request.route_url(route, uuid=instance.uuid, txnid=txn.id) - defaults['main_actions'] = [ - self.make_action('view', icon='zoomin', url=url), - ] - defaults.update(kwargs) - return defaults - - def make_mobile_grid_kwargs(self, **kwargs): - """ - Must return a dictionary of kwargs to be passed to the factory when - creating new mobile grid instances. - """ - defaults = { - 'model_class': getattr(self, 'model_class', None), - 'pageable': self.mobile_pageable, - 'sortable': False, - 'filterable': self.mobile_filterable, - 'renderers': self.make_mobile_grid_renderers(), - 'url': lambda obj: self.get_action_url('view', obj, mobile=True), - } - # TODO: this seems wrong.. - if self.mobile_filterable: - defaults['filters'] = self.make_mobile_filters() - defaults.update(kwargs) - return defaults - - def make_mobile_row_grid_kwargs(self, **kwargs): - """ - Must return a dictionary of kwargs to be passed to the factory when - creating new mobile *row* grid instances. - """ - defaults = { - 'model_class': self.model_row_class, - # TODO - 'pageable': self.pageable, - 'sortable': False, - 'filterable': self.mobile_rows_filterable, - 'renderers': self.make_mobile_row_grid_renderers(), - 'url': lambda obj: self.get_row_action_url('view', obj, mobile=True), - } - # TODO: this seems wrong.. - if self.mobile_rows_filterable: - defaults['filters'] = self.make_mobile_row_filters() - defaults.update(kwargs) - return defaults - - def make_mobile_grid_renderers(self): - return { - 'listitem': self.render_mobile_listitem, - } - - def render_mobile_listitem(self, obj, i): - return obj - - def make_mobile_row_grid_renderers(self): - return { - 'listitem': self.render_mobile_row_listitem, - } - - def render_mobile_row_listitem(self, obj, i): - return obj - - def grid_extra_class(self, obj, i): - """ - Returns string of extra class(es) for the table row corresponding to - the given object, or ``None``. - """ - - def row_grid_extra_class(self, obj, i): - """ - Returns string of extra class(es) for the table row corresponding to - the given row object, or ``None``. - """ - - def set_labels(self, obj): - for key, label in self.labels.items(): - obj.set_label(key, label) - - def configure_grid(self, grid): - self.set_labels(grid) - - def configure_row_grid(self, grid): - pass - - def configure_version_grid(self, g): - g.set_sort_defaults('issued_at', 'desc') - g.set_renderer('comment', self.render_version_comment) - g.set_label('issued_at', "Changed") - g.set_label('user', "Changed by") - g.set_label('remote_addr', "IP Address") - # TODO: why does this render '#' as url? - # g.set_link('issued_at') - - def render_version_comment(self, transaction, column): - return transaction.meta.get('comment', "") - - def configure_mobile_grid(self, grid): - pass - - def configure_mobile_row_grid(self, grid): - pass diff --git a/tailbone/views/master3.py b/tailbone/views/master3.py deleted file mode 100644 index 08bbabb7..00000000 --- a/tailbone/views/master3.py +++ /dev/null @@ -1,178 +0,0 @@ -# -*- coding: utf-8; -*- -################################################################################ -# -# Rattail -- Retail Software Framework -# Copyright © 2010-2017 Lance Edgar -# -# This file is part of Rattail. -# -# Rattail 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. -# -# Rattail 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 -# Rattail. If not, see . -# -################################################################################ -""" -Master View -""" - -from __future__ import unicode_literals, absolute_import - -import os - -from sqlalchemy import orm - -import deform -from webhelpers2.html import tags - -from tailbone import forms2 as forms -from tailbone.views import MasterView2 - - -class MasterView3(MasterView2): - """ - Base "master" view class. All model master views should derive from this. - """ - - @classmethod - def get_form_factory(cls): - """ - Returns the grid factory or class which is to be used when creating new - grid instances. - """ - return getattr(cls, 'form_factory', forms.Form) - - def make_form(self, instance=None, factory=None, fields=None, schema=None, **kwargs): - """ - Creates a new form for the given model class/instance - """ - if factory is None: - factory = self.get_form_factory() - if fields is None: - fields = self.get_form_fields() - if schema is None: - schema = self.make_form_schema() - - # TODO: SQLAlchemy class instance is assumed *unless* we get a dict - # (seems like we should be smarter about this somehow) - # if not self.creating and not isinstance(instance, dict): - if not self.creating: - kwargs['model_instance'] = instance - kwargs = self.make_form_kwargs(**kwargs) - form = factory(fields, schema, **kwargs) - self.configure_form(form) - return form - - def make_form_schema(self): - if not self.model_class: - # TODO - raise NotImplementedError - - def get_form_fields(self): - if hasattr(self, 'form_fields'): - return self.form_fields - # TODO - # raise NotImplementedError - - def make_form_kwargs(self, **kwargs): - """ - Return a dictionary of kwargs to be passed to the factory when creating - new form instances. - """ - defaults = { - 'request': self.request, - 'readonly': self.viewing, - 'model_class': getattr(self, 'model_class', None), - 'action_url': self.request.current_route_url(_query=None), - } - if self.creating: - kwargs.setdefault('cancel_url', self.get_index_url()) - else: - instance = kwargs['model_instance'] - kwargs.setdefault('cancel_url', self.get_action_url('view', instance)) - defaults.update(kwargs) - return defaults - - def configure_form(self, form): - """ - Configure the primary form. By default this just sets any primary key - fields to be readonly (if we have a :attr:`model_class`). - """ - if self.editing: - model_class = self.get_model_class(error=False) - if model_class: - mapper = orm.class_mapper(model_class) - for key in mapper.primary_key: - for field in form.fields: - if field == key.name: - form.set_readonly(field) - break - - form.remove_field('uuid') - - self.set_labels(form) - - def render_file_field(self, path, url=None, filename=None): - """ - Convenience for rendering a file with optional download link - """ - if not filename: - filename = os.path.basename(path) - content = "{} ({})".format(filename, self.readable_size(path)) - if url: - return tags.link_to(content, url) - return content - - def readable_size(self, path): - # TODO: this was shamelessly copied from FormAlchemy ... - length = self.get_size(path) - if length == 0: - return '0 KB' - if length <= 1024: - return '1 KB' - if length > 1048576: - return '%0.02f MB' % (length / 1048576.0) - return '%0.02f KB' % (length / 1024.0) - - def get_size(self, path): - try: - return os.path.getsize(path) - except os.error: - return 0 - - def validate_form(self, form): - controls = self.request.POST.items() - try: - self.form_deserialized = form.validate(controls) - except deform.ValidationFailure: - return False - return True - - def objectify(self, form, data): - obj = form.schema.objectify(data, context=form.model_instance) - return obj - - def save_create_form(self, form): - self.before_create(form) - with self.Session().no_autoflush: - obj = self.objectify(form, self.form_deserialized) - self.before_create_flush(obj, form) - self.Session.add(obj) - self.Session.flush() - return obj - - def before_create_flush(self, obj, form): - pass - - def save_edit_form(self, form): - obj = self.objectify(form, self.form_deserialized) - self.after_edit(obj) - self.Session.flush() diff --git a/tailbone/views/master4.py b/tailbone/views/master4.py deleted file mode 100644 index 07ae1247..00000000 --- a/tailbone/views/master4.py +++ /dev/null @@ -1,319 +0,0 @@ -# -*- coding: utf-8; -*- -################################################################################ -# -# Rattail -- Retail Software Framework -# Copyright © 2010-2018 Lance Edgar -# -# This file is part of Rattail. -# -# Rattail 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. -# -# Rattail 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 -# Rattail. If not, see . -# -################################################################################ -""" -Master View (v4) -""" - -from __future__ import unicode_literals, absolute_import - -import deform - -from tailbone import forms2 as forms -from tailbone.views import MasterView3 - - -class MasterView4(MasterView3): - """ - Base "master" view class. All model master views should derive from this. - """ - row_labels = {} - - def make_mobile_form(self, instance=None, factory=None, fields=None, schema=None, **kwargs): - """ - Creates a new mobile form for the given model class/instance. - """ - if factory is None: - factory = self.get_mobile_form_factory() - if fields is None: - fields = self.get_mobile_form_fields() - if schema is None: - schema = self.make_mobile_form_schema() - - if not self.creating: - kwargs['model_instance'] = instance - kwargs = self.make_mobile_form_kwargs(**kwargs) - form = factory(fields, schema, **kwargs) - self.configure_mobile_form(form) - return form - - def make_row_form(self, instance=None, factory=None, fields=None, schema=None, **kwargs): - """ - Creates a new row form for the given model class/instance. - """ - if factory is None: - factory = self.get_row_form_factory() - if fields is None: - fields = self.get_row_form_fields() - if schema is None: - schema = self.make_row_form_schema() - - if not self.creating: - kwargs['model_instance'] = instance - kwargs = self.make_row_form_kwargs(**kwargs) - form = factory(fields, schema, **kwargs) - self.configure_row_form(form) - return form - - def make_mobile_row_form(self, instance=None, factory=None, fields=None, schema=None, **kwargs): - """ - Creates a new mobile form for the given model class/instance. - """ - if factory is None: - factory = self.get_mobile_row_form_factory() - if fields is None: - fields = self.get_mobile_row_form_fields() - if schema is None: - schema = self.make_mobile_row_form_schema() - - if not self.creating: - kwargs['model_instance'] = instance - kwargs = self.make_mobile_row_form_kwargs(**kwargs) - form = factory(fields, schema, **kwargs) - self.configure_mobile_row_form(form) - return form - - @classmethod - def get_mobile_form_factory(cls): - """ - Returns the factory or class which is to be used when creating new - mobile forms. - """ - return getattr(cls, 'mobile_form_factory', forms.Form) - - @classmethod - def get_row_form_factory(cls): - """ - Returns the factory or class which is to be used when creating new row - forms. - """ - return getattr(cls, 'row_form_factory', forms.Form) - - @classmethod - def get_mobile_row_form_factory(cls): - """ - Returns the factory or class which is to be used when creating new - mobile row forms. - """ - return getattr(cls, 'mobile_row_form_factory', forms.Form) - - def make_mobile_form_schema(self): - if not self.model_class: - # TODO - raise NotImplementedError - - def make_row_form_schema(self): - if not self.model_row_class: - # TODO - raise NotImplementedError - - def make_mobile_row_form_schema(self): - if not self.model_row_class: - # TODO - raise NotImplementedError - - def get_mobile_form_fields(self): - if hasattr(self, 'mobile_form_fields'): - return self.mobile_form_fields - # TODO - # raise NotImplementedError - - def get_row_form_fields(self): - if hasattr(self, 'row_form_fields'): - return self.row_form_fields - # TODO - # raise NotImplementedError - - def get_mobile_row_form_fields(self): - if hasattr(self, 'mobile_row_form_fields'): - return self.mobile_row_form_fields - # TODO - # raise NotImplementedError - - def make_mobile_form_kwargs(self, **kwargs): - """ - Return a dictionary of kwargs to be passed to the factory when creating - new mobile forms. - """ - defaults = { - 'request': self.request, - 'readonly': self.viewing, - 'model_class': getattr(self, 'model_class', None), - 'action_url': self.request.current_route_url(_query=None), - } - if self.creating: - defaults['cancel_url'] = self.get_index_url(mobile=True) - else: - instance = kwargs['model_instance'] - defaults['cancel_url'] = self.get_action_url('view', instance, mobile=True) - defaults.update(kwargs) - return defaults - - def make_row_form_kwargs(self, **kwargs): - """ - Return a dictionary of kwargs to be passed to the factory when creating - new row forms. - """ - defaults = { - 'request': self.request, - 'readonly': self.viewing, - 'model_class': getattr(self, 'model_row_class', None), - 'action_url': self.request.current_route_url(_query=None), - } - if self.creating: - kwargs.setdefault('cancel_url', self.request.get_referrer()) - else: - instance = kwargs['model_instance'] - kwargs.setdefault('cancel_url', self.get_row_action_url('view', instance)) - defaults.update(kwargs) - return defaults - - def make_mobile_row_form_kwargs(self, **kwargs): - """ - Return a dictionary of kwargs to be passed to the factory when creating - new mobile row forms. - """ - defaults = { - 'request': self.request, - 'readonly': self.viewing, - 'model_class': getattr(self, 'model_row_class', None), - 'action_url': self.request.current_route_url(_query=None), - } - if self.creating: - defaults['cancel_url'] = self.request.get_referrer() - else: - instance = kwargs['model_instance'] - defaults['cancel_url'] = self.get_row_action_url('view', instance, mobile=True) - defaults.update(kwargs) - return defaults - - def configure_mobile_form(self, form): - """ - Configure the primary mobile form. - """ - # TODO: is any of this stuff from configure_form() needed? - # if self.editing: - # model_class = self.get_model_class(error=False) - # if model_class: - # mapper = orm.class_mapper(model_class) - # for key in mapper.primary_key: - # for field in form.fields: - # if field == key.name: - # form.set_readonly(field) - # break - # form.remove_field('uuid') - - self.set_labels(form) - - def configure_row_grid(self, grid): - super(MasterView4, self).configure_row_grid(grid) - self.set_row_labels(grid) - - def configure_row_form(self, form): - """ - Configure a row form. - """ - # TODO: is any of this stuff from configure_form() needed? - # if self.editing: - # model_class = self.get_model_class(error=False) - # if model_class: - # mapper = orm.class_mapper(model_class) - # for key in mapper.primary_key: - # for field in form.fields: - # if field == key.name: - # form.set_readonly(field) - # break - # form.remove_field('uuid') - - self.set_row_labels(form) - - def configure_mobile_row_form(self, form): - """ - Configure the mobile row form. - """ - # TODO: is any of this stuff from configure_form() needed? - # if self.editing: - # model_class = self.get_model_class(error=False) - # if model_class: - # mapper = orm.class_mapper(model_class) - # for key in mapper.primary_key: - # for field in form.fields: - # if field == key.name: - # form.set_readonly(field) - # break - # form.remove_field('uuid') - - self.set_row_labels(form) - - def set_row_labels(self, obj): - for key, label in self.row_labels.items(): - obj.set_label(key, label) - - def validate_mobile_form(self, form): - controls = self.request.POST.items() - try: - self.form_deserialized = form.validate(controls) - except deform.ValidationFailure: - return False - return True - - def validate_row_form(self, form): - controls = self.request.POST.items() - try: - self.form_deserialized = form.validate(controls) - except deform.ValidationFailure: - return False - return True - - def validate_mobile_row_form(self, form): - controls = self.request.POST.items() - try: - self.form_deserialized = form.validate(controls) - except deform.ValidationFailure: - return False - return True - - def save_mobile_create_form(self, form): - self.before_create(form) - with self.Session.no_autoflush: - obj = self.objectify(form, self.form_deserialized) - self.before_create_flush(obj, form) - self.Session.add(obj) - self.Session.flush() - return obj - - # TODO: still need to verify this logic - def save_create_row_form(self, form): - # self.before_create(form) - # with self.Session().no_autoflush: - # obj = self.objectify(form, self.form_deserialized) - # self.before_create_flush(obj, form) - obj = self.objectify(form, self.form_deserialized) - self.Session.add(obj) - self.Session.flush() - return obj - - def save_edit_row_form(self, form): - obj = self.objectify(form, self.form_deserialized) - self.after_edit_row(obj) - self.Session.flush() - return obj diff --git a/tailbone/views/messages.py b/tailbone/views/messages.py index d216e8ce..5d2060a5 100644 --- a/tailbone/views/messages.py +++ b/tailbone/views/messages.py @@ -37,7 +37,7 @@ from pyramid import httpexceptions from webhelpers2.html import tags, HTML from tailbone.db import Session -from tailbone.views import MasterView4 as MasterView +from tailbone.views import MasterView from tailbone.util import raw_datetime diff --git a/tailbone/views/people.py b/tailbone/views/people.py index 3a3f1773..051d19af 100644 --- a/tailbone/views/people.py +++ b/tailbone/views/people.py @@ -34,7 +34,7 @@ from rattail.db import model, api from pyramid.httpexceptions import HTTPFound, HTTPNotFound from webhelpers2.html import HTML, tags -from tailbone.views import MasterView4 as MasterView, AutocompleteView +from tailbone.views import MasterView, AutocompleteView class PeopleView(MasterView): diff --git a/tailbone/views/principal.py b/tailbone/views/principal.py index 494fda2d..dd82522b 100644 --- a/tailbone/views/principal.py +++ b/tailbone/views/principal.py @@ -35,7 +35,7 @@ import wtforms from webhelpers2.html import HTML from tailbone.db import Session -from tailbone.views import MasterView4 as MasterView +from tailbone.views import MasterView class PrincipalMasterView(MasterView): diff --git a/tailbone/views/products.py b/tailbone/views/products.py index f8e07a6d..48b445d7 100644 --- a/tailbone/views/products.py +++ b/tailbone/views/products.py @@ -47,7 +47,7 @@ from webhelpers2.html import tags, HTML from tailbone import forms2 as forms, grids from tailbone.db import Session -from tailbone.views import MasterView4 as MasterView, AutocompleteView +from tailbone.views import MasterView, AutocompleteView from tailbone.progress import SessionProgress from tailbone.util import raw_datetime diff --git a/tailbone/views/purchases/core.py b/tailbone/views/purchases/core.py index c8d0ddc8..d214b53a 100644 --- a/tailbone/views/purchases/core.py +++ b/tailbone/views/purchases/core.py @@ -31,7 +31,7 @@ from rattail.db import model from webhelpers2.html import HTML, tags from tailbone.db import Session -from tailbone.views import MasterView4 as MasterView +from tailbone.views import MasterView class PurchaseView(MasterView): diff --git a/tailbone/views/purchases/credits.py b/tailbone/views/purchases/credits.py index 2188df3d..0d4b46ad 100644 --- a/tailbone/views/purchases/credits.py +++ b/tailbone/views/purchases/credits.py @@ -31,7 +31,7 @@ from rattail.db import model from webhelpers2.html import tags from tailbone import grids -from tailbone.views import MasterView4 as MasterView +from tailbone.views import MasterView class PurchaseCreditView(MasterView): diff --git a/tailbone/views/purchasing/batch.py b/tailbone/views/purchasing/batch.py index 6a41cdca..d6699bd4 100644 --- a/tailbone/views/purchasing/batch.py +++ b/tailbone/views/purchasing/batch.py @@ -38,7 +38,7 @@ from webhelpers2.html import tags # from tailbone import forms from tailbone import forms2 -from tailbone.views.batch import BatchMasterView4 as BatchMasterView +from tailbone.views.batch import BatchMasterView class PurchasingBatchView(BatchMasterView): diff --git a/tailbone/views/reportcodes.py b/tailbone/views/reportcodes.py index db82a743..b50d138f 100644 --- a/tailbone/views/reportcodes.py +++ b/tailbone/views/reportcodes.py @@ -28,7 +28,7 @@ from __future__ import unicode_literals, absolute_import from rattail.db import model -from tailbone.views import MasterView4 as MasterView +from tailbone.views import MasterView class ReportCodesView(MasterView): diff --git a/tailbone/views/settings.py b/tailbone/views/settings.py index 5eff7d34..508fd9c6 100644 --- a/tailbone/views/settings.py +++ b/tailbone/views/settings.py @@ -32,7 +32,7 @@ from rattail.db import model import colander -from tailbone.views import MasterView4 as MasterView +from tailbone.views import MasterView class SettingsView(MasterView): diff --git a/tailbone/views/shifts/core.py b/tailbone/views/shifts/core.py index 0a15aa05..bb343143 100644 --- a/tailbone/views/shifts/core.py +++ b/tailbone/views/shifts/core.py @@ -33,7 +33,7 @@ import humanize from rattail.db import model from rattail.time import localtime -from tailbone.views import MasterView4 as MasterView +from tailbone.views import MasterView def render_shift_length(shift, field): diff --git a/tailbone/views/stores.py b/tailbone/views/stores.py index 7fd989b4..660e14e3 100644 --- a/tailbone/views/stores.py +++ b/tailbone/views/stores.py @@ -32,7 +32,7 @@ from rattail.db import model import colander -from tailbone.views import MasterView4 as MasterView +from tailbone.views import MasterView class StoresView(MasterView): diff --git a/tailbone/views/subdepartments.py b/tailbone/views/subdepartments.py index 31b6a43d..4550999f 100644 --- a/tailbone/views/subdepartments.py +++ b/tailbone/views/subdepartments.py @@ -29,7 +29,7 @@ from __future__ import unicode_literals, absolute_import from rattail.db import model from tailbone.db import Session -from tailbone.views import MasterView4 as MasterView +from tailbone.views import MasterView class SubdepartmentsView(MasterView): diff --git a/tailbone/views/tables.py b/tailbone/views/tables.py index e8ddfc77..3c373de1 100644 --- a/tailbone/views/tables.py +++ b/tailbone/views/tables.py @@ -26,7 +26,7 @@ Views with info about the underlying Rattail tables from __future__ import unicode_literals, absolute_import -from tailbone.views import MasterView4 as MasterView +from tailbone.views import MasterView class TablesView(MasterView): diff --git a/tailbone/views/taxes.py b/tailbone/views/taxes.py index 4a26dd35..99177dea 100644 --- a/tailbone/views/taxes.py +++ b/tailbone/views/taxes.py @@ -28,7 +28,7 @@ from __future__ import unicode_literals, absolute_import from rattail.db import model -from tailbone.views import MasterView4 as MasterView +from tailbone.views import MasterView class TaxesView(MasterView): diff --git a/tailbone/views/tempmon/core.py b/tailbone/views/tempmon/core.py index 11a8cb55..6b85944e 100644 --- a/tailbone/views/tempmon/core.py +++ b/tailbone/views/tempmon/core.py @@ -32,7 +32,7 @@ from tailbone import views from tailbone.db import TempmonSession -class MasterView(views.MasterView4): +class MasterView(views.MasterView): """ Base class for tempmon views. """ diff --git a/tailbone/views/trainwreck.py b/tailbone/views/trainwreck.py index 6c851524..2c8c2c62 100644 --- a/tailbone/views/trainwreck.py +++ b/tailbone/views/trainwreck.py @@ -31,7 +31,7 @@ import six from rattail.time import localtime from tailbone.db import TrainwreckSession -from tailbone.views import MasterView4 as MasterView +from tailbone.views import MasterView class TransactionView(MasterView): diff --git a/tailbone/views/upgrades.py b/tailbone/views/upgrades.py index 13447ff7..733be540 100644 --- a/tailbone/views/upgrades.py +++ b/tailbone/views/upgrades.py @@ -43,7 +43,7 @@ from rattail.upgrades import get_upgrade_handler from deform import widget as dfwidget from webhelpers2.html import tags, HTML -from tailbone.views import MasterView4 as MasterView +from tailbone.views import MasterView from tailbone.progress import SessionProgress, get_progress_session diff --git a/tailbone/views/users.py b/tailbone/views/users.py index 48e05315..0e00e5f2 100644 --- a/tailbone/views/users.py +++ b/tailbone/views/users.py @@ -40,7 +40,7 @@ from webhelpers2.html import HTML, tags from tailbone import forms2 as forms from tailbone.db import Session -from tailbone.views import MasterView4 as MasterView +from tailbone.views import MasterView from tailbone.views.principal import PrincipalMasterView, PermissionsRenderer diff --git a/tailbone/views/vendors/catalogs.py b/tailbone/views/vendors/catalogs.py index 16e8417a..a09110cf 100644 --- a/tailbone/views/vendors/catalogs.py +++ b/tailbone/views/vendors/catalogs.py @@ -39,7 +39,7 @@ from webhelpers2.html import tags from tailbone import forms2 as forms from tailbone.db import Session -from tailbone.views.batch import FileBatchMasterView4 as FileBatchMasterView +from tailbone.views.batch import FileBatchMasterView log = logging.getLogger(__name__) diff --git a/tailbone/views/vendors/core.py b/tailbone/views/vendors/core.py index c36bcb25..60e43682 100644 --- a/tailbone/views/vendors/core.py +++ b/tailbone/views/vendors/core.py @@ -32,7 +32,7 @@ from rattail.db import model from webhelpers2.html import tags -from tailbone.views import MasterView4 as MasterView, AutocompleteView +from tailbone.views import MasterView, AutocompleteView class VendorsView(MasterView): diff --git a/tailbone/views/vendors/invoices.py b/tailbone/views/vendors/invoices.py index ad7dee11..dfc9f78b 100644 --- a/tailbone/views/vendors/invoices.py +++ b/tailbone/views/vendors/invoices.py @@ -33,7 +33,7 @@ from rattail.vendors.invoices import iter_invoice_parsers, require_invoice_parse import colander from deform import widget as dfwidget -from tailbone.views.batch import FileBatchMasterView4 as FileBatchMasterView +from tailbone.views.batch import FileBatchMasterView class VendorInvoicesView(FileBatchMasterView):