# -*- 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 . # ################################################################################ """ 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.continuum import model_transaction_query import formalchemy from pyramid.httpexceptions import HTTPNotFound from tailbone.db import Session from tailbone.views import PagedAlchemyGridView, View 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.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))