Commit d2b6f20a authored by Florian Sachs's avatar Florian Sachs

Merge remote-tracking branch 'upstream/master'

parents 57ff13b7 79f4f907
...@@ -106,10 +106,11 @@ configuration properties and methods. ...@@ -106,10 +106,11 @@ configuration properties and methods.
Multiple Primary Keys Multiple Primary Keys
--------------------- ---------------------
Models with multiple primary keys have limited support, as a few pitfalls are waiting for you. Flask-Admin has limited support for models with multiple primary keys. It only covers specific case when
With using multiple primary keys, weak entities can be used with Flask-Admin. all but one primary keys are foreign keys to another model. For example, model inheritance following
this convention.
Lets Model a car with it's tyres:: Lets Model a car with its tyres::
class Car(db.Model): class Car(db.Model):
__tablename__ = 'cars' __tablename__ = 'cars'
...@@ -129,19 +130,17 @@ Lets Model a car with it's tyres:: ...@@ -129,19 +130,17 @@ Lets Model a car with it's tyres::
A specific tyre is identified by using the two primary key columns of the ``Tyre`` class, of which the ``car_id`` key A specific tyre is identified by using the two primary key columns of the ``Tyre`` class, of which the ``car_id`` key
is itself a foreign key to the class ``Car``. is itself a foreign key to the class ``Car``.
To be able to CRUD the ``Tyre`` class, two steps are necessary, when definig the AdminView:: To be able to CRUD the ``Tyre`` class, you need to enumerate columns when defining the AdminView::
class TyreAdmin(sqla.ModelView): class TyreAdmin(sqla.ModelView):
form_columns = ['car', 'tyre_id', 'desc'] form_columns = ['car', 'tyre_id', 'desc']
The ``form_columns`` needs to be explizit, as per default only one primary key is displayed. When, like in this The ``form_columns`` needs to be explicit, as per default only one primary key is displayed.
example, one part of the key is a foreign key, do not include the foreign-key-columns here, but the
corresponding relationship.
When having multiple primary keys, **no** validation for uniqueness *prior* to saving of the object will be done. Saving When having multiple primary keys, **no** validation for uniqueness *prior* to saving of the object will be done. Saving
a model that violates a unique-constraint leads to an Sqlalchemy-Integrity-Error. In this case, ``Flask-Admin`` displays a model that violates a unique-constraint leads to an Sqlalchemy-Integrity-Error. In this case, ``Flask-Admin`` displays
a proper error message and you can change the data in the form. When the application has been started with ``debug=True`` a proper error message and you can change the data in the form. When the application has been started with ``debug=True``
the ``werkzeug`` debugger catches the exception and displays the stacktrace. the ``werkzeug`` debugger will catch the exception and will display the stacktrace.
A standalone script with the Examples from above can be found in the examples directory. A standalone script with the Examples from above can be found in the examples directory.
......
...@@ -4,3 +4,4 @@ except ImportError: ...@@ -4,3 +4,4 @@ except ImportError:
raise Exception('Please install flask-mongoengine in order to use mongoengine backend') raise Exception('Please install flask-mongoengine in order to use mongoengine backend')
from .view import ModelView from .view import ModelView
from .form import EmbeddedForm
...@@ -7,7 +7,7 @@ from wtforms import fields, validators ...@@ -7,7 +7,7 @@ from wtforms import fields, validators
from flask.ext.mongoengine.wtf import orm, fields as mongo_fields from flask.ext.mongoengine.wtf import orm, fields as mongo_fields
from flask.ext.admin import form from flask.ext.admin import form
from flask.ext.admin.model.form import FieldPlaceholder, InlineFormAdmin from flask.ext.admin.model.form import FieldPlaceholder, InlineBaseFormAdmin
from flask.ext.admin.model.fields import InlineFieldList from flask.ext.admin.model.fields import InlineFieldList
from flask.ext.admin.model.widgets import InlineFormWidget from flask.ext.admin.model.widgets import InlineFormWidget
from flask.ext.admin._compat import iteritems from flask.ext.admin._compat import iteritems
...@@ -15,6 +15,10 @@ from flask.ext.admin._compat import iteritems ...@@ -15,6 +15,10 @@ from flask.ext.admin._compat import iteritems
from .fields import ModelFormField, MongoFileField, MongoImageField from .fields import ModelFormField, MongoFileField, MongoImageField
class EmbeddedForm(InlineBaseFormAdmin):
pass
class CustomModelConverter(orm.ModelConverter): class CustomModelConverter(orm.ModelConverter):
""" """
Customized MongoEngine form conversion class. Customized MongoEngine form conversion class.
...@@ -39,18 +43,16 @@ class CustomModelConverter(orm.ModelConverter): ...@@ -39,18 +43,16 @@ class CustomModelConverter(orm.ModelConverter):
def _get_subdocument_config(self, name): def _get_subdocument_config(self, name):
config = getattr(self.view, 'form_subdocuments', {}) config = getattr(self.view, 'form_subdocuments', {})
print 'x', name, config
p = config.get(name) p = config.get(name)
if not p: if not p:
return InlineFormAdmin() return EmbeddedForm()
if isinstance(p, dict): if isinstance(p, dict):
return InlineFormAdmin(**p) return EmbeddedForm(**p)
elif isinstance(p, InlineFormAdmin): elif isinstance(p, EmbeddedForm):
return p return p
raise ValueError('Invalid subdocument type: expecting dict or instance of InlineFormAdmin, got %s' % type(p)) raise ValueError('Invalid subdocument type: expecting dict or instance of flask.ext.admin.contrib.mongoengine.EmbeddedForm, got %s' % type(p))
def clone_converter(self, view): def clone_converter(self, view):
return self.__class__(view) return self.__class__(view)
......
...@@ -9,7 +9,6 @@ from flask.ext.admin._compat import iteritems, string_types ...@@ -9,7 +9,6 @@ from flask.ext.admin._compat import iteritems, string_types
import mongoengine import mongoengine
import gridfs import gridfs
from mongoengine.fields import GridFSProxy, ImageGridFsProxy
from mongoengine.connection import get_db from mongoengine.connection import get_db
from bson.objectid import ObjectId from bson.objectid import ObjectId
...@@ -98,12 +97,12 @@ class ModelView(BaseModelView): ...@@ -98,12 +97,12 @@ class ModelView(BaseModelView):
List of allowed search field types. List of allowed search field types.
""" """
form_subdocuments = None form_subdocuments = {}
""" """
Subdocument configuration options. Subdocument configuration options.
This field accepts dictionary, where key is field name and value is either dictionary or instance of the This field accepts dictionary, where key is field name and value is either dictionary or instance of the
`InlineFormAdmin`. `flask.ext.admin.contrib.EmbeddedForm`.
Consider following example:: Consider following example::
...@@ -127,7 +126,7 @@ class ModelView(BaseModelView): ...@@ -127,7 +126,7 @@ class ModelView(BaseModelView):
It is also possible to use class-based embedded document configuration: It is also possible to use class-based embedded document configuration:
class CommentEmbed(InlineFormAdmin): class CommentEmbed(EmbeddedForm):
form_columns = ('name',) form_columns = ('name',)
class MyAdmin(ModelView): class MyAdmin(ModelView):
...@@ -137,10 +136,10 @@ class ModelView(BaseModelView): ...@@ -137,10 +136,10 @@ class ModelView(BaseModelView):
Arbitrary depth nesting is supported:: Arbitrary depth nesting is supported::
class SomeEmbed(InlineFormAdmin): class SomeEmbed(EmbeddedForm):
form_excluded_columns = ('test',) form_excluded_columns = ('test',)
class CommentEmbed(InlineFormAdmin): class CommentEmbed(EmbeddedForm):
form_columns = ('name',) form_columns = ('name',)
form_subdocuments = { form_subdocuments = {
'inner': SomeEmbed() 'inner': SomeEmbed()
......
...@@ -4,7 +4,7 @@ from sqlalchemy import Boolean, Column ...@@ -4,7 +4,7 @@ from sqlalchemy import Boolean, Column
from flask.ext.admin import form from flask.ext.admin import form
from flask.ext.admin.form import Select2Field from flask.ext.admin.form import Select2Field
from flask.ext.admin.model.form import (converts, ModelConverterBase, from flask.ext.admin.model.form import (converts, ModelConverterBase,
InlineModelFormAdmin, InlineModelConverterBase, InlineFormAdmin, InlineModelConverterBase,
FieldPlaceholder) FieldPlaceholder)
from flask.ext.admin.model.helpers import prettify_name from flask.ext.admin.model.helpers import prettify_name
from flask.ext.admin._backwards import get_property from flask.ext.admin._backwards import get_property
...@@ -439,7 +439,7 @@ class InlineModelConverter(InlineModelConverterBase): ...@@ -439,7 +439,7 @@ class InlineModelConverter(InlineModelConverterBase):
Flask-Admin view object Flask-Admin view object
:param model_converter: :param model_converter:
Model converter class. Will be automatically instantiated with Model converter class. Will be automatically instantiated with
appropriate `InlineModelFormAdmin` instance. appropriate `InlineFormAdmin` instance.
""" """
super(InlineModelConverter, self).__init__(view) super(InlineModelConverter, self).__init__(view)
self.session = session self.session = session
...@@ -451,7 +451,7 @@ class InlineModelConverter(InlineModelConverterBase): ...@@ -451,7 +451,7 @@ class InlineModelConverter(InlineModelConverterBase):
# Special case for model instances # Special case for model instances
if info is None: if info is None:
if hasattr(p, '_sa_class_manager'): if hasattr(p, '_sa_class_manager'):
return InlineModelFormAdmin(p) return InlineFormAdmin(p)
else: else:
model = getattr(p, 'model', None) model = getattr(p, 'model', None)
...@@ -463,9 +463,9 @@ class InlineModelConverter(InlineModelConverterBase): ...@@ -463,9 +463,9 @@ class InlineModelConverter(InlineModelConverterBase):
if not attr.startswith('_') and attr != 'model': if not attr.startswith('_') and attr != 'model':
attrs[attr] = getattr(p, attr) attrs[attr] = getattr(p, attr)
return InlineModelFormAdmin(model, **attrs) return InlineFormAdmin(model, **attrs)
info = InlineModelFormAdmin(model, **attrs) info = InlineFormAdmin(model, **attrs)
return info return info
......
...@@ -21,7 +21,6 @@ except ImportError: ...@@ -21,7 +21,6 @@ except ImportError:
Image = None Image = None
ImageOps = None ImageOps = None
__all__ = ['FileUploadInput', 'FileUploadField', __all__ = ['FileUploadInput', 'FileUploadField',
'ImageUploadInput', 'ImageUploadField', 'ImageUploadInput', 'ImageUploadField',
'namegen_filename', 'thumbgen_filename'] 'namegen_filename', 'thumbgen_filename']
...@@ -103,8 +102,6 @@ class ImageUploadInput(object): ...@@ -103,8 +102,6 @@ class ImageUploadInput(object):
if field.url_relative_path: if field.url_relative_path:
filename = urljoin(field.url_relative_path, filename) filename = urljoin(field.url_relative_path, filename)
return url_for(field.endpoint, filename)
return url_for(field.endpoint, filename=field.data) return url_for(field.endpoint, filename=field.data)
...@@ -229,6 +226,9 @@ class FileUploadField(fields.TextField): ...@@ -229,6 +226,9 @@ class FileUploadField(fields.TextField):
def _save_file(self, data, filename): def _save_file(self, data, filename):
path = self._get_path(filename) path = self._get_path(filename)
if not op.exists(op.dirname(path)):
os.makedirs(os.path.dirname(path), 0o666)
data.save(path) data.save(path)
return filename return filename
...@@ -328,6 +328,7 @@ class ImageUploadField(FileUploadField): ...@@ -328,6 +328,7 @@ class ImageUploadField(FileUploadField):
self.thumbnail_size = thumbnail_size self.thumbnail_size = thumbnail_size
self.endpoint = endpoint self.endpoint = endpoint
self.image = None self.image = None
self.url_relative_path = url_relative_path
if not allowed_extensions: if not allowed_extensions:
allowed_extensions = ('gif', 'jpg', 'jpeg', 'png', 'tiff') allowed_extensions = ('gif', 'jpg', 'jpeg', 'png', 'tiff')
...@@ -362,6 +363,10 @@ class ImageUploadField(FileUploadField): ...@@ -362,6 +363,10 @@ class ImageUploadField(FileUploadField):
# Saving # Saving
def _save_file(self, data, filename): def _save_file(self, data, filename):
path = self._get_path(filename)
if not op.exists(op.dirname(path)):
os.makedirs(os.path.dirname(path), 0o666)
if self.image and self.max_size: if self.image and self.max_size:
filename, format = self._get_save_format(filename, self.image) filename, format = self._get_save_format(filename, self.image)
...@@ -369,7 +374,8 @@ class ImageUploadField(FileUploadField): ...@@ -369,7 +374,8 @@ class ImageUploadField(FileUploadField):
self._get_path(filename), self._get_path(filename),
format) format)
else: else:
data.save(self._get_path(filename)) data.seek(0)
data.save(path)
self._save_thumbnail(data, filename) self._save_thumbnail(data, filename)
...@@ -389,11 +395,14 @@ class ImageUploadField(FileUploadField): ...@@ -389,11 +395,14 @@ class ImageUploadField(FileUploadField):
if force: if force:
return ImageOps.fit(self.image, (width, height), Image.ANTIALIAS) return ImageOps.fit(self.image, (width, height), Image.ANTIALIAS)
else: else:
return self.image.copy().thumbnail((width, height), Image.ANTIALIAS) thumb = self.image.copy()
thumb.thumbnail((width, height), Image.ANTIALIAS)
return thumb
return image return image
def _save_image(self, image, path, format='JPEG'): def _save_image(self, image, path, format='JPEG'):
image = image.convert('RGB')
with open(path, 'wb') as fp: with open(path, 'wb') as fp:
image.save(fp, format) image.save(fp, format)
......
...@@ -41,6 +41,26 @@ class InlineFieldList(FieldList): ...@@ -41,6 +41,26 @@ class InlineFieldList(FieldList):
return res return res
def validate(self, form, extra_validators=tuple()):
"""
Validate this FieldList.
Note that FieldList validation differs from normal field validation in
that FieldList validates all its enclosed fields first before running any
of its own validators.
"""
self.errors = []
# Run validators on all entries within
for subfield in self.entries:
if not self.should_delete(subfield) and not subfield.validate(form):
self.errors.append(subfield.errors)
chain = itertools.chain(self.validators, extra_validators)
self._run_validation_chain(form, chain)
return len(self.errors) == 0
def should_delete(self, field): def should_delete(self, field):
return getattr(field, '_should_delete', False) return getattr(field, '_should_delete', False)
......
...@@ -11,14 +11,14 @@ def converts(*args): ...@@ -11,14 +11,14 @@ def converts(*args):
return _inner return _inner
class InlineFormAdmin(object): class InlineBaseFormAdmin(object):
""" """
Settings for inline form administration. Settings for inline form administration.
You can use this class to customize displayed form. You can use this class to customize displayed form.
For example:: For example::
class MyUserInfoForm(InlineFormAdmin): class MyUserInfoForm(InlineBaseFormAdmin):
form_columns = ('name', 'email') form_columns = ('name', 'email')
""" """
_defaults = ['form_base_class', 'form_columns', 'form_excluded_columns', 'form_args', 'form_extra_fields'] _defaults = ['form_base_class', 'form_columns', 'form_excluded_columns', 'form_args', 'form_extra_fields']
...@@ -72,7 +72,7 @@ class InlineFormAdmin(object): ...@@ -72,7 +72,7 @@ class InlineFormAdmin(object):
pass pass
class InlineModelFormAdmin(InlineFormAdmin): class InlineFormAdmin(InlineBaseFormAdmin):
""" """
Settings for inline form administration. Used by relational backends (SQLAlchemy, Peewee), where model Settings for inline form administration. Used by relational backends (SQLAlchemy, Peewee), where model
class can not be inherited from the parent model definition. class can not be inherited from the parent model definition.
...@@ -86,7 +86,7 @@ class InlineModelFormAdmin(InlineFormAdmin): ...@@ -86,7 +86,7 @@ class InlineModelFormAdmin(InlineFormAdmin):
""" """
self.model = model self.model = model
super(InlineModelFormAdmin, self).__init__(**kwargs) super(InlineFormAdmin, self).__init__(**kwargs)
class ModelConverterBase(object): class ModelConverterBase(object):
...@@ -173,8 +173,8 @@ class InlineModelConverterBase(object): ...@@ -173,8 +173,8 @@ class InlineModelConverterBase(object):
- Model class - Model class
""" """
if isinstance(p, tuple): if isinstance(p, tuple):
return InlineModelFormAdmin(p[0], **p[1]) return InlineFormAdmin(p[0], **p[1])
elif isinstance(p, InlineModelFormAdmin): elif isinstance(p, InlineFormAdmin):
return p return p
return None return None
......
...@@ -63,7 +63,7 @@ ...@@ -63,7 +63,7 @@
this.applyGlobalStyles = function(parent) { this.applyGlobalStyles = function(parent) {
$('[data-role=select2]', parent).select2({width: 'resolve'}); $('[data-role=select2]', parent).select2({width: 'resolve'});
$('[data-role=select2blank]', parent).select2({allowClear: true, width: 'resolve'}); $('[data-role=select2blank]', parent).select2({allowClear: true, width: 'resolve'});
$('[data-role=select2tags]', parent).select2({tags: [], tokenSeparators: [','], width: 'resolve'}); $('[data-role=select2tags]', parent).select2({multiple: true, tokenSeparators: [','], width: 'resolve'});
$('[data-role=datepicker]', parent).datepicker(); $('[data-role=datepicker]', parent).datepicker();
$('[data-role=datetimepicker]', parent).datepicker({displayTime: true}); $('[data-role=datetimepicker]', parent).datepicker({displayTime: true});
}; };
......
{% import 'admin/lib.html' as lib with context %} {% import 'admin/lib.html' as lib with context %}
<div class="inline-form"> <div class="fa-inline-field">
{{ lib.render_form_fields(field, False) }} {{ lib.render_form_fields(field, False) }}
</div> </div>
...@@ -259,7 +259,7 @@ def test_subdocument_config(): ...@@ -259,7 +259,7 @@ def test_subdocument_config():
def test_subdocument_class_config(): def test_subdocument_class_config():
app, db, admin = setup() app, db, admin = setup()
from flask.ext.admin.model.form import InlineFormAdmin from flask.ext.admin.contrib.mongoengine import EmbeddedForm
class Comment(db.EmbeddedDocument): class Comment(db.EmbeddedDocument):
name = db.StringField(max_length=20, required=True) name = db.StringField(max_length=20, required=True)
...@@ -269,7 +269,7 @@ def test_subdocument_class_config(): ...@@ -269,7 +269,7 @@ def test_subdocument_class_config():
test1 = db.StringField(max_length=20) test1 = db.StringField(max_length=20)
subdoc = db.EmbeddedDocumentField(Comment) subdoc = db.EmbeddedDocumentField(Comment)
class EmbeddedConfig(InlineFormAdmin): class EmbeddedConfig(EmbeddedForm):
form_columns = ('name',) form_columns = ('name',)
# Check only # Check only
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment