Commit ac5fe084 authored by Serge S. Koval's avatar Serge S. Koval

Merge branch 'master' of github.com:mrjoes/flask-admin

parents d4480885 1b36345c
This diff is collapsed.
try:
import wtforms_appengine
except ImportError:
raise Exception('Please install wtforms_appengine in order to use appengine backend')
from .view import ModelView
import logging
from flask.ext.admin.model import BaseModelView
from wtforms_appengine import db as wt_db
from wtforms_appengine import ndb as wt_ndb
from google.appengine.ext import db
from google.appengine.ext import ndb
class NdbModelView(BaseModelView):
"""
AppEngine NDB model scaffolding.
"""
def get_pk_value(self, model):
return model.key.urlsafe()
def scaffold_list_columns(self):
return sorted([k for (k, v) in self.model.__dict__.iteritems() if isinstance(v, ndb.Property)])
def scaffold_sortable_columns(self):
return [k for (k, v) in self.model.__dict__.iteritems() if isinstance(v, ndb.Property) and v._indexed]
def init_search(self):
return None
def is_valid_filter(self):
pass
def scaffold_filters(self):
#TODO: implement
pass
def scaffold_form(self):
return wt_ndb.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)
q = self.model.query()
if sort_field:
order_field = getattr(self.model, sort_field)
if sort_desc:
order_field = -order_field
q = q.order(order_field)
results = q.fetch(self.page_size, offset=page*self.page_size)
return q.count(), results
def get_one(self, urlsafe_key):
return ndb.Key(urlsafe=urlsafe_key).get()
def create_model(self, form):
try:
model = self.model()
form.populate_obj(model)
model.put()
return True
except Exception as ex:
if not self.handle_view_exception(ex):
#flash(gettext('Failed to create record. %(error)s',
# error=ex), 'error')
logging.exception('Failed to create record.')
return False
def update_model(self, form, model):
try:
form.populate_obj(model)
model.put()
return True
except Exception as ex:
if not self.handle_view_exception(ex):
#flash(gettext('Failed to update record. %(error)s',
# error=ex), 'error')
logging.exception('Failed to update record.')
return False
def delete_model(self, model):
try:
model.key.delete()
return True
except Exception as ex:
if not self.handle_view_exception(ex):
#flash(gettext('Failed to delete record. %(error)s',
# error=ex),
# 'error')
logging.exception('Failed to delete record.')
return False
class DbModelView(BaseModelView):
"""
AppEngine DB model scaffolding.
"""
def get_pk_value(self, model):
return str(model.key())
def scaffold_list_columns(self):
return sorted([k for (k, v) in self.model.__dict__.iteritems() if isinstance(v, db.Property)])
def scaffold_sortable_columns(self):
return [k for (k, v) in self.model.__dict__.iteritems() if isinstance(v, db.Property) and v._indexed]
def init_search(self):
return None
def is_valid_filter(self):
pass
def scaffold_filters(self):
#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)
q = self.model.all()
if sort_field:
if sort_desc:
sort_field = "-" + sort_field
q.order(sort_field)
results = q.fetch(self.page_size, offset=page*self.page_size)
return q.count(), results
def get_one(self, encoded_key):
return db.get(db.Key(encoded=encoded_key))
def create_model(self, form):
try:
model = self.model()
form.populate_obj(model)
model.put()
return True
except Exception as ex:
if not self.handle_view_exception(ex):
#flash(gettext('Failed to create record. %(error)s',
# error=ex), 'error')
logging.exception('Failed to create record.')
return False
def update_model(self, form, model):
try:
form.populate_obj(model)
model.put()
return True
except Exception as ex:
if not self.handle_view_exception(ex):
#flash(gettext('Failed to update record. %(error)s',
# error=ex), 'error')
logging.exception('Failed to update record.')
return False
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',
# error=ex),
# 'error')
logging.exception('Failed to delete record.')
return False
def ModelView(model):
if issubclass(model, ndb.Model):
return NdbModelView(model)
elif issubclass(model, db.Model):
return DbModelView(model)
else:
raise ValueError("Unsupported model: %s" % model)
This diff is collapsed.
......@@ -80,8 +80,7 @@ class ModelView(BaseModelView):
'searchable_columns',
None)
"""
Collection of the searchable columns. Only text-based columns
are searchable (`String`, `Unicode`, `Text`, `UnicodeText`).
Collection of the searchable columns.
Example::
......@@ -491,10 +490,6 @@ class ModelView(BaseModelView):
for column in self._get_columns_for_field(attr):
column_type = type(column.type).__name__
if not self.is_text_column_type(column_type):
raise Exception('Can only search on text columns. ' +
'Failed to setup search for "%s"' % p)
self._search_fields.append(column)
# Store joins, avoid duplicates
......
from re import sub
from jinja2 import contextfunction
from flask import g, request, url_for
from flask import g, request, url_for, flash
from wtforms.validators import DataRequired, InputRequired
from flask.ext.admin._compat import urljoin, urlparse
from flask.ext.admin._compat import urljoin, urlparse, iteritems
from flask.ext.admin.babel import gettext
from ._compat import string_types
......@@ -95,6 +95,11 @@ def is_field_error(errors):
return False
def flash_errors(form, message):
for field_name, errors in iteritems(form.errors):
errors = form[field_name].label.text + u": " + u", ".join(errors)
flash(gettext(message, error=str(errors)), 'error')
@contextfunction
def resolve_ctx(context):
"""
......
......@@ -14,7 +14,7 @@ from flask.ext.admin.form import BaseForm, FormOpts, rules
from flask.ext.admin.model import filters, typefmt
from flask.ext.admin.actions import ActionsMixin
from flask.ext.admin.helpers import (get_form_data, validate_form_on_submit,
get_redirect_target)
get_redirect_target, flash_errors)
from flask.ext.admin.tools import rec_getattr
from flask.ext.admin._backwards import ObsoleteAttr
from flask.ext.admin._compat import iteritems, OrderedDict, as_unicode
......@@ -972,6 +972,9 @@ class BaseModelView(BaseView, ActionsMixin):
Instantiate model delete form and return it.
Override to implement custom behavior.
The delete form originally used a GET request, so delete_form
accepts both GET and POST request for backwards compatibility.
"""
if request.form:
return self._delete_form_class(request.form)
......@@ -1558,12 +1561,7 @@ class BaseModelView(BaseView, ActionsMixin):
flash(gettext('Record was successfully deleted.'))
return redirect(return_url)
else:
# flash validation errors
for field_name, errors in iteritems(form.errors):
errors = field_name + u": " + u", ".join(errors)
flash(gettext('Failed to delete record. %(error)s',
error=str(errors)),
'error')
flash_errors(form, message='Failed to delete record. %(error)s')
return redirect(return_url)
......
......@@ -58,7 +58,8 @@
{% if is_dir %}
{% if name != '..' and admin_view.can_delete_dirs %}
<form class="icon" method="POST" action="{{ get_url('.delete') }}">
<input type="hidden" name="path" value="{{ path }}"></input>
{{ delete_form.path(value=path) }}
{{ delete_form.csrf_token }}
<button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\' recursively?', name=name) }}')">
<i class="icon-remove"></i>
</button>
......@@ -66,7 +67,8 @@
{% endif %}
{% else %}
<form class="icon" method="POST" action="{{ get_url('.delete') }}">
<input type="hidden" name="path" value="{{ path }}"></input>
{{ delete_form.path(value=path) }}
{{ delete_form.csrf_token }}
<button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\'?', name=name) }}')">
<i class="icon-remove"></i>
</button>
......
{% macro menu_icon(item) -%}
{% set icon_type = item.get_icon_type() %}
{% if icon_type %}
{%- if icon_type %}
{% set icon_value = item.get_icon_value() %}
{% if icon_type == 'glyph' %}
<i class="{{ icon_value }}"></i>
......@@ -13,45 +13,43 @@
{%- endmacro %}
{% macro menu() %}
{% for item in admin_view.admin.menu() %}
{% if item.is_category() %}
{%- for item in admin_view.admin.menu() %}
{%- if item.is_category() -%}
{% set children = item.get_children() %}
{% if children %}
{%- if children %}
{% set class_name = item.get_class_name() %}
{% if item.is_active(admin_view) %}
{%- if item.is_active(admin_view) %}
<li class="active dropdown{% if class_name %} {{class_name}}{% endif %}">
{% else %}
{% else -%}
<li class="dropdown{% if class_name %} {{class_name}}{% endif %}">
{% endif %}
<a class="dropdown-toggle" data-toggle="dropdown" href="javascript:void(0)">
{{ menu_icon(item) }}{{ item.name }}<b class="caret"></b>
</a>
{%- endif %}
<a class="dropdown-toggle" data-toggle="dropdown" href="javascript:void(0)">{{ menu_icon(item) }}{{ item.name }}<b class="caret"></b></a>
<ul class="dropdown-menu">
{% for child in children %}
{%- for child in children -%}
{% set class_name = child.get_class_name() %}
{% if child.is_active(admin_view) %}
{%- if child.is_active(admin_view) %}
<li class="active{% if class_name %} {{class_name}}{% endif %}">
{% else %}
<li{% if class_name %} class="{{class_name}}"{% endif %}>
{% endif %}
{%- endif %}
<a href="{{ child.get_url() }}">{{ menu_icon(child) }}{{ child.name }}</a>
</li>
{% endfor %}
{%- endfor %}
</ul>
</li>
{% endif %}
{% else %}
{% if item.is_accessible() and item.is_visible() %}
{%- else %}
{%- if item.is_accessible() and item.is_visible() -%}
{% set class_name = item.get_class_name() %}
{% if item.is_active(admin_view) %}
{%- if item.is_active(admin_view) %}
<li class="active{% if class_name %} {{class_name}}{% endif %}">
{% else %}
{%- else %}
<li{% if class_name %} class="{{class_name}}"{% endif %}>
{% endif %}
{%- endif %}
<a href="{{ item.get_url() }}">{{ menu_icon(item) }}{{ item.name }}</a>
</li>
{% endif %}
{% endif %}
{%- endif -%}
{% endif -%}
{% endfor %}
{% endmacro %}
......
{% macro render_inline_fields(field, template, render, check=None) %}
<div class="inline-field">
<div class="inline-field" id="{{ field.id }}">
{# existing inline form fields #}
<div class="inline-field-list">
{% for subfield in field %}
......
......@@ -58,7 +58,8 @@
{% if is_dir %}
{% if name != '..' and admin_view.can_delete_dirs %}
<form class="icon" method="POST" action="{{ get_url('.delete') }}">
<input type="hidden" name="path" value="{{ path }}"></input>
{{ delete_form.path(value=path) }}
{{ delete_form.csrf_token }}
<button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\' recursively?', name=name) }}')">
<i class="glyphicon glyphicon-remove"></i>
</button>
......@@ -66,7 +67,8 @@
{% endif %}
{% else %}
<form class="icon" method="POST" action="{{ get_url('.delete') }}">
<input type="hidden" name="path" value="{{ path }}"></input>
{{ delete_form.path(value=path) }}
{{ delete_form.csrf_token }}
<button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\'?', name=name) }}')">
<i class="glyphicon glyphicon-trash"></i>
</button>
......
{% macro menu_icon(item) -%}
{% set icon_type = item.get_icon_type() %}
{% if icon_type %}
{%- if icon_type %}
{% set icon_value = item.get_icon_value() %}
{% if icon_type == 'glyph' %}
<i class="glyphicon {{ icon_value }}"></i>
......@@ -13,42 +13,43 @@
{%- endmacro %}
{% macro menu() %}
{% for item in admin_view.admin.menu() %}
{% if item.is_category() %}
{%- for item in admin_view.admin.menu() %}
{%- if item.is_category() -%}
{% set children = item.get_children() %}
{% if children %}
{% if item.is_active(admin_view) %}
{%- if children %}
{% set class_name = item.get_class_name() %}
{%- if item.is_active(admin_view) %}
<li class="active dropdown{% if class_name %} {{class_name}}{% endif %}">
{% else %}
{% else -%}
<li class="dropdown{% if class_name %} {{class_name}}{% endif %}">
{% endif %}
{%- endif %}
<a class="dropdown-toggle" data-toggle="dropdown" href="javascript:void(0)">{{ menu_icon(item) }}{{ item.name }}<b class="caret"></b></a>
<ul class="dropdown-menu">
{% for child in children %}
{%- for child in children -%}
{% set class_name = child.get_class_name() %}
{% if child.is_active(admin_view) %}
<li class="active"{% if class_name %} {{class_name}}{% endif %}>
{%- if child.is_active(admin_view) %}
<li class="active{% if class_name %} {{class_name}}{% endif %}">
{% else %}
<li{% if class_name %} class="{{class_name}}"{% endif %}>
{% endif %}
{%- endif %}
<a href="{{ child.get_url() }}">{{ menu_icon(child) }}{{ child.name }}</a>
</li>
{% endfor %}
{%- endfor %}
</ul>
</li>
{% endif %}
{% else %}
{% if item.is_accessible() and item.is_visible() %}
{%- else %}
{%- if item.is_accessible() and item.is_visible() -%}
{% set class_name = item.get_class_name() %}
{% if item.is_active(admin_view) %}
<li class="active"{% if class_name %} {{class_name}}{% endif %}>
{% else %}
{%- if item.is_active(admin_view) %}
<li class="active{% if class_name %} {{class_name}}{% endif %}">
{%- else %}
<li{% if class_name %} class="{{class_name}}"{% endif %}>
{% endif %}
{%- endif %}
<a href="{{ item.get_url() }}">{{ menu_icon(item) }}{{ item.name }}</a>
</li>
{% endif %}
{% endif %}
{%- endif -%}
{% endif -%}
{% endfor %}
{% endmacro %}
......
{% macro render_inline_fields(field, template, render, check=None) %}
<div class="inline-field">
<div class="inline-field" id="{{ field.id }}">
{# existing inline form fields #}
<div class="inline-field-list">
{% for subfield in field %}
......
from nose.tools import eq_, ok_
import os.path as op
from nose.tools import eq_, ok_
from flask.ext.admin.contrib import fileadmin
from . import setup
try:
from StringIO import StringIO
except ImportError:
from io import StringIO
def create_view():
app, admin = setup()
class MyFileAdmin(fileadmin.FileAdmin):
editable_extensions = ('txt',)
path = op.join(op.dirname(__file__), 'files')
view = fileadmin.FileAdmin(path, '/files/', name='Files')
view = MyFileAdmin(path, '/files/', name='Files')
admin.add_view(view)
return app, admin, view
......@@ -21,8 +30,104 @@ def test_file_admin():
client = app.test_client()
rv = client.get('/admin/fileadmin/')
# index
rv = client.get('/admin/myfileadmin/')
eq_(rv.status_code, 200)
ok_('path=dummy.txt' in rv.data.decode('utf-8'))
# edit
rv = client.get('/admin/myfileadmin/edit/?path=dummy.txt')
eq_(rv.status_code, 200)
ok_('dummy.txt' in rv.data.decode('utf-8'))
# TODO: Check actions, etc
rv = client.post('/admin/myfileadmin/edit/?path=dummy.txt', data=dict(
content='new_string'
))
eq_(rv.status_code, 302)
rv = client.get('/admin/myfileadmin/edit/?path=dummy.txt')
eq_(rv.status_code, 200)
ok_('dummy.txt' in rv.data.decode('utf-8'))
ok_('new_string' in rv.data.decode('utf-8'))
# rename
rv = client.get('/admin/myfileadmin/rename/?path=dummy.txt')
eq_(rv.status_code, 200)
ok_('dummy.txt' in rv.data.decode('utf-8'))
rv = client.post('/admin/myfileadmin/rename/?path=dummy.txt', data=dict(
name='dummy_renamed.txt',
path='dummy.txt'
))
eq_(rv.status_code, 302)
rv = client.get('/admin/myfileadmin/')
eq_(rv.status_code, 200)
ok_('path=dummy_renamed.txt' in rv.data.decode('utf-8'))
ok_('path=dummy.txt' not in rv.data.decode('utf-8'))
# upload
rv = client.get('/admin/myfileadmin/upload/')
eq_(rv.status_code, 200)
rv = client.post('/admin/myfileadmin/upload/', data=dict(
upload=(StringIO(""), 'dummy.txt'),
))
eq_(rv.status_code, 302)
rv = client.get('/admin/myfileadmin/')
eq_(rv.status_code, 200)
ok_('path=dummy.txt' in rv.data.decode('utf-8'))
ok_('path=dummy_renamed.txt' in rv.data.decode('utf-8'))
# delete
rv = client.post('/admin/myfileadmin/delete/', data=dict(
path='dummy_renamed.txt'
))
eq_(rv.status_code, 302)
rv = client.get('/admin/myfileadmin/')
eq_(rv.status_code, 200)
ok_('path=dummy_renamed.txt' not in rv.data.decode('utf-8'))
ok_('path=dummy.txt' in rv.data.decode('utf-8'))
# mkdir
rv = client.get('/admin/myfileadmin/mkdir/')
eq_(rv.status_code, 200)
rv = client.post('/admin/myfileadmin/mkdir/', data=dict(
name='dummy_dir'
))
eq_(rv.status_code, 302)
rv = client.get('/admin/myfileadmin/')
eq_(rv.status_code, 200)
ok_('path=dummy.txt' in rv.data.decode('utf-8'))
ok_('path=dummy_dir' in rv.data.decode('utf-8'))
# rename - directory
rv = client.get('/admin/myfileadmin/rename/?path=dummy_dir')
eq_(rv.status_code, 200)
ok_('dummy_dir' in rv.data.decode('utf-8'))
rv = client.post('/admin/myfileadmin/rename/?path=dummy_dir', data=dict(
name='dummy_renamed_dir',
path='dummy_dir'
))
eq_(rv.status_code, 302)
rv = client.get('/admin/myfileadmin/')
eq_(rv.status_code, 200)
ok_('path=dummy_renamed_dir' in rv.data.decode('utf-8'))
ok_('path=dummy_dir' not in rv.data.decode('utf-8'))
# delete - directory
rv = client.post('/admin/myfileadmin/delete/', data=dict(
path='dummy_renamed_dir'
))
eq_(rv.status_code, 302)
rv = client.get('/admin/myfileadmin/')
eq_(rv.status_code, 200)
ok_('path=dummy_renamed_dir' not in rv.data.decode('utf-8'))
ok_('path=dummy.txt' in rv.data.decode('utf-8'))
......@@ -253,27 +253,32 @@ def test_column_searchable_list():
Model1, Model2 = create_models(db)
view = CustomModelView(Model1, db.session,
column_searchable_list=['test1', 'test2'])
view = CustomModelView(Model2, db.session,
column_searchable_list=['string_field', 'int_field'])
admin.add_view(view)
eq_(view._search_supported, True)
eq_(len(view._search_fields), 2)
ok_(isinstance(view._search_fields[0], db.Column))
ok_(isinstance(view._search_fields[1], db.Column))
eq_(view._search_fields[0].name, 'test1')
eq_(view._search_fields[1].name, 'test2')
eq_(view._search_fields[0].name, 'string_field')
eq_(view._search_fields[1].name, 'int_field')
db.session.add(Model1('model1'))
db.session.add(Model1('model2'))
db.session.add(Model2('model1-test', 5000))
db.session.add(Model2('model2-test', 9000))
db.session.commit()
client = app.test_client()
rv = client.get('/admin/model1/?search=model1')
rv = client.get('/admin/model2/?search=model1')
data = rv.data.decode('utf-8')
ok_('model1-test' in data)
ok_('model2-test' not in data)
rv = client.get('/admin/model2/?search=9000')
data = rv.data.decode('utf-8')
ok_('model1' in data)
ok_('model2' not in data)
ok_('model1-test' not in data)
ok_('model2-test' in data)
def test_complex_searchable_list():
......
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