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;
}
/* ===========================================================
* bootstrap-datepicker.js v1.3.0
* http://twitter.github.com/bootstrap/javascript.html#datepicker
* ===========================================================
* Copyright 2011 Twitter, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributed by Scott Torborg - github.com/storborg
* Loosely based on jquery.date_input.js by Jon Leighton, heavily updated and
* rewritten to match bootstrap javascript approach and add UI features.
*
* Enhancements and fixes by Serge S. Koval - github.com/mrjoes
*
* =========================================================== */
!function ( $ ) {
var all = [];
function clearDatePickers(except) {
var ii;
for(ii = 0; ii < all.length; ii++) {
if(all[ii] != except) {
all[ii].hide();
}
}
}
function DatePicker( element, options ) {
this.$el = $(element);
this.proxy('show').proxy('ahead').proxy('hide').proxy('keyHandler').proxy('selectDate');
var options = $.extend({}, $.fn.datepicker.defaults, options );
// If custom parse/format function was not passed in options and time-based display
// is enabled, use time-compatible parse/format functions
if (options.displayTime && !!!options.parse && !!!options.format) {
options.parse = this.parseDateTime;
}
$.extend(this, options);
this.$el.data('datepicker', this);
all.push(this);
this.init();
}
DatePicker.prototype = {
init: function() {
var $months = this.nav('months', 1);
var $years = this.nav('years', 12);
var $nav = $('<div>').addClass('nav').append($months, $years);
this.$month = $('.name', $months);
this.$year = $('.name', $years);
$calendar = $("<div>").addClass('calendar');
// Populate day of week headers, realigned by startOfWeek.
for (var i = 0; i < this.shortDayNames.length; i++) {
$calendar.append('<div class="dow">' + this.shortDayNames[(i + this.startOfWeek) % 7] + '</div>');
};
this.$days = $('<div>').addClass('days');
$calendar.append(this.$days);
this.$picker = $('<div>')
.addClass('datepicker')
.append($nav, $calendar)
.insertAfter(this.$el);
this.$picker.children()
.click(function(e) { e.stopPropagation() })
// Use this to prevent accidental text selection.
.mousedown(function(e) { e.preventDefault() });
if (this.displayTime) {
this.$time = $('<div class="time">')
.append('<label for="_time">Time</label>')
.append('<input id="_time" type="text" />')
.click(function(e) { e.stopPropagation(); });
this.$picker.append(this.$time);
}
this.$el
.focus(this.show)
.click(this.show)
.change($.proxy(function() { this.selectDate(); }, this));
this.selectDate();
this.hide();
}
, nav: function( c, months ) {
var $subnav = $('<div>' +
'<span class="prev button">&larr;</span>' +
'<span class="name"></span>' +
'<span class="next button">&rarr;</span>' +
'</div>').addClass(c)
$('.prev', $subnav).click($.proxy(function() { this.ahead(-months, 0) }, this));
$('.next', $subnav).click($.proxy(function() { this.ahead(months, 0) }, this));
return $subnav;
}
, updateName: function($area, s) {
// Update either the month or year field, with a background flash
// animation.
var cur = $area.find('.fg').text(),
$fg = $('<div>').addClass('fg').append(s);
$area.empty();
if(cur != s) {
var $bg = $('<div>').addClass('bg');
$area.append($bg, $fg);
$bg.fadeOut('slow', function() {
$(this).remove();
});
} else {
$area.append($fg);
}
}
, selectMonth: function(date) {
this.displayedDate = date;
var newMonth = new Date(date.getFullYear(), date.getMonth(), 1);
if (!this.curMonth || !(this.curMonth.getFullYear() == newMonth.getFullYear() &&
this.curMonth.getMonth() == newMonth.getMonth())) {
this.curMonth = newMonth;
var rangeStart = this.rangeStart(date), rangeEnd = this.rangeEnd(date);
var num_days = this.daysBetween(rangeStart, rangeEnd);
this.$days.empty();
for (var ii = 0; ii <= num_days; ii++) {
var thisDay = new Date(rangeStart.getFullYear(), rangeStart.getMonth(), rangeStart.getDate() + ii, 12, 00);
var $day = $('<div>').attr('date', this.format(thisDay));
$day.text(thisDay.getDate());
if (thisDay.getMonth() != date.getMonth()) {
$day.addClass('overlap');
};
this.$days.append($day);
};
this.updateName(this.$month, this.monthNames[date.getMonth()]);
this.updateName(this.$year, this.curMonth.getFullYear());
$('div', this.$days).click($.proxy(function(e) {
var $targ = $(e.target);
// The date= attribute is used here to provide relatively fast
// selectors for setting certain date cells.
this.update($targ.attr("date"));
// Don't consider this selection final if we're just going to an
// adjacent month.
if(!$targ.hasClass('overlap')) {
this.hide();
}
}, this));
$("[date='" + this.format(new Date()) + "']", this.$days).addClass('today');
};
$('.selected', this.$days).removeClass('selected');
$('[date="' + this.selectedDateStr + '"]', this.$days).addClass('selected');
}
, selectDate: function(date) {
if (typeof(date) == "undefined") {
date = this.parse(this.$el.val());
};
if (!date) {
date = new Date();
if (this.displayTime) {
date = new Date(date.getFullYear(), date.getMonth(), date.getDate());
}
}
this.selectedDate = date;
this.displayedDate = date;
this.selectedDateStr = this.format(this.selectedDate);
this.selectMonth(this.selectedDate);
if (this.displayTime) {
$('input', this.$time).val(this.formatTime(date));
}
}
, update: function(s) {
if (this.displayTime)
s = s + ' ' + this.getTimeString()
this.$el.val(s).change();
}
, show: function(e) {
e && e.stopPropagation();
// Hide all other datepickers.
clearDatePickers(this);
this.selectDate();
var offset = this.$el.offset();
this.$picker.css({
top: offset.top + this.$el.outerHeight() + 2,
left: offset.left
}).show();
$('html').on('keydown', this.keyHandler);
}
, hide: function() {
this.$picker.hide();
$('html').off('keydown', this.keyHandler);
}
, keyHandler: function(e) {
// Keyboard navigation shortcuts.
switch (e.keyCode)
{
case 9:
case 27:
// Tab or escape hides the datepicker. In this case, just return
// instead of breaking, so that the e doesn't get stopped.
this.hide(); return;
case 13:
// Enter selects the currently highlighted date.
this.update(this.selectedDateStr); this.hide(); break;
default:
return;
}
e.preventDefault();
}
, getTimeString: function(s) {
var time = $('input', this.$time).val();
return this.validateTime(time) ? time : '12:00:00';
}
, pad: function(s) {
if (s.length === 1)
s = '0' + s;
return s;
}
, parse: function(s) {
// Parse a partial RFC 3339 string into a Date.
var m;
if ((m = s.match(/^(\d{4,4})-(\d{2,2})-(\d{2,2})$/))) {
return new Date(m[1], m[2] - 1, m[3]);
} else {
return null;
}
}
, parseDateTime: function(s) {
// Parse a partial RFC 3339 string into a Date.
var m;
if ((m = s.match(/^(\d{4,4})-(\d{2,2})-(\d{2,2}) (\d{2,2}):(\d{2,2}):(\d{2,2})$/))) {
return new Date(m[1], m[2] - 1, m[3], m[4], m[5], m[6]);
} else {
return null;
}
}
, validateTime: function(s) {
return s.match(/^(\d{2,2}):(\d{2,2}):(\d{2,2})$/);
}
, format: function(date) {
// Format a Date into a string as specified by RFC 3339.
var month = this.pad((date.getMonth() + 1).toString()),
dom = this.pad(date.getDate().toString());
return date.getFullYear() + '-' + month + "-" + dom;
}
, formatTime: function(date) {
var hour = this.pad(date.getHours().toString()),
min = this.pad(date.getMinutes().toString()),
sec = this.pad(date.getSeconds().toString());
return hour + ':' + min + ':' + sec;
}
, ahead: function(months, days) {
// Move ahead ``months`` months and ``days`` days, both integers, can be
// negative.
this.selectMonth(new Date(this.displayedDate.getFullYear(),
this.displayedDate.getMonth() + months,
this.displayedDate.getDate() + days));
}
, proxy: function(meth) {
// Bind a method so that it always gets the datepicker instance for
// ``this``. Return ``this`` so chaining calls works.
this[meth] = $.proxy(this[meth], this);
return this;
}
, daysBetween: function(start, end) {
// Return number of days between ``start`` Date object and ``end``.
var start = Date.UTC(start.getFullYear(), start.getMonth(), start.getDate());
var end = Date.UTC(end.getFullYear(), end.getMonth(), end.getDate());
return (end - start) / 86400000;
}
, findClosest: function(dow, date, direction) {
// From a starting date, find the first day ahead of behind it that is
// a given day of the week.
var difference = direction * (Math.abs(date.getDay() - dow - (direction * 7)) % 7);
return new Date(date.getFullYear(), date.getMonth(), date.getDate() + difference);
}
, rangeStart: function(date) {
// Get the first day to show in the current calendar view.
return this.findClosest(this.startOfWeek,
new Date(date.getFullYear(), date.getMonth()),
-1);
}
, rangeEnd: function(date) {
// Get the last day to show in the current calendar view.
return this.findClosest((this.startOfWeek - 1) % 7,
new Date(date.getFullYear(), date.getMonth() + 1, 0),
1);
}
};
/* DATEPICKER PLUGIN DEFINITION
* ============================ */
$.fn.datepicker = function( options ) {
return this.each(function() { new DatePicker(this, options); });
};
$(function() {
$('html').click(clearDatePickers);
});
$.fn.datepicker.DatePicker = DatePicker;
$.fn.datepicker.defaults = {
monthNames: ["January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"]
, shortDayNames: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
, startOfWeek: 1
, displayTime: false
};
}( window.jQuery || window.ender );
$(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