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

Time, DateTime, Date widgets and fields, documentation update.

parent d463725e
- Core
- Right-side menu items (auth?)
- Pregenerate URLs for menu
- Conditional js include for forms or pages
- More form widgets (date time, time, etc)
- Model Admin
- Ability to sort by fields that are not visible?
- SQLA Model Admin
- Use of date time widgets
- Validation of the joins in the query
- Automatic joined load for foreign keys
- Built-in filtering support
- Many2Many support
- Ability to override form field types
- WYSIWYG editor support
- File admin
- Documentation
- Unit tests
......
......@@ -8,7 +8,7 @@ Introduction
------------
While developing the library, I attempted to make it as flexible as possible. Developer should
not patch a library to achieve desired functionality.
not monkey-patch anything to achieve desired functionality.
Library uses one simple, but powerful concept - administrative pieces are built as classes with
view methods.
......@@ -30,12 +30,12 @@ implementing reusable functional pieces that are highly customizable.
For example, Flask-AdminEx provides ready-to-use SQLAlchemy model interface. It is implemented as a
class which accepts two parameters: model and a database session. While it exposes some
class-level variables which change behavior of the interface (somewhat similar to django.contrib.admin),
nothing prohibits you from overriding form creation or database access methods or adding more views.
nothing prohibits you from overriding form creation logic, database access methods or adding more views.
Initialization
--------------
To start using Admin, you have to create `Admin` class instance and associate it with Flask application::
To start using Flask-AdminEx, you have to create `Admin` class instance and associate it with Flask application::
from flask import Flask
from flask.ext.adminex import Admin
......@@ -47,7 +47,7 @@ To start using Admin, you have to create `Admin` class instance and associate it
app.run()
If you will run this application and will navigate to `http://localhost:5000/admin/ <http://localhost:5000/admin/>`_,
If you start this application and navigate to `http://localhost:5000/admin/ <http://localhost:5000/admin/>`_,
you should see empty "Home" page with a navigation bar on top.
You can change application name by passing `name` parameter to the `Admin` class constructor::
......
......@@ -31,6 +31,7 @@ class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(120))
text = db.Column(db.Text)
date = db.Column(db.DateTime)
user_id = db.Column(db.Integer, db.ForeignKey(User.id))
user = db.relationship(User, backref='posts')
......
......@@ -3,13 +3,14 @@ from sqlalchemy.orm.interfaces import MANYTOONE, ONETOMANY
from sqlalchemy.orm.attributes import InstrumentedAttribute
from sqlalchemy.sql.expression import desc
from wtforms.ext.sqlalchemy.orm import model_form, ModelConverter
from wtforms import fields
from wtforms.ext.sqlalchemy.orm import model_form, converts, ModelConverter
from wtforms.ext.sqlalchemy.fields import QuerySelectField, QuerySelectMultipleField
from flask import flash
from flask.ext.adminex.model import BaseModelView
from flask.ext.adminex.form import AdminForm
from flask.ext.adminex import form
class AdminModelConverter(ModelConverter):
......@@ -40,13 +41,17 @@ class AdminModelConverter(ModelConverter):
return self.view.session.query(remote_model)
if prop.direction is MANYTOONE:
return QuerySelectField(query_factory=query_factory, **kwargs)
return QuerySelectField(query_factory=query_factory,
widget=form.ChosenSelectWidget(),
**kwargs)
elif prop.direction is ONETOMANY:
# Skip backrefs
if not local_column.foreign_keys and self.view.hide_backrefs:
return None
return QuerySelectMultipleField(query_factory=query_factory, **kwargs)
return QuerySelectMultipleField(query_factory=query_factory,
widget=form.ChosenSelectWidget(multiple=True),
**kwargs)
else:
# Ignore pk/fk
if isinstance(prop, ColumnProperty):
......@@ -58,6 +63,20 @@ class AdminModelConverter(ModelConverter):
return super(AdminModelConverter, self).convert(model, mapper,
prop, field_args)
@converts('Date')
def conv_date(self, field_args, **kwargs):
field_args['widget'] = form.DatePickerWidget()
return fields.DateField(**field_args)
@converts('DateTime')
def conv_datetime(self, field_args, **kwargs):
field_args['widget'] = form.DateTimePickerWidget()
return fields.DateTimeField(**field_args)
@converts('Time')
def conv_time(self, field_args, **kwargs):
return form.TimeField(**field_args)
class ModelView(BaseModelView):
"""
......@@ -151,7 +170,7 @@ class ModelView(BaseModelView):
Create form from the model.
"""
return model_form(self.model,
AdminForm,
form.AdminForm,
self.form_columns,
field_args=self.form_args,
converter=AdminModelConverter(self))
......
import time
import datetime
from flask.ext import wtf
from wtforms import fields, widgets
class AdminForm(wtf.Form):
......@@ -17,3 +21,77 @@ class AdminForm(wtf.Form):
return True
return False
class TimeField(fields.Field):
"""
A text field which stores a `datetime.time` matching a format.
"""
widget = widgets.TextInput()
def __init__(self, label=None, validators=None, formats=None, **kwargs):
"""
Constructor
`label`
Label
`validators`
Field validators
`formats`
Supported time formats, as a enumerable.
`kwargs`
Any additional parameters
"""
super(TimeField, self).__init__(label, validators, **kwargs)
self.format = formats or ('%H:%M:%S', '%H:%M',
'%I:%M:%S%p', '%I:%M%p',
'%I:%M:%S %p', '%I:%M %p')
def _value(self):
if self.raw_data:
return u' '.join(self.raw_data)
else:
return self.data and self.data.strftime(self.format) or u''
def process_formdata(self, valuelist):
if valuelist:
date_str = u' '.join(valuelist)
for format in self.formats:
try:
timetuple = time.strptime(date_str, format)
self.data = datetime.time(timetuple.tm_hour,
timetuple.tm_min,
timetuple.tm_sec)
return
except ValueError:
pass
raise ValueError('Invalid time format')
class ChosenSelectWidget(widgets.Select):
def __call__(self, field, **kwargs):
if field.allow_blank and not self.multiple:
kwargs['data-role'] = u'chosenblank'
else:
kwargs['data-role'] = u'chosen'
return super(ChosenSelectWidget, self).__call__(field, **kwargs)
class ChosenSelectField(fields.SelectField):
widget = ChosenSelectWidget
class DatePickerWidget(widgets.TextInput):
def __call__(self, field, **kwargs):
kwargs['data-role'] = u'datepicker'
return super(DatePickerWidget, self).__call__(field, **kwargs)
class DateTimePickerWidget(widgets.TextInput):
def __call__(self, field, **kwargs):
kwargs['data-role'] = u'datetimepicker'
return super(DateTimePickerWidget, self).__call__(field, **kwargs)
......@@ -98,7 +98,7 @@ class BaseModelView(BaseView):
form_args = None
"""
Dictionary of form field arguments. Refer to WTForm documentation on
Dictionary of form field arguments. Refer to WTForm documentation for
list of possible options.
Example::
......@@ -106,7 +106,7 @@ class BaseModelView(BaseView):
class MyModelView(BaseModelView):
form_args = dict(
name=dict(label='First Name', validators=[wtf.required()])
}
)
"""
# Various settings
......
.datepicker {
background-color: #ffffff;
border-color: #999;
border-color: rgba(0, 0, 0, 0.2);
border-style: solid;
border-width: 1px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
-webkit-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
-moz-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
-webkit-background-clip: padding-box;
-moz-background-clip: padding-box;
background-clip: padding-box;
display: none;
position: absolute;
z-index: 900;
margin-left: 0;
margin-right: 0;
margin-bottom: 18px;
padding-bottom: 4px;
width: 218px;
}
.datepicker .nav {
font-weight: bold;
width: 100%;
padding: 4px 0;
background-color: #f5f5f5;
color: #808080;
border-bottom: 1px solid #ddd;
-webkit-box-shadow: inset 0 1px 0 #ffffff;
-moz-box-shadow: inset 0 1px 0 #ffffff;
box-shadow: inset 0 1px 0 #ffffff;
zoom: 1;
}
.datepicker .nav:before, .datepicker .nav:after {
display: table;
content: "";
zoom: 1;
*display: inline;
}
.datepicker .nav:after {
clear: both;
}
.datepicker .nav span {
display: block;
float: left;
text-align: center;
height: 28px;
line-height: 28px;
position: relative;
}
.datepicker .nav .bg {
width: 100%;
background-color: #fdf5d9;
height: 28px;
position: absolute;
top: 0;
left: 0;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
}
.datepicker .nav .fg {
width: 100%;
position: absolute;
top: 0;
left: 0;
}
.datepicker .button {
cursor: pointer;
padding: 0 4px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
}
.datepicker .button:hover {
background-color: #808080;
color: #ffffff;
}
.datepicker .months {
float: left;
margin-left: 4px;
}
.datepicker .months .name {
width: 72px;
padding: 0;
}
.datepicker .years {
float: right;
margin-right: 4px;
}
.datepicker .years .name {
width: 36px;
padding: 0;
}
.datepicker .dow, .datepicker .days div {
float: left;
width: 30px;
line-height: 25px;
text-align: center;
}
.datepicker .dow {
font-weight: bold;
color: #808080;
}
.datepicker .calendar {
padding: 4px;
}
.datepicker .days div {
cursor: pointer;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
}
.datepicker .days div:hover {
background-color: #0064cd;
color: #ffffff;
}
.datepicker .overlap {
color: #bfbfbf;
}
.datepicker .today {
background-color: #fee9cc;
}
.datepicker .selected {
background-color: #bfbfbf;
color: #ffffff;
}
.datepicker .time {
clear: both;
padding-top: 8px;
margin-left: 4px;
}
.datepicker .time label {
text-align: center;
}
.datepicker .time input {
width: 200px;
}
This diff is collapsed.
$(function() {
$('[data-role=chosen]').chosen();
$('[data-role=chosenblank]').chosen({allow_single_deselect: true});
$('[data-role=datepicker]').datepicker();
$('[data-role=datetimepicker]').datepicker({displayTime: true});
});
<html>
<head>
<link href="../bootstrap/css/bootstrap.css" rel="stylesheet">
<link href="../css/datepicker.css" rel="stylesheet">
</head>
<body>
<form action="">
<input data-datepicker="datepicker" type="text" value="2011-10-12" />
</form>
<script src="http://code.jquery.com/jquery-1.7.min.js"></script>
<script src="bootstrap-datepicker.js"></script>
</body>
</html>
\ No newline at end of file
......@@ -2,6 +2,7 @@
{% block head %}
<link href="{{ url_for('admin.static', filename='chosen/chosen.css') }}" rel="stylesheet">
<link href="{{ url_for('admin.static', filename='css/datepicker.css') }}" rel="stylesheet">
{% endblock %}
{% block body %}
......@@ -37,7 +38,6 @@
{% endblock %}
{% block tail %}
<script>
$("select").chosen({allow_single_deselect: true});
</script>
<script src="{{ url_for('admin.static', filename='js/bootstrap-datepicker.js') }}"></script>
<script src="{{ url_for('admin.static', filename='js/form.js') }}"></script>
{% endblock %}
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