Add initial versioning support with SQLAlchemy-Continuum.

This commit is contained in:
Lance Edgar 2015-02-09 15:33:16 -06:00
parent 41dd2ef17b
commit def466935b
43 changed files with 717 additions and 26 deletions

View file

@ -26,17 +26,124 @@
Database Stuff
"""
import sqlalchemy as sa
from zope.sqlalchemy import datamanager
import sqlalchemy_continuum as continuum
from sqlalchemy.orm import sessionmaker, scoped_session
Session = scoped_session(sessionmaker())
from rattail.db import SessionBase
from rattail.db import model
try:
# Requires zope.sqlalchemy >= 0.7.4
from zope.sqlalchemy import register
except ImportError:
from zope.sqlalchemy import ZopeTransactionExtension
Session.configure(extension=ZopeTransactionExtension())
else:
Session = scoped_session(sessionmaker(class_=SessionBase))
class TailboneSessionDataManager(datamanager.SessionDataManager):
"""Integrate a top level sqlalchemy session transaction into a zope transaction
One phase variant.
.. note::
This class appears to be necessary in order for the Continuum
integration to work alongside the Zope transaction integration.
"""
def tpc_vote(self, trans):
# for a one phase data manager commit last in tpc_vote
if self.tx is not None: # there may have been no work to do
# Force creation of Continuum versions for current session.
mgr = continuum.get_versioning_manager(model.Product) # any ol' model will do
uow = mgr.unit_of_work(self.session)
uow.make_versions(self.session)
self.tx.commit()
self._finish('committed')
def join_transaction(session, initial_state=datamanager.STATUS_ACTIVE, transaction_manager=datamanager.zope_transaction.manager, keep_session=False):
"""Join a session to a transaction using the appropriate datamanager.
It is safe to call this multiple times, if the session is already joined
then it just returns.
`initial_state` is either STATUS_ACTIVE, STATUS_INVALIDATED or STATUS_READONLY
If using the default initial status of STATUS_ACTIVE, you must ensure that
mark_changed(session) is called when data is written to the database.
The ZopeTransactionExtesion SessionExtension can be used to ensure that this is
called automatically after session write operations.
.. note::
This function is copied from upstream, and tweaked so that our custom
:class:`TailboneSessionDataManager` will be used.
"""
if datamanager._SESSION_STATE.get(id(session), None) is None:
if session.twophase:
DataManager = datamanager.TwoPhaseSessionDataManager
else:
DataManager = TailboneSessionDataManager
DataManager(session, initial_state, transaction_manager, keep_session=keep_session)
class ZopeTransactionExtension(datamanager.ZopeTransactionExtension):
"""Record that a flush has occurred on a session's connection. This allows
the DataManager to rollback rather than commit on read only transactions.
.. note::
This class is copied from upstream, and tweaked so that our custom
:func:`join_transaction()` will be used.
"""
def after_begin(self, session, transaction, connection):
join_transaction(session, self.initial_state, self.transaction_manager, self.keep_session)
def after_attach(self, session, instance):
join_transaction(session, self.initial_state, self.transaction_manager, self.keep_session)
def register(session, initial_state=datamanager.STATUS_ACTIVE,
transaction_manager=datamanager.zope_transaction.manager, keep_session=False):
"""Register ZopeTransaction listener events on the
given Session or Session factory/class.
This function requires at least SQLAlchemy 0.7 and makes use
of the newer sqlalchemy.event package in order to register event listeners
on the given Session.
The session argument here may be a Session class or subclass, a
sessionmaker or scoped_session instance, or a specific Session instance.
Event listening will be specific to the scope of the type of argument
passed, including specificity to its subclass as well as its identity.
.. note::
This function is copied from upstream, and tweaked so that our custom
:class:`ZopeTransactionExtension` will be used.
"""
from sqlalchemy import __version__
assert tuple(int(x) for x in __version__.split(".")) >= (0, 7), \
"SQLAlchemy version 0.7 or greater required to use register()"
from sqlalchemy import event
ext = ZopeTransactionExtension(
initial_state=initial_state,
transaction_manager=transaction_manager,
keep_session=keep_session,
)
event.listen(session, "after_begin", ext.after_begin)
event.listen(session, "after_attach", ext.after_attach)
event.listen(session, "after_flush", ext.after_flush)
event.listen(session, "after_bulk_update", ext.after_bulk_update)
event.listen(session, "after_bulk_delete", ext.after_bulk_delete)
event.listen(session, "before_commit", ext.before_commit)
# TODO: We can probably assume a new SA version since we use Continuum now.
if tuple(int(x) for x in sa.__version__.split('.')) >= (0, 7):
register(Session)
else:
Session.configure(extension=ZopeTransactionExtension())

View file

@ -97,6 +97,8 @@ def context_found(event):
uuid = authenticated_userid(request)
if uuid:
request.user = Session.query(User).get(uuid)
if request.user:
Session().set_continuum_user(request.user)
def has_perm(perm):
return has_permission(Session(), request.user, perm)

View file

@ -8,6 +8,9 @@
% elif form.updating:
<li>${h.link_to("View this Brand", url('brand.read', uuid=form.fieldset.model.uuid))}</li>
% endif
% if not form.creating and request.has_perm('brand.versions.view'):
<li>${h.link_to("View Change History ({0})".format(version_count), url('brand.versions', uuid=form.fieldset.model.uuid))}</li>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,3 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/index.mako" />
${parent.body()}

View file

@ -0,0 +1,3 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/view.mako" />
${parent.body()}

View file

@ -8,6 +8,9 @@
% elif form.updating:
<li>${h.link_to("View this Category", url('category.read', uuid=form.fieldset.model.uuid))}</li>
% endif
% if not form.creating and request.has_perm('category.versions.view'):
<li>${h.link_to("View Change History ({0})".format(version_count), url('category.versions', uuid=form.fieldset.model.uuid))}</li>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,3 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/index.mako" />
${parent.body()}

View file

@ -0,0 +1,3 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/view.mako" />
${parent.body()}

View file

@ -8,6 +8,9 @@
% elif form.updating:
<li>${h.link_to("View this Department", url('department.read', uuid=form.fieldset.model.uuid))}</li>
% endif
% if not form.creating and request.has_perm('department.versions.view'):
<li>${h.link_to("View Change History ({0})".format(version_count), url('department.versions', uuid=form.fieldset.model.uuid))}</li>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,3 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/index.mako" />
${parent.body()}

View file

@ -0,0 +1,3 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/view.mako" />
${parent.body()}

View file

@ -11,6 +11,9 @@
<li>${h.link_to("Edit Printer Settings", url('label_profile.printer_settings', uuid=profile.uuid))}</li>
% endif
% endif
% if not form.creating and request.has_perm('labelprofile.versions.view'):
<li>${h.link_to("View Change History ({0})".format(version_count), url('labelprofile.versions', uuid=form.fieldset.model.uuid))}</li>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,3 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/index.mako" />
${parent.body()}

View file

@ -0,0 +1,3 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/view.mako" />
${parent.body()}

View file

@ -8,6 +8,9 @@
% elif form.updating:
<li>${h.link_to("View this Product", url('product.read', uuid=form.fieldset.model.uuid))}</li>
% endif
% if not form.creating and request.has_perm('product.versions.view'):
<li>${h.link_to("View Change History ({0})".format(version_count), url('product.versions', uuid=form.fieldset.model.uuid))}</li>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,3 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/index.mako" />
${parent.body()}

View file

@ -0,0 +1,3 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/view.mako" />
${parent.body()}

View file

@ -14,6 +14,9 @@
<li>${h.link_to("View this Role", url('role.read', uuid=form.fieldset.model.uuid))}</li>
% endif
<li>${h.link_to("Delete this Role", url('role.delete', uuid=form.fieldset.model.uuid), class_='delete')}</li>
% if not form.creating and request.has_perm('role.versions.view'):
<li>${h.link_to("View Change History ({0})".format(version_count), url('role.versions', uuid=form.fieldset.model.uuid))}</li>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,3 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/index.mako" />
${parent.body()}

View file

@ -0,0 +1,3 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/view.mako" />
${parent.body()}

View file

@ -8,6 +8,9 @@
% elif form.updating:
<li>${h.link_to("View this Subdepartment", url('subdepartment.read', uuid=form.fieldset.model.uuid))}</li>
% endif
% if not form.creating and request.has_perm('subdepartment.versions.view'):
<li>${h.link_to("View Change History ({0})".format(version_count), url('subdepartment.versions', uuid=form.fieldset.model.uuid))}</li>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,3 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/index.mako" />
${parent.body()}

View file

@ -0,0 +1,3 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/view.mako" />
${parent.body()}

View file

@ -8,6 +8,9 @@
% elif form.updating:
<li>${h.link_to("View this User", url('user.read', uuid=form.fieldset.model.uuid))}</li>
% endif
% if not form.creating and request.has_perm('user.versions.view'):
<li>${h.link_to("View Change History ({0})".format(version_count), url('user.versions', uuid=form.fieldset.model.uuid))}</li>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,3 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/index.mako" />
${parent.body()}

View file

@ -0,0 +1,3 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/view.mako" />
${parent.body()}

View file

@ -8,6 +8,9 @@
% elif form.updating:
<li>${h.link_to("View this Vendor", url('vendor.read', uuid=form.fieldset.model.uuid))}</li>
% endif
% if not form.creating and request.has_perm('vendor.versions.view'):
<li>${h.link_to("View Change History ({0})".format(version_count), url('vendor.versions', uuid=form.fieldset.model.uuid))}</li>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,3 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/index.mako" />
${parent.body()}

View file

@ -0,0 +1,3 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/view.mako" />
${parent.body()}

View file

@ -0,0 +1,15 @@
## -*- coding: utf-8 -*-
<%inherit file="/grid.mako" />
<%def name="title()">${model_title} Change History</%def>
<%def name="context_menu_items()">
<li>${h.link_to("Back to all {0}".format(model_title_plural), url(route_model_list))}</li>
<li>${h.link_to("Back to current {0}".format(model_title), url(route_model_view, uuid=model_instance.uuid))}</li>
</%def>
<%def name="form()">
<h2>Changes for ${model_title}:&nbsp; ${model_instance}</h2>
</%def>
${parent.body()}

View file

@ -0,0 +1,103 @@
## -*- coding: utf-8 -*-
<%inherit file="/base.mako" />
<%def name="title()">${model_title} Version Details</%def>
<%def name="head_tags()">
<style type="text/css">
td.oldvalue {
background-color: #fcc;
}
td.newvalue {
background-color: #cfc;
}
</style>
</%def>
<%def name="context_menu_items()">
<li>${h.link_to("Back to all {0}".format(model_title_plural), url(route_model_list))}</li>
<li>${h.link_to("Back to current {0}".format(model_title), url(route_model_view, uuid=model_instance.uuid))}</li>
<li>${h.link_to("Back to Version History", url('{0}.versions'.format(route_prefix), uuid=model_instance.uuid))}</li>
</%def>
<div class="form-wrapper">
<ul class="context-menu">
${self.context_menu_items()}
</ul>
<div class="form">
<div>
% if previous_transaction or next_transaction:
% if previous_transaction:
${h.link_to("<< older version", url('{0}.version'.format(route_prefix), uuid=model_instance.uuid, transaction_id=previous_transaction.id))}
% else:
<span>(oldest version)</span>
% endif
&nbsp; | &nbsp;
% if next_transaction:
${h.link_to("newer version >>", url('{0}.version'.format(route_prefix), uuid=model_instance.uuid, transaction_id=next_transaction.id))}
% else:
<span>(newest version)</span>
% endif
% else:
<span>(only version)</span>
% endif
</div>
<div class="fieldset">
<div class="field-wrapper">
<label>When:</label>
<div class="field">${h.pretty_datetime(request.rattail_config, transaction.issued_at)}</div>
</div>
<div class="field-wrapper">
<label>Who:</label>
<div class="field">${transaction.user or "(unknown / system)"}</div>
</div>
<div class="field-wrapper">
<label>Where:</label>
<div class="field">${transaction.remote_addr}</div>
</div>
% for ver in versions:
<div class="field-wrapper">
<label>What:</label>
<div class="field" style="font-weight: bold;">${ver.version_parent.__class__.__name__}:&nbsp; ${ver.version_parent}</div>
</div>
<div class="field-wrapper">
<label>Changes:</label>
<div class="field">
<div class="grid">
<table>
<thead>
<tr>
<th>Field</th>
<th>Old Value</th>
<th>New Value</th>
</tr>
</thead>
<tbody>
% for key in sorted(ver.changeset):
<tr>
<td>${key}</td>
<td class="oldvalue">${ver.changeset[key][0]}</td>
<td class="newvalue">${ver.changeset[key][1]}</td>
</tr>
% endfor
</tbody>
</table>
</div>
</div>
</div>
% endfor
</div>
</div>
</div>

View file

@ -1,9 +1,8 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
# Copyright © 2010-2015 Lance Edgar
#
# This file is part of Rattail.
#
@ -21,15 +20,18 @@
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Brand Views
"""
from . import SearchableAlchemyGridView, CrudView, AutocompleteView
from __future__ import unicode_literals
from rattail.db import model
from rattail.db.model import Brand
from . import SearchableAlchemyGridView, CrudView, AutocompleteView
from .continuum import VersionView, version_defaults
class BrandsGrid(SearchableAlchemyGridView):
@ -81,6 +83,14 @@ class BrandCrud(CrudView):
return fs
class BrandVersionView(VersionView):
"""
View which shows version history for a brand.
"""
parent_class = model.Brand
route_model_view = 'brand.read'
class BrandsAutocomplete(AutocompleteView):
mapped_class = Brand
@ -122,3 +132,5 @@ def includeme(config):
config.add_view(BrandCrud, attr='delete',
route_name='brand.delete',
permission='brands.delete')
version_defaults(config, BrandVersionView, 'brand')

View file

@ -26,9 +26,11 @@ Category Views
from __future__ import unicode_literals
from rattail.db import model
from rattail.db.model import Category
from . import SearchableAlchemyGridView, CrudView
from .continuum import VersionView, version_defaults
class CategoriesGrid(SearchableAlchemyGridView):
@ -85,6 +87,16 @@ class CategoryCrud(CrudView):
return fs
class CategoryVersionView(VersionView):
"""
View which shows version history for a category.
"""
parent_class = model.Category
model_title_plural = "Categories"
route_model_list = 'categories'
route_model_view = 'category.read'
def add_routes(config):
config.add_route('categories', '/categories')
config.add_route('category.create', '/categories/new')
@ -109,3 +121,5 @@ def includeme(config):
renderer='/categories/crud.mako', permission='categories.update')
config.add_view(CategoryCrud, attr='delete', route_name='category.delete',
permission='categories.delete')
version_defaults(config, CategoryVersionView, 'category', template_prefix='/categories')

231
tailbone/views/continuum.py Normal file
View file

@ -0,0 +1,231 @@
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2015 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 Affero 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 Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Continuum Version Views
"""
from __future__ import unicode_literals
import sqlalchemy as sa
import sqlalchemy_continuum as continuum
from rattail.db import model
from rattail.db.model.continuum import model_transaction_query
import formalchemy
from pyramid.httpexceptions import HTTPNotFound
from tailbone.db import Session
from tailbone.views import PagedAlchemyGridView, View
from tailbone.forms import DateTimeFieldRenderer
class VersionView(PagedAlchemyGridView):
"""
View which shows version history for a model instance.
"""
@property
def parent_class(self):
"""
Model class which is "parent" to the version class.
"""
raise NotImplementedError("Please set `parent_class` on your `VersionView` subclass.")
@property
def child_classes(self):
"""
Model class(es) which are "children" to the version's parent class.
"""
return []
@property
def model_title(self):
"""
Human-friendly title for the parent model class.
"""
return self.parent_class.__name__
@property
def model_title_plural(self):
"""
Plural version of the human-friendly title for the parent model class.
"""
return '{0}s'.format(self.model_title)
@property
def prefix(self):
return self.parent_class.__name__.lower()
@property
def config_prefix(self):
return self.prefix
@property
def transaction_class(self):
return continuum.transaction_class(self.parent_class)
@property
def mapped_class(self):
return self.transaction_class
@property
def version_class(self):
return continuum.version_class(self.parent_class)
@property
def route_model_list(self):
return '{0}s'.format(self.prefix)
@property
def route_model_view(self):
return self.prefix
def join_map(self):
return {
'user':
lambda q: q.outerjoin(model.User, self.transaction_class.user_uuid == model.User.uuid),
}
def sort_config(self):
return self.make_sort_config(sort='issued_at', dir='desc')
def sort_map(self):
return self.make_sort_map('issued_at', 'remote_addr',
user=self.sorter(model.User.username))
def transaction_query(self, session=Session):
uuid = self.request.matchdict['uuid']
return model_transaction_query(session, uuid, self.parent_class,
child_classes=self.child_classes)
def make_query(self, session=Session):
query = self.transaction_query(session)
return self.modify_query(query)
def grid(self):
g = self.make_grid()
g.issued_at.set(renderer=DateTimeFieldRenderer(self.request.rattail_config))
g.configure(
include=[
g.issued_at.label("When"),
g.user.label("Who"),
g.remote_addr.label("Client IP"),
],
readonly=True)
g.viewable = True
g.view_route_name = '{0}.version'.format(self.prefix)
g.view_route_kwargs = self.view_route_kwargs
return g
def render_kwargs(self):
instance = Session.query(self.parent_class).get(self.request.matchdict['uuid'])
return {'model_title': self.model_title,
'model_title_plural': self.model_title_plural,
'model_instance': instance,
'route_model_list': self.route_model_list,
'route_model_view': self.route_model_view}
def view_route_kwargs(self, transaction):
return {'uuid': self.request.matchdict['uuid'],
'transaction_id': transaction.id}
def list(self):
"""
View which shows the version history list for a model instance.
"""
return self()
def details(self):
"""
View which shows the change details of a model version.
"""
kwargs = self.render_kwargs()
uuid = self.request.matchdict['uuid']
transaction_id = self.request.matchdict['transaction_id']
transaction = Session.query(self.transaction_class).get(transaction_id)
if not transaction:
raise HTTPNotFound
version = Session.query(self.version_class).get((uuid, transaction_id))
def normalize_child_classes():
classes = []
for cls in self.child_classes:
if not isinstance(cls, tuple):
cls = (cls, 'uuid')
classes.append(cls)
return classes
versions = []
if version:
versions.append(version)
for model_class, attr in normalize_child_classes():
if isinstance(model_class, type) and issubclass(model_class, model.Base):
cls = continuum.version_class(model_class)
ver = Session.query(cls).filter_by(transaction_id=transaction_id, **{attr: uuid}).first()
if ver:
versions.append(ver)
previous_transaction = self.transaction_query()\
.order_by(self.transaction_class.id.desc())\
.filter(self.transaction_class.id < transaction.id)\
.first()
next_transaction = self.transaction_query()\
.order_by(self.transaction_class.id.asc())\
.filter(self.transaction_class.id > transaction.id)\
.first()
kwargs.update({
'route_prefix': self.prefix,
'version': version,
'transaction': transaction,
'versions': versions,
'parent_class': continuum.parent_class,
'previous_transaction': previous_transaction,
'next_transaction': next_transaction,
})
return kwargs
def version_defaults(config, VersionView, prefix, template_prefix=None):
"""
Apply default route/view configuration for the given ``VersionView``.
"""
if template_prefix is None:
template_prefix = '/{0}s'.format(prefix)
template_prefix = template_prefix.rstrip('/')
# list changesets
config.add_route('{0}.versions'.format(prefix), '/{0}/{{uuid}}/changesets/'.format(prefix))
config.add_view(VersionView, attr='list', route_name='{0}.versions'.format(prefix),
renderer='{0}/versions/index.mako'.format(template_prefix),
permission='{0}.versions.view'.format(prefix))
# view changeset
config.add_route('{0}.version'.format(prefix), '/{0}/{{uuid}}/changeset/{{transaction_id}}'.format(prefix))
config.add_view(VersionView, attr='details', route_name='{0}.version'.format(prefix),
renderer='{0}/versions/view.mako'.format(template_prefix),
permission='{0}.versions.view'.format(prefix))

View file

@ -32,16 +32,23 @@ except ImportError:
inspect = None
from sqlalchemy.orm import class_mapper
import sqlalchemy as sa
from sqlalchemy_continuum import transaction_class, version_class
from rattail.db import model
from rattail.db.model.continuum import count_versions, model_transaction_query
from pyramid.httpexceptions import HTTPFound, HTTPNotFound
from .core import View
from ..forms import AlchemyForm
from formalchemy import FieldSet
from ..db import Session
from edbob.util import prettify
from tailbone.db import Session
__all__ = ['CrudView']
@ -51,6 +58,7 @@ class CrudView(View):
readonly = False
allow_successive_creates = False
update_cancel_route = None
child_version_classes = []
@property
def mapped_class(self):
@ -161,7 +169,21 @@ class CrudView(View):
pass
def template_kwargs(self, form):
return {}
if form.creating:
return {}
return {'version_count': self.count_versions()}
def count_versions(self):
query = self.transaction_query()
return query.count()
def transaction_query(self, parent_class=None, child_classes=None):
uuid = self.request.matchdict['uuid']
if parent_class is None:
parent_class = self.mapped_class
if child_classes is None:
child_classes = self.child_version_classes
return model_transaction_query(Session, uuid, parent_class, child_classes=child_classes)
def post_save(self, form):
pass

View file

@ -26,9 +26,11 @@ Department Views
from __future__ import unicode_literals
from rattail.db import model
from rattail.db.model import Department, Product, ProductCost, Vendor
from . import SearchableAlchemyGridView, CrudView, AlchemyGridView, AutocompleteView
from .continuum import VersionView, version_defaults
class DepartmentsGrid(SearchableAlchemyGridView):
@ -83,6 +85,14 @@ class DepartmentCrud(CrudView):
return fs
class DepartmentVersionView(VersionView):
"""
View which shows version history for a department.
"""
parent_class = model.Department
route_model_view = 'department.read'
class DepartmentsByVendorGrid(AlchemyGridView):
mapped_class = Department
@ -150,3 +160,5 @@ def includeme(config):
renderer='/departments/crud.mako', permission='departments.update')
config.add_view(DepartmentCrud, attr='delete', route_name='department.delete',
permission='departments.delete')
version_defaults(config, DepartmentVersionView, 'department')

View file

@ -26,6 +26,9 @@ Label Views
from __future__ import unicode_literals
from rattail.db import model
from rattail.db.model import LabelProfile
from pyramid.httpexceptions import HTTPFound
import formalchemy
@ -36,7 +39,7 @@ from ..db import Session
from . import SearchableAlchemyGridView, CrudView
from ..grids.search import BooleanSearchFilter
from rattail.db.model import LabelProfile
from .continuum import VersionView, version_defaults
class ProfilesGrid(SearchableAlchemyGridView):
@ -129,6 +132,16 @@ class ProfileCrud(CrudView):
uuid=form.fieldset.model.uuid)
class LabelProfileVersionView(VersionView):
"""
View which shows version history for a label profile.
"""
parent_class = model.LabelProfile
model_title = "Label Profile"
route_model_list = 'label_profiles'
route_model_view = 'label_profile.read'
def printer_settings(request):
uuid = request.matchdict['uuid']
profile = Session.query(LabelProfile).get(uuid) if uuid else None
@ -187,3 +200,5 @@ def includeme(config):
config.add_view(printer_settings, route_name='label_profile.printer_settings',
renderer='/labels/profiles/printer.mako',
permission='label_profiles.update')
version_defaults(config, LabelProfileVersionView, 'labelprofile', template_prefix='/labels/profiles')

View file

@ -56,6 +56,7 @@ from rattail.pod import get_image_url, get_image_path
from ..db import Session
from ..forms import GPCFieldRenderer, BrandFieldRenderer, PriceFieldRenderer
from . import CrudView
from .continuum import VersionView, version_defaults
from ..progress import SessionProgress
@ -237,9 +238,16 @@ class ProductsGrid(SearchableAlchemyGridView):
class ProductCrud(CrudView):
"""
Product CRUD view class.
"""
mapped_class = Product
home_route = 'products'
child_version_classes = [
(model.ProductCode, 'product_uuid'),
(model.ProductCost, 'product_uuid'),
(model.ProductPrice, 'product_uuid'),
]
def get_model(self, key):
model = super(ProductCrud, self).get_model(key)
@ -277,7 +285,8 @@ class ProductCrud(CrudView):
return fs
def template_kwargs(self, form):
kwargs = {'image': False}
kwargs = super(ProductCrud, self).template_kwargs(form)
kwargs['image'] = False
product = form.fieldset.model
if product.upc:
kwargs['image_url'] = get_image_url(
@ -289,6 +298,19 @@ class ProductCrud(CrudView):
return kwargs
class ProductVersionView(VersionView):
"""
View which shows version history for a product.
"""
parent_class = model.Product
route_model_view = 'product.read'
child_classes = [
(model.ProductCode, 'product_uuid'),
(model.ProductCost, 'product_uuid'),
(model.ProductPrice, 'product_uuid'),
]
def products_search(request):
"""
Locate a product(s) by UPC.
@ -443,3 +465,5 @@ def includeme(config):
permission='products.delete')
config.add_view(products_search, route_name='products.search',
renderer='json', permission='products.list')
version_defaults(config, ProductVersionView, 'product')

View file

@ -26,6 +26,8 @@
Role Views
"""
from rattail.db import model
from . import SearchableAlchemyGridView, CrudView
from pyramid.httpexceptions import HTTPFound
@ -37,6 +39,8 @@ import formalchemy
from webhelpers.html import tags
from webhelpers.html import HTML
from .continuum import VersionView, version_defaults
default_permissions = [
("Batches", [
@ -264,6 +268,14 @@ class RoleCrud(CrudView):
return HTTPFound(location=self.request.get_referrer())
class RoleVersionView(VersionView):
"""
View which shows version history for a role.
"""
parent_class = model.Role
route_model_view = 'role.read'
def includeme(config):
config.add_route('roles', '/roles')
@ -294,3 +306,5 @@ def includeme(config):
config.add_route('role.delete', '/roles/{uuid}/delete')
config.add_view(RoleCrud, attr='delete', route_name='role.delete',
permission='roles.delete')
version_defaults(config, RoleVersionView, 'role')

View file

@ -26,9 +26,11 @@ Subdepartment Views
from __future__ import unicode_literals
from rattail.db import model
from rattail.db.model import Subdepartment
from . import SearchableAlchemyGridView, CrudView
from .continuum import VersionView, version_defaults
class SubdepartmentsGrid(SearchableAlchemyGridView):
@ -85,6 +87,14 @@ class SubdepartmentCrud(CrudView):
return fs
class SubdepartmentVersionView(VersionView):
"""
View which shows version history for a subdepartment.
"""
parent_class = model.Subdepartment
route_model_view = 'subdepartment.read'
def add_routes(config):
config.add_route('subdepartments', '/subdepartments')
config.add_route('subdepartment.create', '/subdepartments/new')
@ -109,3 +119,5 @@ def includeme(config):
renderer='/subdepartments/crud.mako', permission='subdepartments.update')
config.add_view(SubdepartmentCrud, attr='delete', route_name='subdepartment.delete',
permission='subdepartments.delete')
version_defaults(config, SubdepartmentVersionView, 'subdepartment')

View file

@ -26,6 +26,7 @@ User Views
from __future__ import unicode_literals
from rattail.db import model
from rattail.db.model import User, Person, Role
from rattail.db.auth import guest_role, set_user_password
@ -40,6 +41,8 @@ from ..forms import PersonFieldLinkRenderer
from ..db import Session
from tailbone.grids.search import BooleanSearchFilter
from .continuum import VersionView, version_defaults
class UsersGrid(SearchableAlchemyGridView):
@ -205,6 +208,14 @@ class UserCrud(CrudView):
return fs
class UserVersionView(VersionView):
"""
View which shows version history for a user.
"""
parent_class = model.User
route_model_view = 'user.read'
def add_routes(config):
config.add_route(u'users', u'/users')
config.add_route(u'user.create', u'/users/new')
@ -233,3 +244,5 @@ def includeme(config):
permission='users.update')
config.add_view(UserCrud, attr='delete', route_name='user.delete',
permission='users.delete')
version_defaults(config, UserVersionView, 'user')

View file

@ -26,7 +26,8 @@ Views pertaining to vendors
from __future__ import unicode_literals
from .core import VendorsGrid, VendorCrud, VendorsAutocomplete, add_routes
from .core import (VendorsGrid, VendorCrud, VendorVersionView,
VendorsAutocomplete, add_routes)
def includeme(config):

View file

@ -1,9 +1,8 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
# Copyright © 2010-2015 Lance Edgar
#
# This file is part of Rattail.
#
@ -21,15 +20,19 @@
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Vendor Views
"""
from tailbone.views import SearchableAlchemyGridView, CrudView, AutocompleteView
from tailbone.forms import AssociationProxyField, PersonFieldRenderer
from __future__ import unicode_literals
from rattail.db import model
from rattail.db.model import Vendor
from tailbone.views import SearchableAlchemyGridView, CrudView, AutocompleteView
from tailbone.views.continuum import VersionView, version_defaults
from tailbone.forms import AssociationProxyField, PersonFieldRenderer
class VendorsGrid(SearchableAlchemyGridView):
@ -93,6 +96,14 @@ class VendorCrud(CrudView):
return fs
class VendorVersionView(VersionView):
"""
View which shows version history for a vendor.
"""
parent_class = model.Vendor
route_model_view = 'vendor.read'
class VendorsAutocomplete(AutocompleteView):
mapped_class = Vendor
@ -127,3 +138,5 @@ def includeme(config):
permission='vendors.update')
config.add_view(VendorCrud, attr='delete', route_name='vendor.delete',
permission='vendors.delete')
version_defaults(config, VendorVersionView, 'vendor')