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

File admin, use POST to delete models/files.

parent 18b3c956
- Core
- Right-side menu items (auth?)
- Pregenerate URLs for menu
- Conditional js include for forms or pages
- Flask app in constructor
- Calendar - add validation for time without seconds (automatically add seconds)
- Model Admin
- Ability to sort by fields that are not visible?
- SQLA Model Admin
......@@ -12,5 +12,6 @@
- Many2Many support
- WYSIWYG editor support
- File admin
- Documentation
- Header title
- Mass-delete functionality
- Unit tests
import os
import os.path as op
from flask import Flask
from flask.ext import adminex
from flask.ext.adminex.ext import fileadmin
# Create flask app
app = Flask(__name__, template_folder='templates', static_folder='files')
# Create dummy secrey key so we can use flash
app.config['SECRET_KEY'] = '123456790'
# Flask views
@app.route('/')
def index():
return '<a href="/admin/">Click me to get to Admin!</a>'
if __name__ == '__main__':
# Create directory
path = op.join(op.dirname(__file__), 'files')
try:
os.mkdir(path)
except OSError:
pass
# Create admin interface
admin = adminex.Admin()
admin.add_view(fileadmin.FileAdmin(path, '/files/', name='Files'))
admin.setup_app(app)
# Start app
app.debug = True
app.run()
......@@ -23,7 +23,6 @@ class AnotherAdminView(adminex.BaseView):
# Create flask app
app = Flask(__name__, template_folder='templates')
# Flask views
@app.route('/')
def index():
......
import os
import os.path as op
import platform
import urlparse
import re
import shutil
from operator import itemgetter
from flask import flash, url_for, redirect, abort, request
from werkzeug import secure_filename
from flask.ext.adminex.base import BaseView, expose
from flask.ext.adminex import form
from flask.ext import wtf
class NameForm(wtf.Form):
"""
Form with a filename input field.
Validates if provided name is valid for *nix and Windows systems.
"""
name = wtf.TextField()
regexp = re.compile(r'^(?!^(PRN|AUX|CLOCK\$|NUL|CON|COM\d|LPT\d|\..*)(\..+)?$)[^\x00-\x1f\\?*:\";|/]+$')
def validate_name(self, field):
if not self.regexp.match(field.data):
raise wtf.ValidationError('Invalid directory name')
class UploadForm(form.AdminForm):
"""
File upload form. Works with FileAdmin instance to check if it is allowed
to upload file with given extension.
"""
upload = wtf.FileField('File to upload', validators=[wtf.file_required()])
def __init__(self, form, admin):
self.admin = admin
super(UploadForm, self).__init__(form)
def validate_upload(self, field):
filename = field.file.filename
if not self.admin.is_file_allowed(filename):
raise wtf.ValidationError('Invalid file type')
class FileAdmin(BaseView):
"""
Simple file-management interface.
Requires two parameters:
`path`
Path to the directory which will be managed
`url`
Base URL for the directory. Will be used to generate
static links to the files.
Sample usage::
admin = Admin()
path = op.join(op.dirname(__file__), 'static')
admin.add_view(path, '/static/', name='Static Files')
admin.setup_app(app)
"""
can_upload = True
"""
Is file upload allowed.
"""
can_delete = True
"""
Is file deletion allowed.
"""
can_delete_dirs = True
"""
Is recursive directory deletion is allowed.
"""
can_mkdir = True
"""
Is directory creation allowed.
"""
can_rename = True
"""
Is file and directory renaming allowed.
"""
allowed_extensions = None
"""
List of allowed extensions for uploads, in lower case.
Example::
class MyAdmin(FileAdmin):
allowed_extensions = ('swf', 'jpg', 'gif', 'png')
"""
def __init__(self, base_path, base_url,
name=None, category=None, endpoint=None, url=None):
"""
Constructor.
`base_path`
Base file storage location
`base_url`
Base URL for the files
`name`
Name of this view. If not provided, will be defaulted to the class name.
`category`
View category
`endpoint`
Endpoint name for the view
`url`
URL for view
"""
self.base_path = base_path
self.base_url = base_url
self._on_windows = platform.system() == 'Windows'
# Convert allowed_extensions to set for quick validation
if (self.allowed_extensions
and not isinstance(self.allowed_extensions, set)):
self.allowed_extensions = set(self.allowed_extensions)
super(FileAdmin, self).__init__(name, category, endpoint, url)
def is_accessible_path(self, path):
"""
Verify if path is accessible for current user.
Override to customize behavior.
`path`
Relative path to the root
"""
return True
def get_base_path(self):
"""
Return base path. Override to customize behavior (per-user
directories, etc)
"""
return self.base_path
def get_base_url(self):
"""
Return base URL. Override to customize behavior (per-user
directories, etc)
"""
return self.base_url
def is_file_allowed(self, filename):
"""
Verify if file can be uploaded.
Override to customize behavior.
`filename`
Source file name
"""
ext = op.splitext(filename)[1].lower()
if ext.startswith('.'):
ext = ext[1:]
if self.allowed_extensions and ext not in self.allowed_extensions:
return False
return True
def is_in_folder(self, base_path, directory):
"""
Verify if `directory` is in `base_path` folder
"""
return op.normpath(directory).startswith(base_path)
def _get_dir_url(self, endpoint, path, **kwargs):
"""
Return prettified URL
`endpoint`
Endpoint name
`path`
Directory path
`kwargs`
Additional arguments
"""
if not path:
return url_for(endpoint)
else:
if self._on_windows:
path = path.replace('\\', '/')
kwargs['path'] = path
return url_for(endpoint, **kwargs)
def _get_file_url(self, path):
"""
Return static file url
`path`
Static file path
"""
base_url = self.get_base_url()
return urlparse.urljoin(base_url, path)
def _normalize_path(self, path):
"""
Verify and normalize path.
If path is not relative to the base directory, will throw 404 exception.
If path does not exist, will also throw 404 exception.
"""
base_path = self.get_base_path()
if path is None:
directory = base_path
path = ''
else:
path = op.normpath(path)
directory = op.normpath(op.join(base_path, path))
if not self.is_in_folder(base_path, directory):
abort(404)
if not op.exists(directory):
abort(404)
return base_path, directory, path
@expose('/')
@expose('/b/<path:path>')
def index(self, path=None):
"""
Index view method
`path`
Optional directory path. If not provided, will use base directory
"""
# Get path and verify if it is valid
base_path, directory, path = self._normalize_path(path)
# Get directory listing
items = []
# Parent directory
if directory != base_path:
parent_path = op.normpath(op.join(path, '..'))
if parent_path == '.':
parent_path = None
items.append(('..', parent_path, True, 0))
for f in os.listdir(directory):
fp = op.join(directory, f)
items.append((f, op.join(path, f), op.isdir(fp), op.getsize(fp)))
# Sort by type
items.sort(key=itemgetter(2), reverse=True)
# Generate breadcrumbs
accumulator = ''
breadcrumbs = [(n, op.join(accumulator, n)) for n in path.split(os.sep)]
print breadcrumbs, path, breadcrumbs[:-1], breadcrumbs[-1]
return self.render('admin/file/list.html',
dir_path=path,
breadcrumbs=breadcrumbs,
get_dir_url=self._get_dir_url,
get_file_url=self._get_file_url,
items=items)
@expose('/upload/', methods=('GET', 'POST'))
@expose('/upload/<path:path>', methods=('GET', 'POST'))
def upload(self, path=None):
"""
Upload view method
`path`
Optional directory path. If not provided, will use base directory
"""
# Get path and verify if it is valid
base_path, directory, path = self._normalize_path(path)
form = UploadForm(request.form, self)
if form.validate_on_submit():
filename = op.join(directory,
secure_filename(form.upload.file.filename))
if op.exists(filename):
flash('File "%s" already exists.' % form.upload.file.filename,
'error')
else:
form.upload.file.save(filename)
return redirect(self._get_dir_url('.index', path))
return self.render('admin/file/form.html', form=form)
@expose('/mkdir/', methods=('GET', 'POST'))
@expose('/mkdir/<path:path>', methods=('GET', 'POST'))
def mkdir(self, path=None):
"""
Directory creation view method
`path`
Optional directory path. If not provided, will use base directory
"""
# Get path and verify if it is valid
base_path, directory, path = self._normalize_path(path)
dir_url = self._get_dir_url('.index', path)
form = NameForm(request.form)
if form.validate_on_submit():
try:
os.mkdir(op.join(directory, form.name.data))
return redirect(dir_url)
except Exception, ex:
flash('Failed to create directory: %s' % ex, 'error')
return self.render('admin/file/form.html',
form=form,
dir_url=dir_url)
@expose('/delete/', methods=('POST',))
def delete(self):
"""
Delete view method
"""
path = request.form.get('path')
if not path:
return redirect(url_for('.index'))
# Get path and verify if it is valid
base_path, full_path, path = self._normalize_path(path)
return_url = self._get_dir_url('.index', op.dirname(path))
if op.isdir(full_path):
if not self.can_delete_dirs:
return redirect(return_url)
try:
shutil.rmtree(full_path)
flash('Directory "%s" was successfully deleted.' % path)
except Exception, ex:
flash('Failed to delete directory: %s' % ex, 'error')
else:
try:
os.remove(full_path)
flash('File "%s" was successfully deleted.' % path)
except Exception, ex:
flash('Failed to delete file: %s' % ex, 'error')
return redirect(return_url)
@expose('/rename/', methods=('GET', 'POST'))
def rename(self):
"""
Rename view method
"""
path = request.args.get('path')
if not path:
return redirect(url_for('.index'))
base_path, full_path, path = self._normalize_path(path)
return_url = self._get_dir_url('.index', op.dirname(path))
if not op.exists(full_path):
flash('Path does not exist.')
return redirect(return_url)
form = NameForm(request.form, name=op.basename(path))
if form.validate_on_submit():
try:
dir_base = op.dirname(full_path)
filename = secure_filename(form.name.data)
os.rename(full_path, op.join(dir_base, filename))
flash('Successfully renamed "%s" to "%s"' % (
op.basename(path),
filename))
except Exception, ex:
flash('Failed to rename: %s' % ex, 'error')
return redirect(return_url)
return self.render('admin/file/rename.html',
form=form,
path=op.dirname(path),
name=op.basename(path),
dir_url=return_url)
......@@ -34,8 +34,6 @@ class AdminModelConverter(ModelConverter):
'default': None
}
print prop, kwargs, local_column
if field_args:
kwargs.update(field_args)
......
......@@ -494,10 +494,10 @@ class BaseModelView(BaseView):
form=form,
return_url=return_url or url_for('.index_view'))
@expose('/delete/<int:id>/')
@expose('/delete/<int:id>/', methods=('POST',))
def delete_view(self, id):
"""
Delete model view
Delete model view. Only POST method is allowed.
"""
return_url = request.args.get('return')
......
......@@ -3,3 +3,19 @@ body
{
padding-top: 50px;
}
form.icon {
display: inline;
}
form.icon button {
border: none;
background: transparent;
text-decoration: none;
padding: 0;
line-height: normal;
}
a.icon {
text-decoration: none;
}
{% extends 'admin/master.html' %}
{% import 'admin/lib.html' as lib %}
{% block body %}
{{ lib.render_form(form, dir_url) }}
{% endblock %}
\ No newline at end of file
{% extends 'admin/master.html' %}
{% import 'admin/lib.html' as lib %}
{% block body %}
<ul class="breadcrumb">
<li>
<a href="{{ get_dir_url('.index', path=None) }}">Root</a>
</li>
{% for name, path in breadcrumbs[:-1] %}
<li>
<span class="divider">/</span><a href="{{ get_dir_url('.index', path=path) }}">{{ name }}</a>
</li>
{% endfor %}
{% if breadcrumbs %}
<li>
<span class="divider">/</span><a href="{{ get_dir_url('.index', path=breadcrumbs[-1][1]) }}">{{ breadcrumbs[-1][0] }}</a>
</li>
{% endif %}
</ul>
<table class="table table-striped table-bordered model-list">
<thead>
<tr>
<th class="span1">&nbsp;</th>
<th>Name</th>
<th>Size</th>
</tr>
</thead>
{% for name, path, is_dir, size in items %}
<tr>
<td>
{% if admin_view.can_rename and path and name != '..' %}
<a class="icon" href="{{ url_for('.rename', path=path) }}">
<i class="icon-pencil"></i>
</a>
{% endif %}
{%- if admin_view.can_delete and path -%}
{% if is_dir %}
{% if name != '..' %}
<form class="icon" method="POST" action="{{ url_for('.delete') }}">
<input type="hidden" name="path" value="{{ path }}"></input>
<button onclick="return confirm('Are you sure you want to delete \'{{ name }}\' recursively?')">
<i class="icon-remove"></i>
</button>
</form>
{% endif %}
{% else %}
<form class="icon" method="POST" action="{{ url_for('.delete') }}">
<input type="hidden" name="path" value="{{ path }}"></input>
<button onclick="return confirm('Are you sure you want to delete \'{{ name }}\'?')">
<i class="icon-remove"></i>
</button>
</form>
{% endif %}
{%- endif -%}
</td>
{% if is_dir %}
<td colspan="2">
<a href="{{ get_dir_url('.index', path)|safe }}">
<i class="icon-folder-close"></i> <span>{{ name }}</span>
</a>
</td>
{% else %}
<td>
<a href="{{ get_file_url(path)|safe }}">{{ name }}</a>
</td>
<td>
{{ size }}
</td>
{% endif %}
</tr>
{% endfor %}
</table>
{% if admin_view.can_upload %}
<a class="btn btn-primary btn-large" href="{{ get_dir_url('.upload', path=dir_path) }}">Upload File</a>
{% endif %}
{% if admin_view.can_mkdir %}
<a class="btn btn-primary btn-large" href="{{ get_dir_url('.mkdir', path=dir_path) }}">Create Directory</a>
{% endif %}
{% endblock %}
{% extends 'admin/master.html' %}
{% import 'admin/lib.html' as lib %}
{% block body %}
<h3>Please provide new name for <i>{{ name }}</i></h3>
{{ lib.render_form(form, dir_url) }}
{% endblock %}
\ No newline at end of file
......@@ -72,3 +72,42 @@
</div>
{% endif %}
{%- endmacro %}
{% macro render_form(form, cancel_url) -%}
<form action="" method="POST" class="form-horizontal"{% if form.has_file_field %} enctype="multipart/form-data"{% endif %}>
<fieldset>
{{ form.csrf }}
{% for f in form if f.label.text != 'Csrf' %}
<div class="control-group{% if f.errors %} error{% endif %}">
{{ f.label(class='control-label') }}
<div class="controls">
<div>
{% if not focus_set %}
{{ f(autofocus='autofocus') }}
{% set focus_set = True %}
{% else %}
{{ f() }}
{% endif %}
</div>
{% if f.errors %}
<ul>
{% for e in f.errors %}
<li>{{ e }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
</div>
{% endfor %}
<div class="control-group">
<div class="controls">
<input type="submit" class="btn btn-primary btn-large" />
{% if cancel_url %}
<a href="{{ cancel_url }}" class="btn btn-large">Cancel</a>
{% endif %}
</div>
</div>
</fieldset>
</form>
{% endmacro %}
{% extends 'admin/master.html' %}
{% import 'admin/lib.html' as lib %}
{% block head %}
<link href="{{ url_for('admin.static', filename='chosen/chosen.css') }}" rel="stylesheet">
......@@ -6,35 +7,7 @@
{% endblock %}
{% block body %}
<form action="" method="POST" class="form-horizontal"{% if form.has_file_field %} enctype="multipart/form-data"{% endif %}>
<fieldset>
{{ form.csrf }}
{% for f in form if f.label.text != 'Csrf' %}
<div class="control-group{% if f.errors %} error{% endif %}">
{{ f.label(class='control-label') }}
<div class="controls">
<div>
{{ f }}
</div>
{% if f.errors %}
<ul>
{% for e in f.errors %}
<li>{{ e }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
</div>
{% endfor %}
<div class="control-group">
<div class="controls">
<input type="submit" class="btn btn-primary btn-large" />
<a href="{{ return_url }}" class="btn btn-large">Cancel</a>
</div>
</div>
</fieldset>
</form>
{{ lib.render_form(form, return_url) }}
{% endblock %}
{% block tail %}
......
......@@ -34,14 +34,16 @@
<tr>
<td>
{%- if admin_view.can_edit -%}
<a href="{{ url_for('.edit_view', id=row.id, return=return_url) }}">
<a class="icon" href="{{ url_for('.edit_view', id=row.id, return=return_url) }}">
<i class="icon-pencil"></i>
</a>
{%- endif -%}
{%- if admin_view.can_delete -%}
<a href="{{ url_for('.delete_view', id=row.id, return=return_url) }}" onclick="return confirm('You sure you want to delete this item?')">
<form class="icon" method="POST" action="{{ url_for('.delete_view', id=row.id, return=return_url) }}">
<button onclick="return confirm('You sure you want to delete this item?')">
<i class="icon-remove"></i>
</a>
</button>
</form>
{%- endif -%}
</td>
{% for c, name in list_columns %}
......
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