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

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

parent d463725e
- Core - Core
- Right-side menu items (auth?) - Right-side menu items (auth?)
- Pregenerate URLs for menu - Pregenerate URLs for menu
- Conditional js include for forms or pages
- More form widgets (date time, time, etc)
- Model Admin - Model Admin
- Ability to sort by fields that are not visible? - Ability to sort by fields that are not visible?
- SQLA Model Admin - SQLA Model Admin
- Use of date time widgets
- Validation of the joins in the query - Validation of the joins in the query
- Automatic joined load for foreign keys - Automatic joined load for foreign keys
- Built-in filtering support - Built-in filtering support
- Many2Many support - Many2Many support
- Ability to override form field types
- WYSIWYG editor support
- File admin - File admin
- Documentation - Documentation
- Unit tests - Unit tests
......
...@@ -8,7 +8,7 @@ Introduction ...@@ -8,7 +8,7 @@ Introduction
------------ ------------
While developing the library, I attempted to make it as flexible as possible. Developer should 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 Library uses one simple, but powerful concept - administrative pieces are built as classes with
view methods. view methods.
...@@ -30,12 +30,12 @@ implementing reusable functional pieces that are highly customizable. ...@@ -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 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 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), 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 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 import Flask
from flask.ext.adminex import Admin from flask.ext.adminex import Admin
...@@ -47,7 +47,7 @@ To start using Admin, you have to create `Admin` class instance and associate it ...@@ -47,7 +47,7 @@ To start using Admin, you have to create `Admin` class instance and associate it
app.run() 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 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:: You can change application name by passing `name` parameter to the `Admin` class constructor::
......
...@@ -31,6 +31,7 @@ class Post(db.Model): ...@@ -31,6 +31,7 @@ class Post(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(120)) title = db.Column(db.String(120))
text = db.Column(db.Text) text = db.Column(db.Text)
date = db.Column(db.DateTime)
user_id = db.Column(db.Integer, db.ForeignKey(User.id)) user_id = db.Column(db.Integer, db.ForeignKey(User.id))
user = db.relationship(User, backref='posts') user = db.relationship(User, backref='posts')
......
...@@ -3,13 +3,14 @@ from sqlalchemy.orm.interfaces import MANYTOONE, ONETOMANY ...@@ -3,13 +3,14 @@ from sqlalchemy.orm.interfaces import MANYTOONE, ONETOMANY
from sqlalchemy.orm.attributes import InstrumentedAttribute from sqlalchemy.orm.attributes import InstrumentedAttribute
from sqlalchemy.sql.expression import desc 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 wtforms.ext.sqlalchemy.fields import QuerySelectField, QuerySelectMultipleField
from flask import flash from flask import flash
from flask.ext.adminex.model import BaseModelView from flask.ext.adminex.model import BaseModelView
from flask.ext.adminex.form import AdminForm from flask.ext.adminex import form
class AdminModelConverter(ModelConverter): class AdminModelConverter(ModelConverter):
...@@ -40,13 +41,17 @@ class AdminModelConverter(ModelConverter): ...@@ -40,13 +41,17 @@ class AdminModelConverter(ModelConverter):
return self.view.session.query(remote_model) return self.view.session.query(remote_model)
if prop.direction is MANYTOONE: 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: elif prop.direction is ONETOMANY:
# Skip backrefs # Skip backrefs
if not local_column.foreign_keys and self.view.hide_backrefs: if not local_column.foreign_keys and self.view.hide_backrefs:
return None return None
return QuerySelectMultipleField(query_factory=query_factory, **kwargs) return QuerySelectMultipleField(query_factory=query_factory,
widget=form.ChosenSelectWidget(multiple=True),
**kwargs)
else: else:
# Ignore pk/fk # Ignore pk/fk
if isinstance(prop, ColumnProperty): if isinstance(prop, ColumnProperty):
...@@ -58,6 +63,20 @@ class AdminModelConverter(ModelConverter): ...@@ -58,6 +63,20 @@ class AdminModelConverter(ModelConverter):
return super(AdminModelConverter, self).convert(model, mapper, return super(AdminModelConverter, self).convert(model, mapper,
prop, field_args) 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): class ModelView(BaseModelView):
""" """
...@@ -151,7 +170,7 @@ class ModelView(BaseModelView): ...@@ -151,7 +170,7 @@ class ModelView(BaseModelView):
Create form from the model. Create form from the model.
""" """
return model_form(self.model, return model_form(self.model,
AdminForm, form.AdminForm,
self.form_columns, self.form_columns,
field_args=self.form_args, field_args=self.form_args,
converter=AdminModelConverter(self)) converter=AdminModelConverter(self))
......
import time
import datetime
from flask.ext import wtf from flask.ext import wtf
from wtforms import fields, widgets
class AdminForm(wtf.Form): class AdminForm(wtf.Form):
...@@ -17,3 +21,77 @@ class AdminForm(wtf.Form): ...@@ -17,3 +21,77 @@ class AdminForm(wtf.Form):
return True return True
return False 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): ...@@ -98,7 +98,7 @@ class BaseModelView(BaseView):
form_args = None 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. list of possible options.
Example:: Example::
...@@ -106,7 +106,7 @@ class BaseModelView(BaseView): ...@@ -106,7 +106,7 @@ class BaseModelView(BaseView):
class MyModelView(BaseModelView): class MyModelView(BaseModelView):
form_args = dict( form_args = dict(
name=dict(label='First Name', validators=[wtf.required()]) name=dict(label='First Name', validators=[wtf.required()])
} )
""" """
# Various settings # 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 @@ ...@@ -2,6 +2,7 @@
{% block head %} {% block head %}
<link href="{{ url_for('admin.static', filename='chosen/chosen.css') }}" rel="stylesheet"> <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 %} {% endblock %}
{% block body %} {% block body %}
...@@ -37,7 +38,6 @@ ...@@ -37,7 +38,6 @@
{% endblock %} {% endblock %}
{% block tail %} {% block tail %}
<script> <script src="{{ url_for('admin.static', filename='js/bootstrap-datepicker.js') }}"></script>
$("select").chosen({allow_single_deselect: true}); <script src="{{ url_for('admin.static', filename='js/form.js') }}"></script>
</script>
{% endblock %} {% 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