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.
Multiple Primary Keys
---------------------
Models with multiple primary keys have limited support, as a few pitfalls are waiting for you.
With using multiple primary keys, weak entities can be used with Flask-Admin.
Flask-Admin has limited support for models with multiple primary keys. It only covers specific case when
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):
__tablename__ = 'cars'
......@@ -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
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):
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
example, one part of the key is a foreign key, do not include the foreign-key-columns here, but the
corresponding relationship.
The ``form_columns`` needs to be explicit, as per default only one primary key is displayed.
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 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.
......
......@@ -4,3 +4,4 @@ except ImportError:
raise Exception('Please install flask-mongoengine in order to use mongoengine backend')
from .view import ModelView
from .form import EmbeddedForm
......@@ -7,7 +7,7 @@ from wtforms import fields, validators
from flask.ext.mongoengine.wtf import orm, fields as mongo_fields
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.widgets import InlineFormWidget
from flask.ext.admin._compat import iteritems
......@@ -15,6 +15,10 @@ from flask.ext.admin._compat import iteritems
from .fields import ModelFormField, MongoFileField, MongoImageField
class EmbeddedForm(InlineBaseFormAdmin):
pass
class CustomModelConverter(orm.ModelConverter):
"""
Customized MongoEngine form conversion class.
......@@ -39,18 +43,16 @@ class CustomModelConverter(orm.ModelConverter):
def _get_subdocument_config(self, name):
config = getattr(self.view, 'form_subdocuments', {})
print 'x', name, config
p = config.get(name)
if not p:
return InlineFormAdmin()
return EmbeddedForm()
if isinstance(p, dict):
return InlineFormAdmin(**p)
elif isinstance(p, InlineFormAdmin):
return EmbeddedForm(**p)
elif isinstance(p, EmbeddedForm):
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):
return self.__class__(view)
......
......@@ -9,7 +9,6 @@ from flask.ext.admin._compat import iteritems, string_types
import mongoengine
import gridfs
from mongoengine.fields import GridFSProxy, ImageGridFsProxy
from mongoengine.connection import get_db
from bson.objectid import ObjectId
......@@ -98,12 +97,12 @@ class ModelView(BaseModelView):
List of allowed search field types.
"""
form_subdocuments = None
form_subdocuments = {}
"""
Subdocument configuration options.
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::
......@@ -127,7 +126,7 @@ class ModelView(BaseModelView):
It is also possible to use class-based embedded document configuration:
class CommentEmbed(InlineFormAdmin):
class CommentEmbed(EmbeddedForm):
form_columns = ('name',)
class MyAdmin(ModelView):
......@@ -137,10 +136,10 @@ class ModelView(BaseModelView):
Arbitrary depth nesting is supported::
class SomeEmbed(InlineFormAdmin):
class SomeEmbed(EmbeddedForm):
form_excluded_columns = ('test',)
class CommentEmbed(InlineFormAdmin):
class CommentEmbed(EmbeddedForm):
form_columns = ('name',)
form_subdocuments = {
'inner': SomeEmbed()
......
......@@ -4,7 +4,7 @@ from sqlalchemy import Boolean, Column
from flask.ext.admin import form
from flask.ext.admin.form import Select2Field
from flask.ext.admin.model.form import (converts, ModelConverterBase,
InlineModelFormAdmin, InlineModelConverterBase,
InlineFormAdmin, InlineModelConverterBase,
FieldPlaceholder)
from flask.ext.admin.model.helpers import prettify_name
from flask.ext.admin._backwards import get_property
......@@ -439,7 +439,7 @@ class InlineModelConverter(InlineModelConverterBase):
Flask-Admin view object
:param model_converter:
Model converter class. Will be automatically instantiated with
appropriate `InlineModelFormAdmin` instance.
appropriate `InlineFormAdmin` instance.
"""
super(InlineModelConverter, self).__init__(view)
self.session = session
......@@ -451,7 +451,7 @@ class InlineModelConverter(InlineModelConverterBase):
# Special case for model instances
if info is None:
if hasattr(p, '_sa_class_manager'):
return InlineModelFormAdmin(p)
return InlineFormAdmin(p)
else:
model = getattr(p, 'model', None)
......@@ -463,9 +463,9 @@ class InlineModelConverter(InlineModelConverterBase):
if not attr.startswith('_') and attr != 'model':
attrs[attr] = getattr(p, attr)
return InlineModelFormAdmin(model, **attrs)
return InlineFormAdmin(model, **attrs)
info = InlineModelFormAdmin(model, **attrs)
info = InlineFormAdmin(model, **attrs)
return info
......
......@@ -21,7 +21,6 @@ except ImportError:
Image = None
ImageOps = None
__all__ = ['FileUploadInput', 'FileUploadField',
'ImageUploadInput', 'ImageUploadField',
'namegen_filename', 'thumbgen_filename']
......@@ -103,8 +102,6 @@ class ImageUploadInput(object):
if field.url_relative_path:
filename = urljoin(field.url_relative_path, filename)
return url_for(field.endpoint, filename)
return url_for(field.endpoint, filename=field.data)
......@@ -229,6 +226,9 @@ class FileUploadField(fields.TextField):
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)
data.save(path)
return filename
......@@ -328,6 +328,7 @@ class ImageUploadField(FileUploadField):
self.thumbnail_size = thumbnail_size
self.endpoint = endpoint
self.image = None
self.url_relative_path = url_relative_path
if not allowed_extensions:
allowed_extensions = ('gif', 'jpg', 'jpeg', 'png', 'tiff')
......@@ -362,6 +363,10 @@ class ImageUploadField(FileUploadField):
# Saving
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:
filename, format = self._get_save_format(filename, self.image)
......@@ -369,7 +374,8 @@ class ImageUploadField(FileUploadField):
self._get_path(filename),
format)
else:
data.save(self._get_path(filename))
data.seek(0)
data.save(path)
self._save_thumbnail(data, filename)
......@@ -389,11 +395,14 @@ class ImageUploadField(FileUploadField):
if force:
return ImageOps.fit(self.image, (width, height), Image.ANTIALIAS)
else:
return self.image.copy().thumbnail((width, height), Image.ANTIALIAS)
thumb = self.image.copy()
thumb.thumbnail((width, height), Image.ANTIALIAS)
return thumb
return image
def _save_image(self, image, path, format='JPEG'):
image = image.convert('RGB')
with open(path, 'wb') as fp:
image.save(fp, format)
......
......@@ -41,6 +41,26 @@ class InlineFieldList(FieldList):
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):
return getattr(field, '_should_delete', False)
......
......@@ -11,14 +11,14 @@ def converts(*args):
return _inner
class InlineFormAdmin(object):
class InlineBaseFormAdmin(object):
"""
Settings for inline form administration.
You can use this class to customize displayed form.
For example::
class MyUserInfoForm(InlineFormAdmin):
class MyUserInfoForm(InlineBaseFormAdmin):
form_columns = ('name', 'email')
"""
_defaults = ['form_base_class', 'form_columns', 'form_excluded_columns', 'form_args', 'form_extra_fields']
......@@ -72,7 +72,7 @@ class InlineFormAdmin(object):
pass
class InlineModelFormAdmin(InlineFormAdmin):
class InlineFormAdmin(InlineBaseFormAdmin):
"""
Settings for inline form administration. Used by relational backends (SQLAlchemy, Peewee), where model
class can not be inherited from the parent model definition.
......@@ -86,7 +86,7 @@ class InlineModelFormAdmin(InlineFormAdmin):
"""
self.model = model
super(InlineModelFormAdmin, self).__init__(**kwargs)
super(InlineFormAdmin, self).__init__(**kwargs)
class ModelConverterBase(object):
......@@ -173,8 +173,8 @@ class InlineModelConverterBase(object):
- Model class
"""
if isinstance(p, tuple):
return InlineModelFormAdmin(p[0], **p[1])
elif isinstance(p, InlineModelFormAdmin):
return InlineFormAdmin(p[0], **p[1])
elif isinstance(p, InlineFormAdmin):
return p
return None
......
......@@ -63,7 +63,7 @@
this.applyGlobalStyles = function(parent) {
$('[data-role=select2]', parent).select2({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=datetimepicker]', parent).datepicker({displayTime: true});
};
......
{% import 'admin/lib.html' as lib with context %}
<div class="inline-form">
<div class="fa-inline-field">
{{ lib.render_form_fields(field, False) }}
</div>
......@@ -259,7 +259,7 @@ def test_subdocument_config():
def test_subdocument_class_config():
app, db, admin = setup()
from flask.ext.admin.model.form import InlineFormAdmin
from flask.ext.admin.contrib.mongoengine import EmbeddedForm
class Comment(db.EmbeddedDocument):
name = db.StringField(max_length=20, required=True)
......@@ -269,7 +269,7 @@ def test_subdocument_class_config():
test1 = db.StringField(max_length=20)
subdoc = db.EmbeddedDocumentField(Comment)
class EmbeddedConfig(InlineFormAdmin):
class EmbeddedConfig(EmbeddedForm):
form_columns = ('name',)
# 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