Commit 30f15ba5 authored by PJ Janse van Rensburg's avatar PJ Janse van Rensburg

Merge branch 'master' into upgrade-leaflet

parents 186ea42d 23dddb0e
......@@ -26,3 +26,4 @@ examples/appengine/lib
env
*.egg
.eggs
.tox/
sudo: false
language: python
python:
- "2.6"
- "2.7"
- "3.3"
- "3.4"
- "3.5"
env:
- WTFORMS_VERSION=1
- WTFORMS_VERSION=2
matrix:
include:
- python: 2.6
env: TOX_ENV=py26-WTForms1
- python: 2.6
env: TOX_ENV=py26-WTForms2
- python: 2.7
env: TOX_ENV=py27-WTForms1
- python: 2.7
env: TOX_ENV=py27-WTForms2
- python: 2.7
env: TOX_ENV=flake8
- python: 3.3
env: TOX_ENV=py33-WTForms1
- python: 3.3
env: TOX_ENV=py33-WTForms2
- python: 3.4
env: TOX_ENV=py34-WTForms1
- python: 3.4
env: TOX_ENV=py34-WTForms2
- python: 3.5
env: TOX_ENV=py35-WTForms1
- python: 3.5
env: TOX_ENV=py35-WTForms2
- python: 3.6
env: TOX_ENV=py36-WTForms1
- python: 3.6
env: TOX_ENV=py36-WTForms2
addons:
postgresql: "9.4"
......@@ -17,17 +35,16 @@ addons:
services:
- postgresql
- mongodb
before_script:
- psql -U postgres -c 'CREATE DATABASE flask_admin_test;'
- psql -U postgres -c 'CREATE EXTENSION postgis;' flask_admin_test
- psql -U postgres -c 'CREATE EXTENSION hstore;' flask_admin_test
install:
- pip install "wtforms<$WTFORMS_VERSION.99"
- pip install -r requirements-dev.txt
- pip install tox
script: nosetests flask_admin/tests --with-coverage --cover-erase --cover-inclusive
script: tox -e $TOX_ENV
after_success:
- coveralls
......@@ -4,10 +4,10 @@ Flask-Admin
The project was recently moved into its own organization. Please update your
references to *git@github.com:flask-admin/flask-admin.git*.
.. image:: https://d322cqt584bo4o.cloudfront.net/flask-admin/localized.png
.. image:: https://d322cqt584bo4o.cloudfront.net/flask-admin/localized.svg
:target: https://crowdin.com/project/flask-admin
.. image:: https://travis-ci.org/flask-admin/flask-admin.png?branch=master
.. image:: https://travis-ci.org/flask-admin/flask-admin.svg?branch=master
:target: https://travis-ci.org/flask-admin/flask-admin
Introduction
......@@ -101,6 +101,8 @@ For all the tests to pass successfully, you'll need Postgres & MongoDB to be run
CREATE EXTENSION hstore;
\q
You can also run the tests on multiple environments using *tox*.
3rd Party Stuff
---------------
......
This diff is collapsed.
Changelog
=========
1.4.2
1.5.0
-----
* Fixed CSRF generation logic for multi-process deployments
* Added WTForms >= 3.0 support
* Flask-Admin would not recursively save inline models, allowing arbitrary nesting
* Added configuration properties that allow injection of additional CSS and JS dependencies into templates without overriding them
* SQLAlchemy backend
- Updated hybrid property detection using new SQLAlchemy APIs
- Added support for association proxies
- Added support for remote hybrid properties filters
- Added support for ARRAY column type
* Localization-related fixes
* MongoEngine backend is now properly formats model labels
* Improved Google App Engine support:
- Added TextProperty, KeyProperty and SelectField support
- Added support for form_args, excluded_columns, page_size and after_model_update
* Fixed URL generation with localized named filters
* FileAdmin has Bootstrap 2 support now
* Geoalchemy fixes
- Use Google Places (by default) for place search
* Updated translations
* Bug fixes
1.4.2
-----
* Small bug fix release. Fixes regression that prevented usage of "virtual" columns with a custom formatter.
1.4.1
......
......@@ -102,7 +102,7 @@ could be as simple as::
class MicroBlogModelView(sqla.ModelView):
def is_accessible(self):
return login.current_user.is_authenticated()
return login.current_user.is_authenticated
def inaccessible_callback(self, name, **kwargs):
# redirect to login page if user doesn't have access
......
Flask
Flask-Admin
pymongo==2.4.1
bson
__version__ = '1.4.2'
__version__ = '1.5.0'
__author__ = 'Flask-Admin team'
__email__ = 'serge.koval+github@gmail.com'
from .base import expose, expose_plugview, Admin, BaseView, AdminIndexView
from .base import expose, expose_plugview, Admin, BaseView, AdminIndexView # noqa: F401
# -*- coding: utf-8 -*-
# flake8: noqa
"""
flask_admin._compat
~~~~~~~~~~~~~~~~~~~~~~~
......@@ -87,17 +88,4 @@ def with_metaclass(meta, *bases):
try:
from collections import OrderedDict
except ImportError:
# Bare-bones OrderedDict implementation for Python2.6 compatibility
class OrderedDict(dict):
def __init__(self, *args, **kwargs):
dict.__init__(self, *args, **kwargs)
self.ordered_keys = []
def __setitem__(self, key, value):
self.ordered_keys.append(key)
dict.__setitem__(self, key, value)
def __iter__(self):
return (k for k in self.ordered_keys)
def iteritems(self):
return ((k, self[k]) for k in self.ordered_keys)
def items(self):
return list(self.iteritems())
from ordereddict import OrderedDict
......@@ -3,7 +3,7 @@ from flask import request, redirect
from flask_admin import tools
from flask_admin._compat import text_type
from flask_admin.helpers import get_redirect_target
from flask_admin.helpers import get_redirect_target, flash_errors
def action(name, text, confirmation=None):
......@@ -104,16 +104,22 @@ class ActionsMixin(object):
If not provided, will return user to the return url in the form
or the list view.
"""
action = request.form.get('action')
ids = request.form.getlist('rowid')
form = self.action_form()
handler = self._actions_data.get(action)
if self.validate_form(form):
# using getlist instead of FieldList for backward compatibility
ids = request.form.getlist('rowid')
action = form.action.data
if handler and self.is_action_allowed(action):
response = handler[0](ids)
handler = self._actions_data.get(action)
if response is not None:
return response
if handler and self.is_action_allowed(action):
response = handler[0](ids)
if response is not None:
return response
else:
flash_errors(form, message='Failed to perform action. %(error)s')
if return_view:
url = self.get_url('.' + return_view)
......
......@@ -9,7 +9,7 @@ from flask_admin._compat import with_metaclass, as_unicode
from flask_admin import helpers as h
# For compatibility reasons import MenuLink
from flask_admin.menu import MenuCategory, MenuView, MenuLink
from flask_admin.menu import MenuCategory, MenuView, MenuLink # noqa: F401
def expose(url='/', methods=('GET',)):
......@@ -664,13 +664,17 @@ class Admin(object):
self._init_extension()
# Register Index view
self._set_admin_index_view(index_view=index_view, endpoint=endpoint, url=url)
if index_view is not None:
self._set_admin_index_view(
index_view=index_view,
endpoint=endpoint,
url=url
)
# Register views
for view in self._views:
app.register_blueprint(view.create_blueprint(self))
def _init_extension(self):
if not hasattr(self.app, 'extensions'):
self.app.extensions = dict()
......@@ -699,4 +703,4 @@ class Admin(object):
"""
Return menu links.
"""
return self._menu_links
\ No newline at end of file
return self._menu_links
# flake8: noqa
try:
import wtforms_appengine
except ImportError:
......
......@@ -3,6 +3,7 @@ from google.appengine.ext import ndb
import decimal
class GeoPtPropertyField(StringField):
def process_formdata(self, valuelist):
if valuelist:
......
......@@ -2,6 +2,7 @@ from wtforms_appengine.ndb import ModelConverter
from .fields import GeoPtPropertyField
from flask_admin.model.form import converts
class AdminModelConverter(ModelConverter):
@converts('GeoPt')
def convert_GeoPtProperty(self, model, prop, kwargs):
......
......@@ -11,6 +11,7 @@ from flask_wtf import Form
from flask_admin.model.form import create_editable_list_form
from .form import AdminModelConverter
class NdbModelView(BaseModelView):
"""
AppEngine NDB model scaffolding.
......@@ -32,7 +33,7 @@ class NdbModelView(BaseModelView):
pass
def scaffold_filters(self):
#TODO: implement
# TODO: implement
pass
form_args = None
......@@ -75,7 +76,7 @@ class NdbModelView(BaseModelView):
def get_list(self, page, sort_field, sort_desc, search, filters,
page_size=None):
#TODO: implement filters (don't think search can work here)
# TODO: implement filters (don't think search can work here)
q = self.model.query()
......@@ -88,7 +89,7 @@ class NdbModelView(BaseModelView):
if not page_size:
page_size = self.page_size
results = q.fetch(page_size, offset=page*page_size)
results = q.fetch(page_size, offset=page * page_size)
return q.count(), results
......@@ -102,7 +103,7 @@ class NdbModelView(BaseModelView):
model.put()
except Exception as ex:
if not self.handle_view_exception(ex):
#flash(gettext('Failed to create record. %(error)s',
# flash(gettext('Failed to create record. %(error)s',
# error=ex), 'error')
logging.exception('Failed to create record.')
return False
......@@ -117,7 +118,7 @@ class NdbModelView(BaseModelView):
model.put()
except Exception as ex:
if not self.handle_view_exception(ex):
#flash(gettext('Failed to update record. %(error)s',
# flash(gettext('Failed to update record. %(error)s',
# error=ex), 'error')
logging.exception('Failed to update record.')
return False
......@@ -126,19 +127,19 @@ class NdbModelView(BaseModelView):
return True
def delete_model(self, model):
def delete_model(self, model):
try:
model.key.delete()
except Exception as ex:
if not self.handle_view_exception(ex):
#flash(gettext('Failed to delete record. %(error)s',
# flash(gettext('Failed to delete record. %(error)s',
# error=ex),
# 'error')
logging.exception('Failed to delete record.')
return False
else:
self.after_model_delete(model)
return True
......@@ -155,7 +156,8 @@ class DbModelView(BaseModelView):
def scaffold_sortable_columns(self):
# We use getattr() because ReferenceProperty does not specify a 'indexed' field
return [k for (k, v) in self.model.__dict__.iteritems() if isinstance(v, db.Property) and getattr(v, 'indexed', None)]
return [k for (k, v) in self.model.__dict__.iteritems()
if isinstance(v, db.Property) and getattr(v, 'indexed', None)]
def init_search(self):
return None
......@@ -164,14 +166,14 @@ class DbModelView(BaseModelView):
pass
def scaffold_filters(self):
#TODO: implement
# TODO: implement
pass
def scaffold_form(self):
return wt_db.model_form(self.model())
def get_list(self, page, sort_field, sort_desc, search, filters):
#TODO: implement filters (don't think search can work here)
# TODO: implement filters (don't think search can work here)
q = self.model.all()
......@@ -180,7 +182,7 @@ class DbModelView(BaseModelView):
sort_field = "-" + sort_field
q.order(sort_field)
results = q.fetch(self.page_size, offset=page*self.page_size)
results = q.fetch(self.page_size, offset=page * self.page_size)
return q.count(), results
def get_one(self, encoded_key):
......@@ -194,7 +196,7 @@ class DbModelView(BaseModelView):
return model
except Exception as ex:
if not self.handle_view_exception(ex):
#flash(gettext('Failed to create record. %(error)s',
# flash(gettext('Failed to create record. %(error)s',
# error=ex), 'error')
logging.exception('Failed to create record.')
return False
......@@ -206,23 +208,24 @@ class DbModelView(BaseModelView):
return True
except Exception as ex:
if not self.handle_view_exception(ex):
#flash(gettext('Failed to update record. %(error)s',
# flash(gettext('Failed to update record. %(error)s',
# error=ex), 'error')
logging.exception('Failed to update record.')
return False
def delete_model(self, model):
def delete_model(self, model):
try:
model.delete()
return True
except Exception as ex:
if not self.handle_view_exception(ex):
#flash(gettext('Failed to delete record. %(error)s',
# flash(gettext('Failed to delete record. %(error)s',
# error=ex),
# 'error')
logging.exception('Failed to delete record.')
return False
def ModelView(model):
if issubclass(model, ndb.Model):
return NdbModelView(model)
......
......@@ -300,12 +300,12 @@ class BaseFileAdmin(BaseView, ActionsMixin):
# Convert allowed_extensions to set for quick validation
if (self.allowed_extensions and
not isinstance(self.allowed_extensions, set)):
not isinstance(self.allowed_extensions, set)):
self.allowed_extensions = set(self.allowed_extensions)
# Convert editable_extensions to set for quick validation
if (self.editable_extensions and
not isinstance(self.editable_extensions, set)):
not isinstance(self.editable_extensions, set)):
self.editable_extensions = set(self.editable_extensions)
super(BaseFileAdmin, self).__init__(name, category, endpoint, url,
......@@ -413,6 +413,19 @@ class BaseFileAdmin(BaseView, ActionsMixin):
return DeleteForm
def get_action_form(self):
"""
Create form class for model action.
Override to implement customized behavior.
"""
class ActionForm(self.form_base_class):
action = fields.HiddenField()
url = fields.HiddenField()
# rowid is retrieved using getlist, for backward compatibility
return ActionForm
def upload_form(self):
"""
Instantiate file upload form and return it.
......@@ -423,7 +436,7 @@ class BaseFileAdmin(BaseView, ActionsMixin):
if request.form:
# Workaround for allowing both CSRF token + FileField to be submitted
# https://bitbucket.org/danjac/flask-wtf/issue/12/fieldlist-filefield-does-not-follow
formdata = request.form.copy() # as request.form is immutable
formdata = request.form.copy() # as request.form is immutable
formdata.update(request.files)
# admin=self allows the form to use self.is_file_allowed
......@@ -471,6 +484,18 @@ class BaseFileAdmin(BaseView, ActionsMixin):
else:
return delete_form_class()
def action_form(self):
"""
Instantiate action form and return it.
Override to implement custom behavior.
"""
action_form_class = self.get_action_form()
if request.form:
return action_form_class(request.form)
else:
return action_form_class()
def is_file_allowed(self, filename):
"""
Verify if file can be uploaded.
......@@ -802,7 +827,7 @@ class BaseFileAdmin(BaseView, ActionsMixin):
# Sort by type
items.sort(key=itemgetter(2), reverse=True)
# Sort by modified date
items.sort(key=lambda values: (values[0], values[1], values[2], values[3], datetime.fromtimestamp(values[4])), reverse=True)
items.sort(key=lambda x: (x[0], x[1], x[2], x[3], datetime.fromtimestamp(x[4])), reverse=True)
else:
column_index = self.possible_columns.index(sort_column)
items.sort(key=itemgetter(column_index), reverse=sort_desc)
......@@ -812,6 +837,10 @@ class BaseFileAdmin(BaseView, ActionsMixin):
# Actions
actions, actions_confirmation = self.get_actions_list()
if actions:
action_form = self.action_form()
else:
action_form = None
def sort_url(column, invert=False):
desc = None
......@@ -829,6 +858,7 @@ class BaseFileAdmin(BaseView, ActionsMixin):
items=items,
actions=actions,
actions_confirmation=actions_confirmation,
action_form=action_form,
delete_form=delete_form,
sort_column=sort_column,
sort_desc=sort_desc,
......
......@@ -55,10 +55,11 @@ class S3Storage(object):
raise ValueError('Could not import boto. You can install boto by '
'using pip install boto')
connection = s3.connect_to_region(region,
aws_access_key_id=aws_access_key_id,
aws_secret_access_key=
aws_secret_access_key)
connection = s3.connect_to_region(
region,
aws_access_key_id=aws_access_key_id,
aws_secret_access_key=aws_secret_access_key,
)
self.bucket = connection.get_bucket(bucket_name)
self.separator = '/'
......
# flake8: noqa
try:
import geoalchemy2
import shapely
......
......@@ -38,9 +38,9 @@ class LeafletWidget(TextArea):
kwargs.setdefault('data-geometry-type', gtype)
# set optional values from constructor
if not "data-width" in kwargs:
if "data-width" not in kwargs:
kwargs["data-width"] = self.width
if not "data-height" in kwargs:
if "data-height" not in kwargs:
kwargs["data-height"] = self.height
if self.center:
kwargs["data-lat"] = lat(self.center)
......
# flake8: noqa
try:
import flask_mongoengine
except ImportError:
......
......@@ -7,7 +7,6 @@ from flask_mongoengine.wtf import orm, fields as mongo_fields
from flask_admin import form
from flask_admin.model.form import FieldPlaceholder
from flask_admin.model.fields import InlineFieldList, AjaxSelectField, AjaxSelectMultipleField
from flask_admin.model.widgets import InlineFormWidget
from flask_admin._compat import iteritems
from .fields import ModelFormField, MongoFileField, MongoImageField
......@@ -60,7 +59,7 @@ class CustomModelConverter(orm.ModelConverter):
return form.recreate_field(field.field)
kwargs = {
'label': getattr(field, 'verbose_name', field.name),
'label': getattr(field, 'verbose_name', None),
'description': getattr(field, 'help_text', ''),
'validators': [],
'filters': [],
......
......@@ -18,6 +18,7 @@ def convert_subdocuments(values):
elif isinstance(p, EmbeddedForm):
result[name] = p
else:
raise ValueError('Invalid subdocument type: expecting dict or instance of flask_admin.contrib.mongoengine.EmbeddedForm, got %s' % type(p))
raise ValueError('Invalid subdocument type: expecting dict or '
'instance of flask_admin.contrib.mongoengine.EmbeddedForm, got %s' % type(p))
return result
......@@ -2,7 +2,7 @@ def parse_like_term(term):
"""
Parse search term into (operation, term) tuple. Recognizes operators
in the beginning of the search term.
* = case insensitive (can precede other operators)
^ = starts with
= = exact
......@@ -24,5 +24,5 @@ def parse_like_term(term):
oper = 'contains'
# add case insensitive flag
if case_insensitive:
oper = 'i'+oper
oper = 'i' + oper
return oper, term
......@@ -323,7 +323,7 @@ class ModelView(BaseModelView):
field_class = type(f)
if (field_class == mongoengine.ListField and
isinstance(f.field, mongoengine.EmbeddedDocumentField)):
isinstance(f.field, mongoengine.EmbeddedDocumentField)):
continue
if field_class == mongoengine.EmbeddedDocumentField:
......@@ -626,7 +626,6 @@ class ModelView(BaseModelView):
return True
# FileField access API
@expose('/api/file/')
def api_file_view(self):
......@@ -645,9 +644,7 @@ class ModelView(BaseModelView):
return Response(data.read(),
content_type=data.content_type,
headers={
'Content-Length': data.length
})
headers={'Content-Length': data.length})
# Default model actions
def is_action_allowed(self, name):
......
# flake8: noqa
try:
import peewee
import wtfpeewee
......
......@@ -319,7 +319,7 @@ class FilterConverter(filters.BaseFilterConverter):
def conv_bool(self, column, name):
return [f(column, name) for f in self.bool_filters]
@filters.convert('IntegerField', 'BigIntegerField')
@filters.convert('IntegerField', 'BigIntegerField', 'PrimaryKeyField')
def conv_int(self, column, name):
return [f(column, name) for f in self.int_filters]
......
......@@ -12,6 +12,11 @@ from flask_admin.model.fields import InlineModelFormField, InlineFieldList, Ajax
from .tools import get_primary_key, get_meta_fields
from .ajax import create_ajax_loader
try:
from playhouse.postgres_ext import JSONField, BinaryJSONField
pg_ext = True
except:
pg_ext = False
class InlineModelFormList(InlineFieldList):
......@@ -36,15 +41,22 @@ class InlineModelFormList(InlineFieldList):
def display_row_controls(self, field):
return field.get_pk() is not None
# *** bryhoyt removed def process() entirely, because I believe it was buggy
# (but worked because another part of the code had a complimentary bug)
# and I'm not sure why it was necessary anyway.
# If we want it back in, we need to fix the following bogus query:
# self.model.select().where(attr == data).execute() # `data` is not an ID, and only happened to be so because we patched it in in .contribute() below
#
# For reference:
# .process() introduced in https://github.com/flask-admin/flask-admin/commit/2845e4b28cb40b25e2bf544b327f6202dc7e5709
# Fixed, brokenly I think, in https://github.com/flask-admin/flask-admin/commit/4383eef3ce7eb01878f086928f8773adb9de79f8#diff-f87e7cd76fb9bc48c8681b24f238fb13R30
""" bryhoyt removed def process() entirely, because I believe it was buggy
(but worked because another part of the code had a complimentary bug)
and I'm not sure why it was necessary anyway.
If we want it back in, we need to fix the following bogus query:
self.model.select().where(attr == data).execute()
`data` is not an ID, and only happened to be so because we patched it
in in .contribute() below
For reference, .process() introduced in:
https://github.com/flask-admin/flask-admin/commit/2845e4b28cb40b25e2bf544b327f6202dc7e5709
Fixed, brokenly I think, in:
https://github.com/flask-admin/flask-admin/commit/4383eef3ce7eb01878f086928f8773adb9de79f8#diff-f87e7cd76fb9bc48c8681b24f238fb13R30
"""
def populate_obj(self, obj, name):
pass
......@@ -99,6 +111,10 @@ class CustomModelConverter(ModelConverter):
self.converters[DateField] = self.handle_date
self.converters[TimeField] = self.handle_time
if pg_ext:
self.converters[JSONField] = self.handle_json
self.converters[BinaryJSONField] = self.handle_json
self.overrides = getattr(self.view, 'form_overrides', None) or {}
def handle_foreign_key(self, model, field, **kwargs):
......@@ -127,6 +143,9 @@ class CustomModelConverter(ModelConverter):
def handle_time(self, model, field, **kwargs):
return field.name, form.TimeField(**kwargs)
def handle_json(self, model, field, **kwargs):
return field.name, form.JSONField(**kwargs)
def get_form(model, converter,
base_class=form.BaseForm,
......@@ -246,7 +265,6 @@ class InlineModelConverter(InlineModelConverterBase):
allow_pk=True,
converter=converter)
prop_name = reverse_field.related_name
label = self.get_label(info, prop_name)
......
def setup():
import warnings
warnings.warn('Flask-Admin peewee integration module was renamed as flask_admin.contrib.peewee, please use it instead.')
warnings.warn('Flask-Admin peewee integration module was renamed as '
'flask_admin.contrib.peewee, please use it instead.')
from flask_admin._backwards import import_redirect
import_redirect(__name__, 'flask_admin.contrib.peewee')
setup()
del setup
from ..peewee.view import ModelView
from ..peewee.view import ModelView # noqa: F401
# flake8: noqa
try:
import pymongo
except ImportError:
......
import re
def parse_like_term(term):
"""
Parse search term into (operation, term) tuple
......
......@@ -82,8 +82,10 @@ class RedisCli(BaseView):
self._contribute_commands()
if self.shlex_check and VER < (2, 7, 3):
warnings.warn('Warning: rediscli uses shlex library and it does not work with unicode until Python 2.7.3. ' +
'To remove this warning, upgrade to Python 2.7.3 or suppress it by setting shlex_check attribute ' +
warnings.warn('Warning: rediscli uses shlex library and it does '
'not work with unicode until Python 2.7.3. To '
'remove this warning, upgrade to Python 2.7.3 or '
'suppress it by setting shlex_check attribute '
'to False.')
def _inspect_commands(self):
......
# flake8: noqa
from .view import ModelView
......@@ -86,9 +86,9 @@ class FilterSmaller(BaseSQLAFilter):
class FilterEmpty(BaseSQLAFilter, filters.BaseBooleanFilter):
def apply(self, query, value, alias=None):
if value == '1':
return query.filter(self.get_column(alias) == None)
return query.filter(self.get_column(alias) == None) # noqa: E711
else:
return query.filter(self.get_column(alias) != None)
return query.filter(self.get_column(alias) != None) # noqa: E711
def operation(self):
return lazy_gettext('empty')
......@@ -112,7 +112,7 @@ class FilterNotInList(FilterInList):
def apply(self, query, value, alias=None):
# NOT IN can exclude NULL values, so "or_ == None" needed to be added
column = self.get_column(alias)
return query.filter(or_(~column.in_(value), column == None))
return query.filter(or_(~column.in_(value), column == None)) # noqa: E711
def operation(self):
return lazy_gettext('not in list')
......@@ -323,7 +323,7 @@ class EnumFilterInList(FilterInList):
def clean(self, value):
values = super(EnumFilterInList, self).clean(value)
if self.enum_class is not None:
values = [self.enum_class(value) for value in values]
values = [self.enum_class(val) for val in values]
return values
......@@ -335,7 +335,7 @@ class EnumFilterNotInList(FilterNotInList):
def clean(self, value):
values = super(EnumFilterNotInList, self).clean(value)
if self.enum_class is not None:
values = [self.enum_class(value) for value in values]
values = [self.enum_class(val) for val in values]
return values
......
......@@ -201,10 +201,10 @@ class AdminModelConverter(ModelConverterBase):
optional_types = getattr(self.view, 'form_optional_types', (Boolean,))
if (
not column.nullable
and not isinstance(column.type, optional_types)
and not column.default
and not column.server_default
not column.nullable and
not isinstance(column.type, optional_types) and
not column.default and
not column.server_default
):
kwargs['validators'].append(validators.InputRequired())
......@@ -222,7 +222,7 @@ class AdminModelConverter(ModelConverterBase):
if value is not None:
if getattr(default, 'is_callable', False):
value = lambda: default.arg(None)
value = lambda: default.arg(None) # noqa: E731
else:
if not getattr(default, 'is_scalar', True):
value = None
......@@ -243,7 +243,7 @@ class AdminModelConverter(ModelConverterBase):
form_choices = getattr(self.view, 'form_choices', None)
if mapper.class_ == self.view.model and form_choices:
choices = form_choices.get(column.key)
choices = form_choices.get(prop.key)
if choices:
return form.Select2Field(
choices=choices,
......@@ -537,7 +537,7 @@ class InlineModelConverter(InlineModelConverterBase):
def _calculate_mapping_key_pair(self, model, info):
"""
Calculate mapping property key pair between `model` and inline model,
including the forward one for `model` and the reverse one for inline model.
including the forward one for `model` and the reverse one for inline model.
Override the method to map your own inline models.
:param model:
......@@ -547,8 +547,6 @@ class InlineModelConverter(InlineModelConverterBase):
:return:
A tuple of forward property key and reverse property key
"""
mapper = model._sa_class_manager.mapper
# Find property from target model to current model
......
......@@ -9,7 +9,7 @@ from sqlalchemy.exc import DBAPIError
from sqlalchemy.orm.attributes import InstrumentedAttribute
from flask_admin._compat import filter_list, string_types
from flask_admin.tools import iterencode, iterdecode, escape
from flask_admin.tools import iterencode, iterdecode, escape # noqa: F401
def parse_like_term(term):
......@@ -79,9 +79,9 @@ def tuple_operator_in(model_pk, ids):
for id in ids:
k = []
for i in range(len(model_pk)):
k.append(eq(model_pk[i],id[i]))
k.append(eq(model_pk[i], id[i]))
l.append(and_(*k))
if len(l)>=1:
if len(l) >= 1:
return or_(*l)
else:
return None
......@@ -117,10 +117,10 @@ def get_query_for_ids(modelquery, model, ids):
def get_columns_for_field(field):
if (not field or
not hasattr(field, 'property') or
not hasattr(field.property, 'columns') or
not field.property.columns):
raise Exception('Invalid field %s: does not contains any columns.' % field)
not hasattr(field, 'property') or
not hasattr(field.property, 'columns') or
not field.property.columns):
raise Exception('Invalid field %s: does not contains any columns.' % field)
return field.property.columns
......@@ -196,7 +196,7 @@ def is_hybrid_property(model, attr_name):
if isinstance(attr_name, string_types):
names = attr_name.split('.')
last_model = model
for i in range(len(names)-1):
for i in range(len(names) - 1):
attr = getattr(last_model, names[i])
if is_association_proxy(attr):
attr = attr.remote_attr
......
......@@ -2,7 +2,6 @@ import logging
import warnings
import inspect
from speaklater import is_lazy_string, make_lazy_string
from sqlalchemy.orm.attributes import InstrumentedAttribute
from sqlalchemy.orm import joinedload, aliased
from sqlalchemy.sql.expression import desc
......@@ -53,7 +52,7 @@ class ModelView(BaseModelView):
"""
column_select_related_list = ObsoleteAttr('column_select_related',
'list_select_related',
'list_select_related',
None)
"""
List of parameters for SQLAlchemy `subqueryload`. Overrides `column_auto_select_related`
......@@ -631,7 +630,7 @@ class ModelView(BaseModelView):
# If filter related to relation column (represented by
# relation_name.target_column) we collect here relation name
joined_column_name = None
if '.' in name:
if isinstance(name, string_types) and '.' in name:
joined_column_name = name.split('.')[0]
# Join not needed for hybrid properties
......@@ -652,14 +651,11 @@ class ModelView(BaseModelView):
if not isinstance(name, string_types):
visible_name = self.get_column_name(name.property.key)
else:
column_name = self.get_column_name(name)
def prettify():
return column_name.replace('.', ' / ')
if is_lazy_string(column_name):
visible_name = make_lazy_string(prettify)
if self.column_labels and name in self.column_labels:
visible_name = self.column_labels[name]
else:
visible_name = prettify()
visible_name = self.get_column_name(name)
visible_name = visible_name.replace('.', ' / ')
type_name = type(column.type).__name__
......@@ -835,12 +831,12 @@ class ModelView(BaseModelView):
if isinstance(column, tuple):
query = query.order_by(*map(desc, column))
else:
query = query.order_by(desc(column))
query = query.order_by(desc(column))
else:
if isinstance(column, tuple):
query = query.order_by(*column)
else:
query = query.order_by(column)
query = query.order_by(column)
return query, joins
......@@ -944,7 +940,8 @@ class ModelView(BaseModelView):
spec = inspect.getargspec(flt.apply)
if len(spec.args) == 3:
warnings.warn('Please update your custom filter %s to include additional `alias` parameter.' % repr(flt))
warnings.warn('Please update your custom filter %s to '
'include additional `alias` parameter.' % repr(flt))
else:
raise
......
def setup():
import warnings
warnings.warn('Flask-Admin sqlalchemy integration module was renamed as flask_admin.contrib.sqla, please use it instead.')
warnings.warn('Flask-Admin sqlalchemy integration module was renamed as '
'flask_admin.contrib.sqla, please use it instead.')
from flask_admin._backwards import import_redirect
import_redirect(__name__, 'flask_admin.contrib.sqla')
setup()
del setup
from ..sqla.view import ModelView
from ..sqla.view import ModelView # noqa: F401
......@@ -2,9 +2,9 @@ from wtforms import form, __version__ as wtforms_version
from wtforms.fields.core import UnboundField
from flask_admin.babel import Translations
from .fields import *
from .widgets import *
from .upload import *
from .fields import * # noqa: F403,F401
from .widgets import * # noqa: F403,F401
from .upload import * # noqa: F403,F401
class BaseForm(form.Form):
......
......@@ -9,7 +9,8 @@ from flask_admin._compat import text_type, as_unicode
from . import widgets as admin_widgets
"""
An understanding of WTForms's Custom Widgets is helpful for understanding this code: http://wtforms.simplecodes.com/docs/0.6.2/widgets.html#custom-widgets
An understanding of WTForms's Custom Widgets is helpful for understanding this code:
http://wtforms.simplecodes.com/docs/0.6.2/widgets.html#custom-widgets
"""
__all__ = ['DateTimeField', 'TimeField', 'Select2Field', 'Select2TagsField',
......@@ -21,6 +22,7 @@ class DateTimeField(fields.DateTimeField):
Allows modifying the datetime format of a DateTimeField using form_args.
"""
widget = admin_widgets.DateTimePickerWidget()
def __init__(self, label=None, validators=None, format=None, **kwargs):
"""
Constructor
......@@ -166,10 +168,11 @@ class Select2TagsField(fields.StringField):
super(Select2TagsField, self).__init__(label, validators, **kwargs)
def process_formdata(self, valuelist):
if self.save_as_list:
self.data = [self.coerce(v.strip()) for v in valuelist[0].split(',') if v.strip()]
else:
self.data = self.coerce(valuelist[0])
if valuelist:
if self.save_as_list:
self.data = [self.coerce(v.strip()) for v in valuelist[0].split(',') if v.strip()]
else:
self.data = self.coerce(valuelist[0])
def _value(self):
if isinstance(self.data, (list, tuple)):
......
......@@ -83,6 +83,7 @@ class ImageUploadInput(object):
data_template = ('<div class="image-thumbnail">'
' <img %(image)s>'
' <input type="checkbox" name="%(marker)s">Delete</input>'
' <input %(text)s>'
'</div>'
'<input %(file)s>')
......@@ -91,6 +92,9 @@ class ImageUploadInput(object):
kwargs.setdefault('name', field.name)
args = {
'text': html_params(type='hidden',
value=field.data,
name=field.name),
'file': html_params(type='file',
**kwargs),
'marker': '_%s-delete' % field.name
......@@ -196,9 +200,7 @@ class FileUploadField(fields.StringField):
map(lambda x: x.lower(), self.allowed_extensions))
def _is_uploaded_file(self, data):
return (data
and isinstance(data, FileStorage)
and data.filename)
return (data and isinstance(data, FileStorage) and data.filename)
def pre_validate(self, form):
if self._is_uploaded_file(self.data) and not self.is_file_allowed(self.data.filename):
......@@ -275,7 +277,7 @@ class FileUploadField(fields.StringField):
if not op.exists(op.dirname(path)):
os.makedirs(os.path.dirname(path), self.permission | 0o111)
if self._allow_overwrite == False and os.path.exists(path):
if (self._allow_overwrite is False) and os.path.exists(path):
raise ValueError(gettext('File "%s" already exists.' % path))
data.save(path)
......
......@@ -33,12 +33,10 @@ class Select2TagsWidget(widgets.TextInput):
You must include select2.js, form-x.x.x.js and select2 stylesheet for it to work.
"""
def __call__(self, field, **kwargs):
kwargs.setdefault('data-role', u'select2')
kwargs.setdefault('data-tags', u'1')
kwargs.setdefault('data-role', u'select2-tags')
return super(Select2TagsWidget, self).__call__(field, **kwargs)
class DatePickerWidget(widgets.TextInput):
"""
Date picker widget.
......
......@@ -101,6 +101,7 @@ def flash_errors(form, message):
errors = form[field_name].label.text + u": " + u", ".join(errors)
flash(gettext(message, error=str(errors)), 'error')
@contextfunction
def resolve_ctx(context):
"""
......
# flake8: noqa
from .base import BaseModelView
from .form import InlineFormAdmin
from flask_admin.actions import action
......@@ -42,7 +42,8 @@ class ViewArgs(object):
"""
List view arguments.
"""
def __init__(self, page=None, page_size=None, sort=None, sort_desc=None, search=None, filters=None, extra_args=None):
def __init__(self, page=None, page_size=None, sort=None, sort_desc=None,
search=None, filters=None, extra_args=None):
self.page = page
self.page_size = page_size
self.sort = sort
......@@ -599,7 +600,10 @@ class BaseModelView(BaseView, ActionsMixin):
start=dict(format='%Y-%m-%d %I:%M %p') # changes how the input is parsed by strptime (12 hour time)
)
form_widget_args = dict(
start={'data-date-format': u'yyyy-mm-dd HH:ii P', 'data-show-meridian': 'True'} # changes how the DateTimeField displays the time
start={
'data-date-format': u'yyyy-mm-dd HH:ii P',
'data-show-meridian': 'True'
} # changes how the DateTimeField displays the time
)
"""
......@@ -795,6 +799,7 @@ class BaseModelView(BaseView, ActionsMixin):
self._create_form_class = self.get_create_form()
self._edit_form_class = self.get_edit_form()
self._delete_form_class = self.get_delete_form()
self._action_form_class = self.get_action_form()
# List View In-Line Editing
if self.column_editable_list:
......@@ -855,7 +860,8 @@ class BaseModelView(BaseView, ActionsMixin):
self._sortable_columns = self.get_sortable_columns()
# Details view
self._details_columns = self.get_details_columns()
if self.can_view_details:
self._details_columns = self.get_details_columns()
# Export view
self._export_columns = self.get_export_columns()
......@@ -937,7 +943,8 @@ class BaseModelView(BaseView, ActionsMixin):
def get_list_row_actions(self):
"""
Return list of row action objects, each is instance of :class:`~flask_admin.model.template.BaseListRowAction`
Return list of row action objects, each is instance of
:class:`~flask_admin.model.template.BaseListRowAction`
"""
actions = []
......@@ -992,11 +999,12 @@ class BaseModelView(BaseView, ActionsMixin):
Uses `get_column_names` to get a list of tuples with the model
field name and formatted name for the columns in `column_details_list`
and not in `column_details_exclude_list`. If `column_details_list`
is not set, it will attempt to use the columns from `column_list`
or finally the columns from `scaffold_list_columns` will be used.
is not set, the columns from `scaffold_list_columns` will be used.
"""
only_columns = (self.column_details_list or self.column_list or
self.scaffold_list_columns())
try:
only_columns = self.column_details_list or self.scaffold_list_columns()
except NotImplementedError:
raise Exception('Please define column_details_list')
return self.get_column_names(
only_columns=only_columns,
......@@ -1254,6 +1262,19 @@ class BaseModelView(BaseView, ActionsMixin):
return DeleteForm
def get_action_form(self):
"""
Create form class for a model action.
Override to implement customized behavior.
"""
class ActionForm(self.form_base_class):
action = HiddenField()
url = HiddenField()
# rowid is retrieved using getlist, for backward compatibility
return ActionForm
def create_form(self, obj=None):
"""
Instantiate model creation form and return it.
......@@ -1295,6 +1316,14 @@ class BaseModelView(BaseView, ActionsMixin):
"""
return self._list_form_class(get_form_data(), obj=obj)
def action_form(self, obj=None):
"""
Instantiate model action form and return it.
Override to implement custom behavior.
"""
return self._action_form_class(get_form_data(), obj=obj)
def validate_form(self, form):
"""
Validate the form on submit.
......@@ -1540,7 +1569,7 @@ class BaseModelView(BaseView, ActionsMixin):
"""
pass
def on_form_prefill (self, form, id):
def on_form_prefill(self, form, id):
"""
Perform additional actions to pre-fill the edit form.
......@@ -1659,6 +1688,24 @@ class BaseModelView(BaseView, ActionsMixin):
search=request.args.get('search', None),
filters=self._get_list_filter_args())
def _get_filters(self, filters):
"""
Get active filters as dictionary of URL arguments and values
:param filters:
List of filters from ViewArgs object
"""
kwargs = {}
if filters:
for i, pair in enumerate(filters):
idx, flt_name, value = pair
key = 'flt%d_%s' % (i, self.get_filter_arg(idx, self._filters[idx]))
kwargs[key] = value
return kwargs
# URL generation helpers
def _get_list_url(self, view_args):
"""
......@@ -1679,12 +1726,7 @@ class BaseModelView(BaseView, ActionsMixin):
if view_args.page_size:
kwargs['page_size'] = view_args.page_size
if view_args.filters:
for i, pair in enumerate(view_args.filters):
idx, flt_name, value = pair
key = 'flt%d_%s' % (i, self.get_filter_arg(idx, self._filters[idx]))
kwargs[key] = value
kwargs.update(self._get_filters(view_args.filters))
return self.get_url('.index_view', **kwargs)
......@@ -1874,6 +1916,10 @@ class BaseModelView(BaseView, ActionsMixin):
# Actions
actions, actions_confirmation = self.get_actions_list()
if actions:
action_form = self.action_form()
else:
action_form = None
clear_search_url = self._get_list_url(view_args.clone(page=0,
sort=view_args.sort,
......@@ -1886,6 +1932,7 @@ class BaseModelView(BaseView, ActionsMixin):
data=data,
list_forms=list_forms,
delete_form=delete_form,
action_form=action_form,
# List
list_columns=self._list_columns,
......@@ -1901,6 +1948,7 @@ class BaseModelView(BaseView, ActionsMixin):
page_size_url=page_size_url,
page=view_args.page,
page_size=page_size,
default_page_size=self.page_size,
# Sorting
sort_column=view_args.sort,
......@@ -1916,6 +1964,7 @@ class BaseModelView(BaseView, ActionsMixin):
filters=self._filters,
filter_groups=self._get_filter_groups(),
active_filters=view_args.filters,
filter_args=self._get_filters(view_args.filters),
# Actions
actions=actions,
......@@ -2070,7 +2119,7 @@ class BaseModelView(BaseView, ActionsMixin):
form = self.delete_form()
if self.validate_form(form):
# id is InputRequired()
# id is InputRequired()
id = form.id.data
model = self.get_one(id)
......
......@@ -248,10 +248,10 @@ class BaseTimeBetweenFilter(BaseFilter):
def clean(self, value):
timetuples = [time.strptime(range, '%H:%M:%S')
for range in value.split(' to ')]
return [datetime.time(timetuple.tm_hour,
timetuple.tm_min,
timetuple.tm_sec)
for timetuple in timetuples]
return [
datetime.time(timetuple.tm_hour, timetuple.tm_min, timetuple.tm_sec)
for timetuple in timetuples
]
def operation(self):
return lazy_gettext('between')
......
......@@ -9,6 +9,7 @@ def prettify_name(name):
"""
return name.replace('_', ' ').title()
def get_mdict_item_or_list(mdict, key):
"""
Return the value for the given key of the multidict.
......
......@@ -126,4 +126,3 @@ def macro(name):
return m(model=model, column=column)
return inner
......@@ -14,11 +14,13 @@
{% macro form(actions, url) %}
{% if actions %}
<form id="action_form" action="{{ url }}" method="POST" style="display: none">
{% if csrf_token %}
{% if action_form.csrf_token %}
{{ action_form.csrf_token }}
{% elif csrf_token %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
{% endif %}
<input type="hidden" name="url" value="{{ return_url }}">
<input type="hidden" id="action" name="action" />
{{ action_form.url(value=return_url) }}
{{ action_form.action() }}
</form>
{% endif %}
{% endmacro %}
......
......@@ -34,6 +34,18 @@
{% macro filter_form() %}
<form id="filter_form" method="GET" action="{{ return_url }}">
{% if sort_column is not none %}
<input type="hidden" name="sort" value="{{ sort_column }}">
{% endif %}
{% if sort_desc %}
<input type="hidden" name="desc" value="{{ sort_desc }}">
{% endif %}
{% if search %}
<input type="hidden" name="search" value="{{ search }}">
{% endif %}
{% if page_size != default_page_size %}
<input type="hidden" name="page_size" value="{{ page_size }}">
{% endif %}
<div class="pull-right">
<button type="submit" class="btn btn-primary" style="display: none">{{ _gettext('Apply') }}</button>
{% if active_filters %}
......@@ -48,6 +60,12 @@
{% macro search_form(input_class="span2") %}
<form method="GET" action="{{ return_url }}" class="search-form">
{% for flt_name, flt_value in filter_args.items() %}
<input type="hidden" name="{{ flt_name }}" value="{{ flt_value }}">
{% endfor %}
{% if page_size != default_page_size %}
<input type="hidden" name="page_size" value="{{ page_size }}">
{% endif %}
{% if sort_column is not none %}
<input type="hidden" name="sort" value="{{ sort_column }}">
{% endif %}
......
......@@ -82,8 +82,8 @@
<th class="span1">&nbsp;</th>
{% endif %}
{% endblock %}
{% set column = 0 %}
{% for c, name in list_columns %}
{% set column = loop.index0 %}
<th class="column-header col-{{c}}">
{% if admin_view.is_sortable(c) %}
{% if sort_column == column %}
......@@ -108,7 +108,6 @@
></a>
{% endif %}
</th>
{% set column = column + 1 %}
{% endfor %}
{% endblock %}
</tr>
......
......@@ -14,11 +14,13 @@
{% macro form(actions, url) %}
{% if actions %}
<form id="action_form" action="{{ url }}" method="POST" style="display: none">
{% if csrf_token %}
{% if action_form.csrf_token %}
{{ action_form.csrf_token }}
{% elif csrf_token %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
{% endif %}
<input type="hidden" name="url" value="{{ return_url }}">
<input type="hidden" id="action" name="action" />
{{ action_form.url(value=return_url) }}
{{ action_form.action() }}
</form>
{% endif %}
{% endmacro %}
......
......@@ -34,6 +34,18 @@
{% macro filter_form() %}
<form id="filter_form" method="GET" action="{{ return_url }}">
{% if sort_column is not none %}
<input type="hidden" name="sort" value="{{ sort_column }}">
{% endif %}
{% if sort_desc %}
<input type="hidden" name="desc" value="{{ sort_desc }}">
{% endif %}
{% if search %}
<input type="hidden" name="search" value="{{ search }}">
{% endif %}
{% if page_size != default_page_size %}
<input type="hidden" name="page_size" value="{{ page_size }}">
{% endif %}
<div class="pull-right">
<button type="submit" class="btn btn-primary" style="display: none">{{ _gettext('Apply') }}</button>
{% if active_filters %}
......@@ -48,6 +60,12 @@
{% macro search_form(input_class="col-md-2") %}
<form method="GET" action="{{ return_url }}" class="navbar-form navbar-left" role="search">
{% for flt_name, flt_value in filter_args.items() %}
<input type="hidden" name="{{ flt_name }}" value="{{ flt_value }}">
{% endfor %}
{% if page_size != default_page_size %}
<input type="hidden" name="page_size" value="{{ page_size }}">
{% endif %}
{% if sort_column is not none %}
<input type="hidden" name="sort" value="{{ sort_column }}">
{% endif %}
......
......@@ -81,8 +81,8 @@
<th class="col-md-1">&nbsp;</th>
{% endif %}
{% endblock %}
{% set column = 0 %}
{% for c, name in list_columns %}
{% set column = loop.index0 %}
<th class="column-header col-{{c}}">
{% if admin_view.is_sortable(c) %}
{% if sort_column == column %}
......@@ -107,7 +107,6 @@
></a>
{% endif %}
</th>
{% set column = column + 1 %}
{% endfor %}
{% endblock %}
</tr>
......@@ -202,5 +201,22 @@
);
{% endif %}
})(jQuery);
// Catch exception when closing dialog with <esc> key
// and prevent accidental deletions.
function safeConfirm(msg) {
try {
var isconfirmed = confirm(msg);
if (isconfirmed == true) {
return true;
}
else {
return false;
}
}
catch(err) {
return false;
}
}
</script>
{% endblock %}
......@@ -31,7 +31,7 @@
{% elif csrf_token %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
{% endif %}
<button onclick="return confirm('{{ _gettext('Are you sure you want to delete this record?') }}');" title="Delete record">
<button onclick="return safeConfirm('{{ _gettext('Are you sure you want to delete this record?') }}');" title="Delete record">
<span class="fa fa-trash glyphicon glyphicon-trash"></span>
</button>
</form>
......
......@@ -134,6 +134,7 @@ def test_file_admin():
ok_('path=dummy_renamed_dir' not in rv.data.decode('utf-8'))
ok_('path=dummy.txt' in rv.data.decode('utf-8'))
def test_modal_edit():
# bootstrap 2 - test edit_modal
app_bs2 = Flask(__name__)
......
......@@ -64,7 +64,8 @@ def test_model():
"name": "test1",
"point": '{"type": "Point", "coordinates": [125.8, 10.0]}',
"line": '{"type": "LineString", "coordinates": [[50.2345, 94.2], [50.21, 94.87]]}',
"polygon": '{"type": "Polygon", "coordinates": [[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]]]}',
"polygon": ('{"type": "Polygon", "coordinates": [[[100.0, 0.0], [101.0, 0.0],'
' [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]]]}'),
"multi": '{"type": "MultiPoint", "coordinates": [[100.0, 0.0], [101.0, 1.0]]}',
})
eq_(rv.status_code, 302)
......
from nose.plugins.skip import SkipTest
from wtforms import __version__ as wtforms_version
# Skip test on PY3
from flask_admin._compat import PY2
if not PY2:
raise SkipTest('MongoEngine is not Python 3 compatible')
if int(wtforms_version[0]) < 2:
raise SkipTest('MongoEngine does not support WTForms 1.')
from flask import Flask
from flask_admin import Admin
from flask_mongoengine import MongoEngine
......
from nose.tools import eq_, ok_
from nose.plugins.skip import SkipTest
# Skip test on PY3
from flask_admin._compat import PY2, as_unicode
if not PY2:
raise SkipTest('MongoEngine is not Python 3 compatible')
from wtforms import fields, validators
from flask_admin import form
from flask_admin._compat import as_unicode
from flask_admin.contrib.mongoengine import ModelView
from . import setup
from datetime import datetime
class CustomModelView(ModelView):
def __init__(self, model,
name=None, category=None, endpoint=None, url=None,
......@@ -65,8 +61,8 @@ def fill_db(Model1, Model2):
Model2('string_field_val_4', 9000, 75.5).save()
Model2('string_field_val_5', 6169453081680413441).save()
Model1('datetime_obj1', datetime_field=datetime(2014,4,3,1,9,0)).save()
Model1('datetime_obj2', datetime_field=datetime(2013,3,2,0,8,0)).save()
Model1('datetime_obj1', datetime_field=datetime(2014, 4, 3, 1, 9, 0)).save()
Model1('datetime_obj2', datetime_field=datetime(2013, 3, 2, 0, 8, 0)).save()
def test_model():
......@@ -161,7 +157,7 @@ def test_column_editable_list():
ok_('data-role="x-editable"' in data)
# Form - Test basic in-line edit functionality
obj1 = Model1.objects.get(test1 = 'test1_val_3')
obj1 = Model1.objects.get(test1='test1_val_3')
rv = client.post('/admin/model1/ajax/update/', data={
'list_form_pk': str(obj1.id),
'test1': 'change-success-1',
......@@ -175,7 +171,7 @@ def test_column_editable_list():
ok_('change-success-1' in data)
# Test validation error
obj2 = Model1.objects.get(test1 = 'datetime_obj1')
obj2 = Model1.objects.get(test1='datetime_obj1')
rv = client.post('/admin/model1/ajax/update/', data={
'list_form_pk': str(obj2.id),
'datetime_field': 'problematic-input',
......@@ -202,7 +198,7 @@ def test_column_editable_list():
view = CustomModelView(Model2, column_editable_list=['model1'])
admin.add_view(view)
obj3 = Model2.objects.get(string_field = 'string_field_val_1')
obj3 = Model2.objects.get(string_field='string_field_val_1')
rv = client.post('/admin/model2/ajax/update/', data={
'list_form_pk': str(obj3.id),
'model1': str(obj1.id),
......@@ -287,7 +283,8 @@ def test_column_filters():
eq_(len(view._filters), 7)
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Test1']],
eq_(
[(f['index'], f['operation']) for f in view._filter_groups[u'Test1']],
[
(0, 'contains'),
(1, 'not contains'),
......@@ -296,7 +293,8 @@ def test_column_filters():
(4, 'empty'),
(5, 'in list'),
(6, 'not in list'),
])
]
)
# Make some test clients
client = app.test_client()
......@@ -367,7 +365,8 @@ def test_column_filters():
view = CustomModelView(Model2, column_filters=['int_field'])
admin.add_view(view)
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Int Field']],
eq_(
[(f['index'], f['operation']) for f in view._filter_groups[u'Int Field']],
[
(0, 'equals'),
(1, 'not equal'),
......@@ -376,7 +375,8 @@ def test_column_filters():
(4, 'empty'),
(5, 'in list'),
(6, 'not in list'),
])
]
)
# integer - equals
rv = client.get('/admin/model2/?flt0_0=5000')
......@@ -473,11 +473,13 @@ def test_column_filters():
endpoint="_bools")
admin.add_view(view)
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Bool Field']],
eq_(
[(f['index'], f['operation']) for f in view._filter_groups[u'Bool Field']],
[
(0, 'equals'),
(1, 'not equal'),
])
]
)
# boolean - equals - Yes
rv = client.get('/admin/_bools/?flt0_0=1')
......@@ -485,7 +487,6 @@ def test_column_filters():
data = rv.data.decode('utf-8')
ok_('string_field_val_1' in data)
ok_('string_field_val_2' not in data)
#ok_('string_field_val_3' not in data)
# boolean - equals - No
rv = client.get('/admin/_bools/?flt0_0=0')
......@@ -493,7 +494,6 @@ def test_column_filters():
data = rv.data.decode('utf-8')
ok_('string_field_val_1' not in data)
ok_('string_field_val_2' in data)
#ok_('string_field_val_3' in data)
# boolean - not equals - Yes
rv = client.get('/admin/_bools/?flt0_1=1')
......@@ -501,7 +501,6 @@ def test_column_filters():
data = rv.data.decode('utf-8')
ok_('string_field_val_1' not in data)
ok_('string_field_val_2' in data)
#ok_('string_field_val_3' in data)
# boolean - not equals - No
rv = client.get('/admin/_bools/?flt0_1=0')
......@@ -509,14 +508,14 @@ def test_column_filters():
data = rv.data.decode('utf-8')
ok_('string_field_val_1' in data)
ok_('string_field_val_2' not in data)
#ok_('string_field_val_3' not in data)
# Test float filter
view = CustomModelView(Model2, column_filters=['float_field'],
endpoint="_float")
admin.add_view(view)
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Float Field']],
eq_(
[(f['index'], f['operation']) for f in view._filter_groups[u'Float Field']],
[
(0, 'equals'),
(1, 'not equal'),
......@@ -525,7 +524,8 @@ def test_column_filters():
(4, 'empty'),
(5, 'in list'),
(6, 'not in list'),
])
]
)
# float - equals
rv = client.get('/admin/_float/?flt0_0=25.9')
......@@ -609,7 +609,8 @@ def test_column_filters():
endpoint="_datetime")
admin.add_view(view)
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Datetime Field']],
eq_(
[(f['index'], f['operation']) for f in view._filter_groups[u'Datetime Field']],
[
(0, 'equals'),
(1, 'not equal'),
......@@ -618,7 +619,8 @@ def test_column_filters():
(4, 'between'),
(5, 'not between'),
(6, 'empty'),
])
]
)
# datetime - equals
rv = client.get('/admin/_datetime/?flt0_0=2014-04-03+01%3A09%3A00')
......@@ -678,6 +680,7 @@ def test_column_filters():
ok_('datetime_obj1' in data)
ok_('datetime_obj2' in data)
def test_default_sort():
app, db, admin = setup()
M1, _ = create_models(db)
......@@ -784,7 +787,7 @@ def test_subdocument_config():
# Check only
view1 = CustomModelView(
Model1,
form_subdocuments = {
form_subdocuments={
'subdoc': {
'form_columns': ('name',)
}
......@@ -800,7 +803,7 @@ def test_subdocument_config():
# Check exclude
view2 = CustomModelView(
Model1,
form_subdocuments = {
form_subdocuments={
'subdoc': {
'form_excluded_columns': ('value',)
}
......@@ -831,7 +834,7 @@ def test_subdocument_class_config():
# Check only
view1 = CustomModelView(
Model1,
form_subdocuments = {
form_subdocuments={
'subdoc': EmbeddedConfig()
}
)
......@@ -859,7 +862,7 @@ def test_nested_subdocument_config():
view1 = CustomModelView(
Model1,
form_subdocuments = {
form_subdocuments={
'nested': {
'form_subdocuments': {
'comment': {
......@@ -889,7 +892,7 @@ def test_nested_list_subdocument():
# Check only
view1 = CustomModelView(
Model1,
form_subdocuments = {
form_subdocuments={
'subdoc': {
'form_subdocuments': {
None: {
......@@ -922,14 +925,13 @@ def test_nested_sortedlist_subdocument():
# Check only
view1 = CustomModelView(
Model1,
form_subdocuments = {
form_subdocuments={
'subdoc': {
'form_subdocuments': {
None: {
'form_columns': ('name',)
}
}
}
}
)
......@@ -965,6 +967,7 @@ def test_sortedlist_subdocument_validation():
eq_(rv.status_code, 200)
ok_('This field is required' in rv.data)
def test_list_subdocument_validation():
app, db, admin = setup()
......@@ -989,6 +992,7 @@ def test_list_subdocument_validation():
eq_(rv.status_code, 200)
ok_('This field is required' in rv.data)
def test_ajax_fk():
app, db, admin = setup()
......@@ -1070,7 +1074,7 @@ def test_nested_ajax_refs():
view1 = CustomModelView(
Model1,
form_subdocuments = {
form_subdocuments={
'nested': {
'form_ajax_refs': {
'comment': {
......@@ -1131,7 +1135,7 @@ def test_form_args_embeddeddoc():
view = CustomModelView(
Model,
form_args= {
form_args={
'info': {'label': 'Information'},
'timestamp': {'label': 'Last Updated Time'}
}
......
......@@ -100,12 +100,12 @@ def fill_db(Model1, Model2):
Model2('char_field_val_4', 9000, 75.5).save()
Model2('char_field_val_5', 6169453081680413441).save()
Model1('date_obj1', date_field=date(2014,11,17)).save()
Model1('date_obj2', date_field=date(2013,10,16)).save()
Model1('timeonly_obj1', timeonly_field=time(11,10,9)).save()
Model1('timeonly_obj2', timeonly_field=time(10,9,8)).save()
Model1('datetime_obj1', datetime_field=datetime(2014,4,3,1,9,0)).save()
Model1('datetime_obj2', datetime_field=datetime(2013,3,2,0,8,0)).save()
Model1('date_obj1', date_field=date(2014, 11, 17)).save()
Model1('date_obj2', date_field=date(2013, 10, 16)).save()
Model1('timeonly_obj1', timeonly_field=time(11, 10, 9)).save()
Model1('timeonly_obj2', timeonly_field=time(10, 9, 8)).save()
Model1('datetime_obj1', datetime_field=datetime(2014, 4, 3, 1, 9, 0)).save()
Model1('datetime_obj2', datetime_field=datetime(2013, 3, 2, 0, 8, 0)).save()
def test_model():
......@@ -216,7 +216,8 @@ def test_column_editable_list():
# Test validation error
rv = client.post('/admin/model1/ajax/update/', data={
'list_form_pk': '1',
'test1': 'longerthantwentycharacterslongerthantwentycharacterslongerthantwentycharacterslongerthantwentycharacters',
'test1': ('longerthantwentycharacterslongerthantwentycharacterslonger'
'thantwentycharacterslongerthantwentycharacters'),
})
data = rv.data.decode('utf-8')
eq_(rv.status_code, 500)
......@@ -320,7 +321,8 @@ def test_column_filters():
eq_(len(view._filters), 7)
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Test1']],
eq_(
[(f['index'], f['operation']) for f in view._filter_groups[u'Test1']],
[
(0, 'contains'),
(1, 'not contains'),
......@@ -329,7 +331,8 @@ def test_column_filters():
(4, 'empty'),
(5, 'in list'),
(6, 'not in list'),
])
]
)
# Make some test clients
client = app.test_client()
......@@ -400,7 +403,8 @@ def test_column_filters():
view = CustomModelView(Model2, column_filters=['int_field'])
admin.add_view(view)
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Int Field']],
eq_(
[(f['index'], f['operation']) for f in view._filter_groups[u'Int Field']],
[
(0, 'equals'),
(1, 'not equal'),
......@@ -409,7 +413,8 @@ def test_column_filters():
(4, 'empty'),
(5, 'in list'),
(6, 'not in list'),
])
]
)
# integer - equals
rv = client.get('/admin/model2/?flt0_0=5000')
......@@ -506,11 +511,13 @@ def test_column_filters():
endpoint="_bools")
admin.add_view(view)
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Bool Field']],
eq_(
[(f['index'], f['operation']) for f in view._filter_groups[u'Bool Field']],
[
(0, 'equals'),
(1, 'not equal'),
])
]
)
# boolean - equals - Yes
rv = client.get('/admin/_bools/?flt0_0=1')
......@@ -549,7 +556,8 @@ def test_column_filters():
endpoint="_float")
admin.add_view(view)
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Float Field']],
eq_(
[(f['index'], f['operation']) for f in view._filter_groups[u'Float Field']],
[
(0, 'equals'),
(1, 'not equal'),
......@@ -558,7 +566,8 @@ def test_column_filters():
(4, 'empty'),
(5, 'in list'),
(6, 'not in list'),
])
]
)
# float - equals
rv = client.get('/admin/_float/?flt0_0=25.9')
......@@ -642,7 +651,8 @@ def test_column_filters():
endpoint="_datetime")
admin.add_view(view)
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Date Field']],
eq_(
[(f['index'], f['operation']) for f in view._filter_groups[u'Date Field']],
[
(0, 'equals'),
(1, 'not equal'),
......@@ -651,9 +661,11 @@ def test_column_filters():
(4, 'between'),
(5, 'not between'),
(6, 'empty'),
])
]
)
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Datetime Field']],
eq_(
[(f['index'], f['operation']) for f in view._filter_groups[u'Datetime Field']],
[
(7, 'equals'),
(8, 'not equal'),
......@@ -662,9 +674,11 @@ def test_column_filters():
(11, 'between'),
(12, 'not between'),
(13, 'empty'),
])
]
)
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Timeonly Field']],
eq_(
[(f['index'], f['operation']) for f in view._filter_groups[u'Timeonly Field']],
[
(14, 'equals'),
(15, 'not equal'),
......@@ -673,7 +687,8 @@ def test_column_filters():
(18, 'between'),
(19, 'not between'),
(20, 'empty'),
])
]
)
# date - equals
rv = client.get('/admin/_datetime/?flt0_0=2014-11-17')
......@@ -849,6 +864,7 @@ def test_column_filters():
ok_('timeonly_obj1' in data)
ok_('timeonly_obj2' in data)
def test_default_sort():
app, db, admin = setup()
M1, _ = create_models(db)
......
......@@ -15,6 +15,7 @@ def setup():
return app, db, admin
def setup_postgres():
app = Flask(__name__)
app.config['SECRET_KEY'] = '1'
......
This diff is collapsed.
from nose.tools import eq_, ok_, raises
from nose.tools import eq_, ok_
from . import setup
from .test_basic import CustomModelView, create_models
......
# -*- coding: utf-8 -*-
from nose.tools import eq_, ok_, raises
from nose.tools import eq_, ok_
from wtforms import fields
......@@ -62,8 +62,8 @@ def test_inline_form():
eq_(User.query.count(), 1)
eq_(UserInfo.query.count(), 0)
rv = client.post('/admin/user/new/', data={'name': u'fbar', \
'info-0-key': 'foo', 'info-0-val' : 'bar'})
data = {'name': u'fbar', 'info-0-key': 'foo', 'info-0-val': 'bar'}
rv = client.post('/admin/user/new/', data=data)
eq_(rv.status_code, 302)
eq_(User.query.count(), 2)
eq_(UserInfo.query.count(), 1)
......@@ -72,15 +72,28 @@ def test_inline_form():
rv = client.get('/admin/user/edit/?id=2')
eq_(rv.status_code, 200)
# Edit - update
rv = client.post('/admin/user/edit/?id=2', data={'name': u'barfoo', \
'info-0-id': 1, 'info-0-key': u'xxx', 'info-0-val':u'yyy'})
data = {
'name': u'barfoo',
'info-0-id': 1,
'info-0-key': u'xxx',
'info-0-val': u'yyy',
}
rv = client.post('/admin/user/edit/?id=2', data=data)
eq_(UserInfo.query.count(), 1)
eq_(UserInfo.query.one().key, u'xxx')
# Edit - add & delete
rv = client.post('/admin/user/edit/?id=2', data={'name': u'barf', \
'del-info-0': 'on', 'info-0-id': '1', 'info-0-key': 'yyy', 'info-0-val': 'xxx',
'info-1-id': None, 'info-1-key': u'bar', 'info-1-val' : u'foo'})
data = {
'name': u'barf',
'del-info-0': 'on',
'info-0-id': '1',
'info-0-key': 'yyy',
'info-0-val': 'xxx',
'info-1-id': None,
'info-1-key': u'bar',
'info-1-val': u'foo',
}
rv = client.post('/admin/user/edit/?id=2', data=data)
eq_(rv.status_code, 302)
eq_(User.query.count(), 2)
eq_(User.query.get(2).name, u'barf')
......@@ -259,7 +272,7 @@ def test_inline_form_base_class():
# Set up Admin
class UserModelView(ModelView):
inline_models = ((UserEmail,{"form_base_class": StubBaseForm}),)
inline_models = ((UserEmail, {"form_base_class": StubBaseForm}),)
form_args = {
"emails": {"validators": [ItemsRequired()]}
}
......
import json
from nose.tools import eq_, ok_, raises, assert_true
from speaklater import make_lazy_string
from flask_admin.babel import lazy_gettext
from flask_babelex import Babel
from nose.tools import eq_, ok_
from . import setup
from .test_basic import CustomModelView, create_models
class Translator:
translate = False
def __call__(self, string):
if self.translate:
return 'Translated: "{0}"'.format(string)
else:
return string
def test_column_label_translation():
app, db, admin = setup()
Model1, _ = create_models(db)
translated = Translator()
label = make_lazy_string(translated, 'Column1')
app.config['BABEL_DEFAULT_LOCALE'] = 'es'
Babel(app)
label = lazy_gettext('Name')
view = CustomModelView(Model1, db.session,
column_list=['test1', 'test3'],
......@@ -31,12 +22,8 @@ def test_column_label_translation():
column_filters=('test1',))
admin.add_view(view)
translated.translate = True
non_lazy_groups = view._get_filter_groups()
json.dumps(non_lazy_groups) # Filter dict is JSON serializable.
ok_(translated('Column1') in non_lazy_groups) # Label was translated.
client = app.test_client()
# Render index with active filter.
rv = client.get('/admin/model1/?flt1_0=test')
eq_(rv.status_code, 200)
ok_('{"Nombre":' in rv.data.decode('utf-8'))
......@@ -342,7 +342,7 @@ def test_multi_instances_init():
class ManageIndex(base.AdminIndexView):
pass
_ = base.Admin(app, index_view=ManageIndex(url='/manage', endpoint='manage'))
_ = base.Admin(app, index_view=ManageIndex(url='/manage', endpoint='manage')) # noqa: F841
@raises(Exception)
......
......@@ -229,7 +229,6 @@ def test_image_upload_field():
eq_(dummy.upload, 'test1.jpg')
ok_(op.exists(op.join(path, 'test1.jpg')))
# check allowed extensions
for extension in ('gif', 'jpg', 'jpeg', 'png', 'tiff'):
filename = 'copyleft.' + extension
......
......@@ -379,7 +379,7 @@ def test_csrf():
# Create with CSRF token
rv = client.post('/admin/secure/new/', data=dict(name='test1',
csrf_token=csrf_token))
csrf_token=csrf_token))
eq_(rv.status_code, 302)
###############
......@@ -424,6 +424,23 @@ def test_csrf():
eq_(rv.status_code, 200)
ok_(u'Record was successfully deleted.' in rv.data.decode('utf-8'))
################
# actions
################
rv = client.get('/admin/secure/')
eq_(rv.status_code, 200)
ok_(u'name="csrf_token"' in rv.data.decode('utf-8'))
csrf_token = get_csrf_token(rv.data.decode('utf-8'))
# Delete without CSRF token, test validation errors
rv = client.post('/admin/secure/action/',
data=dict(rowid='1', url='/admin/secure/', action='delete'),
follow_redirects=True)
eq_(rv.status_code, 200)
ok_(u'Record was successfully deleted.' not in rv.data.decode('utf-8'))
ok_(u'Failed to perform action.' in rv.data.decode('utf-8'))
def test_custom_form():
app, admin = setup()
......@@ -562,7 +579,7 @@ def test_export_csv():
# test explicit use of column_export_list
view = MockModelView(Model, view_data, can_export=True,
column_list=['col1', 'col2'],
column_export_list=['id','col1','col2'],
column_export_list=['id', 'col1', 'col2'],
endpoint='exportinclusion')
admin.add_view(view)
......@@ -612,7 +629,7 @@ def test_export_csv():
view = MockModelView(
Model, view_data, can_export=True, column_list=['col1', 'col2'],
column_labels={'col1': 'Str Field', 'col2': 'Int Field'},
column_formatters=dict(col2=lambda v, c, m, p: m.col2*2),
column_formatters=dict(col2=lambda v, c, m, p: m.col2 * 2),
endpoint="types_and_formatters"
)
admin.add_view(view)
......@@ -630,8 +647,8 @@ def test_export_csv():
view = MockModelView(
Model, view_data, can_export=True, column_list=['col1', 'col2'],
column_formatters_export=dict(col2=lambda v, c, m, p: m.col2*3),
column_formatters=dict(col2=lambda v, c, m, p: m.col2*2), # overridden
column_formatters_export=dict(col2=lambda v, c, m, p: m.col2 * 3),
column_formatters=dict(col2=lambda v, c, m, p: m.col2 * 2), # overridden
column_type_formatters_export=type_formatters,
endpoint="export_types_and_formatters"
)
......
......@@ -119,6 +119,7 @@ def iterencode(iter):
.replace(CHAR_SEPARATOR, CHAR_ESCAPE + CHAR_SEPARATOR)
for v in iter)
def iterdecode(value):
"""
Decode enumerable from string presentation as a tuple
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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