diff --git a/docs/index.txt b/docs/index.txt index e6fde30..4ecba34 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -7,6 +7,13 @@ Welcome to pyramid_formalchemy's documentation! =============================================== +About +===== + +``pyramid_formalchemy`` provide a CRUD interface for ``pyramid`` based on ``FormAlchemy`` + +It also allow to use ``FormAlchemy`` to render forms in your application. + Installation ============ @@ -46,11 +53,11 @@ load ``package.models`` ``package.models.DBSession`` and ``package.forms`` but you can override this. In this case you don't need to specify a package:: - pyramid_formalchemy.configure(config, - models='formalchemy_project.mymodels', - session_factory='formalchemy_project.session.Session', - forms='formalchemy_project', - use_jquery=True) + config.formalchemy_admin(config, + models='formalchemy_project.mymodels', + session_factory='formalchemy_project.session.Session', + forms='formalchemy_project', + use_jquery=True) You can also change the path ``prefix`` used. pyramid_formalchemy will use ``/admin/`` by default. See :func:`~pyramid_formalchemy.configure`. diff --git a/pyramid_formalchemy/__init__.py b/pyramid_formalchemy/__init__.py index abafd73..37c76a1 100644 --- a/pyramid_formalchemy/__init__.py +++ b/pyramid_formalchemy/__init__.py @@ -10,16 +10,17 @@ def includeme(config): def formalchemy_model(config, route_name, factory='pyramid_formalchemy.resources.ModelListing', - view='pyramid_formalchemy.views.ModelView', - package=None, model=None, forms=None, session_factory=None, **kwargs): + view='pyramid_formalchemy.views.ModelView', model=None, **kwargs): model = config.maybe_dotted(model) - return formalchemy_admin(config, route_name, factory=factory, view=view, package=package, - models=[model], model=model, forms=forms, session_factory=session_factory, **kwargs) + return formalchemy_admin(config, route_name, factory=factory, + view=view, models=[model], model=model, **kwargs) def formalchemy_admin(config, route_name, factory='pyramid_formalchemy.resources.Models', view='pyramid_formalchemy.views.ModelView', - package=None, models=None, forms=None, session_factory=None, **kwargs): + package=None, models=None, forms=None, + session_factory=None, + query_factory=None, **kwargs): """configure formalchemy's admin interface""" route_name = route_name.strip('/') @@ -41,11 +42,19 @@ def formalchemy_admin(config, route_name, if not session_factory: session_factory = config.maybe_dotted('%s.models.DBSession' % package) + if not query_factory: + def query_factory(request, query, id=None): + if id is not None: + return query.get(id) + else: + return query + factory_args = { '__forms__': forms, '__models__': models, '__model_class__': kwargs.get('model'), '__session_factory__': session_factory, + '__query_factory__': staticmethod(query_factory), '__fa_route_name__': route_name, } diff --git a/pyramid_formalchemy/resources.py b/pyramid_formalchemy/resources.py index e1ac2f7..93e7aa0 100644 --- a/pyramid_formalchemy/resources.py +++ b/pyramid_formalchemy/resources.py @@ -12,6 +12,7 @@ class Base(object): self.request = request if hasattr(self, '__fa_route_name__'): request.session_factory = self.__session_factory__ + request.query_factory = self.__query_factory__ request.route_name = self.__fa_route_name__ request.models = self.__models__ request.forms = self.__forms__ @@ -112,6 +113,7 @@ class Model(Base): def __init__(self, request, name): Base.__init__(self, request, name) - request.model_instance = request.session_factory.query(request.model_class).get(name) + query = request.session_factory.query(request.model_class) + request.model_instance = request.query_factory(request, query, id=name) request.model_id = name diff --git a/pyramid_formalchemy/views.py b/pyramid_formalchemy/views.py index dfb1bb8..7194b05 100644 --- a/pyramid_formalchemy/views.py +++ b/pyramid_formalchemy/views.py @@ -3,7 +3,6 @@ from webhelpers.paginate import Page from sqlalchemy.orm import class_mapper from formalchemy.fields import _pk from formalchemy.fields import _stringify -from formalchemy import Grid, FieldSet from formalchemy.i18n import get_translator from formalchemy.fields import Field from formalchemy import fatypes @@ -45,21 +44,10 @@ class ModelView(object): def __init__(self, context, request): self.context = context self.request = request + self.session = request.session_factory - self.FieldSet = FieldSet - self.Grid = Grid - - @property - def model_name(self): - """return ``model_name`` from ``pylons.routes_dict``""" - try: - return self.request.model_name - except AttributeError: - return None - - def Session(self): - """return a Session object. You **must** override this.""" - return self.request.session_factory + self.fieldset_class = request.forms.FieldSet + self.grid_class = request.forms.Grid def models(self, **kwargs): """Models index page""" @@ -86,25 +74,16 @@ class ModelView(object): if not isinstance(obj, type): continue models[key] = request.fa_url(key, request.format) + if kwargs.get('json'): + return models return self.render(models=models) def sync(self, fs, id=None): - """sync a record. If ``id`` is None add a new record else save current one. - - Default is:: - - S = self.Session() - if id: - S.merge(fs.model) - else: - S.add(fs.model) - S.commit() - """ - S = self.Session() + """sync a record. If ``id`` is None add a new record else save current one.""" if id: - S.merge(fs.model) + self.session.merge(fs.model) else: - S.add(fs.model) + self.session.add(fs.model) def breadcrumb(self, fs=None, **kwargs): """return items to build the breadcrumb""" @@ -113,7 +92,7 @@ class ModelView(object): model_name = request.model_name id = request.model_id items.append((request.fa_url(), 'root', 'root_url')) - if self.model_name: + if request.model_name: items.append((request.fa_url(model_name), model_name, 'model_url')) if id and hasattr(fs.model, '__unicode__'): items.append((request.fa_url(model_name, id), u'%s' % self.context.get_instance(), 'instance_url')) @@ -132,7 +111,7 @@ class ModelView(object): raise NotFound() kwargs.update( main = get_renderer('pyramid_formalchemy:templates/admin/master.pt').implementation(), - model_name=self.model_name, + model_name=request.model_name, breadcrumb=self.breadcrumb(**kwargs), F_=get_translator().gettext) return kwargs @@ -152,7 +131,7 @@ class ModelView(object): data = dict(fields=fields) pk = _pk(fs.model) if pk: - data['item_url'] = request.fa_url(self.model_name, 'json', pk) + data['item_url'] = request.fa_url(request.model_name, 'json', pk) else: data = {} data.update(kwargs) @@ -174,24 +153,19 @@ class ModelView(object): def get_page(self, **kwargs): """return a ``webhelpers.paginate.Page`` used to display ``Grid``. - - Default is:: - - S = self.Session() - query = S.query(self.context.get_model()) - kwargs = request.environ.get('pylons.routes_dict', {}) - return Page(query, page=int(request.GET.get('page', '1')), **kwargs) """ - S = self.Session() + request = self.request def get_page_url(page, partial=None): url = "%s?page=%s" % (self.request.path, page) if partial: url += "&partial=1" return url - options = dict(collection=S.query(self.context.get_model()), - page=int(self.request.GET.get('page', '1')), + options = dict(page=int(request.GET.get('page', '1')), url=get_page_url) options.update(kwargs) + if 'collection' not in options: + query = self.session.query(request.model_class) + options['collection'] = request.query_factory(request, query) collection = options.pop('collection') return Page(collection, **options) @@ -200,43 +174,40 @@ class ModelView(object): """ request = self.request model = id and request.model_instance or request.model_class - if request.forms and hasattr(request.forms, self.model_name): - fs = getattr(request.forms, self.model_name) + if hasattr(request.forms, request.model_name): + fs = getattr(request.forms, request.model_name) fs.engine = fs.engine or self.engine return id and fs.bind(model) or fs - fs = self.FieldSet(model) + fs = self.fieldset_class(model) fs.engine = fs.engine or self.engine return fs def get_add_fieldset(self): """return a ``FieldSet`` used for add form. """ + request = self.request + if hasattr(request.forms, request.model_name + 'Add'): + fs = getattr(request.forms, request.model_name) + fs.engine = fs.engine or self.engine + return fs fs = self.get_fieldset() for field in fs.render_fields.itervalues(): if field.is_readonly(): del fs[field.name] return fs - def get_grid(self, model_name=None): - """return a Grid object - - Default is:: - - grid = self.Grid(self.context.get_model()) - grid.engine = self.engine - self.update_grid(grid) - return grid - """ + def get_grid(self): + """return a Grid object""" request = self.request - model_name = model_name or self.model_name - if request.forms and hasattr(request.forms, '%sGrid' % model_name): + model_name = request.model_name + if hasattr(request.forms, '%sGrid' % model_name): g = getattr(request.forms, '%sGrid' % model_name) g.engine = g.engine or self.engine g.readonly = True self.update_grid(g) return g model = self.context.get_model() - grid = self.Grid(model) + grid = self.grid_class(model) grid.engine = self.engine self.update_grid(grid) return grid @@ -252,14 +223,14 @@ class ModelView(object):
- ''' % dict(url=self.request.fa_url(self.model_name, _pk(item), 'edit'), + ''' % dict(url=self.request.fa_url(self.request.model_name, _pk(item), 'edit'), label=get_translator().gettext('edit')) def delete_link(): return lambda item: ''' - ''' % dict(url=self.request.fa_url(self.model_name, _pk(item), 'delete'), + ''' % dict(url=self.request.fa_url(self.request.model_name, _pk(item), 'delete'), label=get_translator().gettext('delete')) grid.append(Field('edit', fatypes.String, edit_link())) grid.append(Field('delete', fatypes.String, delete_link())) @@ -296,9 +267,7 @@ class ModelView(object): return self.render_grid(fs=fs, id=None, pager=pager) def create(self): - """REST api""" request = self.request - S = self.Session() fs = self.get_add_fieldset() if request.format == 'json' and request.method == 'PUT': @@ -307,16 +276,16 @@ class ModelView(object): data = request.POST try: - fs = fs.bind(data=data, session=S) - except: + fs = fs.bind(data=data, session=self.session) + except Exception, e: # non SA forms - fs = fs.bind(self.context.get_model(), data=data, session=S) + fs = fs.bind(self.context.get_model(), data=data, session=self.session) if fs.validate(): fs.sync() self.sync(fs) - S.flush() - if request.format == 'html': - if request.is_xhr: + self.session.flush() + if request.format in ('html', 'xhr'): + if request.is_xhr or request.format == 'xhr': return Response(content_type='text/plain') next = request.POST.get('next') or request.fa_url(request.model_name) return exc.HTTPFound( @@ -326,57 +295,34 @@ class ModelView(object): return self.render(fs=fs) return self.render(fs=fs, action='new', id=None) - def delete(self, **kwargs): - """REST api""" - request = self.request - record = request.model_instance - if record: - S = self.Session() - S.delete(record) - if request.format == 'html': - if request.is_xhr: - response = Response() - response.content_type = 'text/plain' - return response - return exc.HTTPFound(location=request.fa_url(request.model_name)) - return self.render(id=request.model_id) - def show(self): - """REST api""" id = self.request.model_id fs = self.get_fieldset(id=id) fs.readonly = True return self.render(fs=fs, action='show', id=id) def new(self, **kwargs): - """REST api""" fs = self.get_add_fieldset() - fs = fs.bind(session=self.Session()) + fs = fs.bind(session=self.session) return self.render(fs=fs, action='new', id=None) def edit(self, id=None, **kwargs): - """REST api""" id = self.request.model_id fs = self.get_fieldset(id) return self.render(fs=fs, action='edit', id=id) def update(self, **kwargs): - """REST api""" request = self.request - S = self.Session() id = request.model_id fs = self.get_fieldset(id) - if not request.POST: - raise ValueError(request.POST) fs = fs.bind(data=request.POST) if fs.validate(): fs.sync() self.sync(fs, id) - S.flush() - if request.format == 'html': - if request.is_xhr: - response.content_type = 'text/plain' - return '' + self.session.flush() + if request.format in ('html', 'xhr'): + if request.is_xhr or request.format == 'xhr': + return Response(content_type='text/plain') return exc.HTTPFound( location=request.fa_url(request.model_name, _pk(fs.model))) else: @@ -386,3 +332,16 @@ class ModelView(object): else: return self.render(fs=fs, status=1) + def delete(self, **kwargs): + request = self.request + record = request.model_instance + if record: + self.session.delete(record) + else: + raise NotFound() + if request.format == 'html': + if request.is_xhr or request.format == 'xhr': + return Response(content_type='text/plain') + return exc.HTTPFound(location=request.fa_url(request.model_name)) + return self.render(id=request.model_id) + diff --git a/pyramidapp/pyramidapp/tests.py b/pyramidapp/pyramidapp/tests.py index 6688056..23bd758 100644 --- a/pyramidapp/pyramidapp/tests.py +++ b/pyramidapp/pyramidapp/tests.py @@ -150,6 +150,19 @@ class Test_1_UI(unittest.TestCase): response = self.app.delete(str(data['item_url'])) self.assert_(response.json['id'] > 0) + def test_4_xhr(self): + # add page + resp = self.app.post('/admin/Foo/', {'Foo--bar':'value'}, extra_environ={'HTTP_X_REQUESTED_WITH':'XMLHttpRequest'}) + self.assertEqual(resp.content_type, 'text/plain') + + resp = self.app.post('/admin/Foo/1', {'Foo-1-bar':'value'}, extra_environ={'HTTP_X_REQUESTED_WITH':'XMLHttpRequest'}) + self.assertEqual(resp.content_type, 'text/plain') + + # assume all are deleted + response = self.app.delete('/admin/Foo/1', extra_environ={'HTTP_X_REQUESTED_WITH':'XMLHttpRequest'}) + self.assertEqual(resp.content_type, 'text/plain') + + class Test_2_Security(Test_1_UI): config = os.path.join(dirname, 'security.ini')