From b82188f7a666dc366128320a0a80167a14c09113 Mon Sep 17 00:00:00 2001 From: "Serge S. Koval" Date: Mon, 26 Mar 2012 13:17:37 +0300 Subject: [PATCH] Unique field validator for SQLAlchemy forms. Refactored code a bit. --- flask_adminex/ext/fileadmin.py | 4 +- flask_adminex/ext/sqlamodel.py | 93 +++++++++++++++++++++++++--------- flask_adminex/form.py | 6 ++- flask_adminex/model.py | 2 +- 4 files changed, 76 insertions(+), 29 deletions(-) diff --git a/flask_adminex/ext/fileadmin.py b/flask_adminex/ext/fileadmin.py index b3238203a..5153a6e6a 100644 --- a/flask_adminex/ext/fileadmin.py +++ b/flask_adminex/ext/fileadmin.py @@ -16,7 +16,7 @@ from flask.ext import wtf -class NameForm(wtf.Form): +class NameForm(form.BaseForm): """ Form with a filename input field. @@ -31,7 +31,7 @@ def validate_name(self, field): raise wtf.ValidationError('Invalid directory name') -class UploadForm(form.AdminForm): +class UploadForm(form.BaseForm): """ File upload form. Works with FileAdmin instance to check if it is allowed to upload file with given extension. diff --git a/flask_adminex/ext/sqlamodel.py b/flask_adminex/ext/sqlamodel.py index aaeefbe72..dcd0d9dd3 100644 --- a/flask_adminex/ext/sqlamodel.py +++ b/flask_adminex/ext/sqlamodel.py @@ -1,14 +1,47 @@ from sqlalchemy.orm.attributes import InstrumentedAttribute +from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.sql.expression import desc -from wtforms import fields +from wtforms import ValidationError, fields from wtforms.ext.sqlalchemy.orm import model_form, converts, ModelConverter from wtforms.ext.sqlalchemy.fields import QuerySelectField, QuerySelectMultipleField from flask import flash -from flask.ext.adminex.model import BaseModelView -from flask.ext.adminex import form +from flask.ext.adminex import model, form + + +class Unique(object): + """Checks field value unicity against specified table field. + + :param get_session: + A function that return a SQAlchemy Session. + :param model: + The model to check unicity against. + :param column: + The unique column. + :param message: + The error message. + """ + field_flags = ('unique', ) + + def __init__(self, db_session, model, column, message=None): + self.db_session = db_session + self.model = model + self.column = column + self.message = message + + def __call__(self, form, field): + try: + obj = (self.db_session.query(self.model) + .filter(self.column == field.data).one()) + + if not hasattr(form, '_obj') or not form._obj == obj: + if self.message is None: + self.message = field.gettext(u'Already exists.') + raise ValidationError(self.message) + except NoResultFound: + pass class AdminModelConverter(ModelConverter): @@ -30,37 +63,39 @@ def _get_label(self, name, field_args): return None def convert(self, model, mapper, prop, field_args): - if not field_args: - field_args = dict() + kwargs = { + 'validators': [], + 'filters': [] + } + + if field_args: + kwargs.update(field_args) if hasattr(prop, 'direction'): remote_model = prop.mapper.class_ local_column = prop.local_remote_pairs[0][0] - kwargs = { - 'validators': [], - 'filters': [], + kwargs.update({ 'allow_blank': local_column.nullable, - 'label': self._get_label(prop.key, field_args), - 'query_factory': lambda: self.view.session.query(remote_model), - 'default': None - } - - if field_args: - kwargs.update(field_args) + 'label': self._get_label(prop.key, kwargs), + 'query_factory': lambda: self.view.session.query(remote_model) + }) if prop.direction.name == 'MANYTOONE': - return QuerySelectField(widget=form.ChosenSelectWidget(), **kwargs) + return QuerySelectField(widget=form.ChosenSelectWidget(), + **kwargs) elif prop.direction.name == 'ONETOMANY': # Skip backrefs if not local_column.foreign_keys and self.view.hide_backrefs: return None - return QuerySelectMultipleField(widget=form.ChosenSelectWidget(multiple=True), - **kwargs) + return QuerySelectMultipleField( + widget=form.ChosenSelectWidget(multiple=True), + **kwargs) elif prop.direction.name == 'MANYTOMANY': - return QuerySelectMultipleField(widget=form.ChosenSelectWidget(multiple=True), - **kwargs) + return QuerySelectMultipleField( + widget=form.ChosenSelectWidget(multiple=True), + **kwargs) else: # Ignore pk/fk if hasattr(prop, 'columns'): @@ -69,12 +104,19 @@ def convert(self, model, mapper, prop, field_args): if column.foreign_keys or column.primary_key: return None - field_args['label'] = self._get_label(prop.key, field_args) + # If field is unique, validate it + if column.unique: + kwargs['validators'].append(Unique(self.view.session, + model, + column)) + + # Apply label + kwargs['label'] = self._get_label(prop.key, kwargs) return super(AdminModelConverter, self).convert(model, mapper, prop, - field_args) + kwargs) @converts('Date') def convert_date(self, field_args, **extra): @@ -91,7 +133,7 @@ def convert_time(self, field_args, **extra): return form.TimeField(**field_args) -class ModelView(BaseModelView): +class ModelView(model.BaseModelView): """ SQLALchemy model view @@ -166,7 +208,8 @@ def scaffold_sortable_columns(self): # Sanity check if len(p.columns) > 1: raise Exception('Automatic form scaffolding is not supported' + - ' for multi-column properties (%s.%s)' % (self.model.__name__, p.key)) + ' for multi-column properties (%s.%s)' % ( + self.model.__name__, p.key)) column = p.columns[0] @@ -183,7 +226,7 @@ def scaffold_form(self): Create form from the model. """ return model_form(self.model, - form.AdminForm, + form.BaseForm, self.form_columns, field_args=self.form_args, converter=AdminModelConverter(self)) diff --git a/flask_adminex/form.py b/flask_adminex/form.py index 53467f2bf..24662b936 100644 --- a/flask_adminex/form.py +++ b/flask_adminex/form.py @@ -5,10 +5,14 @@ from wtforms import fields, widgets -class AdminForm(wtf.Form): +class BaseForm(wtf.Form): """ Customized form class. """ + def __init__(self, formdata=None, obj=None, prefix='', **kwargs): + super(BaseForm, self).__init__(formdata, obj, prefix, **kwargs) + + self._obj = obj @property def has_file_field(self): diff --git a/flask_adminex/model.py b/flask_adminex/model.py index 7bb76acdd..e2f8bb0e6 100644 --- a/flask_adminex/model.py +++ b/flask_adminex/model.py @@ -227,7 +227,7 @@ def get_sortable_columns(self): def scaffold_form(self): """ - Create `form.AdminForm` class from the model. Must be implemented in + Create `form.BaseForm` inherited class from the model. Must be implemented in the child class. """ raise NotImplemented('Please implement scaffold_form method')