Commit 9f9fee13 authored by Alex Kerney's avatar Alex Kerney

Merge pull request #2 from mrjoes/master

Up to date
parents 94a10946 08a4de57
...@@ -8,6 +8,8 @@ Development Lead ...@@ -8,6 +8,8 @@ Development Lead
Patches and Suggestions Patches and Suggestions
``````````````````````` ```````````````````````
- Paul Brown <paul90brown@gmail.com>
- Petrus Janse van Rensburg <petrus.jvrensburg@gmail.com>
- Priit Laes <plaes@plaes.org> - Priit Laes <plaes@plaes.org>
- Sean Lynch - Sean Lynch
- Andy Wilson <wilson.andrew.j+github@gmail.com> - Andy Wilson <wilson.andrew.j+github@gmail.com>
......
# Translations template for Flask-Admin. # Translations template for Flask-Admin.
# Copyright (C) 2014 ORGANIZATION # Copyright (C) 2015 ORGANIZATION
# This file is distributed under the same license as the Flask-Admin # This file is distributed under the same license as the Flask-Admin
# project. # project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2014. # FIRST AUTHOR <EMAIL@ADDRESS>, 2015.
# #
#, fuzzy #, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Flask-Admin VERSION\n" "Project-Id-Version: Flask-Admin VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2014-11-24 03:04-0600\n" "POT-Creation-Date: 2015-02-28 21:53-0600\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
...@@ -18,153 +18,159 @@ msgstr "" ...@@ -18,153 +18,159 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 1.3\n" "Generated-By: Babel 1.3\n"
#: ../flask_admin/base.py:399 #: ../flask_admin/base.py:426
msgid "Home" msgid "Home"
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:34 #: ../flask_admin/contrib/fileadmin.py:220
msgid "Invalid directory name"
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:42
msgid "File to upload" msgid "File to upload"
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:51 #: ../flask_admin/contrib/fileadmin.py:228
msgid "File required." msgid "File required."
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:56 #: ../flask_admin/contrib/fileadmin.py:233
msgid "Invalid file type." msgid "Invalid file type."
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:60 #: ../flask_admin/contrib/fileadmin.py:244
msgid "Content" msgid "Content"
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:427 #: ../flask_admin/contrib/fileadmin.py:258
msgid "Invalid name"
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:266
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:35
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:35
msgid "Name"
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:544
#, python-format #, python-format
msgid "File \"%(name)s\" already exists." msgid "File \"%(name)s\" already exists."
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:446 #: ../flask_admin/contrib/fileadmin.py:568
#: ../flask_admin/contrib/fileadmin.py:512 #: ../flask_admin/contrib/fileadmin.py:635
#: ../flask_admin/contrib/fileadmin.py:565 #: ../flask_admin/contrib/fileadmin.py:688
#: ../flask_admin/contrib/fileadmin.py:602 #: ../flask_admin/contrib/fileadmin.py:729
#: ../flask_admin/contrib/fileadmin.py:645 #: ../flask_admin/contrib/fileadmin.py:775
#: ../flask_admin/contrib/fileadmin.py:693 #: ../flask_admin/contrib/fileadmin.py:824
msgid "Permission denied." msgid "Permission denied."
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:508 #: ../flask_admin/contrib/fileadmin.py:631
msgid "File uploading is disabled." msgid "File uploading is disabled."
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:521 #: ../flask_admin/contrib/fileadmin.py:644
#, python-format #, python-format
msgid "Failed to save file: %(error)s" msgid "Failed to save file: %(error)s"
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:561 #: ../flask_admin/contrib/fileadmin.py:684
msgid "Directory creation is disabled." msgid "Directory creation is disabled."
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:576 #: ../flask_admin/contrib/fileadmin.py:699
#, python-format #, python-format
msgid "Failed to create directory: %(error)s" msgid "Failed to create directory: %(error)s"
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:598 #: ../flask_admin/contrib/fileadmin.py:725
msgid "Deletion is disabled." msgid "Deletion is disabled."
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:607 #: ../flask_admin/contrib/fileadmin.py:734
msgid "Directory deletion is disabled." msgid "Directory deletion is disabled."
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:613 #: ../flask_admin/contrib/fileadmin.py:740
#, python-format #, python-format
msgid "Directory \"%(path)s\" was successfully deleted." msgid "Directory \"%(path)s\" was successfully deleted."
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:615 #: ../flask_admin/contrib/fileadmin.py:742
#, python-format #, python-format
msgid "Failed to delete directory: %(error)s" msgid "Failed to delete directory: %(error)s"
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:620 #: ../flask_admin/contrib/fileadmin.py:747
#: ../flask_admin/contrib/fileadmin.py:759 #: ../flask_admin/contrib/fileadmin.py:892
#, python-format #, python-format
msgid "File \"%(name)s\" was successfully deleted." msgid "File \"%(name)s\" was successfully deleted."
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:622 #: ../flask_admin/contrib/fileadmin.py:749
#: ../flask_admin/contrib/fileadmin.py:761 #: ../flask_admin/contrib/fileadmin.py:894
#, python-format #, python-format
msgid "Failed to delete file: %(name)s" msgid "Failed to delete file: %(name)s"
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:641 #: ../flask_admin/contrib/fileadmin.py:771
msgid "Renaming is disabled." msgid "Renaming is disabled."
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:649 #: ../flask_admin/contrib/fileadmin.py:779
msgid "Path does not exist." msgid "Path does not exist."
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:661 #: ../flask_admin/contrib/fileadmin.py:790
#, python-format #, python-format
msgid "Successfully renamed \"%(src)s\" to \"%(dst)s\"" msgid "Successfully renamed \"%(src)s\" to \"%(dst)s\""
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:664 #: ../flask_admin/contrib/fileadmin.py:793
#, python-format #, python-format
msgid "Failed to rename: %(error)s" msgid "Failed to rename: %(error)s"
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:709 #: ../flask_admin/contrib/fileadmin.py:840
#, python-format #, python-format
msgid "Error saving changes to %(name)s." msgid "Error saving changes to %(name)s."
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:713 #: ../flask_admin/contrib/fileadmin.py:844
#, python-format #, python-format
msgid "Changes to %(name)s saved successfully." msgid "Changes to %(name)s saved successfully."
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:720 #: ../flask_admin/contrib/fileadmin.py:853
#, python-format #, python-format
msgid "Error reading %(name)s." msgid "Error reading %(name)s."
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:723 #: ../flask_admin/contrib/fileadmin.py:856
#: ../flask_admin/contrib/fileadmin.py:732 #: ../flask_admin/contrib/fileadmin.py:865
#, python-format #, python-format
msgid "Unexpected error while reading from %(name)s" msgid "Unexpected error while reading from %(name)s"
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:729 #: ../flask_admin/contrib/fileadmin.py:862
#, python-format #, python-format
msgid "Cannot edit %(name)s." msgid "Cannot edit %(name)s."
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:746 #: ../flask_admin/contrib/fileadmin.py:879
#: ../flask_admin/contrib/mongoengine/view.py:599 #: ../flask_admin/contrib/mongoengine/view.py:625
#: ../flask_admin/contrib/peewee/view.py:406 #: ../flask_admin/contrib/peewee/view.py:429
#: ../flask_admin/contrib/pymongo/view.py:343 #: ../flask_admin/contrib/pymongo/view.py:348
#: ../flask_admin/contrib/sqla/view.py:924 #: ../flask_admin/contrib/sqla/view.py:974
msgid "Delete" msgid "Delete"
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:747 #: ../flask_admin/contrib/fileadmin.py:880
msgid "Are you sure you want to delete these files?" msgid "Are you sure you want to delete these files?"
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:750 #: ../flask_admin/contrib/fileadmin.py:883
msgid "File deletion is disabled." msgid "File deletion is disabled."
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:763 #: ../flask_admin/contrib/fileadmin.py:896
msgid "Edit" msgid "Edit"
msgstr "" msgstr ""
...@@ -172,125 +178,142 @@ msgstr "" ...@@ -172,125 +178,142 @@ msgstr ""
msgid "Cli: Invalid command." msgid "Cli: Invalid command."
msgstr "" msgstr ""
#: ../flask_admin/contrib/mongoengine/filters.py:36 #: ../flask_admin/contrib/geoa/fields.py:29
#: ../flask_admin/contrib/peewee/filters.py:35 msgid "Invalid JSON"
msgstr ""
#: ../flask_admin/contrib/mongoengine/filters.py:38
#: ../flask_admin/contrib/peewee/filters.py:38
#: ../flask_admin/contrib/pymongo/filters.py:38 #: ../flask_admin/contrib/pymongo/filters.py:38
#: ../flask_admin/contrib/sqla/filters.py:39 #: ../flask_admin/contrib/sqla/filters.py:38
msgid "equals" msgid "equals"
msgstr "" msgstr ""
#: ../flask_admin/contrib/mongoengine/filters.py:45 #: ../flask_admin/contrib/mongoengine/filters.py:47
#: ../flask_admin/contrib/peewee/filters.py:43 #: ../flask_admin/contrib/peewee/filters.py:46
#: ../flask_admin/contrib/pymongo/filters.py:47 #: ../flask_admin/contrib/pymongo/filters.py:47
#: ../flask_admin/contrib/sqla/filters.py:47 #: ../flask_admin/contrib/sqla/filters.py:46
msgid "not equal" msgid "not equal"
msgstr "" msgstr ""
#: ../flask_admin/contrib/mongoengine/filters.py:55 #: ../flask_admin/contrib/mongoengine/filters.py:57
#: ../flask_admin/contrib/peewee/filters.py:52 #: ../flask_admin/contrib/peewee/filters.py:55
#: ../flask_admin/contrib/pymongo/filters.py:57 #: ../flask_admin/contrib/pymongo/filters.py:57
#: ../flask_admin/contrib/sqla/filters.py:56 #: ../flask_admin/contrib/sqla/filters.py:55
msgid "contains" msgid "contains"
msgstr "" msgstr ""
#: ../flask_admin/contrib/mongoengine/filters.py:65 #: ../flask_admin/contrib/mongoengine/filters.py:67
#: ../flask_admin/contrib/peewee/filters.py:61 #: ../flask_admin/contrib/peewee/filters.py:64
#: ../flask_admin/contrib/pymongo/filters.py:67 #: ../flask_admin/contrib/pymongo/filters.py:67
#: ../flask_admin/contrib/sqla/filters.py:65 #: ../flask_admin/contrib/sqla/filters.py:64
msgid "not contains" msgid "not contains"
msgstr "" msgstr ""
#: ../flask_admin/contrib/mongoengine/filters.py:74 #: ../flask_admin/contrib/mongoengine/filters.py:76
#: ../flask_admin/contrib/peewee/filters.py:69 #: ../flask_admin/contrib/peewee/filters.py:72
#: ../flask_admin/contrib/pymongo/filters.py:80 #: ../flask_admin/contrib/pymongo/filters.py:80
#: ../flask_admin/contrib/sqla/filters.py:73 #: ../flask_admin/contrib/sqla/filters.py:72
msgid "greater than" msgid "greater than"
msgstr "" msgstr ""
#: ../flask_admin/contrib/mongoengine/filters.py:83 #: ../flask_admin/contrib/mongoengine/filters.py:85
#: ../flask_admin/contrib/peewee/filters.py:77 #: ../flask_admin/contrib/peewee/filters.py:80
#: ../flask_admin/contrib/pymongo/filters.py:93 #: ../flask_admin/contrib/pymongo/filters.py:93
#: ../flask_admin/contrib/sqla/filters.py:81 #: ../flask_admin/contrib/sqla/filters.py:80
msgid "smaller than" msgid "smaller than"
msgstr "" msgstr ""
#: ../flask_admin/contrib/mongoengine/view.py:493 #: ../flask_admin/contrib/mongoengine/filters.py:97
#: ../flask_admin/contrib/peewee/filters.py:91
#: ../flask_admin/contrib/sqla/filters.py:91
msgid "empty"
msgstr ""
#: ../flask_admin/contrib/mongoengine/filters.py:112
#: ../flask_admin/contrib/peewee/filters.py:105
#: ../flask_admin/contrib/sqla/filters.py:105
msgid "in list"
msgstr ""
#: ../flask_admin/contrib/mongoengine/filters.py:121
#: ../flask_admin/contrib/peewee/filters.py:114
#: ../flask_admin/contrib/sqla/filters.py:114
msgid "not in list"
msgstr ""
#: ../flask_admin/contrib/mongoengine/filters.py:221
#: ../flask_admin/contrib/peewee/filters.py:208
#: ../flask_admin/contrib/peewee/filters.py:245
#: ../flask_admin/contrib/peewee/filters.py:282
#: ../flask_admin/contrib/sqla/filters.py:209
#: ../flask_admin/contrib/sqla/filters.py:246
#: ../flask_admin/contrib/sqla/filters.py:283
msgid "not between"
msgstr ""
#: ../flask_admin/contrib/mongoengine/view.py:519
#, python-format #, python-format
msgid "Failed to get model. %(error)s" msgid "Failed to get model. %(error)s"
msgstr "" msgstr ""
#: ../flask_admin/contrib/mongoengine/view.py:512 #: ../flask_admin/contrib/mongoengine/view.py:538
#: ../flask_admin/contrib/peewee/view.py:357 #: ../flask_admin/contrib/peewee/view.py:380
#: ../flask_admin/contrib/pymongo/view.py:278 #: ../flask_admin/contrib/pymongo/view.py:283
#: ../flask_admin/contrib/sqla/view.py:856 #: ../flask_admin/contrib/sqla/view.py:906
#, python-format #, python-format
msgid "Failed to create record. %(error)s" msgid "Failed to create record. %(error)s"
msgstr "" msgstr ""
#: ../flask_admin/contrib/mongoengine/view.py:538 #: ../flask_admin/contrib/mongoengine/view.py:564
#: ../flask_admin/contrib/peewee/view.py:376 #: ../flask_admin/contrib/peewee/view.py:399
#: ../flask_admin/contrib/pymongo/view.py:303 #: ../flask_admin/contrib/pymongo/view.py:308
#: ../flask_admin/contrib/sqla/view.py:882 #: ../flask_admin/contrib/sqla/view.py:932 ../flask_admin/model/base.py:1613
#: ../flask_admin/model/base.py:1622
#, python-format #, python-format
msgid "Failed to update record. %(error)s" msgid "Failed to update record. %(error)s"
msgstr "" msgstr ""
#: ../flask_admin/contrib/mongoengine/view.py:562 #: ../flask_admin/contrib/mongoengine/view.py:588
#: ../flask_admin/contrib/peewee/view.py:392 #: ../flask_admin/contrib/peewee/view.py:415
#: ../flask_admin/contrib/pymongo/view.py:329 #: ../flask_admin/contrib/pymongo/view.py:334
#: ../flask_admin/contrib/sqla/view.py:908 #: ../flask_admin/contrib/sqla/view.py:958
#, python-format #, python-format
msgid "Failed to delete record. %(error)s" msgid "Failed to delete record. %(error)s"
msgstr "" msgstr ""
#: ../flask_admin/contrib/mongoengine/view.py:600 #: ../flask_admin/contrib/mongoengine/view.py:626
#: ../flask_admin/contrib/peewee/view.py:407 #: ../flask_admin/contrib/peewee/view.py:430
#: ../flask_admin/contrib/pymongo/view.py:344 #: ../flask_admin/contrib/pymongo/view.py:349
#: ../flask_admin/contrib/sqla/view.py:925 #: ../flask_admin/contrib/sqla/view.py:975
msgid "Are you sure you want to delete selected records?" msgid "Are you sure you want to delete selected records?"
msgstr "" msgstr ""
#: ../flask_admin/contrib/mongoengine/view.py:609 #: ../flask_admin/contrib/mongoengine/view.py:635
#: ../flask_admin/contrib/peewee/view.py:423 #: ../flask_admin/contrib/peewee/view.py:446
#: ../flask_admin/contrib/pymongo/view.py:354 #: ../flask_admin/contrib/pymongo/view.py:359
#: ../flask_admin/contrib/sqla/view.py:941 #: ../flask_admin/contrib/sqla/view.py:991 ../flask_admin/model/base.py:1561
#, python-format #, python-format
msgid "Record was successfully deleted." msgid "Record was successfully deleted."
msgid_plural "%(count)s records were successfully deleted." msgid_plural "%(count)s records were successfully deleted."
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: ../flask_admin/contrib/mongoengine/view.py:615 #: ../flask_admin/contrib/mongoengine/view.py:641
#: ../flask_admin/contrib/peewee/view.py:429 #: ../flask_admin/contrib/peewee/view.py:452
#: ../flask_admin/contrib/pymongo/view.py:359 #: ../flask_admin/contrib/pymongo/view.py:364
#: ../flask_admin/contrib/sqla/view.py:949 #: ../flask_admin/contrib/sqla/view.py:999
#, python-format #, python-format
msgid "Failed to delete records. %(error)s" msgid "Failed to delete records. %(error)s"
msgstr "" msgstr ""
#: ../flask_admin/contrib/sqla/fields.py:123 #: ../flask_admin/contrib/sqla/fields.py:123
#: ../flask_admin/contrib/sqla/fields.py:173 #: ../flask_admin/contrib/sqla/fields.py:173
#: ../flask_admin/contrib/sqla/fields.py:178 ../flask_admin/model/fields.py:167 #: ../flask_admin/contrib/sqla/fields.py:178 ../flask_admin/model/fields.py:225
#: ../flask_admin/model/fields.py:216 #: ../flask_admin/model/fields.py:274
msgid "Not a valid choice" msgid "Not a valid choice"
msgstr "" msgstr ""
#: ../flask_admin/contrib/sqla/filters.py:92
msgid "empty"
msgstr ""
#: ../flask_admin/contrib/sqla/filters.py:129
#: ../flask_admin/contrib/sqla/filters.py:179
#: ../flask_admin/contrib/sqla/filters.py:231
msgid "between"
msgstr ""
#: ../flask_admin/contrib/sqla/filters.py:151
#: ../flask_admin/contrib/sqla/filters.py:198
#: ../flask_admin/contrib/sqla/filters.py:252
msgid "not between"
msgstr ""
#: ../flask_admin/contrib/sqla/validators.py:42 #: ../flask_admin/contrib/sqla/validators.py:42
msgid "Already exists." msgid "Already exists."
msgstr "" msgstr ""
...@@ -302,16 +325,16 @@ msgid_plural "At least %d items are required" ...@@ -302,16 +325,16 @@ msgid_plural "At least %d items are required"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: ../flask_admin/contrib/sqla/view.py:835 #: ../flask_admin/contrib/sqla/view.py:885
#, python-format #, python-format
msgid "Integrity error. %(message)s" msgid "Integrity error. %(message)s"
msgstr "" msgstr ""
#: ../flask_admin/form/fields.py:89 #: ../flask_admin/form/fields.py:92
msgid "Invalid time format" msgid "Invalid time format"
msgstr "" msgstr ""
#: ../flask_admin/form/fields.py:133 #: ../flask_admin/form/fields.py:138
msgid "Invalid Choice: could not coerce" msgid "Invalid Choice: could not coerce"
msgstr "" msgstr ""
...@@ -319,20 +342,20 @@ msgstr "" ...@@ -319,20 +342,20 @@ msgstr ""
msgid "Invalid file extension" msgid "Invalid file extension"
msgstr "" msgstr ""
#: ../flask_admin/model/base.py:1094 #: ../flask_admin/model/base.py:1227
msgid "There are no items in the table." msgid "There are no items in the table."
msgstr "" msgstr ""
#: ../flask_admin/model/base.py:1118 #: ../flask_admin/model/base.py:1251
#, python-format #, python-format
msgid "Invalid Filter Value: %(value)s" msgid "Invalid Filter Value: %(value)s"
msgstr "" msgstr ""
#: ../flask_admin/model/base.py:1326 #: ../flask_admin/model/base.py:1484
msgid "Record was successfully created." msgid "Record was successfully created."
msgstr "" msgstr ""
#: ../flask_admin/model/base.py:1363 #: ../flask_admin/model/base.py:1521 ../flask_admin/model/base.py:1618
msgid "Record was successfully saved." msgid "Record was successfully saved."
msgstr "" msgstr ""
...@@ -344,18 +367,23 @@ msgstr "" ...@@ -344,18 +367,23 @@ msgstr ""
msgid "No" msgid "No"
msgstr "" msgstr ""
#: ../flask_admin/model/filters.py:162 ../flask_admin/model/filters.py:202
#: ../flask_admin/model/filters.py:247
msgid "between"
msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/actions.html:4 #: ../flask_admin/templates/bootstrap2/admin/actions.html:4
#: ../flask_admin/templates/bootstrap3/admin/actions.html:4 #: ../flask_admin/templates/bootstrap3/admin/actions.html:4
msgid "With selected" msgid "With selected"
msgstr "" msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/lib.html:156 #: ../flask_admin/templates/bootstrap2/admin/lib.html:156
#: ../flask_admin/templates/bootstrap3/admin/lib.html:149 #: ../flask_admin/templates/bootstrap3/admin/lib.html:150
msgid "Submit" msgid "Submit"
msgstr "" msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/lib.html:161 #: ../flask_admin/templates/bootstrap2/admin/lib.html:161
#: ../flask_admin/templates/bootstrap3/admin/lib.html:154 #: ../flask_admin/templates/bootstrap3/admin/lib.html:155
msgid "Cancel" msgid "Cancel"
msgstr "" msgstr ""
...@@ -370,30 +398,35 @@ msgstr "" ...@@ -370,30 +398,35 @@ msgstr ""
msgid "Root" msgid "Root"
msgstr "" msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:62 #: ../flask_admin/templates/bootstrap2/admin/file/list.html:36
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:62 #: ../flask_admin/templates/bootstrap3/admin/file/list.html:36
msgid "Size"
msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:63
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:63
#, python-format #, python-format
msgid "Are you sure you want to delete \\'%(name)s\\' recursively?" msgid "Are you sure you want to delete \\'%(name)s\\' recursively?"
msgstr "" msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:70 #: ../flask_admin/templates/bootstrap2/admin/file/list.html:72
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:70 #: ../flask_admin/templates/bootstrap3/admin/file/list.html:72
#, python-format #, python-format
msgid "Are you sure you want to delete \\'%(name)s\\'?" msgid "Are you sure you want to delete \\'%(name)s\\'?"
msgstr "" msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:105 #: ../flask_admin/templates/bootstrap2/admin/file/list.html:107
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:105 #: ../flask_admin/templates/bootstrap3/admin/file/list.html:107
msgid "Upload File" msgid "Upload File"
msgstr "" msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:110 #: ../flask_admin/templates/bootstrap2/admin/file/list.html:112
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:110 #: ../flask_admin/templates/bootstrap3/admin/file/list.html:112
msgid "Create Directory" msgid "Create Directory"
msgstr "" msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:127 #: ../flask_admin/templates/bootstrap2/admin/file/list.html:129
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:127 #: ../flask_admin/templates/bootstrap3/admin/file/list.html:129
msgid "Please select at least one file." msgid "Please select at least one file."
msgstr "" msgstr ""
...@@ -427,13 +460,13 @@ msgstr "" ...@@ -427,13 +460,13 @@ msgstr ""
msgid "Save and Continue" msgid "Save and Continue"
msgstr "" msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/model/inline_list_base.html:10 #: ../flask_admin/templates/bootstrap2/admin/model/inline_list_base.html:13
#: ../flask_admin/templates/bootstrap3/admin/model/inline_list_base.html:10 #: ../flask_admin/templates/bootstrap3/admin/model/inline_list_base.html:14
msgid "Delete?" msgid "Delete?"
msgstr "" msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/model/inline_list_base.html:33 #: ../flask_admin/templates/bootstrap2/admin/model/inline_list_base.html:40
#: ../flask_admin/templates/bootstrap3/admin/model/inline_list_base.html:33 #: ../flask_admin/templates/bootstrap3/admin/model/inline_list_base.html:43
msgid "Add" msgid "Add"
msgstr "" msgstr ""
...@@ -455,7 +488,7 @@ msgstr "" ...@@ -455,7 +488,7 @@ msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/model/layout.html:38 #: ../flask_admin/templates/bootstrap2/admin/model/layout.html:38
#: ../flask_admin/templates/bootstrap2/admin/model/layout.html:45 #: ../flask_admin/templates/bootstrap2/admin/model/layout.html:45
#: ../flask_admin/templates/bootstrap3/admin/model/layout.html:38 #: ../flask_admin/templates/bootstrap3/admin/model/layout.html:38
#: ../flask_admin/templates/bootstrap3/admin/model/layout.html:45 #: ../flask_admin/templates/bootstrap3/admin/model/layout.html:43
msgid "Search" msgid "Search"
msgstr "" msgstr ""
...@@ -496,8 +529,8 @@ msgstr "" ...@@ -496,8 +529,8 @@ msgstr ""
msgid "Delete record" msgid "Delete record"
msgstr "" msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:150 #: ../flask_admin/templates/bootstrap2/admin/model/list.html:159
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:150 #: ../flask_admin/templates/bootstrap3/admin/model/list.html:158
msgid "Please select at least one record." msgid "Please select at least one record."
msgstr "" msgstr ""
#!/bin/sh #!/bin/sh
pybabel extract -F babel.ini -k _gettext -k _ngettext -k lazy_gettext -o admin.pot --project Flask-Admin ../flask_admin pybabel extract -F babel.ini -k _gettext -k _ngettext -k lazy_gettext -o admin.pot --project Flask-Admin ../flask_admin
pybabel compile -D admin -d ../flask_admin/translations/ pybabel compile -f -D admin -d ../flask_admin/translations/
Changelog Changelog
========= =========
1.1 (dev) 1.1.0
--------- -----
Mostly bug fix release. Highlights:
* Inline model editing on the list page
* FileAdmin refactoring and fixes
* FileUploadField and ImageUploadField will work with Required() validator
* Bug fixes
1.0.9
-----
Highlights: Highlights:
* Added the ``geoa`` contrib module, for working with `geoalchemy2`_ * Bootstrap 3 support
* WTForms 2.x support
* Updated DateTime picker
* SQLAlchemy backend: support for complex sortables, ability to search for related models, model inheritance support
* Customizable URL generation logic for all views
* New generic filter types: in list, empty, date range
* Added the ``geoa`` contrib module, for working with `geoalchemy2 <http://geoalchemy-2.readthedocs.org/>`_
* Portugese translation
* Lots of bug fixes
.. _geoalchemy2: http://geoalchemy-2.readthedocs.org/
1.0.8 1.0.8
----- -----
...@@ -43,17 +61,3 @@ Highlights: ...@@ -43,17 +61,3 @@ Highlights:
* Redis cli * Redis cli
* SQLAlchemy backend can handle inherited models with multiple PKs * SQLAlchemy backend can handle inherited models with multiple PKs
* Lots of bug fixes * Lots of bug fixes
1.0.6
-----
* Model views now support default sorting order
* Model type/column formatters now accept additional `view` parameter
* `is_visible` for administrative views
* Model views have `after_model_change` method that can be overridden
* In model views, `get_query` was split into `get_count_query` and `get_query`
* Bootstrap 2.3.1
* Bulk deletes go through `delete_model`
* Flask-Admin no longer uses floating navigation bar
* Translations: French, Persian (Farsi), Chinese (Simplified/Traditional), Czech
* Bug fixes
...@@ -37,14 +37,11 @@ Creating simple model ...@@ -37,14 +37,11 @@ Creating simple model
--------------------- ---------------------
GeoAlchemy comes with a `Geometry`_ field that is carefully divorced from the GeoAlchemy comes with a `Geometry`_ field that is carefully divorced from the
`Shapely`_ library. Flask-Admin takes the approach that if you're using spatial `Shapely`_ library. Flask-Admin will use this field so that there are no
objects in your database, and you want an admin interface to edit those objects, changes necessary to other code. ``ModelView`` should be imported from
you're probably already using Shapely, so we provide a Geometry field that is ``geoa`` rather than the one imported from ``sqla``::
integrated with Shapely objects. To make your admin interface works, be sure to
use this field rather that the one that ships with GeoAlchemy when defining your from geoalchemy2 import Geometry
models::
from flask.ext.admin.contrib.geoa.sqltypes import Geometry
from flask.ext.admin.contrib.geoa import ModelView from flask.ext.admin.contrib.geoa import ModelView
# .. flask initialization # .. flask initialization
...@@ -62,9 +59,6 @@ models:: ...@@ -62,9 +59,6 @@ models::
db.create_all() db.create_all()
app.run('0.0.0.0', 8000) app.run('0.0.0.0', 8000)
Note that you also have to use the ``ModelView`` class imported from ``geoa``,
rather than the one imported from ``sqla``.
Limitations Limitations
----------- -----------
......
...@@ -58,13 +58,36 @@ Form Rendering Rule Description ...@@ -58,13 +58,36 @@ Form Rendering Rule Description
Enabling CSRF Validation Enabling CSRF Validation
--------------- ---------------
Adding CSRF validation will require overriding the :class:`flask.ext.admin.form.BaseForm` by using :attr:`flask.ext.admin.model.BaseModelView.form_base_class`.
Flask-Admin does not use Flask-WTF Form class - it uses the wtforms Form class, which does not have CSRF validation. WTForms >=2::
Adding CSRF validation will require importing flask_wtf and overriding the :class:`flask.ext.admin.form.BaseForm` by using :attr:`flask.ext.admin.model.BaseModelView.form_base_class`::
from wtforms.csrf.session import SessionCSRF
from wtforms.meta import DefaultMeta
from flask import session
from datetime import timedelta
from flask.ext.admin import form
from flask.ext.admin.contrib import sqla
class SecureForm(form.BaseForm):
class Meta(DefaultMeta):
csrf = True
csrf_class = SessionCSRF
csrf_secret = b'EPj00jpfj8Gx1SjnyLxwBBSQfnQ9DJYe0Ym'
csrf_time_limit = timedelta(minutes=20)
@property
def csrf_context(self):
return session
class ModelAdmin(sqla.ModelView):
form_base_class = SecureForm
For WTForms 1, you can use use Flask-WTF's Form class::
import os import os
import flask import flask
**import flask_wtf** import flask_wtf
import flask_admin import flask_admin
import flask_sqlalchemy import flask_sqlalchemy
from flask_admin.contrib.sqla import ModelView from flask_admin.contrib.sqla import ModelView
...@@ -74,15 +97,15 @@ Adding CSRF validation will require importing flask_wtf and overriding the :clas ...@@ -74,15 +97,15 @@ Adding CSRF validation will require importing flask_wtf and overriding the :clas
app = flask.Flask(__name__) app = flask.Flask(__name__)
app.config['SECRET_KEY'] = 'Dnit7qz7mfcP0YuelDrF8vLFvk0snhwP' app.config['SECRET_KEY'] = 'Dnit7qz7mfcP0YuelDrF8vLFvk0snhwP'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + DBFILE app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + DBFILE
**app.config['CSRF_ENABLED'] = True** app.config['CSRF_ENABLED'] = True
**flask_wtf.CsrfProtect(app)** flask_wtf.CsrfProtect(app)
db = flask_sqlalchemy.SQLAlchemy(app) db = flask_sqlalchemy.SQLAlchemy(app)
admin = flask_admin.Admin(app, name='Admin') admin = flask_admin.Admin(app, name='Admin')
## Here is the fix:
class MyModelView(ModelView): class MyModelView(ModelView):
**form_base_class = flask_wtf.Form** # Here is the fix:
form_base_class = flask_wtf.Form
class User(db.Model): class User(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
...@@ -92,7 +115,6 @@ Adding CSRF validation will require importing flask_wtf and overriding the :clas ...@@ -92,7 +115,6 @@ Adding CSRF validation will require importing flask_wtf and overriding the :clas
if not os.path.exists(DBFILE): if not os.path.exists(DBFILE):
db.create_all() db.create_all()
## The subclass is used here:
admin.add_view( MyModelView(User, db.session, name='User') ) admin.add_view( MyModelView(User, db.session, name='User') )
app.run(debug=True) app.run(debug=True)
......
...@@ -165,45 +165,18 @@ have access to the view in question:: ...@@ -165,45 +165,18 @@ have access to the view in question::
def is_accessible(self): def is_accessible(self):
return login.current_user.is_authenticated() return login.current_user.is_authenticated()
You can also implement policy-based security, conditionally allowing or disallowing access to parts of the To redirect the user to another page if authentication fails, you will need to specify an *_handle_view* method::
administrative interface. If a user does not have access to a particular view, the menu item won't be visible.
Generating URLs
---------------
Internally, view classes work on top of Flask blueprints, so you can use *url_for* with a dot
prefix to get the URL for a local view::
from flask import url_for
class MyView(BaseView): class MyView(BaseView):
@expose('/') def is_accessible(self):
def index(self) return login.current_user.is_authenticated()
# Get URL for the test view method
url = url_for('.test')
return self.render('index.html', url=url)
@expose('/test/')
def test(self):
return self.render('test.html')
If you want to generate a URL for a particular view method from outside, the following rules apply:
1. You can override the endpoint name by passing *endpoint* parameter to the view class constructor::
admin = Admin(app)
admin.add_view(MyView(endpoint='testadmin'))
In this case, you can generate links by concatenating the view method name with an endpoint::
url_for('testadmin.index')
2. If you don't override the endpoint name, the lower-case class name can be used for generating URLs, like in::
url_for('myview.index')
3. For model-based views the rules differ - the model class name should be used if an endpoint name is not provided. Model-based views will be explained in the next section. def _handle_view(self, name, **kwargs):
if not self.is_accessible():
return redirect(url_for('login', next=request.url))
You can also implement policy-based security, conditionally allowing or disallowing access to parts of the
administrative interface. If a user does not have access to a particular view, the menu item won't be visible.
Model Views Model Views
----------- -----------
...@@ -299,6 +272,51 @@ Sample screenshot: ...@@ -299,6 +272,51 @@ Sample screenshot:
You can disable uploads, disable file or directory deletion, restrict file uploads to certain types and so on. You can disable uploads, disable file or directory deletion, restrict file uploads to certain types and so on.
Check :mod:`flask.ext.admin.contrib.fileadmin` documentation on how to do it. Check :mod:`flask.ext.admin.contrib.fileadmin` documentation on how to do it.
Generating URLs
---------------
Internally, view classes work on top of Flask blueprints, so you can use *url_for* with a dot
prefix to get the URL for a local view::
from flask import url_for
class MyView(BaseView):
@expose('/')
def index(self)
# Get URL for the test view method
url = url_for('.test')
return self.render('index.html', url=url)
@expose('/test/')
def test(self):
return self.render('test.html')
If you want to generate a URL for a particular view method from outside, the following rules apply:
1. You can override the endpoint name by passing *endpoint* parameter to the view class constructor::
admin = Admin(app)
admin.add_view(MyView(endpoint='testadmin'))
In this case, you can generate links by concatenating the view method name with an endpoint::
url_for('testadmin.index')
2. If you don't override the endpoint name, the lower-case class name can be used for generating URLs, like in::
url_for('myview.index')
3. For model-based views the rules differ - the model class name should be used if an endpoint name is not provided. The ModelView also has these endpoints by default: *.index_view*, *.create_view*, and *.edit_view*. So, the following urls can be generated for a model named "User"::
# List View
url_for('user.index_view')
# Create View (redirect back to index_view)
url_for('user.create_view', url=url_for('user.index_view'))
# Edit View for record #1 (redirect back to index_view)
url_for('user.edit_view', id=1, url=url_for('user.index_view'))
Examples Examples
-------- --------
......
__version__ = '1.0.9.dev0' __version__ = '1.1.0'
__author__ = 'Serge S. Koval' __author__ = 'Serge S. Koval'
__email__ = 'serge.koval+github@gmail.com' __email__ = 'serge.koval+github@gmail.com'
......
...@@ -116,6 +116,10 @@ class BaseView(with_metaclass(AdminViewMeta, BaseViewClass)): ...@@ -116,6 +116,10 @@ class BaseView(with_metaclass(AdminViewMeta, BaseViewClass)):
@expose('/') @expose('/')
def index(self): def index(self):
return 'Hello World!' return 'Hello World!'
Icons can be added to the menu by using `menu_icon_type` and `menu_icon_value`. For example::
admin.add_view(MyView(name='My View', menu_icon_type='glyph', menu_icon_value='glyphicon-home'))
""" """
@property @property
def _template_args(self): def _template_args(self):
...@@ -229,9 +233,15 @@ class BaseView(with_metaclass(AdminViewMeta, BaseViewClass)): ...@@ -229,9 +233,15 @@ class BaseView(with_metaclass(AdminViewMeta, BaseViewClass)):
if not self.url.startswith('/'): if not self.url.startswith('/'):
self.url = '%s/%s' % (self.admin.url, self.url) self.url = '%s/%s' % (self.admin.url, self.url)
# If we're working from the root of the site, set prefix to None # If we're working from the root of the site, set prefix to None
if self.url == '/': if self.url == '/':
self.url = None self.url = None
# prevent admin static files from conflicting with flask static files
if not self.static_url_path:
self.static_folder='static'
self.static_url_path='/static/admin'
# If name is not povided, use capitalized endpoint name # If name is not povided, use capitalized endpoint name
if self.name is None: if self.name is None:
...@@ -383,10 +393,22 @@ class AdminIndexView(BaseView): ...@@ -383,10 +393,22 @@ class AdminIndexView(BaseView):
@expose('/') @expose('/')
def index(self): def index(self):
arg1 = 'Hello' arg1 = 'Hello'
return render_template('adminhome.html', arg1=arg1) return self.render('admin/myhome.html', arg1=arg1)
admin = Admin(index_view=MyHomeView()) admin = Admin(index_view=MyHomeView())
Also, you can change the root url from /admin to / with the following::
admin = Admin(
app,
index_view=AdminIndexView(
name='Home',
template='admin/myhome.html',
url='/'
)
)
Default values for the index page are: Default values for the index page are:
* If a name is not provided, 'Home' will be used. * If a name is not provided, 'Home' will be used.
...@@ -397,12 +419,18 @@ class AdminIndexView(BaseView): ...@@ -397,12 +419,18 @@ class AdminIndexView(BaseView):
""" """
def __init__(self, name=None, category=None, def __init__(self, name=None, category=None,
endpoint=None, url=None, endpoint=None, url=None,
template='admin/index.html'): template='admin/index.html',
menu_class_name=None,
menu_icon_type=None,
menu_icon_value=None):
super(AdminIndexView, self).__init__(name or babel.lazy_gettext('Home'), super(AdminIndexView, self).__init__(name or babel.lazy_gettext('Home'),
category, category,
endpoint or 'admin', endpoint or 'admin',
url or '/admin', url or '/admin',
'static') 'static',
menu_class_name=menu_class_name,
menu_icon_type=menu_icon_type,
menu_icon_value=menu_icon_value)
self._template = template self._template = template
@expose() @expose()
......
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)
...@@ -19,48 +19,6 @@ from flask.ext.admin.actions import action, ActionsMixin ...@@ -19,48 +19,6 @@ from flask.ext.admin.actions import action, ActionsMixin
from flask.ext.admin.babel import gettext, lazy_gettext from flask.ext.admin.babel import gettext, lazy_gettext
class NameForm(form.BaseForm):
"""
Form with a filename input field.
Validates if provided name is valid for *nix and Windows systems.
"""
name = fields.StringField()
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 validators.ValidationError(gettext('Invalid directory name'))
class UploadForm(form.BaseForm):
"""
File upload form. Works with FileAdmin instance to check if it is allowed
to upload file with given extension.
"""
upload = fields.FileField(lazy_gettext('File to upload'))
def __init__(self, admin):
self.admin = admin
super(UploadForm, self).__init__(helpers.get_form_data())
def validate_upload(self, field):
if not self.upload.data:
raise validators.ValidationError(gettext('File required.'))
filename = self.upload.data.filename
if not self.admin.is_file_allowed(filename):
raise validators.ValidationError(gettext('Invalid file type.'))
class EditForm(form.BaseForm):
content = fields.TextAreaField(lazy_gettext('Content'),
(validators.required(),))
class FileAdmin(BaseView, ActionsMixin): class FileAdmin(BaseView, ActionsMixin):
""" """
Simple file-management interface. Simple file-management interface.
...@@ -74,11 +32,15 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -74,11 +32,15 @@ class FileAdmin(BaseView, ActionsMixin):
Sample usage:: Sample usage::
import os.path as op
from flask.ext.admin import Admin
from flask.ext.admin.contrib.fileadmin import FileAdmin
admin = Admin() admin = Admin()
path = op.join(op.dirname(__file__), 'static') path = op.join(op.dirname(__file__), 'static')
admin.add_view(FileAdmin(path, '/static/', name='Static Files')) admin.add_view(FileAdmin(path, '/static/', name='Static Files'))
admin.setup_app(app)
""" """
can_upload = True can_upload = True
...@@ -156,9 +118,22 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -156,9 +118,22 @@ class FileAdmin(BaseView, ActionsMixin):
Edit template Edit template
""" """
upload_form = UploadForm form_base_class = form.BaseForm
""" """
Upload form class Base form class. Will be used to create the upload, rename, edit, and delete form.
Allows enabling CSRF validation and useful if you want to have custom
contructor or override some fields.
Example::
class MyBaseForm(Form):
def do_something(self):
pass
class MyAdmin(FileAdmin):
form_base_class = MyBaseForm
""" """
def __init__(self, base_path, base_url=None, def __init__(self, base_path, base_url=None,
...@@ -231,6 +206,139 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -231,6 +206,139 @@ class FileAdmin(BaseView, ActionsMixin):
""" """
return self.base_url return self.base_url
def get_upload_form(self):
"""
Upload form class for file upload view.
Override to implement customized behavior.
"""
class UploadForm(self.form_base_class):
"""
File upload form. Works with FileAdmin instance to check if it
is allowed to upload file with given extension.
"""
upload = fields.FileField(lazy_gettext('File to upload'))
def __init__(self, *args, **kwargs):
super(UploadForm, self).__init__(*args, **kwargs)
self.admin = kwargs['admin']
def validate_upload(self, field):
if not self.upload.data:
raise validators.ValidationError(gettext('File required.'))
filename = self.upload.data.filename
if not self.admin.is_file_allowed(filename):
raise validators.ValidationError(gettext('Invalid file type.'))
return UploadForm
def get_edit_form(self):
"""
Create form class for file editing view.
Override to implement customized behavior.
"""
class EditForm(self.form_base_class):
content = fields.TextAreaField(lazy_gettext('Content'),
(validators.required(),))
return EditForm
def get_name_form(self):
"""
Create form class for renaming and mkdir views.
Override to implement customized behavior.
"""
def validate_name(self, field):
regexp = re.compile(r'^(?!^(PRN|AUX|CLOCK\$|NUL|CON|COM\d|LPT\d|\..*)(\..+)?$)[^\x00-\x1f\\?*:\";|/]+$')
if not regexp.match(field.data):
raise validators.ValidationError(gettext('Invalid name'))
class NameForm(self.form_base_class):
"""
Form with a filename input field.
Validates if provided name is valid for *nix and Windows systems.
"""
name = fields.StringField(lazy_gettext('Name'),
validators=[validators.Required(),
validate_name])
path = fields.HiddenField()
return NameForm
def get_delete_form(self):
"""
Create form class for model delete view.
Override to implement customized behavior.
"""
class DeleteForm(self.form_base_class):
path = fields.HiddenField(validators=[validators.Required()])
return DeleteForm
def upload_form(self):
"""
Instantiate file upload form and return it.
Override to implement custom behavior.
"""
upload_form_class = self.get_upload_form()
if request.form:
# Workaround for allowing both CSRF token + FileField to be submitted
# https://bitbucket.org/danjac/flask-wtf/issue/12/fieldlist-filefield-does-not-follow
formdata = request.form.copy() # as request.form is immutable
formdata.update(request.files)
# admin=self allows the form to use self.is_file_allowed
return upload_form_class(formdata, admin=self)
elif request.files:
return upload_form_class(request.files, admin=self)
else:
return upload_form_class(admin=self)
def name_form(self):
"""
Instantiate form used in rename and mkdir then return it.
Override to implement custom behavior.
"""
name_form_class = self.get_name_form()
if request.form:
return name_form_class(request.form)
elif request.args:
return name_form_class(request.args)
else:
return name_form_class()
def edit_form(self):
"""
Instantiate file editing form and return it.
Override to implement custom behavior.
"""
edit_form_class = self.get_edit_form()
if request.form:
return edit_form_class(request.form)
else:
return edit_form_class()
def delete_form(self):
"""
Instantiate file delete form and return it.
Override to implement custom behavior.
"""
delete_form_class = self.get_delete_form()
if request.form:
return delete_form_class(request.form)
else:
return delete_form_class()
def is_file_allowed(self, filename): def is_file_allowed(self, filename):
""" """
Verify if file can be uploaded. Verify if file can be uploaded.
...@@ -291,6 +399,15 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -291,6 +399,15 @@ class FileAdmin(BaseView, ActionsMixin):
""" """
file_data.save(path) file_data.save(path)
def validate_form(self, form):
"""
Validate the form on submit.
:param form:
Form to validate
"""
return helpers.validate_form_on_submit(form)
def _get_dir_url(self, endpoint, path=None, **kwargs): def _get_dir_url(self, endpoint, path=None, **kwargs):
""" """
Return prettified URL Return prettified URL
...@@ -439,11 +556,16 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -439,11 +556,16 @@ class FileAdmin(BaseView, ActionsMixin):
:param path: :param path:
Optional directory path. If not provided, will use the base directory Optional directory path. If not provided, will use the base directory
""" """
if self.can_delete:
delete_form = self.delete_form()
else:
delete_form = None
# Get path and verify if it is valid # Get path and verify if it is valid
base_path, directory, path = self._normalize_path(path) base_path, directory, path = self._normalize_path(path)
if not self.is_accessible_path(path): if not self.is_accessible_path(path):
flash(gettext('Permission denied.')) flash(gettext('Permission denied.'), 'error')
return redirect(self._get_dir_url('.index')) return redirect(self._get_dir_url('.index'))
# Get directory listing # Get directory listing
...@@ -455,7 +577,7 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -455,7 +577,7 @@ class FileAdmin(BaseView, ActionsMixin):
if parent_path == '.': if parent_path == '.':
parent_path = None parent_path = None
items.append(('..', parent_path, True, 0)) items.append(('..', parent_path, True, 0, 0))
for f in os.listdir(directory): for f in os.listdir(directory):
fp = op.join(directory, f) fp = op.join(directory, f)
...@@ -490,7 +612,8 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -490,7 +612,8 @@ class FileAdmin(BaseView, ActionsMixin):
get_file_url=self._get_file_url, get_file_url=self._get_file_url,
items=items, items=items,
actions=actions, actions=actions,
actions_confirmation=actions_confirmation) actions_confirmation=actions_confirmation,
delete_form=delete_form)
@expose('/upload/', methods=('GET', 'POST')) @expose('/upload/', methods=('GET', 'POST'))
@expose('/upload/<path:path>', methods=('GET', 'POST')) @expose('/upload/<path:path>', methods=('GET', 'POST'))
...@@ -509,16 +632,16 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -509,16 +632,16 @@ class FileAdmin(BaseView, ActionsMixin):
return redirect(self._get_dir_url('.index', path)) return redirect(self._get_dir_url('.index', path))
if not self.is_accessible_path(path): if not self.is_accessible_path(path):
flash(gettext('Permission denied.')) flash(gettext('Permission denied.'), 'error')
return redirect(self._get_dir_url('.index')) return redirect(self._get_dir_url('.index'))
form = self.upload_form(self) form = self.upload_form()
if helpers.validate_form_on_submit(form): if self.validate_form(form):
try: try:
self._save_form_files(directory, path, form) self._save_form_files(directory, path, form)
return redirect(self._get_dir_url('.index', path)) return redirect(self._get_dir_url('.index', path))
except Exception as ex: except Exception as ex:
flash(gettext('Failed to save file: %(error)s', error=ex)) flash(gettext('Failed to save file: %(error)s', error=ex), 'error')
return self.render(self.upload_template, form=form) return self.render(self.upload_template, form=form)
...@@ -562,18 +685,20 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -562,18 +685,20 @@ class FileAdmin(BaseView, ActionsMixin):
return redirect(dir_url) return redirect(dir_url)
if not self.is_accessible_path(path): if not self.is_accessible_path(path):
flash(gettext('Permission denied.')) flash(gettext('Permission denied.'), 'error')
return redirect(self._get_dir_url('.index')) return redirect(self._get_dir_url('.index'))
form = NameForm(helpers.get_form_data()) form = self.name_form()
if helpers.validate_form_on_submit(form): if self.validate_form(form):
try: try:
os.mkdir(op.join(directory, form.name.data)) os.mkdir(op.join(directory, form.name.data))
self.on_mkdir(directory, form.name.data) self.on_mkdir(directory, form.name.data)
return redirect(dir_url) return redirect(dir_url)
except Exception as ex: except Exception as ex:
flash(gettext('Failed to create directory: %(error)s', error=ex), 'error') flash(gettext('Failed to create directory: %(error)s', error=ex), 'error')
else:
helpers.flash_errors(form, message='Failed to create directory: %(error)s')
return self.render(self.mkdir_template, return self.render(self.mkdir_template,
form=form, form=form,
...@@ -584,27 +709,29 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -584,27 +709,29 @@ class FileAdmin(BaseView, ActionsMixin):
""" """
Delete view method Delete view method
""" """
path = request.form.get('path') form = self.delete_form()
if not path: path = form.path.data
return redirect(self.get_url('.index')) if path:
return_url = self._get_dir_url('.index', op.dirname(path))
else:
return_url = self.get_url('.index')
if self.validate_form(form):
# Get path and verify if it is valid # Get path and verify if it is valid
base_path, full_path, path = self._normalize_path(path) base_path, full_path, path = self._normalize_path(path)
return_url = self._get_dir_url('.index', op.dirname(path))
if not self.can_delete: if not self.can_delete:
flash(gettext('Deletion is disabled.')) flash(gettext('Deletion is disabled.'), 'error')
return redirect(return_url) return redirect(return_url)
if not self.is_accessible_path(path): if not self.is_accessible_path(path):
flash(gettext('Permission denied.')) flash(gettext('Permission denied.'), 'error')
return redirect(self._get_dir_url('.index')) return redirect(self._get_dir_url('.index'))
if op.isdir(full_path): if op.isdir(full_path):
if not self.can_delete_dirs: if not self.can_delete_dirs:
flash(gettext('Directory deletion is disabled.')) flash(gettext('Directory deletion is disabled.'), 'error')
return redirect(return_url) return redirect(return_url)
try: try:
...@@ -620,6 +747,8 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -620,6 +747,8 @@ class FileAdmin(BaseView, ActionsMixin):
flash(gettext('File "%(name)s" was successfully deleted.', name=path)) flash(gettext('File "%(name)s" was successfully deleted.', name=path))
except Exception as ex: except Exception as ex:
flash(gettext('Failed to delete file: %(name)s', name=ex), 'error') flash(gettext('Failed to delete file: %(name)s', name=ex), 'error')
else:
helpers.flash_errors(form, message='Failed to delete file. %(error)s')
return redirect(return_url) return redirect(return_url)
...@@ -628,29 +757,29 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -628,29 +757,29 @@ class FileAdmin(BaseView, ActionsMixin):
""" """
Rename view method Rename view method
""" """
path = request.args.get('path') form = self.name_form()
if not path:
return redirect(self.get_url('.index'))
path = form.path.data
if path:
base_path, full_path, path = self._normalize_path(path) base_path, full_path, path = self._normalize_path(path)
return_url = self._get_dir_url('.index', op.dirname(path)) return_url = self._get_dir_url('.index', op.dirname(path))
else:
return redirect(self.get_url('.index'))
if not self.can_rename: if not self.can_rename:
flash(gettext('Renaming is disabled.')) flash(gettext('Renaming is disabled.'), 'error')
return redirect(return_url) return redirect(return_url)
if not self.is_accessible_path(path): if not self.is_accessible_path(path):
flash(gettext('Permission denied.')) flash(gettext('Permission denied.'), 'error')
return redirect(self._get_dir_url('.index')) return redirect(self._get_dir_url('.index'))
if not op.exists(full_path): if not op.exists(full_path):
flash(gettext('Path does not exist.')) flash(gettext('Path does not exist.'), 'error')
return redirect(return_url) return redirect(return_url)
form = NameForm(helpers.get_form_data(), name=op.basename(path)) if self.validate_form(form):
if helpers.validate_form_on_submit(form):
try: try:
dir_base = op.dirname(full_path) dir_base = op.dirname(full_path)
filename = secure_filename(form.name.data) filename = secure_filename(form.name.data)
...@@ -664,6 +793,8 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -664,6 +793,8 @@ class FileAdmin(BaseView, ActionsMixin):
flash(gettext('Failed to rename: %(error)s', error=ex), 'error') flash(gettext('Failed to rename: %(error)s', error=ex), 'error')
return redirect(return_url) return redirect(return_url)
else:
helpers.flash_errors(form, message='Failed to rename: %(error)s')
return self.render(self.rename_template, return self.render(self.rename_template,
form=form, form=form,
...@@ -690,16 +821,16 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -690,16 +821,16 @@ class FileAdmin(BaseView, ActionsMixin):
base_path, full_path, path = self._normalize_path(path) base_path, full_path, path = self._normalize_path(path)
if not self.is_accessible_path(path) or not self.is_file_editable(path): if not self.is_accessible_path(path) or not self.is_file_editable(path):
flash(gettext('Permission denied.')) flash(gettext('Permission denied.'), 'error')
return redirect(self._get_dir_url('.index')) return redirect(self._get_dir_url('.index'))
dir_url = self._get_dir_url('.index', os.path.dirname(path)) dir_url = self._get_dir_url('.index', os.path.dirname(path))
next_url = next_url or dir_url next_url = next_url or dir_url
form = EditForm(helpers.get_form_data()) form = self.edit_form()
error = False error = False
if helpers.validate_form_on_submit(form): if self.validate_form(form):
form.process(request.form, content='') form.process(request.form, content='')
if form.validate(): if form.validate():
try: try:
...@@ -713,8 +844,10 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -713,8 +844,10 @@ class FileAdmin(BaseView, ActionsMixin):
flash(gettext("Changes to %(name)s saved successfully.", name=path)) flash(gettext("Changes to %(name)s saved successfully.", name=path))
return redirect(next_url) return redirect(next_url)
else: else:
helpers.flash_errors(form, message='Failed to edit file. %(error)s')
try: try:
with open(full_path, 'r') as f: with open(full_path, 'rb') as f:
content = f.read() content = f.read()
except IOError: except IOError:
flash(gettext("Error reading %(name)s.", name=path), 'error') flash(gettext("Error reading %(name)s.", name=path), 'error')
......
...@@ -2,6 +2,10 @@ import json ...@@ -2,6 +2,10 @@ import json
from wtforms.fields import TextAreaField from wtforms.fields import TextAreaField
from shapely.geometry import shape, mapping from shapely.geometry import shape, mapping
from .widgets import LeafletWidget from .widgets import LeafletWidget
from sqlalchemy import func
import geoalchemy2
#from types import NoneType
#from .. import db how do you get db.session in a Field?
class JSONField(TextAreaField): class JSONField(TextAreaField):
...@@ -9,7 +13,7 @@ class JSONField(TextAreaField): ...@@ -9,7 +13,7 @@ class JSONField(TextAreaField):
if self.raw_data: if self.raw_data:
return self.raw_data[0] return self.raw_data[0]
if self.data: if self.data:
return self.to_json(self.data) return self.data
return "" return ""
def process_formdata(self, valuelist): def process_formdata(self, valuelist):
...@@ -34,18 +38,31 @@ class JSONField(TextAreaField): ...@@ -34,18 +38,31 @@ class JSONField(TextAreaField):
class GeoJSONField(JSONField): class GeoJSONField(JSONField):
widget = LeafletWidget() widget = LeafletWidget()
def __init__(self, label=None, validators=None, geometry_type="GEOMETRY", **kwargs): def __init__(self, label=None, validators=None, geometry_type="GEOMETRY", srid='-1', session=None, **kwargs):
super(GeoJSONField, self).__init__(label, validators, **kwargs) super(GeoJSONField, self).__init__(label, validators, **kwargs)
self.web_srid = 4326
self.srid = srid
if self.srid is -1:
self.transform_srid = self.web_srid
else:
self.transform_srid = self.srid
self.geometry_type = geometry_type.upper() self.geometry_type = geometry_type.upper()
self.session = session
def _value(self): def _value(self):
if self.raw_data: if self.raw_data:
return self.raw_data[0] return self.raw_data[0]
if self.data: if type(self.data) is geoalchemy2.elements.WKBElement:
self.data = mapping(self.data) if self.srid is -1:
self.data = self.session.scalar(func.ST_AsGeoJson(self.data))
else:
self.data = self.session.scalar(func.ST_AsGeoJson(func.ST_Transform(self.data, self.web_srid)))
return super(GeoJSONField, self)._value() return super(GeoJSONField, self)._value()
def process_formdata(self, valuelist): def process_formdata(self, valuelist):
super(GeoJSONField, self).process_formdata(valuelist) super(GeoJSONField, self).process_formdata(valuelist)
if self.data: if str(self.data) is '':
self.data = shape(self.data) self.data = None
if self.data is not None:
web_shape = self.session.scalar(func.ST_AsText(func.ST_Transform(func.ST_GeomFromText(shape(self.data).wkt, self.web_srid), self.transform_srid)))
self.data = 'SRID='+str(self.srid)+';'+str(web_shape)
...@@ -4,7 +4,9 @@ from .fields import GeoJSONField ...@@ -4,7 +4,9 @@ from .fields import GeoJSONField
class AdminModelConverter(SQLAAdminConverter): class AdminModelConverter(SQLAAdminConverter):
@converts('Geometry') @converts('Geography', 'Geometry')
def convert_geom(self, column, field_args, **extra): def convert_geom(self, column, field_args, **extra):
field_args['geometry_type'] = column.type.geometry_type field_args['geometry_type'] = column.type.geometry_type
field_args['srid'] = column.type.srid
field_args['session'] = self.session
return GeoJSONField(**field_args) return GeoJSONField(**field_args)
from geoalchemy2 import Geometry as BaseGeometry
from geoalchemy2.shape import to_shape
class Geometry(BaseGeometry):
"""
PostGIS datatype that can convert directly to/from Shapely objects,
without worrying about WKTElements or WKBElements.
"""
def result_processor(self, dialect, coltype):
to_wkbelement = super(Geometry, self).result_processor(dialect, coltype)
def process(value):
if value:
return to_shape(to_wkbelement(value))
else:
return None
return process
def bind_processor(self, dialect):
from_wktelement = super(Geometry, self).bind_processor(dialect)
def process(value):
if value:
return from_wktelement(value.wkt)
else:
return None
return process
...@@ -2,8 +2,10 @@ from flask.ext.admin.contrib.sqla.typefmt import DEFAULT_FORMATTERS as BASE_FORM ...@@ -2,8 +2,10 @@ from flask.ext.admin.contrib.sqla.typefmt import DEFAULT_FORMATTERS as BASE_FORM
import json import json
from jinja2 import Markup from jinja2 import Markup
from wtforms.widgets import html_params from wtforms.widgets import html_params
from shapely.geometry import mapping from geoalchemy2.shape import to_shape
from shapely.geometry.base import BaseGeometry from geoalchemy2.elements import WKBElement
from sqlalchemy import func
from flask import current_app
def geom_formatter(view, value): def geom_formatter(view, value):
...@@ -12,12 +14,15 @@ def geom_formatter(view, value): ...@@ -12,12 +14,15 @@ def geom_formatter(view, value):
"disabled": "disabled", "disabled": "disabled",
"data-width": 100, "data-width": 100,
"data-height": 70, "data-height": 70,
"data-geometry-type": value.geom_type, "data-geometry-type": to_shape(value).geom_type,
"data-zoom": 15, "data-zoom": 15,
}) })
geojson = json.dumps(mapping(value)) if value.srid is -1:
geojson = current_app.extensions['sqlalchemy'].db.session.scalar(func.ST_AsGeoJson(value))
else:
geojson = current_app.extensions['sqlalchemy'].db.session.scalar(func.ST_AsGeoJson(value.ST_Transform( 4326)))
return Markup('<textarea %s>%s</textarea>' % (params, geojson)) return Markup('<textarea %s>%s</textarea>' % (params, geojson))
DEFAULT_FORMATTERS = BASE_FORMATTERS.copy() DEFAULT_FORMATTERS = BASE_FORMATTERS.copy()
DEFAULT_FORMATTERS[BaseGeometry] = geom_formatter DEFAULT_FORMATTERS[WKBElement] = geom_formatter
...@@ -10,6 +10,8 @@ def lng(pt): ...@@ -10,6 +10,8 @@ def lng(pt):
class LeafletWidget(TextArea): class LeafletWidget(TextArea):
data_role = 'leaflet'
""" """
`Leaflet <http://leafletjs.com/>`_ styled map widget. Inherits from `Leaflet <http://leafletjs.com/>`_ styled map widget. Inherits from
`TextArea` so that geographic data can be stored via the <textarea> `TextArea` so that geographic data can be stored via the <textarea>
...@@ -31,14 +33,14 @@ class LeafletWidget(TextArea): ...@@ -31,14 +33,14 @@ class LeafletWidget(TextArea):
self.max_bounds = max_bounds self.max_bounds = max_bounds
def __call__(self, field, **kwargs): def __call__(self, field, **kwargs):
kwargs.setdefault('data-role', 'leaflet') kwargs.setdefault('data-role', self.data_role)
gtype = getattr(field, "geometry_type", "GEOMETRY") gtype = getattr(field, "geometry_type", "GEOMETRY")
kwargs.setdefault('data-geometry-type', gtype) kwargs.setdefault('data-geometry-type', gtype)
# set optional values from constructor # set optional values from constructor
if self.width: if not "data-width" in kwargs:
kwargs["data-width"] = self.width kwargs["data-width"] = self.width
if self.height: if not "data-height" in kwargs:
kwargs["data-height"] = self.height kwargs["data-height"] = self.height
if self.center: if self.center:
kwargs["data-lat"] = lat(self.center) kwargs["data-lat"] = lat(self.center)
......
...@@ -238,8 +238,10 @@ class FilterConverter(filters.BaseFilterConverter): ...@@ -238,8 +238,10 @@ class FilterConverter(filters.BaseFilterConverter):
FilterEmpty) FilterEmpty)
def convert(self, type_name, column, name): def convert(self, type_name, column, name):
if type_name in self.converters: filter_name = type_name.lower()
return self.converters[type_name](column, name)
if filter_name in self.converters:
return self.converters[filter_name](column, name)
return None return None
......
...@@ -5,6 +5,8 @@ from flask import request, flash, abort, Response ...@@ -5,6 +5,8 @@ from flask import request, flash, abort, Response
from flask.ext.admin import expose from flask.ext.admin import expose
from flask.ext.admin.babel import gettext, ngettext, lazy_gettext from flask.ext.admin.babel import gettext, ngettext, lazy_gettext
from flask.ext.admin.model import BaseModelView from flask.ext.admin.model import BaseModelView
from flask.ext.admin.model.form import wrap_fields_in_fieldlist
from flask.ext.admin.model.fields import ListEditableFieldList
from flask.ext.admin._compat import iteritems, string_types from flask.ext.admin._compat import iteritems, string_types
import mongoengine import mongoengine
...@@ -21,7 +23,6 @@ from .helpers import format_error ...@@ -21,7 +23,6 @@ from .helpers import format_error
from .ajax import process_ajax_references, create_ajax_loader from .ajax import process_ajax_references, create_ajax_loader
from .subdoc import convert_subdocuments from .subdoc import convert_subdocuments
# Set up logger # Set up logger
log = logging.getLogger("flask-admin.mongo") log = logging.getLogger("flask-admin.mongo")
...@@ -398,6 +399,28 @@ class ModelView(BaseModelView): ...@@ -398,6 +399,28 @@ class ModelView(BaseModelView):
return form_class return form_class
def scaffold_list_form(self, custom_fieldlist=ListEditableFieldList,
validators=None):
"""
Create form for the `index_view` using only the columns from
`self.column_editable_list`.
:param validators:
`form_args` dict with only validators
{'name': {'validators': [required()]}}
:param custom_fieldlist:
A WTForm FieldList class. By default, `ListEditableFieldList`.
"""
form_class = get_form(self.model,
self.model_form_converter(self),
base_class=self.form_base_class,
only=self.column_editable_list,
field_args=validators)
return wrap_fields_in_fieldlist(self.form_base_class,
form_class,
custom_fieldlist)
# AJAX foreignkey support # AJAX foreignkey support
def _create_ajax_loader(self, name, opts): def _create_ajax_loader(self, name, opts):
return create_ajax_loader(self.model, name, name, opts) return create_ajax_loader(self.model, name, name, opts)
...@@ -409,6 +432,26 @@ class ModelView(BaseModelView): ...@@ -409,6 +432,26 @@ class ModelView(BaseModelView):
""" """
return self.model.objects return self.model.objects
def _search(self, query, search_term):
# TODO: Unfortunately, MongoEngine contains bug which
# prevents running complex Q queries and, as a result,
# Flask-Admin does not support per-word searching like
# in other backends
op, term = parse_like_term(search_term)
criteria = None
for field in self._search_fields:
flt = {'%s__%s' % (field.name, op): term}
q = mongoengine.Q(**flt)
if criteria is None:
criteria = q
else:
criteria |= q
return query.filter(criteria)
def get_list(self, page, sort_column, sort_desc, search, filters, def get_list(self, page, sort_column, sort_desc, search, filters,
execute=True): execute=True):
""" """
...@@ -437,24 +480,7 @@ class ModelView(BaseModelView): ...@@ -437,24 +480,7 @@ class ModelView(BaseModelView):
# Search # Search
if self._search_supported and search: if self._search_supported and search:
# TODO: Unfortunately, MongoEngine contains bug which query = self._search(query, search)
# prevents running complex Q queries and, as a result,
# Flask-Admin does not support per-word searching like
# in other backends
op, term = parse_like_term(search)
criteria = None
for field in self._search_fields:
flt = {'%s__%s' % (field.name, op): term}
q = mongoengine.Q(**flt)
if criteria is None:
criteria = q
else:
criteria |= q
query = query.filter(criteria)
# Get count # Get count
count = query.count() count = query.count()
......
...@@ -305,8 +305,10 @@ class FilterConverter(filters.BaseFilterConverter): ...@@ -305,8 +305,10 @@ class FilterConverter(filters.BaseFilterConverter):
FilterEmpty) FilterEmpty)
def convert(self, type_name, column, name): def convert(self, type_name, column, name):
if type_name in self.converters: filter_name = type_name.lower()
return self.converters[type_name](column, name)
if filter_name in self.converters:
return self.converters[filter_name](column, name)
return None return None
......
...@@ -5,6 +5,8 @@ from flask import flash ...@@ -5,6 +5,8 @@ from flask import flash
from flask.ext.admin._compat import string_types from flask.ext.admin._compat import string_types
from flask.ext.admin.babel import gettext, ngettext, lazy_gettext from flask.ext.admin.babel import gettext, ngettext, lazy_gettext
from flask.ext.admin.model import BaseModelView from flask.ext.admin.model import BaseModelView
from flask.ext.admin.model.form import wrap_fields_in_fieldlist
from flask.ext.admin.model.fields import ListEditableFieldList
from peewee import PrimaryKeyField, ForeignKeyField, Field, CharField, TextField from peewee import PrimaryKeyField, ForeignKeyField, Field, CharField, TextField
...@@ -237,6 +239,27 @@ class ModelView(BaseModelView): ...@@ -237,6 +239,27 @@ class ModelView(BaseModelView):
return form_class return form_class
def scaffold_list_form(self, custom_fieldlist=ListEditableFieldList,
validators=None):
"""
Create form for the `index_view` using only the columns from
`self.column_editable_list`.
:param validators:
`form_args` dict with only validators
{'name': {'validators': [required()]}}
:param custom_fieldlist:
A WTForm FieldList class. By default, `ListEditableFieldList`.
"""
form_class = get_form(self.model, self.model_form_converter(self),
base_class=self.form_base_class,
only=self.column_editable_list,
field_args=validators)
return wrap_fields_in_fieldlist(self.form_base_class,
form_class,
custom_fieldlist)
def scaffold_inline_form_models(self, form_class): def scaffold_inline_form_models(self, form_class):
converter = self.model_form_converter(self) converter = self.model_form_converter(self)
inline_converter = self.inline_model_form_converter(self) inline_converter = self.inline_model_form_converter(self)
......
...@@ -146,6 +146,42 @@ class ModelView(BaseModelView): ...@@ -146,6 +146,42 @@ class ModelView(BaseModelView):
""" """
return model.get(name) return model.get(name)
def _search(self, query, search_term):
values = search_term.split(' ')
queries = []
# Construct inner querie
for value in values:
if not value:
continue
regex = parse_like_term(value)
stmt = []
for field in self._search_fields:
stmt.append({field: {'$regex': regex}})
if stmt:
if len(stmt) == 1:
queries.append(stmt[0])
else:
queries.append({'$or': stmt})
# Construct final query
if queries:
if len(queries) == 1:
final = queries[0]
else:
final = {'$and': queries}
if query:
query = {'$and': [query, final]}
else:
query = final
return query
def get_list(self, page, sort_column, sort_desc, search, filters, def get_list(self, page, sort_column, sort_desc, search, filters,
execute=True): execute=True):
""" """
...@@ -182,38 +218,7 @@ class ModelView(BaseModelView): ...@@ -182,38 +218,7 @@ class ModelView(BaseModelView):
# Search # Search
if self._search_supported and search: if self._search_supported and search:
values = search.split(' ') query = self._search(query, search)
queries = []
# Construct inner querie
for value in values:
if not value:
continue
regex = parse_like_term(value)
stmt = []
for field in self._search_fields:
stmt.append({field: {'$regex': regex}})
if stmt:
if len(stmt) == 1:
queries.append(stmt[0])
else:
queries.append({'$or': stmt})
# Construct final query
if queries:
if len(queries) == 1:
final = queries[0]
else:
final = {'$and': queries}
if query:
query = {'$and': [query, final]}
else:
query = final
# Get count # Get count
count = self.coll.find(query).count() count = self.coll.find(query).count()
......
...@@ -58,7 +58,7 @@ class QueryAjaxModelLoader(AjaxModelLoader): ...@@ -58,7 +58,7 @@ class QueryAjaxModelLoader(AjaxModelLoader):
def get_list(self, term, offset=0, limit=DEFAULT_PAGE_SIZE): def get_list(self, term, offset=0, limit=DEFAULT_PAGE_SIZE):
query = self.session.query(self.model) query = self.session.query(self.model)
filters = (field.like(u'%%%s%%' % term) for field in self._cached_fields) filters = (field.ilike(u'%%%s%%' % term) for field in self._cached_fields)
query = query.filter(or_(*filters)) query = query.filter(or_(*filters))
return query.offset(offset).limit(limit).all() return query.offset(offset).limit(limit).all()
......
...@@ -308,8 +308,11 @@ class FilterConverter(filters.BaseFilterConverter): ...@@ -308,8 +308,11 @@ class FilterConverter(filters.BaseFilterConverter):
FilterEmpty) FilterEmpty)
def convert(self, type_name, column, name, **kwargs): def convert(self, type_name, column, name, **kwargs):
if type_name.lower() in self.converters: filter_name = type_name.lower()
return self.converters[type_name.lower()](column, name, **kwargs)
if filter_name in self.converters:
return self.converters[filter_name](column, name, **kwargs)
return None return None
@filters.convert('string', 'char', 'unicode', 'varchar', 'tinytext', @filters.convert('string', 'char', 'unicode', 'varchar', 'tinytext',
......
...@@ -607,9 +607,8 @@ class InlineModelConverter(InlineModelConverterBase): ...@@ -607,9 +607,8 @@ class InlineModelConverter(InlineModelConverterBase):
if label: if label:
kwargs['label'] = label kwargs['label'] = label
view_info = self.get_info(self.view) if self.view.form_args:
if view_info.form_args: field_args = self.view.form_args.get(forward_prop.key, {})
field_args = view_info.form_args.get(forward_prop.key, {})
kwargs.update(**field_args) kwargs.update(**field_args)
# Contribute field # Contribute field
......
...@@ -4,7 +4,7 @@ from sqlalchemy.exc import DBAPIError ...@@ -4,7 +4,7 @@ from sqlalchemy.exc import DBAPIError
from ast import literal_eval from ast import literal_eval
from flask.ext.admin._compat import filter_list from flask.ext.admin._compat import filter_list
from flask.ext.admin.tools import iterencode, iterdecode from flask.ext.admin.tools import iterencode, iterdecode, escape
def parse_like_term(term): def parse_like_term(term):
......
...@@ -11,6 +11,9 @@ from flask import flash ...@@ -11,6 +11,9 @@ from flask import flash
from flask.ext.admin._compat import string_types from flask.ext.admin._compat import string_types
from flask.ext.admin.babel import gettext, ngettext, lazy_gettext from flask.ext.admin.babel import gettext, ngettext, lazy_gettext
from flask.ext.admin.model import BaseModelView from flask.ext.admin.model import BaseModelView
from flask.ext.admin.model.form import wrap_fields_in_fieldlist
from flask.ext.admin.model.fields import ListEditableFieldList
from flask.ext.admin.actions import action from flask.ext.admin.actions import action
from flask.ext.admin._backwards import ObsoleteAttr from flask.ext.admin._backwards import ObsoleteAttr
...@@ -19,7 +22,6 @@ from .typefmt import DEFAULT_FORMATTERS ...@@ -19,7 +22,6 @@ from .typefmt import DEFAULT_FORMATTERS
from .tools import get_query_for_ids from .tools import get_query_for_ids
from .ajax import create_ajax_loader from .ajax import create_ajax_loader
# Set up logger # Set up logger
log = logging.getLogger("flask-admin.sqla") log = logging.getLogger("flask-admin.sqla")
...@@ -78,8 +80,7 @@ class ModelView(BaseModelView): ...@@ -78,8 +80,7 @@ class ModelView(BaseModelView):
'searchable_columns', 'searchable_columns',
None) None)
""" """
Collection of the searchable columns. Only text-based columns Collection of the searchable columns.
are searchable (`String`, `Unicode`, `Text`, `UnicodeText`).
Example:: Example::
...@@ -339,6 +340,18 @@ class ModelView(BaseModelView): ...@@ -339,6 +340,18 @@ class ModelView(BaseModelView):
else: else:
attr = name attr = name
# determine joins if Table.column (relation object) is given
if isinstance(name, InstrumentedAttribute):
columns = self._get_columns_for_field(name)
if len(columns) > 1:
raise Exception('Can only handle one column for %s' % name)
column = columns[0]
if self._need_join(column.table):
join_tables.append(column.table)
return join_tables, attr return join_tables, attr
def _need_join(self, table): def _need_join(self, table):
...@@ -360,7 +373,7 @@ class ModelView(BaseModelView): ...@@ -360,7 +373,7 @@ class ModelView(BaseModelView):
if isinstance(self._primary_key, tuple): if isinstance(self._primary_key, tuple):
return tools.iterencode(getattr(model, attr) for attr in self._primary_key) return tools.iterencode(getattr(model, attr) for attr in self._primary_key)
else: else:
return getattr(model, self._primary_key) return tools.escape(getattr(model, self._primary_key))
def scaffold_list_columns(self): def scaffold_list_columns(self):
""" """
...@@ -439,15 +452,18 @@ class ModelView(BaseModelView): ...@@ -439,15 +452,18 @@ class ModelView(BaseModelView):
for c in self.column_sortable_list: for c in self.column_sortable_list:
if isinstance(c, tuple): if isinstance(c, tuple):
join_tables, column = self._get_field_with_path(c[1]) join_tables, column = self._get_field_with_path(c[1])
column_name = c[0]
result[c[0]] = column elif isinstance(c, InstrumentedAttribute):
join_tables, column = self._get_field_with_path(c)
if join_tables: column_name = str(c)
self._sortable_joins[c[0]] = join_tables
else: else:
join_tables, column = self._get_field_with_path(c) join_tables, column = self._get_field_with_path(c)
column_name = c
result[c] = column result[column_name] = column
if join_tables:
self._sortable_joins[column_name] = join_tables
return result return result
...@@ -474,10 +490,6 @@ class ModelView(BaseModelView): ...@@ -474,10 +490,6 @@ class ModelView(BaseModelView):
for column in self._get_columns_for_field(attr): for column in self._get_columns_for_field(attr):
column_type = type(column.type).__name__ 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) self._search_fields.append(column)
# Store joins, avoid duplicates # Store joins, avoid duplicates
...@@ -488,18 +500,6 @@ class ModelView(BaseModelView): ...@@ -488,18 +500,6 @@ class ModelView(BaseModelView):
return bool(self.column_searchable_list) return bool(self.column_searchable_list)
def is_text_column_type(self, name):
"""
Verify if the provided column type is text-based.
:returns:
``True`` for ``String``, ``Unicode``, ``Text``, ``UnicodeText``, ``varchar``
"""
if name:
name = name.lower()
return name in ('string', 'unicode', 'text', 'unicodetext', 'varchar')
def scaffold_filters(self, name): def scaffold_filters(self, name):
""" """
Return list of enabled filters Return list of enabled filters
...@@ -535,8 +535,8 @@ class ModelView(BaseModelView): ...@@ -535,8 +535,8 @@ class ModelView(BaseModelView):
if join_tables: if join_tables:
self._filter_joins[table.name] = join_tables self._filter_joins[table.name] = join_tables
elif self._need_join(table.name): elif self._need_join(table):
self._filter_joins[table.name] = [table.name] self._filter_joins[table.name] = [table]
filters.extend(flt) filters.extend(flt)
return filters return filters
...@@ -611,6 +611,28 @@ class ModelView(BaseModelView): ...@@ -611,6 +611,28 @@ class ModelView(BaseModelView):
return form_class return form_class
def scaffold_list_form(self, custom_fieldlist=ListEditableFieldList,
validators=None):
"""
Create form for the `index_view` using only the columns from
`self.column_editable_list`.
:param validators:
`form_args` dict with only validators
{'name': {'validators': [required()]}}
:param custom_fieldlist:
A WTForm FieldList class. By default, `ListEditableFieldList`.
"""
converter = self.model_form_converter(self.session, self)
form_class = form.get_form(self.model, converter,
base_class=self.form_base_class,
only=self.column_editable_list,
field_args=validators)
return wrap_fields_in_fieldlist(self.form_base_class,
form_class,
custom_fieldlist)
def scaffold_inline_form_models(self, form_class): def scaffold_inline_form_models(self, form_class):
""" """
Contribute inline models to the form Contribute inline models to the form
...@@ -664,6 +686,14 @@ class ModelView(BaseModelView): ...@@ -664,6 +686,14 @@ class ModelView(BaseModelView):
Return a query for the model type. Return a query for the model type.
If you override this method, don't forget to override `get_count_query` as well. If you override this method, don't forget to override `get_count_query` as well.
This method can be used to set a "persistent filter" on an index_view.
Example::
class MyView(ModelView):
def get_query(self):
return super(MyView, self).get_query().filter(User.username == current_user.username)
""" """
return self.session.query(self.model) return self.session.query(self.model)
...@@ -716,7 +746,7 @@ class ModelView(BaseModelView): ...@@ -716,7 +746,7 @@ class ModelView(BaseModelView):
join_tables, attr = self._get_field_with_path(field) join_tables, attr = self._get_field_with_path(field)
return join_tables, field, direction return join_tables, attr, direction
return None return None
......
...@@ -69,13 +69,16 @@ class TimeField(fields.Field): ...@@ -69,13 +69,16 @@ class TimeField(fields.Field):
def _value(self): def _value(self):
if self.raw_data: if self.raw_data:
return u' '.join(self.raw_data) return u' '.join(self.raw_data)
elif self.data is not None:
return self.data.strftime(self.default_format)
else: else:
return self.data and self.data.strftime(self.default_format) or u'' return u''
def process_formdata(self, valuelist): def process_formdata(self, valuelist):
if valuelist: if valuelist:
date_str = u' '.join(valuelist) date_str = u' '.join(valuelist)
if date_str.strip():
for format in self.formats: for format in self.formats:
try: try:
timetuple = time.strptime(date_str, format) timetuple = time.strptime(date_str, format)
...@@ -87,6 +90,8 @@ class TimeField(fields.Field): ...@@ -87,6 +90,8 @@ class TimeField(fields.Field):
pass pass
raise ValueError(gettext('Invalid time format')) raise ValueError(gettext('Invalid time format'))
else:
self.data = None
class Select2Field(fields.SelectField): class Select2Field(fields.SelectField):
......
...@@ -179,11 +179,13 @@ class FileUploadField(fields.StringField): ...@@ -179,11 +179,13 @@ class FileUploadField(fields.StringField):
filename.rsplit('.', 1)[1].lower() in filename.rsplit('.', 1)[1].lower() in
map(lambda x: x.lower(), self.allowed_extensions)) map(lambda x: x.lower(), self.allowed_extensions))
def _is_uploaded_file(self, data):
return (data
and isinstance(data, FileStorage)
and data.filename)
def pre_validate(self, form): def pre_validate(self, form):
if (self.data if self._is_uploaded_file(self.data) and not self.is_file_allowed(self.data.filename):
and self.data.filename
and isinstance(self.data, FileStorage)
and not self.is_file_allowed(self.data.filename)):
raise ValidationError(gettext('Invalid file extension')) raise ValidationError(gettext('Invalid file extension'))
def process(self, formdata, data=unset_value): def process(self, formdata, data=unset_value):
...@@ -194,6 +196,15 @@ class FileUploadField(fields.StringField): ...@@ -194,6 +196,15 @@ class FileUploadField(fields.StringField):
return super(FileUploadField, self).process(formdata, data) return super(FileUploadField, self).process(formdata, data)
def process_formdata(self, valuelist):
if self._should_delete:
self.data = None
elif valuelist:
data = valuelist[0]
if self._is_uploaded_file(data):
self.data = data
def populate_obj(self, obj, name): def populate_obj(self, obj, name):
field = getattr(obj, name, None) field = getattr(obj, name, None)
if field: if field:
...@@ -203,7 +214,7 @@ class FileUploadField(fields.StringField): ...@@ -203,7 +214,7 @@ class FileUploadField(fields.StringField):
setattr(obj, name, None) setattr(obj, name, None)
return return
if self.data and self.data.filename and isinstance(self.data, FileStorage): if self._is_uploaded_file(self.data):
if field: if field:
self._delete_file(field) self._delete_file(field)
...@@ -299,7 +310,7 @@ class ImageUploadField(FileUploadField): ...@@ -299,7 +310,7 @@ class ImageUploadField(FileUploadField):
upload = FileUploadField('File', namegen=prefix_name) upload = FileUploadField('File', namegen=prefix_name)
:param allowed_extensions: :param allowed_extensions:
List of allowed extensions. If not provided, will allow any file. List of allowed extensions. If not provided, then gif, jpg, jpeg, png and tiff will be allowed.
:param max_size: :param max_size:
Tuple of (width, height, force) or None. If provided, Flask-Admin will Tuple of (width, height, force) or None. If provided, Flask-Admin will
resize image to the desired size. resize image to the desired size.
...@@ -357,9 +368,7 @@ class ImageUploadField(FileUploadField): ...@@ -357,9 +368,7 @@ class ImageUploadField(FileUploadField):
def pre_validate(self, form): def pre_validate(self, form):
super(ImageUploadField, self).pre_validate(form) super(ImageUploadField, self).pre_validate(form)
if (self.data and if self._is_uploaded_file(self.data):
isinstance(self.data, FileStorage) and
self.data.filename):
try: try:
self.image = Image.open(self.data) self.image = Image.open(self.data)
except Exception as e: except Exception as e:
...@@ -396,7 +405,7 @@ class ImageUploadField(FileUploadField): ...@@ -396,7 +405,7 @@ class ImageUploadField(FileUploadField):
self._save_image(image, self._get_path(filename), format) self._save_image(image, self._get_path(filename), format)
else: else:
data.seek(0) data.seek(0)
data.save( self._get_path(filename) ) data.save(self._get_path(filename))
self._save_thumbnail(data, filename, format) self._save_thumbnail(data, filename, format)
......
from re import sub from re import sub
from jinja2 import contextfunction 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 wtforms.validators import DataRequired, InputRequired
from flask.ext.admin._compat import urljoin, urlparse from flask.ext.admin._compat import urljoin, urlparse, iteritems
from ._compat import string_types from ._compat import string_types
...@@ -56,7 +55,7 @@ def is_form_submitted(): ...@@ -56,7 +55,7 @@ def is_form_submitted():
""" """
Check if current method is PUT or POST Check if current method is PUT or POST
""" """
return request and request.method in ("PUT", "POST") return request and request.method in ('PUT', 'POST')
def validate_form_on_submit(form): def validate_form_on_submit(form):
...@@ -95,6 +94,12 @@ def is_field_error(errors): ...@@ -95,6 +94,12 @@ def is_field_error(errors):
return False return False
def flash_errors(form, message):
from flask.ext.admin.babel import gettext
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 @contextfunction
def resolve_ctx(context): def resolve_ctx(context):
""" """
......
import warnings import warnings
import re import re
from flask import request, redirect, flash, abort, json, Response from flask import (request, redirect, flash, abort, json, Response,
get_flashed_messages)
from jinja2 import contextfunction from jinja2 import contextfunction
from wtforms.validators import ValidationError from wtforms.fields import HiddenField
from wtforms.validators import ValidationError, Required
from flask.ext.admin.babel import gettext from flask.ext.admin.babel import gettext
...@@ -11,12 +13,14 @@ from flask.ext.admin.base import BaseView, expose ...@@ -11,12 +13,14 @@ from flask.ext.admin.base import BaseView, expose
from flask.ext.admin.form import BaseForm, FormOpts, rules from flask.ext.admin.form import BaseForm, FormOpts, rules
from flask.ext.admin.model import filters, typefmt from flask.ext.admin.model import filters, typefmt
from flask.ext.admin.actions import ActionsMixin from flask.ext.admin.actions import ActionsMixin
from flask.ext.admin.helpers import get_form_data, validate_form_on_submit, get_redirect_target from flask.ext.admin.helpers import (get_form_data, validate_form_on_submit,
get_redirect_target, flash_errors)
from flask.ext.admin.tools import rec_getattr from flask.ext.admin.tools import rec_getattr
from flask.ext.admin._backwards import ObsoleteAttr from flask.ext.admin._backwards import ObsoleteAttr
from flask.ext.admin._compat import iteritems, OrderedDict, as_unicode from flask.ext.admin._compat import iteritems, OrderedDict, as_unicode
from .helpers import prettify_name, get_mdict_item_or_list from .helpers import prettify_name, get_mdict_item_or_list
from .ajax import AjaxModelLoader from .ajax import AjaxModelLoader
from .fields import ListEditableFieldList
# Used to generate filter query string name # Used to generate filter query string name
...@@ -264,6 +268,16 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -264,6 +268,16 @@ class BaseModelView(BaseView, ActionsMixin):
column_searchable_list = ('name', 'email') column_searchable_list = ('name', 'email')
""" """
column_editable_list = None
"""
Collection of the columns which can be edited from the list view.
For example::
class MyModelView(BaseModelView):
column_editable_list = ('name', 'last_name')
"""
column_choices = None column_choices = None
""" """
Map choices to columns in list view Map choices to columns in list view
...@@ -579,6 +593,13 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -579,6 +593,13 @@ class BaseModelView(BaseView, ActionsMixin):
self._create_form_class = self.get_create_form() self._create_form_class = self.get_create_form()
self._edit_form_class = self.get_edit_form() self._edit_form_class = self.get_edit_form()
self._delete_form_class = self.get_delete_form()
# List View In-Line Editing
if self.column_editable_list:
self._list_form_class = self.get_list_form()
else:
self.column_editable_list = {}
def _refresh_filters_cache(self): def _refresh_filters_cache(self):
self._filters = self.get_filters() self._filters = self.get_filters()
...@@ -833,6 +854,22 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -833,6 +854,22 @@ class BaseModelView(BaseView, ActionsMixin):
""" """
raise NotImplementedError('Please implement scaffold_form method') raise NotImplementedError('Please implement scaffold_form method')
def scaffold_list_form(self, custom_fieldlist=ListEditableFieldList,
validators=None):
"""
Create form for the `index_view` using only the columns from
`self.column_editable_list`.
:param validators:
`form_args` dict with only validators
{'name': {'validators': [required()]}}
:param custom_fieldlist:
A WTForm FieldList class. By default, `ListEditableFieldList`.
Must be implemented in the child class.
"""
raise NotImplementedError('Please implement scaffold_list_form method')
def get_form(self): def get_form(self):
""" """
Get form class. Get form class.
...@@ -847,6 +884,45 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -847,6 +884,45 @@ class BaseModelView(BaseView, ActionsMixin):
return self.scaffold_form() return self.scaffold_form()
def get_list_form(self):
"""
Get form class for the editable list view.
Uses only validators from `form_args` to build the form class.
Allows overriding the editable list view field/widget. For example::
from flask.ext.admin.model.fields import ListEditableFieldList
from flask.ext.admin.model.widgets import XEditableWidget
class CustomWidget(XEditableWidget):
def get_kwargs(self, subfield, kwargs):
if subfield.type == 'TextAreaField':
kwargs['data-type'] = 'textarea'
kwargs['data-rows'] = '20'
# elif: kwargs for other fields
return kwargs
class CustomFieldList(ListEditableFieldList):
widget = CustomWidget()
class MyModelView(BaseModelView):
def get_list_form(self):
return self.scaffold_list_form(CustomFieldList)
"""
if self.form_args:
# get only validators, other form_args can break FieldList wrapper
validators = dict(
(key, {'validators': value["validators"]})
for key, value in iteritems(self.form_args)
if value.get("validators")
)
else:
validators = None
return self.scaffold_list_form(validators=validators)
def get_create_form(self): def get_create_form(self):
""" """
Create form class for model creation view. Create form class for model creation view.
...@@ -863,6 +939,18 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -863,6 +939,18 @@ class BaseModelView(BaseView, ActionsMixin):
""" """
return self.get_form() return self.get_form()
def get_delete_form(self):
"""
Create form class for model delete view.
Override to implement customized behavior.
"""
class DeleteForm(self.form_base_class):
id = HiddenField(validators=[Required()])
url = HiddenField()
return DeleteForm
def create_form(self, obj=None): def create_form(self, obj=None):
""" """
Instantiate model creation form and return it. Instantiate model creation form and return it.
...@@ -879,6 +967,31 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -879,6 +967,31 @@ class BaseModelView(BaseView, ActionsMixin):
""" """
return self._edit_form_class(get_form_data(), obj=obj) return self._edit_form_class(get_form_data(), obj=obj)
def delete_form(self):
"""
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)
elif request.args:
# allow request.args for backward compatibility
return self._delete_form_class(request.args)
else:
return self._delete_form_class()
def list_form(self, obj=None):
"""
Instantiate model editing form for list view and return it.
Override to implement custom behavior.
"""
return self._list_form_class(get_form_data(), obj=obj)
def validate_form(self, form): def validate_form(self, form):
""" """
Validate the form on submit. Validate the form on submit.
...@@ -893,10 +1006,21 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -893,10 +1006,21 @@ class BaseModelView(BaseView, ActionsMixin):
""" """
Verify if column is sortable. Verify if column is sortable.
Not case-sensitive.
:param name:
Column name.
"""
return name.lower() in (x.lower() for x in self._sortable_columns)
def is_editable(self, name):
"""
Verify if column is editable.
:param name: :param name:
Column name. Column name.
""" """
return name in self._sortable_columns return name in self.column_editable_list
def _get_column_by_idx(self, idx): def _get_column_by_idx(self, idx):
""" """
...@@ -1247,6 +1371,16 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1247,6 +1371,16 @@ class BaseModelView(BaseView, ActionsMixin):
""" """
List view List view
""" """
if self.column_editable_list:
form = self.list_form()
else:
form = None
if self.can_delete:
delete_form = self.delete_form()
else:
delete_form = None
# Grab parameters from URL # Grab parameters from URL
view_args = self._get_list_extra_args() view_args = self._get_list_extra_args()
...@@ -1289,29 +1423,33 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1289,29 +1423,33 @@ class BaseModelView(BaseView, ActionsMixin):
search=None, search=None,
filters=None)) filters=None))
return self.render(self.list_template, return self.render(
self.list_template,
data=data, data=data,
form=form,
delete_form=delete_form,
# List # List
list_columns=self._list_columns, list_columns=self._list_columns,
sortable_columns=self._sortable_columns, sortable_columns=self._sortable_columns,
# Stuff editable_columns=self.column_editable_list,
enumerate=enumerate,
get_pk_value=self.get_pk_value,
get_value=self.get_list_value,
return_url=self._get_list_url(view_args),
# Pagination # Pagination
count=count, count=count,
pager_url=pager_url, pager_url=pager_url,
num_pages=num_pages, num_pages=num_pages,
page=view_args.page, page=view_args.page,
# Sorting # Sorting
sort_column=view_args.sort, sort_column=view_args.sort,
sort_desc=view_args.sort_desc, sort_desc=view_args.sort_desc,
sort_url=sort_url, sort_url=sort_url,
# Search # Search
search_supported=self._search_supported, search_supported=self._search_supported,
clear_search_url=clear_search_url, clear_search_url=clear_search_url,
search=view_args.search, search=view_args.search,
# Filters # Filters
filters=self._filters, filters=self._filters,
filter_groups=self._filter_groups, filter_groups=self._filter_groups,
...@@ -1319,7 +1457,14 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1319,7 +1457,14 @@ class BaseModelView(BaseView, ActionsMixin):
# Actions # Actions
actions=actions, actions=actions,
actions_confirmation=actions_confirmation) actions_confirmation=actions_confirmation,
# Misc
enumerate=enumerate,
get_pk_value=self.get_pk_value,
get_value=self.get_list_value,
return_url=self._get_list_url(view_args),
)
@expose('/new/', methods=('GET', 'POST')) @expose('/new/', methods=('GET', 'POST'))
def create_view(self): def create_view(self):
...@@ -1397,18 +1542,26 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1397,18 +1542,26 @@ class BaseModelView(BaseView, ActionsMixin):
""" """
return_url = get_redirect_target() or self.get_url('.index_view') return_url = get_redirect_target() or self.get_url('.index_view')
# TODO: Use post
if not self.can_delete: if not self.can_delete:
return redirect(return_url) return redirect(return_url)
id = get_mdict_item_or_list(request.args, 'id') form = self.delete_form()
if id is None:
return redirect(return_url) if self.validate_form(form):
# id is Required()
id = form.id.data
model = self.get_one(id) model = self.get_one(id)
if model: if model is None:
self.delete_model(model) return redirect(return_url)
# message is flashed from within delete_model if it fails
if self.delete_model(model):
flash(gettext('Record was successfully deleted.'))
return redirect(return_url)
else:
flash_errors(form, message='Failed to delete record. %(error)s')
return redirect(return_url) return redirect(return_url)
...@@ -1433,3 +1586,46 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1433,3 +1586,46 @@ class BaseModelView(BaseView, ActionsMixin):
data = [loader.format(m) for m in loader.get_list(query, offset, limit)] data = [loader.format(m) for m in loader.get_list(query, offset, limit)]
return Response(json.dumps(data), mimetype='application/json') return Response(json.dumps(data), mimetype='application/json')
@expose('/ajax/update/', methods=('POST',))
def ajax_update(self):
"""
Edits a single column of a record in list view.
"""
if not self.column_editable_list:
abort(404)
record = None
form = self.list_form()
# prevent validation issues due to submitting a single field
# delete all fields except the field being submitted
for field in form:
# only the submitted field has a positive last_index
if getattr(field, 'last_index', 0):
record = self.get_one(str(field.last_index))
elif field.name == 'csrf_token':
pass
else:
form.__delitem__(field.name)
if record is None:
return gettext('Failed to update record. %(error)s', error=''), 500
if self.validate_form(form):
if self.update_model(form, record):
# Success
return gettext('Record was successfully saved.')
else:
# Error: No records changed, or problem saving to database.
msgs = ", ".join([msg for msg in get_flashed_messages()])
return gettext('Failed to update record. %(error)s',
error=msgs), 500
else:
for field in form:
for error in field.errors:
# return validation error to x-editable
if isinstance(error, list):
return ", ".join(error), 500
else:
return error, 500
...@@ -3,8 +3,14 @@ import itertools ...@@ -3,8 +3,14 @@ import itertools
from wtforms.validators import ValidationError from wtforms.validators import ValidationError
from wtforms.fields import FieldList, FormField, SelectFieldBase from wtforms.fields import FieldList, FormField, SelectFieldBase
try:
from wtforms.fields import _unset_value as unset_value
except ImportError:
from wtforms.utils import unset_value
from flask.ext.admin._compat import iteritems from flask.ext.admin._compat import iteritems
from .widgets import InlineFieldListWidget, InlineFormWidget, AjaxSelect2Widget from .widgets import (InlineFieldListWidget, InlineFormWidget,
AjaxSelect2Widget, XEditableWidget)
class InlineFieldList(FieldList): class InlineFieldList(FieldList):
...@@ -120,6 +126,58 @@ class InlineModelFormField(FormField): ...@@ -120,6 +126,58 @@ class InlineModelFormField(FormField):
field.populate_obj(obj, name) field.populate_obj(obj, name)
class ListEditableFieldList(FieldList):
"""
Modified FieldList to allow for alphanumeric primary keys.
Used in the editable list view.
"""
widget = XEditableWidget()
def __init__(self, *args, **kwargs):
super(ListEditableFieldList, self).__init__(*args, **kwargs)
# min_entries = 1 is required for the widget to determine the type
self.min_entries = 1
def _extract_indices(self, prefix, formdata):
offset = len(prefix) + 1
for k in formdata:
if k.startswith(prefix):
k = k[offset:].split('-', 1)[0]
# removed "if k.isdigit():"
yield k
def _add_entry(self, formdata=None, data=unset_value, index=None):
assert not self.max_entries or len(self.entries) < self.max_entries, \
'You cannot have more than max_entries entries in this FieldList'
if index is None:
index = self.last_index + 1
self.last_index = index
# '%s-%s' instead of '%s-%d' to allow alphanumeric
name = '%s-%s' % (self.short_name, index)
id = '%s-%s' % (self.id, index)
# support both wtforms 1 and 2
meta = getattr(self, 'meta', None)
if meta:
field = self.unbound_field.bind(
form=None, name=name, prefix=self._prefix, id=id, _meta=meta
)
else:
field = self.unbound_field.bind(
form=None, name=name, prefix=self._prefix, id=id
)
field.process(formdata, data)
self.entries.append(field)
return field
def populate_obj(self, obj, name):
# return data from first item, instead of a list of items
setattr(obj, name, self.data.pop())
class AjaxSelectField(SelectFieldBase): class AjaxSelectField(SelectFieldBase):
""" """
Ajax Model Select Field Ajax Model Select Field
......
...@@ -266,7 +266,7 @@ def convert(*args): ...@@ -266,7 +266,7 @@ def convert(*args):
See :mod:`flask.ext.admin.contrib.sqla.filters` for usage example. See :mod:`flask.ext.admin.contrib.sqla.filters` for usage example.
""" """
def _inner(func): def _inner(func):
func._converter_for = args func._converter_for = list(map(str.lower, args))
return func return func
return _inner return _inner
......
...@@ -3,6 +3,8 @@ import inspect ...@@ -3,6 +3,8 @@ import inspect
from flask.ext.admin.form import BaseForm, rules from flask.ext.admin.form import BaseForm, rules
from flask.ext.admin._compat import iteritems from flask.ext.admin._compat import iteritems
from wtforms.fields.core import UnboundField
def converts(*args): def converts(*args):
def _inner(func): def _inner(func):
...@@ -11,6 +13,35 @@ def converts(*args): ...@@ -11,6 +13,35 @@ def converts(*args):
return _inner return _inner
def wrap_fields_in_fieldlist(form_base_class, form_class, CustomFieldList):
"""
Create a form class with all the fields wrapped in a FieldList.
Wrapping each field in FieldList allows submitting POST requests
in this format: ('<field_name>-<primary_key>', '<value>')
Used in the editable list view.
:param form_base_class:
WTForms form class, by default `form_base_class` from base.
:param form_class:
WTForms form class generated by `form.get_form`.
:param CustomFieldList:
WTForms FieldList class.
By default, `CustomFieldList` is `ListEditableFieldList`.
"""
class FieldListForm(form_base_class):
pass
# iterate FormMeta to get unbound fields
for name, obj in iteritems(form_class.__dict__):
if isinstance(obj, UnboundField):
# wrap field in a WTForms FieldList
setattr(FieldListForm, name, CustomFieldList(obj))
return FieldListForm
class InlineBaseFormAdmin(object): class InlineBaseFormAdmin(object):
""" """
Settings for inline form administration. Settings for inline form administration.
......
...@@ -25,7 +25,13 @@ def get_mdict_item_or_list(mdict, key): ...@@ -25,7 +25,13 @@ def get_mdict_item_or_list(mdict, key):
if hasattr(mdict, 'getlist'): if hasattr(mdict, 'getlist'):
v = mdict.getlist(key) v = mdict.getlist(key)
if len(v) == 1: if len(v) == 1:
return v[0] value = v[0]
# Special case for empty strings, treat them as "no-value"
if value == '':
value = None
return value
elif len(v) == 0: elif len(v) == 0:
return None return None
else: else:
......
...@@ -26,8 +26,8 @@ class AjaxSelect2Widget(object): ...@@ -26,8 +26,8 @@ class AjaxSelect2Widget(object):
self.multiple = multiple self.multiple = multiple
def __call__(self, field, **kwargs): def __call__(self, field, **kwargs):
kwargs['data-role'] = u'select2-ajax' kwargs.setdefault('data-role', 'select2-ajax')
kwargs['data-url'] = get_url('.ajax_lookup', name=field.loader.name) kwargs.setdefault('data-url', get_url('.ajax_lookup', name=field.loader.name))
allow_blank = getattr(field, 'allow_blank', False) allow_blank = getattr(field, 'allow_blank', False)
if allow_blank and not self.multiple: if allow_blank and not self.multiple:
...@@ -61,3 +61,88 @@ class AjaxSelect2Widget(object): ...@@ -61,3 +61,88 @@ class AjaxSelect2Widget(object):
kwargs.setdefault('data-placeholder', placeholder) kwargs.setdefault('data-placeholder', placeholder)
return HTMLString('<input %s>' % html_params(name=field.name, **kwargs)) return HTMLString('<input %s>' % html_params(name=field.name, **kwargs))
class XEditableWidget(object):
"""
WTForms widget that provides in-line editing for the list view.
Determines how to display the x-editable/ajax form based on the
field inside of the FieldList (StringField, IntegerField, etc).
"""
def __call__(self, field, **kwargs):
kwargs.setdefault('data-value', kwargs.pop('value', ''))
kwargs.setdefault('data-role', 'x-editable')
kwargs.setdefault('data-url', './ajax/update/')
kwargs.setdefault('id', field.id)
kwargs.setdefault('name', field.name)
kwargs.setdefault('href', '#')
if not kwargs.get('pk'):
raise Exception('pk required')
kwargs['data-pk'] = str(kwargs.pop("pk"))
kwargs['data-csrf'] = kwargs.pop("csrf", "")
# subfield is the first entry (subfield) from FieldList (field)
subfield = field.entries[0]
kwargs = self.get_kwargs(subfield, kwargs)
return HTMLString(
'<a %s>%s</a>' % (html_params(**kwargs), kwargs['data-value'])
)
def get_kwargs(self, subfield, kwargs):
"""
Return extra kwargs based on the subfield type.
"""
if subfield.type == 'StringField':
kwargs['data-type'] = 'text'
elif subfield.type == 'TextAreaField':
kwargs['data-type'] = 'textarea'
kwargs['data-rows'] = '5'
elif subfield.type == 'BooleanField':
kwargs['data-type'] = 'select'
# data-source = dropdown options
kwargs['data-source'] = {'': 'False', '1': 'True'}
kwargs['data-role'] = 'x-editable-boolean'
elif subfield.type == 'Select2Field':
kwargs['data-type'] = 'select'
kwargs['data-source'] = dict(subfield.choices)
elif subfield.type == 'DateField':
kwargs['data-type'] = 'combodate'
kwargs['data-format'] = 'YYYY-MM-DD'
kwargs['data-template'] = 'YYYY-MM-DD'
elif subfield.type == 'DateTimeField':
kwargs['data-type'] = 'combodate'
kwargs['data-format'] = 'YYYY-MM-DD HH:mm:ss'
kwargs['data-template'] = 'YYYY-MM-DD HH:mm:ss'
# x-editable-combodate uses 1 minute increments
kwargs['data-role'] = 'x-editable-combodate'
elif subfield.type == 'TimeField':
kwargs['data-type'] = 'combodate'
kwargs['data-format'] = 'HH:mm:ss'
kwargs['data-template'] = 'HH:mm:ss'
kwargs['data-role'] = 'x-editable-combodate'
elif subfield.type == 'IntegerField':
kwargs['data-type'] = 'number'
elif subfield.type in ['FloatField', 'DecimalField']:
kwargs['data-type'] = 'number'
kwargs['data-step'] = 'any'
elif subfield.type in ['QuerySelectField', 'ModelSelectField']:
kwargs['data-type'] = 'select'
choices = {}
for choice in subfield:
try:
choices[str(choice._value())] = str(choice.label.text)
except TypeError:
choices[str(choice._value())] = ""
kwargs['data-source'] = choices
else:
raise Exception('Unsupported field type: %s' % (type(subfield),))
return kwargs
/* Global styles */ /* List View - fix trash icon inside table column */
body .model-list form.icon {
{
padding-top: 4px;
}
/* Form customizations */
form.icon {
display: inline; display: inline;
} }
form.icon button { .model-list form.icon button {
border: none; border: none;
background: transparent; background: transparent;
text-decoration: none; text-decoration: none;
...@@ -17,76 +11,62 @@ form.icon button { ...@@ -17,76 +11,62 @@ form.icon button {
line-height: normal; line-height: normal;
} }
a.icon { /* List View - link icons - prevent underline */
.model-list a.icon {
text-decoration: none; text-decoration: none;
} }
/* Model search form */ /* List View - fix checkbox column width */
form.search-form { .list-checkbox-column {
margin: 4px 0 0 0; width: 14px;
}
form.search-form .clear i {
margin: 2px 0 0 0;
} }
form.search-form div input { /* List View - fix gap between actions and table */
margin: 0; .model-list {
position: relative;
margin-top: -1px;
z-index: 999;
} }
/* Filters */ .actions-nav {
table.filters { margin-bottom: 0;
border-collapse: collapse; margin-left: 4px;
border-spacing: 4px; margin-right: 4px;
} }
.filters input #filter_form {
{
margin-bottom: 0; margin-bottom: 0;
} }
.filters a.remove-filter { /* List View Search Form - fix gap between form and table */
margin-bottom: 0; .actions-nav form.search-form {
display: block; margin: -1px 0 0 0;
text-align: left;
} }
.filters .remove-filter /* Filters */
{ table.filters {
vertical-align: middle; border-collapse: collapse;
border-spacing: 4px;
} }
.filters .remove-filter .close-icon /* prevents gap between table and actions while there are no filters set */
{ table.filters:not(:empty) {
font-size: 16px; margin: 12px 0px 20px 0px;
} }
.filters .remove-filter .close-icon:hover /* spacing between filter X button, operation, and value field */
{ /* uses tables instead of form classes for bootstrap2-3 compatibility */
color: black; table.filters tr td {
opacity: 0.4; padding-right: 5px;
padding-bottom: 3px;
} }
/* match filter operation drop-down height with bootstrap input */
.filters .filter-op > a { .filters .filter-op > a {
height: 28px; height: 28px;
line-height: 28px; line-height: 28px;
} }
/* Inline forms */
.inline-field {
padding-bottom: 0.5em;
}
.inline-field-control {
float: right;
}
.inline-field .inline-form-field {
border-left: 1px solid #eeeeee;
padding-left: 8px;
margin-bottom: 4px;
}
/* Image thumbnails */ /* Image thumbnails */
.image-thumbnail img { .image-thumbnail img {
max-width: 100px; max-width: 100px;
...@@ -94,21 +74,13 @@ table.filters { ...@@ -94,21 +74,13 @@ table.filters {
} }
/* Forms */ /* Forms */
.form-horizontal .control-label { .admin-form .control-label {
width: 100px; width: 100px;
text-align: left; text-align: left;
margin-left: 4px; margin-left: 4px;
} }
.form-horizontal .controls { /* add spacing between labels and form fields */
.admin-form .controls {
margin-left: 110px; margin-left: 110px;
} }
\ No newline at end of file
/* Patch Select2 */
.select2-results li {
min-height: 24px !important;
}
.list-checkbox-column {
width: 14px;
}
/* Global styles */ /* List View - fix trash icon inside table column */
body .model-list form.icon {
{
padding-top: 4px;
}
/* Form customizations */
form.icon {
display: inline; display: inline;
} }
form.icon button { .model-list form.icon button {
border: none; border: none;
background: transparent; background: transparent;
text-decoration: none; text-decoration: none;
...@@ -17,28 +11,28 @@ form.icon button { ...@@ -17,28 +11,28 @@ form.icon button {
line-height: normal; line-height: normal;
} }
a.icon, button span.glyphicon { /* List View - prevent link icons from differing from trash icon */
.model-list a.icon {
text-decoration: none; text-decoration: none;
margin-left: 10px; margin-left: 10px;
color: #333; color: inherit;
} }
/* Model search form */ /* List View - fix checkbox column width */
form.navbar-form { .list-checkbox-column {
margin: 1px 0 0 0; width: 14px;
}
form.navbar-form a.clear span {
margin-top: 8px;
margin-left: -20px;
} }
form.navbar-form div.input-append { /* List View - fix overlapping border between actions and table */
display: inline-flex; .model-list {
position: relative;
margin-top: -1px;
z-index: 999;
} }
form.search-form div input { /* List View Search Form - fix gap between form and table */
margin: 0; .actions-nav form.navbar-form {
margin: 1px 0 0 0;
} }
/* Filters */ /* Filters */
...@@ -47,43 +41,19 @@ table.filters { ...@@ -47,43 +41,19 @@ table.filters {
border-spacing: 4px; border-spacing: 4px;
} }
/* prevents gap between table and actions while there are no filters set */
table.filters:not(:empty) { table.filters:not(:empty) {
margin: 12px 0px 20px 0px; margin: 12px 0px 20px 0px;
} }
/* spacing between filter X button, operation, and value field */
/* uses tables instead of form classes for bootstrap2-3 compatibility */
table.filters tr td { table.filters tr td {
padding-right: 5px; padding-right: 5px;
padding-bottom: 3px; padding-bottom: 3px;
} }
table.flters tr td:nth-child(2){ /* Filters - Select2 Boxes */
width: 60%;
}
.filters a.remove-filter {
margin-bottom: 0;
display: block;
text-align: left;
text-decoration: none;
}
.filters .remove-filter
{
vertical-align: middle;
}
.filters .remove-filter .close-icon
{
font-size: 16px;
}
.filters .remove-filter .close-icon:hover
{
color: black;
opacity: 0.4;
}
/* filters */
.filters .filter-op { .filters .filter-op {
width: 130px; width: 130px;
} }
...@@ -92,30 +62,6 @@ table.flters tr td:nth-child(2){ ...@@ -92,30 +62,6 @@ table.flters tr td:nth-child(2){
width: 220px; width: 220px;
} }
/* Inline forms */
.inline-field {
margin-bottom: 5px;
}
.inline-field-control {
float: right;
}
.inline-field .inline-form {
border-left: 1px solid #eeeeee;
padding-left: 8px;
margin-bottom: 4px;
}
.inline-field-control .glyphicon-remove {
margin-left: -30px;
}
.panel {
padding-top: 25px;
padding-left: 30px;
}
/* Image thumbnails */ /* Image thumbnails */
.image-thumbnail img { .image-thumbnail img {
max-width: 100px; max-width: 100px;
...@@ -123,58 +69,14 @@ table.flters tr td:nth-child(2){ ...@@ -123,58 +69,14 @@ table.flters tr td:nth-child(2){
} }
/* Forms */ /* Forms */
.form-horizontal { /* required because form-horizontal removes top padding */
.admin-form {
margin-top: 35px; margin-top: 35px;
} }
.submit-row { /* Form Field Description - Appears when field has 'description' attribute */
margin-top: 5px; /* Test with: form_args = {'name':{'description': 'test'}} */
} /* prevents awkward gap after help-block - This is default for bootstrap2 */
.admin-form .help-block {
td>span.glyphicon {
padding-left: 35%;
}
/* link style */
a.btn-cancel {
border-radius: 4px;
border-color: #a08c8c;
color: #333;
background-color: #f0b8b8;
}
a.btn-link {
border-radius: 4px;
border-color: #95bee2;
}
a.btn-filter, a.btn-filter:hover {
border-radius: 4px;
border-color: #adadad;
color: #333;
background-color: #ebebeb;
max-height: 32px;
line-height: 16px;
}
.help-block {
margin-bottom: 0px; margin-bottom: 0px;
} }
\ No newline at end of file
ul.has-error {
margin-bottom: -8px;
}
.tooltip.top {
font-size: 1.1em;
top: -35px;
}
.checkbox input[type="checkbox"] {
margin-left: 0px;
margin-right: 15px;
}
.list-checkbox-column {
width: 14px;
}
...@@ -101,12 +101,12 @@ var AdminFilters = function(element, filtersElement, filterGroups, activeFilters ...@@ -101,12 +101,12 @@ var AdminFilters = function(element, filtersElement, filterGroups, activeFilters
} }
function addFilter(name, subfilters, selectedIndex, filterValue) { function addFilter(name, subfilters, selectedIndex, filterValue) {
var $el = $('<tr />').appendTo($container); var $el = $('<tr class="form-horizontal" />').appendTo($container);
// Filter list // Filter list
$el.append( $el.append(
$('<td/>').append( $('<td/>').append(
$('<a href="#" class="btn btn-filter remove-filter" />') $('<a href="#" class="btn btn-default remove-filter" />')
.append($('<span class="close-icon">&times;</span>')) .append($('<span class="close-icon">&times;</span>'))
.append('&nbsp;') .append('&nbsp;')
.append(name) .append(name)
......
...@@ -241,6 +241,17 @@ ...@@ -241,6 +241,17 @@
return true; return true;
} }
// make x-editable's POST act like a normal FieldList field
// for x-editable, x-editable-combodate, and x-editable-boolean cases
var overrideXeditableParams = function(params) {
var newParams = {};
newParams[params.name + '-' + params.pk] = params.value;
if ($(this).data('csrf')) {
newParams['csrf_token'] = $(this).data('csrf');
}
return newParams;
}
switch (name) { switch (name) {
case 'select2': case 'select2':
var opts = { var opts = {
...@@ -390,6 +401,32 @@ ...@@ -390,6 +401,32 @@
case 'leaflet': case 'leaflet':
processLeafletWidget($el, name); processLeafletWidget($el, name);
return true; return true;
case 'x-editable':
$el.editable({params: overrideXeditableParams});
return true;
case 'x-editable-combodate':
$el.editable({
params: overrideXeditableParams,
combodate: {
// prevent minutes from showing in 5 minute increments
minuteStep: 1
}
});
return true;
case 'x-editable-boolean':
$el.editable({
params: overrideXeditableParams,
display: function(value, sourceData, response) {
// display new boolean value as an icon
if(response) {
if(value == '1') {
$(this).html('<span class="glyphicon glyphicon-ok-circle icon-ok-circle"></span>');
} else {
$(this).html('<span class="glyphicon glyphicon-minus-sign icon-minus-sign"></span>');
}
}
}
});
} }
}; };
...@@ -408,19 +445,25 @@ ...@@ -408,19 +445,25 @@
var $parentForm = $el.parent().closest('.inline-field'); var $parentForm = $el.parent().closest('.inline-field');
if ($parentForm.length > 0 && elID.indexOf($parentForm.attr('id')) !== 0) { if ($parentForm.hasClass('fresh')) {
id = $parentForm.attr('id') + '-' + elID; id = $parentForm.attr('id') + '-' + elID;
} }
var $fieldList = $el.find('> .inline-field-list'); var $fieldList = $el.find('> .inline-field-list');
var $lastField = $fieldList.children('.inline-field').last(); var maxId = 0;
$fieldList.children('.inline-field').each(function(idx, field) {
var $field = $(field);
var prefix = id + '-0'; var parts = $field.attr('id').split('-');
if ($lastField.length > 0) {
var parts = $lastField.attr('id').split('-');
idx = parseInt(parts[parts.length - 1], 10) + 1; idx = parseInt(parts[parts.length - 1], 10) + 1;
prefix = id + '-' + idx;
if (idx > maxId) {
maxId = idx;
} }
});
var prefix = id + '-' + maxId;
// Get template // Get template
var $template = $($el.find('> .inline-field-template').text()); var $template = $($el.find('> .inline-field-template').text());
...@@ -428,6 +471,9 @@ ...@@ -428,6 +471,9 @@
// Set form ID // Set form ID
$template.attr('id', prefix); $template.attr('id', prefix);
// Mark form that we just created
$template.addClass('fresh');
// Fix form IDs // Fix form IDs
$('[name]', $template).each(function(e) { $('[name]', $template).each(function(e) {
var me = $(this); var me = $(this);
...@@ -460,7 +506,7 @@ ...@@ -460,7 +506,7 @@
this.applyGlobalStyles = function(parent) { this.applyGlobalStyles = function(parent) {
var self = this; var self = this;
$(':input[data-role]', parent).each(function() { $(':input[data-role], a[data-role]', parent).each(function() {
var $el = $(this); var $el = $(this);
self.applyStyle($el, $el.attr('data-role')); self.applyStyle($el, $el.attr('data-role'));
}); });
......
/*! X-editable - v1.5.1
* In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery
* http://github.com/vitalets/x-editable
* Copyright (c) 2013 Vitaliy Potapov; Licensed MIT */
.editableform {
margin-bottom: 0; /* overwrites bootstrap margin */
}
.editableform .control-group {
margin-bottom: 0; /* overwrites bootstrap margin */
white-space: nowrap; /* prevent wrapping buttons on new line */
line-height: 20px; /* overwriting bootstrap line-height. See #133 */
}
/*
BS3 width:1005 for inputs breaks editable form in popup
See: https://github.com/vitalets/x-editable/issues/393
*/
.editableform .form-control {
width: auto;
}
.editable-buttons {
display: inline-block; /* should be inline to take effect of parent's white-space: nowrap */
vertical-align: top;
margin-left: 7px;
/* inline-block emulation for IE7*/
zoom: 1;
*display: inline;
}
.editable-buttons.editable-buttons-bottom {
display: block;
margin-top: 7px;
margin-left: 0;
}
.editable-input {
vertical-align: top;
display: inline-block; /* should be inline to take effect of parent's white-space: nowrap */
width: auto; /* bootstrap-responsive has width: 100% that breakes layout */
white-space: normal; /* reset white-space decalred in parent*/
/* display-inline emulation for IE7*/
zoom: 1;
*display: inline;
}
.editable-buttons .editable-cancel {
margin-left: 7px;
}
/*for jquery-ui buttons need set height to look more pretty*/
.editable-buttons button.ui-button-icon-only {
height: 24px;
width: 30px;
}
.editableform-loading {
background: url('../img/loading.gif') center center no-repeat;
height: 25px;
width: auto;
min-width: 25px;
}
.editable-inline .editableform-loading {
background-position: left 5px;
}
.editable-error-block {
max-width: 300px;
margin: 5px 0 0 0;
width: auto;
white-space: normal;
}
/*add padding for jquery ui*/
.editable-error-block.ui-state-error {
padding: 3px;
}
.editable-error {
color: red;
}
/* ---- For specific types ---- */
.editableform .editable-date {
padding: 0;
margin: 0;
float: left;
}
/* move datepicker icon to center of add-on button. See https://github.com/vitalets/x-editable/issues/183 */
.editable-inline .add-on .icon-th {
margin-top: 3px;
margin-left: 1px;
}
/* checklist vertical alignment */
.editable-checklist label input[type="checkbox"],
.editable-checklist label span {
vertical-align: middle;
margin: 0;
}
.editable-checklist label {
white-space: nowrap;
}
/* set exact width of textarea to fit buttons toolbar */
.editable-wysihtml5 {
width: 566px;
height: 250px;
}
/* clear button shown as link in date inputs */
.editable-clear {
clear: both;
font-size: 0.9em;
text-decoration: none;
text-align: right;
}
/* IOS-style clear button for text inputs */
.editable-clear-x {
background: url('../img/clear.png') center center no-repeat;
display: block;
width: 13px;
height: 13px;
position: absolute;
opacity: 0.6;
z-index: 100;
top: 50%;
right: 6px;
margin-top: -6px;
}
.editable-clear-x:hover {
opacity: 1;
}
.editable-pre-wrapped {
white-space: pre-wrap;
}
.editable-container.editable-popup {
max-width: none !important; /* without this rule poshytip/tooltip does not stretch */
}
.editable-container.popover {
width: auto; /* without this rule popover does not stretch */
}
.editable-container.editable-inline {
display: inline-block;
vertical-align: middle;
width: auto;
/* inline-block emulation for IE7*/
zoom: 1;
*display: inline;
}
.editable-container.ui-widget {
font-size: inherit; /* jqueryui widget font 1.1em too big, overwrite it */
z-index: 9990; /* should be less than select2 dropdown z-index to close dropdown first when click */
}
.editable-click,
a.editable-click,
a.editable-click:hover {
text-decoration: none;
border-bottom: dashed 1px #0088cc;
}
.editable-click.editable-disabled,
a.editable-click.editable-disabled,
a.editable-click.editable-disabled:hover {
color: #585858;
cursor: default;
border-bottom: none;
}
.editable-empty, .editable-empty:hover, .editable-empty:focus{
font-style: italic;
color: #DD1144;
/* border-bottom: none; */
text-decoration: none;
}
.editable-unsaved {
font-weight: bold;
}
.editable-unsaved:after {
/* content: '*'*/
}
.editable-bg-transition {
-webkit-transition: background-color 1400ms ease-out;
-moz-transition: background-color 1400ms ease-out;
-o-transition: background-color 1400ms ease-out;
-ms-transition: background-color 1400ms ease-out;
transition: background-color 1400ms ease-out;
}
/*see https://github.com/vitalets/x-editable/issues/139 */
.form-horizontal .editable
{
padding-top: 5px;
display:inline-block;
}
/*!
* Datepicker for Bootstrap
*
* Copyright 2012 Stefan Petre
* Improvements by Andrew Rowls
* Licensed under the Apache License v2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
*/
.datepicker {
padding: 4px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
direction: ltr;
/*.dow {
border-top: 1px solid #ddd !important;
}*/
}
.datepicker-inline {
width: 220px;
}
.datepicker.datepicker-rtl {
direction: rtl;
}
.datepicker.datepicker-rtl table tr td span {
float: right;
}
.datepicker-dropdown {
top: 0;
left: 0;
}
.datepicker-dropdown:before {
content: '';
display: inline-block;
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-bottom: 7px solid #ccc;
border-bottom-color: rgba(0, 0, 0, 0.2);
position: absolute;
top: -7px;
left: 6px;
}
.datepicker-dropdown:after {
content: '';
display: inline-block;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid #ffffff;
position: absolute;
top: -6px;
left: 7px;
}
.datepicker > div {
display: none;
}
.datepicker.days div.datepicker-days {
display: block;
}
.datepicker.months div.datepicker-months {
display: block;
}
.datepicker.years div.datepicker-years {
display: block;
}
.datepicker table {
margin: 0;
}
.datepicker td,
.datepicker th {
text-align: center;
width: 20px;
height: 20px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
border: none;
}
.table-striped .datepicker table tr td,
.table-striped .datepicker table tr th {
background-color: transparent;
}
.datepicker table tr td.day:hover {
background: #eeeeee;
cursor: pointer;
}
.datepicker table tr td.old,
.datepicker table tr td.new {
color: #999999;
}
.datepicker table tr td.disabled,
.datepicker table tr td.disabled:hover {
background: none;
color: #999999;
cursor: default;
}
.datepicker table tr td.today,
.datepicker table tr td.today:hover,
.datepicker table tr td.today.disabled,
.datepicker table tr td.today.disabled:hover {
background-color: #fde19a;
background-image: -moz-linear-gradient(top, #fdd49a, #fdf59a);
background-image: -ms-linear-gradient(top, #fdd49a, #fdf59a);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fdd49a), to(#fdf59a));
background-image: -webkit-linear-gradient(top, #fdd49a, #fdf59a);
background-image: -o-linear-gradient(top, #fdd49a, #fdf59a);
background-image: linear-gradient(top, #fdd49a, #fdf59a);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fdd49a', endColorstr='#fdf59a', GradientType=0);
border-color: #fdf59a #fdf59a #fbed50;
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
color: #000;
}
.datepicker table tr td.today:hover,
.datepicker table tr td.today:hover:hover,
.datepicker table tr td.today.disabled:hover,
.datepicker table tr td.today.disabled:hover:hover,
.datepicker table tr td.today:active,
.datepicker table tr td.today:hover:active,
.datepicker table tr td.today.disabled:active,
.datepicker table tr td.today.disabled:hover:active,
.datepicker table tr td.today.active,
.datepicker table tr td.today:hover.active,
.datepicker table tr td.today.disabled.active,
.datepicker table tr td.today.disabled:hover.active,
.datepicker table tr td.today.disabled,
.datepicker table tr td.today:hover.disabled,
.datepicker table tr td.today.disabled.disabled,
.datepicker table tr td.today.disabled:hover.disabled,
.datepicker table tr td.today[disabled],
.datepicker table tr td.today:hover[disabled],
.datepicker table tr td.today.disabled[disabled],
.datepicker table tr td.today.disabled:hover[disabled] {
background-color: #fdf59a;
}
.datepicker table tr td.today:active,
.datepicker table tr td.today:hover:active,
.datepicker table tr td.today.disabled:active,
.datepicker table tr td.today.disabled:hover:active,
.datepicker table tr td.today.active,
.datepicker table tr td.today:hover.active,
.datepicker table tr td.today.disabled.active,
.datepicker table tr td.today.disabled:hover.active {
background-color: #fbf069 \9;
}
.datepicker table tr td.today:hover:hover {
color: #000;
}
.datepicker table tr td.today.active:hover {
color: #fff;
}
.datepicker table tr td.range,
.datepicker table tr td.range:hover,
.datepicker table tr td.range.disabled,
.datepicker table tr td.range.disabled:hover {
background: #eeeeee;
-webkit-border-radius: 0;
-moz-border-radius: 0;
border-radius: 0;
}
.datepicker table tr td.range.today,
.datepicker table tr td.range.today:hover,
.datepicker table tr td.range.today.disabled,
.datepicker table tr td.range.today.disabled:hover {
background-color: #f3d17a;
background-image: -moz-linear-gradient(top, #f3c17a, #f3e97a);
background-image: -ms-linear-gradient(top, #f3c17a, #f3e97a);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f3c17a), to(#f3e97a));
background-image: -webkit-linear-gradient(top, #f3c17a, #f3e97a);
background-image: -o-linear-gradient(top, #f3c17a, #f3e97a);
background-image: linear-gradient(top, #f3c17a, #f3e97a);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f3c17a', endColorstr='#f3e97a', GradientType=0);
border-color: #f3e97a #f3e97a #edde34;
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
-webkit-border-radius: 0;
-moz-border-radius: 0;
border-radius: 0;
}
.datepicker table tr td.range.today:hover,
.datepicker table tr td.range.today:hover:hover,
.datepicker table tr td.range.today.disabled:hover,
.datepicker table tr td.range.today.disabled:hover:hover,
.datepicker table tr td.range.today:active,
.datepicker table tr td.range.today:hover:active,
.datepicker table tr td.range.today.disabled:active,
.datepicker table tr td.range.today.disabled:hover:active,
.datepicker table tr td.range.today.active,
.datepicker table tr td.range.today:hover.active,
.datepicker table tr td.range.today.disabled.active,
.datepicker table tr td.range.today.disabled:hover.active,
.datepicker table tr td.range.today.disabled,
.datepicker table tr td.range.today:hover.disabled,
.datepicker table tr td.range.today.disabled.disabled,
.datepicker table tr td.range.today.disabled:hover.disabled,
.datepicker table tr td.range.today[disabled],
.datepicker table tr td.range.today:hover[disabled],
.datepicker table tr td.range.today.disabled[disabled],
.datepicker table tr td.range.today.disabled:hover[disabled] {
background-color: #f3e97a;
}
.datepicker table tr td.range.today:active,
.datepicker table tr td.range.today:hover:active,
.datepicker table tr td.range.today.disabled:active,
.datepicker table tr td.range.today.disabled:hover:active,
.datepicker table tr td.range.today.active,
.datepicker table tr td.range.today:hover.active,
.datepicker table tr td.range.today.disabled.active,
.datepicker table tr td.range.today.disabled:hover.active {
background-color: #efe24b \9;
}
.datepicker table tr td.selected,
.datepicker table tr td.selected:hover,
.datepicker table tr td.selected.disabled,
.datepicker table tr td.selected.disabled:hover {
background-color: #9e9e9e;
background-image: -moz-linear-gradient(top, #b3b3b3, #808080);
background-image: -ms-linear-gradient(top, #b3b3b3, #808080);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#b3b3b3), to(#808080));
background-image: -webkit-linear-gradient(top, #b3b3b3, #808080);
background-image: -o-linear-gradient(top, #b3b3b3, #808080);
background-image: linear-gradient(top, #b3b3b3, #808080);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#b3b3b3', endColorstr='#808080', GradientType=0);
border-color: #808080 #808080 #595959;
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
color: #fff;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.datepicker table tr td.selected:hover,
.datepicker table tr td.selected:hover:hover,
.datepicker table tr td.selected.disabled:hover,
.datepicker table tr td.selected.disabled:hover:hover,
.datepicker table tr td.selected:active,
.datepicker table tr td.selected:hover:active,
.datepicker table tr td.selected.disabled:active,
.datepicker table tr td.selected.disabled:hover:active,
.datepicker table tr td.selected.active,
.datepicker table tr td.selected:hover.active,
.datepicker table tr td.selected.disabled.active,
.datepicker table tr td.selected.disabled:hover.active,
.datepicker table tr td.selected.disabled,
.datepicker table tr td.selected:hover.disabled,
.datepicker table tr td.selected.disabled.disabled,
.datepicker table tr td.selected.disabled:hover.disabled,
.datepicker table tr td.selected[disabled],
.datepicker table tr td.selected:hover[disabled],
.datepicker table tr td.selected.disabled[disabled],
.datepicker table tr td.selected.disabled:hover[disabled] {
background-color: #808080;
}
.datepicker table tr td.selected:active,
.datepicker table tr td.selected:hover:active,
.datepicker table tr td.selected.disabled:active,
.datepicker table tr td.selected.disabled:hover:active,
.datepicker table tr td.selected.active,
.datepicker table tr td.selected:hover.active,
.datepicker table tr td.selected.disabled.active,
.datepicker table tr td.selected.disabled:hover.active {
background-color: #666666 \9;
}
.datepicker table tr td.active,
.datepicker table tr td.active:hover,
.datepicker table tr td.active.disabled,
.datepicker table tr td.active.disabled:hover {
background-color: #006dcc;
background-image: -moz-linear-gradient(top, #0088cc, #0044cc);
background-image: -ms-linear-gradient(top, #0088cc, #0044cc);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));
background-image: -webkit-linear-gradient(top, #0088cc, #0044cc);
background-image: -o-linear-gradient(top, #0088cc, #0044cc);
background-image: linear-gradient(top, #0088cc, #0044cc);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0);
border-color: #0044cc #0044cc #002a80;
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
color: #fff;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.datepicker table tr td.active:hover,
.datepicker table tr td.active:hover:hover,
.datepicker table tr td.active.disabled:hover,
.datepicker table tr td.active.disabled:hover:hover,
.datepicker table tr td.active:active,
.datepicker table tr td.active:hover:active,
.datepicker table tr td.active.disabled:active,
.datepicker table tr td.active.disabled:hover:active,
.datepicker table tr td.active.active,
.datepicker table tr td.active:hover.active,
.datepicker table tr td.active.disabled.active,
.datepicker table tr td.active.disabled:hover.active,
.datepicker table tr td.active.disabled,
.datepicker table tr td.active:hover.disabled,
.datepicker table tr td.active.disabled.disabled,
.datepicker table tr td.active.disabled:hover.disabled,
.datepicker table tr td.active[disabled],
.datepicker table tr td.active:hover[disabled],
.datepicker table tr td.active.disabled[disabled],
.datepicker table tr td.active.disabled:hover[disabled] {
background-color: #0044cc;
}
.datepicker table tr td.active:active,
.datepicker table tr td.active:hover:active,
.datepicker table tr td.active.disabled:active,
.datepicker table tr td.active.disabled:hover:active,
.datepicker table tr td.active.active,
.datepicker table tr td.active:hover.active,
.datepicker table tr td.active.disabled.active,
.datepicker table tr td.active.disabled:hover.active {
background-color: #003399 \9;
}
.datepicker table tr td span {
display: block;
width: 23%;
height: 54px;
line-height: 54px;
float: left;
margin: 1%;
cursor: pointer;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
}
.datepicker table tr td span:hover {
background: #eeeeee;
}
.datepicker table tr td span.disabled,
.datepicker table tr td span.disabled:hover {
background: none;
color: #999999;
cursor: default;
}
.datepicker table tr td span.active,
.datepicker table tr td span.active:hover,
.datepicker table tr td span.active.disabled,
.datepicker table tr td span.active.disabled:hover {
background-color: #006dcc;
background-image: -moz-linear-gradient(top, #0088cc, #0044cc);
background-image: -ms-linear-gradient(top, #0088cc, #0044cc);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));
background-image: -webkit-linear-gradient(top, #0088cc, #0044cc);
background-image: -o-linear-gradient(top, #0088cc, #0044cc);
background-image: linear-gradient(top, #0088cc, #0044cc);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0);
border-color: #0044cc #0044cc #002a80;
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
color: #fff;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.datepicker table tr td span.active:hover,
.datepicker table tr td span.active:hover:hover,
.datepicker table tr td span.active.disabled:hover,
.datepicker table tr td span.active.disabled:hover:hover,
.datepicker table tr td span.active:active,
.datepicker table tr td span.active:hover:active,
.datepicker table tr td span.active.disabled:active,
.datepicker table tr td span.active.disabled:hover:active,
.datepicker table tr td span.active.active,
.datepicker table tr td span.active:hover.active,
.datepicker table tr td span.active.disabled.active,
.datepicker table tr td span.active.disabled:hover.active,
.datepicker table tr td span.active.disabled,
.datepicker table tr td span.active:hover.disabled,
.datepicker table tr td span.active.disabled.disabled,
.datepicker table tr td span.active.disabled:hover.disabled,
.datepicker table tr td span.active[disabled],
.datepicker table tr td span.active:hover[disabled],
.datepicker table tr td span.active.disabled[disabled],
.datepicker table tr td span.active.disabled:hover[disabled] {
background-color: #0044cc;
}
.datepicker table tr td span.active:active,
.datepicker table tr td span.active:hover:active,
.datepicker table tr td span.active.disabled:active,
.datepicker table tr td span.active.disabled:hover:active,
.datepicker table tr td span.active.active,
.datepicker table tr td span.active:hover.active,
.datepicker table tr td span.active.disabled.active,
.datepicker table tr td span.active.disabled:hover.active {
background-color: #003399 \9;
}
.datepicker table tr td span.old,
.datepicker table tr td span.new {
color: #999999;
}
.datepicker th.datepicker-switch {
width: 145px;
}
.datepicker thead tr:first-child th,
.datepicker tfoot tr th {
cursor: pointer;
}
.datepicker thead tr:first-child th:hover,
.datepicker tfoot tr th:hover {
background: #eeeeee;
}
.datepicker .cw {
font-size: 10px;
width: 12px;
padding: 0 2px 0 5px;
vertical-align: middle;
}
.datepicker thead tr:first-child th.cw {
cursor: default;
background-color: transparent;
}
.input-append.date .add-on i,
.input-prepend.date .add-on i {
display: block;
cursor: pointer;
width: 16px;
height: 16px;
}
.input-daterange input {
text-align: center;
}
.input-daterange input:first-child {
-webkit-border-radius: 3px 0 0 3px;
-moz-border-radius: 3px 0 0 3px;
border-radius: 3px 0 0 3px;
}
.input-daterange input:last-child {
-webkit-border-radius: 0 3px 3px 0;
-moz-border-radius: 0 3px 3px 0;
border-radius: 0 3px 3px 0;
}
.input-daterange .add-on {
display: inline-block;
width: auto;
min-width: 16px;
height: 18px;
padding: 4px 5px;
font-weight: normal;
line-height: 18px;
text-align: center;
text-shadow: 0 1px 0 #ffffff;
vertical-align: middle;
background-color: #eeeeee;
border: 1px solid #ccc;
margin-left: -5px;
margin-right: -5px;
}
/*! X-editable - v1.5.1
* In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery
* http://github.com/vitalets/x-editable
* Copyright (c) 2013 Vitaliy Potapov; Licensed MIT */
.editableform {
margin-bottom: 0; /* overwrites bootstrap margin */
}
.editableform .control-group {
margin-bottom: 0; /* overwrites bootstrap margin */
white-space: nowrap; /* prevent wrapping buttons on new line */
line-height: 20px; /* overwriting bootstrap line-height. See #133 */
}
/*
BS3 width:1005 for inputs breaks editable form in popup
See: https://github.com/vitalets/x-editable/issues/393
*/
.editableform .form-control {
width: auto;
}
.editable-buttons {
display: inline-block; /* should be inline to take effect of parent's white-space: nowrap */
vertical-align: top;
margin-left: 7px;
/* inline-block emulation for IE7*/
zoom: 1;
*display: inline;
}
.editable-buttons.editable-buttons-bottom {
display: block;
margin-top: 7px;
margin-left: 0;
}
.editable-input {
vertical-align: top;
display: inline-block; /* should be inline to take effect of parent's white-space: nowrap */
width: auto; /* bootstrap-responsive has width: 100% that breakes layout */
white-space: normal; /* reset white-space decalred in parent*/
/* display-inline emulation for IE7*/
zoom: 1;
*display: inline;
}
.editable-buttons .editable-cancel {
margin-left: 7px;
}
/*for jquery-ui buttons need set height to look more pretty*/
.editable-buttons button.ui-button-icon-only {
height: 24px;
width: 30px;
}
.editableform-loading {
background: url('../img/loading.gif') center center no-repeat;
height: 25px;
width: auto;
min-width: 25px;
}
.editable-inline .editableform-loading {
background-position: left 5px;
}
.editable-error-block {
max-width: 300px;
margin: 5px 0 0 0;
width: auto;
white-space: normal;
}
/*add padding for jquery ui*/
.editable-error-block.ui-state-error {
padding: 3px;
}
.editable-error {
color: red;
}
/* ---- For specific types ---- */
.editableform .editable-date {
padding: 0;
margin: 0;
float: left;
}
/* move datepicker icon to center of add-on button. See https://github.com/vitalets/x-editable/issues/183 */
.editable-inline .add-on .icon-th {
margin-top: 3px;
margin-left: 1px;
}
/* checklist vertical alignment */
.editable-checklist label input[type="checkbox"],
.editable-checklist label span {
vertical-align: middle;
margin: 0;
}
.editable-checklist label {
white-space: nowrap;
}
/* set exact width of textarea to fit buttons toolbar */
.editable-wysihtml5 {
width: 566px;
height: 250px;
}
/* clear button shown as link in date inputs */
.editable-clear {
clear: both;
font-size: 0.9em;
text-decoration: none;
text-align: right;
}
/* IOS-style clear button for text inputs */
.editable-clear-x {
background: url('../img/clear.png') center center no-repeat;
display: block;
width: 13px;
height: 13px;
position: absolute;
opacity: 0.6;
z-index: 100;
top: 50%;
right: 6px;
margin-top: -6px;
}
.editable-clear-x:hover {
opacity: 1;
}
.editable-pre-wrapped {
white-space: pre-wrap;
}
.editable-container.editable-popup {
max-width: none !important; /* without this rule poshytip/tooltip does not stretch */
}
.editable-container.popover {
width: auto; /* without this rule popover does not stretch */
}
.editable-container.editable-inline {
display: inline-block;
vertical-align: middle;
width: auto;
/* inline-block emulation for IE7*/
zoom: 1;
*display: inline;
}
.editable-container.ui-widget {
font-size: inherit; /* jqueryui widget font 1.1em too big, overwrite it */
z-index: 9990; /* should be less than select2 dropdown z-index to close dropdown first when click */
}
.editable-click,
a.editable-click,
a.editable-click:hover {
text-decoration: none;
border-bottom: dashed 1px #0088cc;
}
.editable-click.editable-disabled,
a.editable-click.editable-disabled,
a.editable-click.editable-disabled:hover {
color: #585858;
cursor: default;
border-bottom: none;
}
.editable-empty, .editable-empty:hover, .editable-empty:focus{
font-style: italic;
color: #DD1144;
/* border-bottom: none; */
text-decoration: none;
}
.editable-unsaved {
font-weight: bold;
}
.editable-unsaved:after {
/* content: '*'*/
}
.editable-bg-transition {
-webkit-transition: background-color 1400ms ease-out;
-moz-transition: background-color 1400ms ease-out;
-o-transition: background-color 1400ms ease-out;
-ms-transition: background-color 1400ms ease-out;
transition: background-color 1400ms ease-out;
}
/*see https://github.com/vitalets/x-editable/issues/139 */
.form-horizontal .editable
{
padding-top: 5px;
display:inline-block;
}
/*!
* Datepicker for Bootstrap
*
* Copyright 2012 Stefan Petre
* Improvements by Andrew Rowls
* Licensed under the Apache License v2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
*/
.datepicker {
padding: 4px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
direction: ltr;
/*.dow {
border-top: 1px solid #ddd !important;
}*/
}
.datepicker-inline {
width: 220px;
}
.datepicker.datepicker-rtl {
direction: rtl;
}
.datepicker.datepicker-rtl table tr td span {
float: right;
}
.datepicker-dropdown {
top: 0;
left: 0;
}
.datepicker-dropdown:before {
content: '';
display: inline-block;
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-bottom: 7px solid #ccc;
border-bottom-color: rgba(0, 0, 0, 0.2);
position: absolute;
top: -7px;
left: 6px;
}
.datepicker-dropdown:after {
content: '';
display: inline-block;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid #ffffff;
position: absolute;
top: -6px;
left: 7px;
}
.datepicker > div {
display: none;
}
.datepicker.days div.datepicker-days {
display: block;
}
.datepicker.months div.datepicker-months {
display: block;
}
.datepicker.years div.datepicker-years {
display: block;
}
.datepicker table {
margin: 0;
}
.datepicker td,
.datepicker th {
text-align: center;
width: 20px;
height: 20px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
border: none;
}
.table-striped .datepicker table tr td,
.table-striped .datepicker table tr th {
background-color: transparent;
}
.datepicker table tr td.day:hover {
background: #eeeeee;
cursor: pointer;
}
.datepicker table tr td.old,
.datepicker table tr td.new {
color: #999999;
}
.datepicker table tr td.disabled,
.datepicker table tr td.disabled:hover {
background: none;
color: #999999;
cursor: default;
}
.datepicker table tr td.today,
.datepicker table tr td.today:hover,
.datepicker table tr td.today.disabled,
.datepicker table tr td.today.disabled:hover {
background-color: #fde19a;
background-image: -moz-linear-gradient(top, #fdd49a, #fdf59a);
background-image: -ms-linear-gradient(top, #fdd49a, #fdf59a);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fdd49a), to(#fdf59a));
background-image: -webkit-linear-gradient(top, #fdd49a, #fdf59a);
background-image: -o-linear-gradient(top, #fdd49a, #fdf59a);
background-image: linear-gradient(top, #fdd49a, #fdf59a);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fdd49a', endColorstr='#fdf59a', GradientType=0);
border-color: #fdf59a #fdf59a #fbed50;
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
color: #000;
}
.datepicker table tr td.today:hover,
.datepicker table tr td.today:hover:hover,
.datepicker table tr td.today.disabled:hover,
.datepicker table tr td.today.disabled:hover:hover,
.datepicker table tr td.today:active,
.datepicker table tr td.today:hover:active,
.datepicker table tr td.today.disabled:active,
.datepicker table tr td.today.disabled:hover:active,
.datepicker table tr td.today.active,
.datepicker table tr td.today:hover.active,
.datepicker table tr td.today.disabled.active,
.datepicker table tr td.today.disabled:hover.active,
.datepicker table tr td.today.disabled,
.datepicker table tr td.today:hover.disabled,
.datepicker table tr td.today.disabled.disabled,
.datepicker table tr td.today.disabled:hover.disabled,
.datepicker table tr td.today[disabled],
.datepicker table tr td.today:hover[disabled],
.datepicker table tr td.today.disabled[disabled],
.datepicker table tr td.today.disabled:hover[disabled] {
background-color: #fdf59a;
}
.datepicker table tr td.today:active,
.datepicker table tr td.today:hover:active,
.datepicker table tr td.today.disabled:active,
.datepicker table tr td.today.disabled:hover:active,
.datepicker table tr td.today.active,
.datepicker table tr td.today:hover.active,
.datepicker table tr td.today.disabled.active,
.datepicker table tr td.today.disabled:hover.active {
background-color: #fbf069 \9;
}
.datepicker table tr td.today:hover:hover {
color: #000;
}
.datepicker table tr td.today.active:hover {
color: #fff;
}
.datepicker table tr td.range,
.datepicker table tr td.range:hover,
.datepicker table tr td.range.disabled,
.datepicker table tr td.range.disabled:hover {
background: #eeeeee;
-webkit-border-radius: 0;
-moz-border-radius: 0;
border-radius: 0;
}
.datepicker table tr td.range.today,
.datepicker table tr td.range.today:hover,
.datepicker table tr td.range.today.disabled,
.datepicker table tr td.range.today.disabled:hover {
background-color: #f3d17a;
background-image: -moz-linear-gradient(top, #f3c17a, #f3e97a);
background-image: -ms-linear-gradient(top, #f3c17a, #f3e97a);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f3c17a), to(#f3e97a));
background-image: -webkit-linear-gradient(top, #f3c17a, #f3e97a);
background-image: -o-linear-gradient(top, #f3c17a, #f3e97a);
background-image: linear-gradient(top, #f3c17a, #f3e97a);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f3c17a', endColorstr='#f3e97a', GradientType=0);
border-color: #f3e97a #f3e97a #edde34;
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
-webkit-border-radius: 0;
-moz-border-radius: 0;
border-radius: 0;
}
.datepicker table tr td.range.today:hover,
.datepicker table tr td.range.today:hover:hover,
.datepicker table tr td.range.today.disabled:hover,
.datepicker table tr td.range.today.disabled:hover:hover,
.datepicker table tr td.range.today:active,
.datepicker table tr td.range.today:hover:active,
.datepicker table tr td.range.today.disabled:active,
.datepicker table tr td.range.today.disabled:hover:active,
.datepicker table tr td.range.today.active,
.datepicker table tr td.range.today:hover.active,
.datepicker table tr td.range.today.disabled.active,
.datepicker table tr td.range.today.disabled:hover.active,
.datepicker table tr td.range.today.disabled,
.datepicker table tr td.range.today:hover.disabled,
.datepicker table tr td.range.today.disabled.disabled,
.datepicker table tr td.range.today.disabled:hover.disabled,
.datepicker table tr td.range.today[disabled],
.datepicker table tr td.range.today:hover[disabled],
.datepicker table tr td.range.today.disabled[disabled],
.datepicker table tr td.range.today.disabled:hover[disabled] {
background-color: #f3e97a;
}
.datepicker table tr td.range.today:active,
.datepicker table tr td.range.today:hover:active,
.datepicker table tr td.range.today.disabled:active,
.datepicker table tr td.range.today.disabled:hover:active,
.datepicker table tr td.range.today.active,
.datepicker table tr td.range.today:hover.active,
.datepicker table tr td.range.today.disabled.active,
.datepicker table tr td.range.today.disabled:hover.active {
background-color: #efe24b \9;
}
.datepicker table tr td.selected,
.datepicker table tr td.selected:hover,
.datepicker table tr td.selected.disabled,
.datepicker table tr td.selected.disabled:hover {
background-color: #9e9e9e;
background-image: -moz-linear-gradient(top, #b3b3b3, #808080);
background-image: -ms-linear-gradient(top, #b3b3b3, #808080);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#b3b3b3), to(#808080));
background-image: -webkit-linear-gradient(top, #b3b3b3, #808080);
background-image: -o-linear-gradient(top, #b3b3b3, #808080);
background-image: linear-gradient(top, #b3b3b3, #808080);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#b3b3b3', endColorstr='#808080', GradientType=0);
border-color: #808080 #808080 #595959;
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
color: #fff;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.datepicker table tr td.selected:hover,
.datepicker table tr td.selected:hover:hover,
.datepicker table tr td.selected.disabled:hover,
.datepicker table tr td.selected.disabled:hover:hover,
.datepicker table tr td.selected:active,
.datepicker table tr td.selected:hover:active,
.datepicker table tr td.selected.disabled:active,
.datepicker table tr td.selected.disabled:hover:active,
.datepicker table tr td.selected.active,
.datepicker table tr td.selected:hover.active,
.datepicker table tr td.selected.disabled.active,
.datepicker table tr td.selected.disabled:hover.active,
.datepicker table tr td.selected.disabled,
.datepicker table tr td.selected:hover.disabled,
.datepicker table tr td.selected.disabled.disabled,
.datepicker table tr td.selected.disabled:hover.disabled,
.datepicker table tr td.selected[disabled],
.datepicker table tr td.selected:hover[disabled],
.datepicker table tr td.selected.disabled[disabled],
.datepicker table tr td.selected.disabled:hover[disabled] {
background-color: #808080;
}
.datepicker table tr td.selected:active,
.datepicker table tr td.selected:hover:active,
.datepicker table tr td.selected.disabled:active,
.datepicker table tr td.selected.disabled:hover:active,
.datepicker table tr td.selected.active,
.datepicker table tr td.selected:hover.active,
.datepicker table tr td.selected.disabled.active,
.datepicker table tr td.selected.disabled:hover.active {
background-color: #666666 \9;
}
.datepicker table tr td.active,
.datepicker table tr td.active:hover,
.datepicker table tr td.active.disabled,
.datepicker table tr td.active.disabled:hover {
background-color: #006dcc;
background-image: -moz-linear-gradient(top, #0088cc, #0044cc);
background-image: -ms-linear-gradient(top, #0088cc, #0044cc);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));
background-image: -webkit-linear-gradient(top, #0088cc, #0044cc);
background-image: -o-linear-gradient(top, #0088cc, #0044cc);
background-image: linear-gradient(top, #0088cc, #0044cc);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0);
border-color: #0044cc #0044cc #002a80;
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
color: #fff;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.datepicker table tr td.active:hover,
.datepicker table tr td.active:hover:hover,
.datepicker table tr td.active.disabled:hover,
.datepicker table tr td.active.disabled:hover:hover,
.datepicker table tr td.active:active,
.datepicker table tr td.active:hover:active,
.datepicker table tr td.active.disabled:active,
.datepicker table tr td.active.disabled:hover:active,
.datepicker table tr td.active.active,
.datepicker table tr td.active:hover.active,
.datepicker table tr td.active.disabled.active,
.datepicker table tr td.active.disabled:hover.active,
.datepicker table tr td.active.disabled,
.datepicker table tr td.active:hover.disabled,
.datepicker table tr td.active.disabled.disabled,
.datepicker table tr td.active.disabled:hover.disabled,
.datepicker table tr td.active[disabled],
.datepicker table tr td.active:hover[disabled],
.datepicker table tr td.active.disabled[disabled],
.datepicker table tr td.active.disabled:hover[disabled] {
background-color: #0044cc;
}
.datepicker table tr td.active:active,
.datepicker table tr td.active:hover:active,
.datepicker table tr td.active.disabled:active,
.datepicker table tr td.active.disabled:hover:active,
.datepicker table tr td.active.active,
.datepicker table tr td.active:hover.active,
.datepicker table tr td.active.disabled.active,
.datepicker table tr td.active.disabled:hover.active {
background-color: #003399 \9;
}
.datepicker table tr td span {
display: block;
width: 23%;
height: 54px;
line-height: 54px;
float: left;
margin: 1%;
cursor: pointer;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
}
.datepicker table tr td span:hover {
background: #eeeeee;
}
.datepicker table tr td span.disabled,
.datepicker table tr td span.disabled:hover {
background: none;
color: #999999;
cursor: default;
}
.datepicker table tr td span.active,
.datepicker table tr td span.active:hover,
.datepicker table tr td span.active.disabled,
.datepicker table tr td span.active.disabled:hover {
background-color: #006dcc;
background-image: -moz-linear-gradient(top, #0088cc, #0044cc);
background-image: -ms-linear-gradient(top, #0088cc, #0044cc);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));
background-image: -webkit-linear-gradient(top, #0088cc, #0044cc);
background-image: -o-linear-gradient(top, #0088cc, #0044cc);
background-image: linear-gradient(top, #0088cc, #0044cc);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0);
border-color: #0044cc #0044cc #002a80;
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
color: #fff;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.datepicker table tr td span.active:hover,
.datepicker table tr td span.active:hover:hover,
.datepicker table tr td span.active.disabled:hover,
.datepicker table tr td span.active.disabled:hover:hover,
.datepicker table tr td span.active:active,
.datepicker table tr td span.active:hover:active,
.datepicker table tr td span.active.disabled:active,
.datepicker table tr td span.active.disabled:hover:active,
.datepicker table tr td span.active.active,
.datepicker table tr td span.active:hover.active,
.datepicker table tr td span.active.disabled.active,
.datepicker table tr td span.active.disabled:hover.active,
.datepicker table tr td span.active.disabled,
.datepicker table tr td span.active:hover.disabled,
.datepicker table tr td span.active.disabled.disabled,
.datepicker table tr td span.active.disabled:hover.disabled,
.datepicker table tr td span.active[disabled],
.datepicker table tr td span.active:hover[disabled],
.datepicker table tr td span.active.disabled[disabled],
.datepicker table tr td span.active.disabled:hover[disabled] {
background-color: #0044cc;
}
.datepicker table tr td span.active:active,
.datepicker table tr td span.active:hover:active,
.datepicker table tr td span.active.disabled:active,
.datepicker table tr td span.active.disabled:hover:active,
.datepicker table tr td span.active.active,
.datepicker table tr td span.active:hover.active,
.datepicker table tr td span.active.disabled.active,
.datepicker table tr td span.active.disabled:hover.active {
background-color: #003399 \9;
}
.datepicker table tr td span.old,
.datepicker table tr td span.new {
color: #999999;
}
.datepicker th.datepicker-switch {
width: 145px;
}
.datepicker thead tr:first-child th,
.datepicker tfoot tr th {
cursor: pointer;
}
.datepicker thead tr:first-child th:hover,
.datepicker tfoot tr th:hover {
background: #eeeeee;
}
.datepicker .cw {
font-size: 10px;
width: 12px;
padding: 0 2px 0 5px;
vertical-align: middle;
}
.datepicker thead tr:first-child th.cw {
cursor: default;
background-color: transparent;
}
.input-append.date .add-on i,
.input-prepend.date .add-on i {
display: block;
cursor: pointer;
width: 16px;
height: 16px;
}
.input-daterange input {
text-align: center;
}
.input-daterange input:first-child {
-webkit-border-radius: 3px 0 0 3px;
-moz-border-radius: 3px 0 0 3px;
border-radius: 3px 0 0 3px;
}
.input-daterange input:last-child {
-webkit-border-radius: 0 3px 3px 0;
-moz-border-radius: 0 3px 3px 0;
border-radius: 0 3px 3px 0;
}
.input-daterange .add-on {
display: inline-block;
width: auto;
min-width: 16px;
height: 18px;
padding: 4px 5px;
font-weight: normal;
line-height: 18px;
text-align: center;
text-shadow: 0 1px 0 #ffffff;
vertical-align: middle;
background-color: #eeeeee;
border: 1px solid #ccc;
margin-left: -5px;
margin-right: -5px;
}
/*! X-editable - v1.5.1
* In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery
* http://github.com/vitalets/x-editable
* Copyright (c) 2013 Vitaliy Potapov; Licensed MIT */
!function(a){"use strict";var b=function(b,c){this.options=a.extend({},a.fn.editableform.defaults,c),this.$div=a(b),this.options.scope||(this.options.scope=this)};b.prototype={constructor:b,initInput:function(){this.input=this.options.input,this.value=this.input.str2value(this.options.value),this.input.prerender()},initTemplate:function(){this.$form=a(a.fn.editableform.template)},initButtons:function(){var b=this.$form.find(".editable-buttons");b.append(a.fn.editableform.buttons),"bottom"===this.options.showbuttons&&b.addClass("editable-buttons-bottom")},render:function(){this.$loading=a(a.fn.editableform.loading),this.$div.empty().append(this.$loading),this.initTemplate(),this.options.showbuttons?this.initButtons():this.$form.find(".editable-buttons").remove(),this.showLoading(),this.isSaving=!1,this.$div.triggerHandler("rendering"),this.initInput(),this.$form.find("div.editable-input").append(this.input.$tpl),this.$div.append(this.$form),a.when(this.input.render()).then(a.proxy(function(){if(this.options.showbuttons||this.input.autosubmit(),this.$form.find(".editable-cancel").click(a.proxy(this.cancel,this)),this.input.error)this.error(this.input.error),this.$form.find(".editable-submit").attr("disabled",!0),this.input.$input.attr("disabled",!0),this.$form.submit(function(a){a.preventDefault()});else{this.error(!1),this.input.$input.removeAttr("disabled"),this.$form.find(".editable-submit").removeAttr("disabled");var b=null===this.value||void 0===this.value||""===this.value?this.options.defaultValue:this.value;this.input.value2input(b),this.$form.submit(a.proxy(this.submit,this))}this.$div.triggerHandler("rendered"),this.showForm(),this.input.postrender&&this.input.postrender()},this))},cancel:function(){this.$div.triggerHandler("cancel")},showLoading:function(){var a,b;this.$form?(a=this.$form.outerWidth(),b=this.$form.outerHeight(),a&&this.$loading.width(a),b&&this.$loading.height(b),this.$form.hide()):(a=this.$loading.parent().width(),a&&this.$loading.width(a)),this.$loading.show()},showForm:function(a){this.$loading.hide(),this.$form.show(),a!==!1&&this.input.activate(),this.$div.triggerHandler("show")},error:function(b){var c,d=this.$form.find(".control-group"),e=this.$form.find(".editable-error-block");if(b===!1)d.removeClass(a.fn.editableform.errorGroupClass),e.removeClass(a.fn.editableform.errorBlockClass).empty().hide();else{if(b){c=(""+b).split("\n");for(var f=0;f<c.length;f++)c[f]=a("<div>").text(c[f]).html();b=c.join("<br>")}d.addClass(a.fn.editableform.errorGroupClass),e.addClass(a.fn.editableform.errorBlockClass).html(b).show()}},submit:function(b){b.stopPropagation(),b.preventDefault();var c=this.input.input2value(),d=this.validate(c);if("object"===a.type(d)&&void 0!==d.newValue){if(c=d.newValue,this.input.value2input(c),"string"==typeof d.msg)return this.error(d.msg),this.showForm(),void 0}else if(d)return this.error(d),this.showForm(),void 0;if(!this.options.savenochange&&this.input.value2str(c)==this.input.value2str(this.value))return this.$div.triggerHandler("nochange"),void 0;var e=this.input.value2submit(c);this.isSaving=!0,a.when(this.save(e)).done(a.proxy(function(a){this.isSaving=!1;var b="function"==typeof this.options.success?this.options.success.call(this.options.scope,a,c):null;return b===!1?(this.error(!1),this.showForm(!1),void 0):"string"==typeof b?(this.error(b),this.showForm(),void 0):(b&&"object"==typeof b&&b.hasOwnProperty("newValue")&&(c=b.newValue),this.error(!1),this.value=c,this.$div.triggerHandler("save",{newValue:c,submitValue:e,response:a}),void 0)},this)).fail(a.proxy(function(a){this.isSaving=!1;var b;b="function"==typeof this.options.error?this.options.error.call(this.options.scope,a,c):"string"==typeof a?a:a.responseText||a.statusText||"Unknown error!",this.error(b),this.showForm()},this))},save:function(b){this.options.pk=a.fn.editableutils.tryParseJson(this.options.pk,!0);var c,d="function"==typeof this.options.pk?this.options.pk.call(this.options.scope):this.options.pk,e=!!("function"==typeof this.options.url||this.options.url&&("always"===this.options.send||"auto"===this.options.send&&null!==d&&void 0!==d));return e?(this.showLoading(),c={name:this.options.name||"",value:b,pk:d},"function"==typeof this.options.params?c=this.options.params.call(this.options.scope,c):(this.options.params=a.fn.editableutils.tryParseJson(this.options.params,!0),a.extend(c,this.options.params)),"function"==typeof this.options.url?this.options.url.call(this.options.scope,c):a.ajax(a.extend({url:this.options.url,data:c,type:"POST"},this.options.ajaxOptions))):void 0},validate:function(a){return void 0===a&&(a=this.value),"function"==typeof this.options.validate?this.options.validate.call(this.options.scope,a):void 0},option:function(a,b){a in this.options&&(this.options[a]=b),"value"===a&&this.setValue(b)},setValue:function(a,b){this.value=b?this.input.str2value(a):a,this.$form&&this.$form.is(":visible")&&this.input.value2input(this.value)}},a.fn.editableform=function(c){var d=arguments;return this.each(function(){var e=a(this),f=e.data("editableform"),g="object"==typeof c&&c;f||e.data("editableform",f=new b(this,g)),"string"==typeof c&&f[c].apply(f,Array.prototype.slice.call(d,1))})},a.fn.editableform.Constructor=b,a.fn.editableform.defaults={type:"text",url:null,params:null,name:null,pk:null,value:null,defaultValue:null,send:"auto",validate:null,success:null,error:null,ajaxOptions:null,showbuttons:!0,scope:null,savenochange:!1},a.fn.editableform.template='<form class="form-inline editableform"><div class="control-group"><div><div class="editable-input"></div><div class="editable-buttons"></div></div><div class="editable-error-block"></div></div></form>',a.fn.editableform.loading='<div class="editableform-loading"></div>',a.fn.editableform.buttons='<button type="submit" class="editable-submit">ok</button><button type="button" class="editable-cancel">cancel</button>',a.fn.editableform.errorGroupClass=null,a.fn.editableform.errorBlockClass="editable-error",a.fn.editableform.engine="jquery"}(window.jQuery),function(a){"use strict";a.fn.editableutils={inherit:function(a,b){var c=function(){};c.prototype=b.prototype,a.prototype=new c,a.prototype.constructor=a,a.superclass=b.prototype},setCursorPosition:function(a,b){if(a.setSelectionRange)a.setSelectionRange(b,b);else if(a.createTextRange){var c=a.createTextRange();c.collapse(!0),c.moveEnd("character",b),c.moveStart("character",b),c.select()}},tryParseJson:function(a,b){if("string"==typeof a&&a.length&&a.match(/^[\{\[].*[\}\]]$/))if(b)try{a=new Function("return "+a)()}catch(c){}finally{return a}else a=new Function("return "+a)();return a},sliceObj:function(b,c,d){var e,f,g={};if(!a.isArray(c)||!c.length)return g;for(var h=0;h<c.length;h++)e=c[h],b.hasOwnProperty(e)&&(g[e]=b[e]),d!==!0&&(f=e.toLowerCase(),b.hasOwnProperty(f)&&(g[e]=b[f]));return g},getConfigData:function(b){var c={};return a.each(b.data(),function(a,b){("object"!=typeof b||b&&"object"==typeof b&&(b.constructor===Object||b.constructor===Array))&&(c[a]=b)}),c},objectKeys:function(a){if(Object.keys)return Object.keys(a);if(a!==Object(a))throw new TypeError("Object.keys called on a non-object");var b,c=[];for(b in a)Object.prototype.hasOwnProperty.call(a,b)&&c.push(b);return c},escape:function(b){return a("<div>").text(b).html()},itemsByValue:function(b,c,d){if(!c||null===b)return[];if("function"!=typeof d){var e=d||"value";d=function(a){return a[e]}}var f=a.isArray(b),g=[],h=this;return a.each(c,function(c,e){if(e.children)g=g.concat(h.itemsByValue(b,e.children,d));else if(f)a.grep(b,function(a){return a==(e&&"object"==typeof e?d(e):e)}).length&&g.push(e);else{var i=e&&"object"==typeof e?d(e):e;b==i&&g.push(e)}}),g},createInput:function(b){var c,d,e,f=b.type;return"date"===f&&("inline"===b.mode?a.fn.editabletypes.datefield?f="datefield":a.fn.editabletypes.dateuifield&&(f="dateuifield"):a.fn.editabletypes.date?f="date":a.fn.editabletypes.dateui&&(f="dateui"),"date"!==f||a.fn.editabletypes.date||(f="combodate")),"datetime"===f&&"inline"===b.mode&&(f="datetimefield"),"wysihtml5"!==f||a.fn.editabletypes[f]||(f="textarea"),"function"==typeof a.fn.editabletypes[f]?(c=a.fn.editabletypes[f],d=this.sliceObj(b,this.objectKeys(c.defaults)),e=new c(d)):(a.error("Unknown type: "+f),!1)},supportsTransitions:function(){var a=document.body||document.documentElement,b=a.style,c="transition",d=["Moz","Webkit","Khtml","O","ms"];if("string"==typeof b[c])return!0;c=c.charAt(0).toUpperCase()+c.substr(1);for(var e=0;e<d.length;e++)if("string"==typeof b[d[e]+c])return!0;return!1}}}(window.jQuery),function(a){"use strict";var b=function(a,b){this.init(a,b)},c=function(a,b){this.init(a,b)};b.prototype={containerName:null,containerDataName:null,innerCss:null,containerClass:"editable-container editable-popup",defaults:{},init:function(c,d){this.$element=a(c),this.options=a.extend({},a.fn.editableContainer.defaults,d),this.splitOptions(),this.formOptions.scope=this.$element[0],this.initContainer(),this.delayedHide=!1,this.$element.on("destroyed",a.proxy(function(){this.destroy()},this)),a(document).data("editable-handlers-attached")||(a(document).on("keyup.editable",function(b){27===b.which&&a(".editable-open").editableContainer("hide")}),a(document).on("click.editable",function(c){var d,e=a(c.target),f=[".editable-container",".ui-datepicker-header",".datepicker",".modal-backdrop",".bootstrap-wysihtml5-insert-image-modal",".bootstrap-wysihtml5-insert-link-modal"];if(a.contains(document.documentElement,c.target)&&!e.is(document)){for(d=0;d<f.length;d++)if(e.is(f[d])||e.parents(f[d]).length)return;b.prototype.closeOthers(c.target)}}),a(document).data("editable-handlers-attached",!0))},splitOptions:function(){if(this.containerOptions={},this.formOptions={},!a.fn[this.containerName])throw new Error(this.containerName+" not found. Have you included corresponding js file?");for(var b in this.options)b in this.defaults?this.containerOptions[b]=this.options[b]:this.formOptions[b]=this.options[b]},tip:function(){return this.container()?this.container().$tip:null},container:function(){var a;return this.containerDataName&&(a=this.$element.data(this.containerDataName))?a:a=this.$element.data(this.containerName)},call:function(){this.$element[this.containerName].apply(this.$element,arguments)},initContainer:function(){this.call(this.containerOptions)},renderForm:function(){this.$form.editableform(this.formOptions).on({save:a.proxy(this.save,this),nochange:a.proxy(function(){this.hide("nochange")},this),cancel:a.proxy(function(){this.hide("cancel")},this),show:a.proxy(function(){this.delayedHide?(this.hide(this.delayedHide.reason),this.delayedHide=!1):this.setPosition()},this),rendering:a.proxy(this.setPosition,this),resize:a.proxy(this.setPosition,this),rendered:a.proxy(function(){this.$element.triggerHandler("shown",a(this.options.scope).data("editable"))},this)}).editableform("render")},show:function(b){this.$element.addClass("editable-open"),b!==!1&&this.closeOthers(this.$element[0]),this.innerShow(),this.tip().addClass(this.containerClass),this.$form,this.$form=a("<div>"),this.tip().is(this.innerCss)?this.tip().append(this.$form):this.tip().find(this.innerCss).append(this.$form),this.renderForm()},hide:function(a){if(this.tip()&&this.tip().is(":visible")&&this.$element.hasClass("editable-open")){if(this.$form.data("editableform").isSaving)return this.delayedHide={reason:a},void 0;this.delayedHide=!1,this.$element.removeClass("editable-open"),this.innerHide(),this.$element.triggerHandler("hidden",a||"manual")}},innerShow:function(){},innerHide:function(){},toggle:function(a){this.container()&&this.tip()&&this.tip().is(":visible")?this.hide():this.show(a)},setPosition:function(){},save:function(a,b){this.$element.triggerHandler("save",b),this.hide("save")},option:function(a,b){this.options[a]=b,a in this.containerOptions?(this.containerOptions[a]=b,this.setContainerOption(a,b)):(this.formOptions[a]=b,this.$form&&this.$form.editableform("option",a,b))},setContainerOption:function(a,b){this.call("option",a,b)},destroy:function(){this.hide(),this.innerDestroy(),this.$element.off("destroyed"),this.$element.removeData("editableContainer")},innerDestroy:function(){},closeOthers:function(b){a(".editable-open").each(function(c,d){if(d!==b&&!a(d).find(b).length){var e=a(d),f=e.data("editableContainer");f&&("cancel"===f.options.onblur?e.data("editableContainer").hide("onblur"):"submit"===f.options.onblur&&e.data("editableContainer").tip().find("form").submit())}})},activate:function(){this.tip&&this.tip().is(":visible")&&this.$form&&this.$form.data("editableform").input.activate()}},a.fn.editableContainer=function(d){var e=arguments;return this.each(function(){var f=a(this),g="editableContainer",h=f.data(g),i="object"==typeof d&&d,j="inline"===i.mode?c:b;h||f.data(g,h=new j(this,i)),"string"==typeof d&&h[d].apply(h,Array.prototype.slice.call(e,1))})},a.fn.editableContainer.Popup=b,a.fn.editableContainer.Inline=c,a.fn.editableContainer.defaults={value:null,placement:"top",autohide:!0,onblur:"cancel",anim:!1,mode:"popup"},jQuery.event.special.destroyed={remove:function(a){a.handler&&a.handler()}}}(window.jQuery),function(a){"use strict";a.extend(a.fn.editableContainer.Inline.prototype,a.fn.editableContainer.Popup.prototype,{containerName:"editableform",innerCss:".editable-inline",containerClass:"editable-container editable-inline",initContainer:function(){this.$tip=a("<span></span>"),this.options.anim||(this.options.anim=0)},splitOptions:function(){this.containerOptions={},this.formOptions=this.options},tip:function(){return this.$tip},innerShow:function(){this.$element.hide(),this.tip().insertAfter(this.$element).show()},innerHide:function(){this.$tip.hide(this.options.anim,a.proxy(function(){this.$element.show(),this.innerDestroy()},this))},innerDestroy:function(){this.tip()&&this.tip().empty().remove()}})}(window.jQuery),function(a){"use strict";var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.editable.defaults,c,a.fn.editableutils.getConfigData(this.$element)),this.options.selector?this.initLive():this.init(),this.options.highlight&&!a.fn.editableutils.supportsTransitions()&&(this.options.highlight=!1)};b.prototype={constructor:b,init:function(){var b,c=!1;if(this.options.name=this.options.name||this.$element.attr("id"),this.options.scope=this.$element[0],this.input=a.fn.editableutils.createInput(this.options),this.input){switch(void 0===this.options.value||null===this.options.value?(this.value=this.input.html2value(a.trim(this.$element.html())),c=!0):(this.options.value=a.fn.editableutils.tryParseJson(this.options.value,!0),this.value="string"==typeof this.options.value?this.input.str2value(this.options.value):this.options.value),this.$element.addClass("editable"),"textarea"===this.input.type&&this.$element.addClass("editable-pre-wrapped"),"manual"!==this.options.toggle?(this.$element.addClass("editable-click"),this.$element.on(this.options.toggle+".editable",a.proxy(function(a){if(this.options.disabled||a.preventDefault(),"mouseenter"===this.options.toggle)this.show();else{var b="click"!==this.options.toggle;this.toggle(b)}},this))):this.$element.attr("tabindex",-1),"function"==typeof this.options.display&&(this.options.autotext="always"),this.options.autotext){case"always":b=!0;break;case"auto":b=!a.trim(this.$element.text()).length&&null!==this.value&&void 0!==this.value&&!c;break;default:b=!1}a.when(b?this.render():!0).then(a.proxy(function(){this.options.disabled?this.disable():this.enable(),this.$element.triggerHandler("init",this)},this))}},initLive:function(){var b=this.options.selector;this.options.selector=!1,this.options.autotext="never",this.$element.on(this.options.toggle+".editable",b,a.proxy(function(b){var c=a(b.target);c.data("editable")||(c.hasClass(this.options.emptyclass)&&c.empty(),c.editable(this.options).trigger(b))},this))},render:function(a){return this.options.display!==!1?this.input.value2htmlFinal?this.input.value2html(this.value,this.$element[0],this.options.display,a):"function"==typeof this.options.display?this.options.display.call(this.$element[0],this.value,a):this.input.value2html(this.value,this.$element[0]):void 0},enable:function(){this.options.disabled=!1,this.$element.removeClass("editable-disabled"),this.handleEmpty(this.isEmpty),"manual"!==this.options.toggle&&"-1"===this.$element.attr("tabindex")&&this.$element.removeAttr("tabindex")},disable:function(){this.options.disabled=!0,this.hide(),this.$element.addClass("editable-disabled"),this.handleEmpty(this.isEmpty),this.$element.attr("tabindex",-1)},toggleDisabled:function(){this.options.disabled?this.enable():this.disable()},option:function(b,c){return b&&"object"==typeof b?(a.each(b,a.proxy(function(b,c){this.option(a.trim(b),c)},this)),void 0):(this.options[b]=c,"disabled"===b?c?this.disable():this.enable():("value"===b&&this.setValue(c),this.container&&this.container.option(b,c),this.input.option&&this.input.option(b,c),void 0))},handleEmpty:function(b){this.options.display!==!1&&(this.isEmpty=void 0!==b?b:"function"==typeof this.input.isEmpty?this.input.isEmpty(this.$element):""===a.trim(this.$element.html()),this.options.disabled?this.isEmpty&&(this.$element.empty(),this.options.emptyclass&&this.$element.removeClass(this.options.emptyclass)):this.isEmpty?(this.$element.html(this.options.emptytext),this.options.emptyclass&&this.$element.addClass(this.options.emptyclass)):this.options.emptyclass&&this.$element.removeClass(this.options.emptyclass))},show:function(b){if(!this.options.disabled){if(this.container){if(this.container.tip().is(":visible"))return}else{var c=a.extend({},this.options,{value:this.value,input:this.input});this.$element.editableContainer(c),this.$element.on("save.internal",a.proxy(this.save,this)),this.container=this.$element.data("editableContainer")}this.container.show(b)}},hide:function(){this.container&&this.container.hide()},toggle:function(a){this.container&&this.container.tip().is(":visible")?this.hide():this.show(a)},save:function(a,b){if(this.options.unsavedclass){var c=!1;c=c||"function"==typeof this.options.url,c=c||this.options.display===!1,c=c||void 0!==b.response,c=c||this.options.savenochange&&this.input.value2str(this.value)!==this.input.value2str(b.newValue),c?this.$element.removeClass(this.options.unsavedclass):this.$element.addClass(this.options.unsavedclass)}if(this.options.highlight){var d=this.$element,e=d.css("background-color");d.css("background-color",this.options.highlight),setTimeout(function(){"transparent"===e&&(e=""),d.css("background-color",e),d.addClass("editable-bg-transition"),setTimeout(function(){d.removeClass("editable-bg-transition")},1700)},10)}this.setValue(b.newValue,!1,b.response)},validate:function(){return"function"==typeof this.options.validate?this.options.validate.call(this,this.value):void 0},setValue:function(b,c,d){this.value=c?this.input.str2value(b):b,this.container&&this.container.option("value",this.value),a.when(this.render(d)).then(a.proxy(function(){this.handleEmpty()},this))},activate:function(){this.container&&this.container.activate()},destroy:function(){this.disable(),this.container&&this.container.destroy(),this.input.destroy(),"manual"!==this.options.toggle&&(this.$element.removeClass("editable-click"),this.$element.off(this.options.toggle+".editable")),this.$element.off("save.internal"),this.$element.removeClass("editable editable-open editable-disabled"),this.$element.removeData("editable")}},a.fn.editable=function(c){var d={},e=arguments,f="editable";switch(c){case"validate":return this.each(function(){var b,c=a(this),e=c.data(f);e&&(b=e.validate())&&(d[e.options.name]=b)}),d;case"getValue":return 2===arguments.length&&arguments[1]===!0?d=this.eq(0).data(f).value:this.each(function(){var b=a(this),c=b.data(f);c&&void 0!==c.value&&null!==c.value&&(d[c.options.name]=c.input.value2submit(c.value))}),d;case"submit":var g=arguments[1]||{},h=this,i=this.editable("validate");if(a.isEmptyObject(i)){var j={};if(1===h.length){var k=h.data("editable"),l={name:k.options.name||"",value:k.input.value2submit(k.value),pk:"function"==typeof k.options.pk?k.options.pk.call(k.options.scope):k.options.pk};"function"==typeof k.options.params?l=k.options.params.call(k.options.scope,l):(k.options.params=a.fn.editableutils.tryParseJson(k.options.params,!0),a.extend(l,k.options.params)),j={url:k.options.url,data:l,type:"POST"},g.success=g.success||k.options.success,g.error=g.error||k.options.error}else{var m=this.editable("getValue");j={url:g.url,data:m,type:"POST"}}j.success="function"==typeof g.success?function(a){g.success.call(h,a,g)}:a.noop,j.error="function"==typeof g.error?function(){g.error.apply(h,arguments)}:a.noop,g.ajaxOptions&&a.extend(j,g.ajaxOptions),g.data&&a.extend(j.data,g.data),a.ajax(j)}else"function"==typeof g.error&&g.error.call(h,i);return this}return this.each(function(){var d=a(this),g=d.data(f),h="object"==typeof c&&c;return h&&h.selector?(g=new b(this,h),void 0):(g||d.data(f,g=new b(this,h)),"string"==typeof c&&g[c].apply(g,Array.prototype.slice.call(e,1)),void 0)})},a.fn.editable.defaults={type:"text",disabled:!1,toggle:"click",emptytext:"Empty",autotext:"auto",value:null,display:null,emptyclass:"editable-empty",unsavedclass:"editable-unsaved",selector:null,highlight:"#FFFF80"}}(window.jQuery),function(a){"use strict";a.fn.editabletypes={};var b=function(){};b.prototype={init:function(b,c,d){this.type=b,this.options=a.extend({},d,c)},prerender:function(){this.$tpl=a(this.options.tpl),this.$input=this.$tpl,this.$clear=null,this.error=null},render:function(){},value2html:function(b,c){a(c)[this.options.escape?"text":"html"](a.trim(b))},html2value:function(b){return a("<div>").html(b).text()},value2str:function(a){return a},str2value:function(a){return a},value2submit:function(a){return a},value2input:function(a){this.$input.val(a)},input2value:function(){return this.$input.val()},activate:function(){this.$input.is(":visible")&&this.$input.focus()},clear:function(){this.$input.val(null)},escape:function(b){return a("<div>").text(b).html()},autosubmit:function(){},destroy:function(){},setClass:function(){this.options.inputclass&&this.$input.addClass(this.options.inputclass)},setAttr:function(a){void 0!==this.options[a]&&null!==this.options[a]&&this.$input.attr(a,this.options[a])},option:function(a,b){this.options[a]=b}},b.defaults={tpl:"",inputclass:null,escape:!0,scope:null,showbuttons:!0},a.extend(a.fn.editabletypes,{abstractinput:b})}(window.jQuery),function(a){"use strict";var b=function(){};a.fn.editableutils.inherit(b,a.fn.editabletypes.abstractinput),a.extend(b.prototype,{render:function(){var b=a.Deferred();return this.error=null,this.onSourceReady(function(){this.renderList(),b.resolve()},function(){this.error=this.options.sourceError,b.resolve()}),b.promise()},html2value:function(){return null},value2html:function(b,c,d,e){var f=a.Deferred(),g=function(){"function"==typeof d?d.call(c,b,this.sourceData,e):this.value2htmlFinal(b,c),f.resolve()};return null===b?g.call(this):this.onSourceReady(g,function(){f.resolve()}),f.promise()},onSourceReady:function(b,c){var d;if(a.isFunction(this.options.source)?(d=this.options.source.call(this.options.scope),this.sourceData=null):d=this.options.source,this.options.sourceCache&&a.isArray(this.sourceData))return b.call(this),void 0;try{d=a.fn.editableutils.tryParseJson(d,!1)}catch(e){return c.call(this),void 0}if("string"==typeof d){if(this.options.sourceCache){var f,g=d;if(a(document).data(g)||a(document).data(g,{}),f=a(document).data(g),f.loading===!1&&f.sourceData)return this.sourceData=f.sourceData,this.doPrepend(),b.call(this),void 0;if(f.loading===!0)return f.callbacks.push(a.proxy(function(){this.sourceData=f.sourceData,this.doPrepend(),b.call(this)},this)),f.err_callbacks.push(a.proxy(c,this)),void 0;f.loading=!0,f.callbacks=[],f.err_callbacks=[]}var h=a.extend({url:d,type:"get",cache:!1,dataType:"json",success:a.proxy(function(d){f&&(f.loading=!1),this.sourceData=this.makeArray(d),a.isArray(this.sourceData)?(f&&(f.sourceData=this.sourceData,a.each(f.callbacks,function(){this.call()})),this.doPrepend(),b.call(this)):(c.call(this),f&&a.each(f.err_callbacks,function(){this.call()}))},this),error:a.proxy(function(){c.call(this),f&&(f.loading=!1,a.each(f.err_callbacks,function(){this.call()}))},this)},this.options.sourceOptions);a.ajax(h)}else this.sourceData=this.makeArray(d),a.isArray(this.sourceData)?(this.doPrepend(),b.call(this)):c.call(this)},doPrepend:function(){null!==this.options.prepend&&void 0!==this.options.prepend&&(a.isArray(this.prependData)||(a.isFunction(this.options.prepend)&&(this.options.prepend=this.options.prepend.call(this.options.scope)),this.options.prepend=a.fn.editableutils.tryParseJson(this.options.prepend,!0),"string"==typeof this.options.prepend&&(this.options.prepend={"":this.options.prepend}),this.prependData=this.makeArray(this.options.prepend)),a.isArray(this.prependData)&&a.isArray(this.sourceData)&&(this.sourceData=this.prependData.concat(this.sourceData)))},renderList:function(){},value2htmlFinal:function(){},makeArray:function(b){var c,d,e,f,g=[];if(!b||"string"==typeof b)return null;if(a.isArray(b)){f=function(a,b){return d={value:a,text:b},c++>=2?!1:void 0};for(var h=0;h<b.length;h++)e=b[h],"object"==typeof e?(c=0,a.each(e,f),1===c?g.push(d):c>1&&(e.children&&(e.children=this.makeArray(e.children)),g.push(e))):g.push({value:e,text:e})}else a.each(b,function(a,b){g.push({value:a,text:b})});return g},option:function(a,b){this.options[a]=b,"source"===a&&(this.sourceData=null),"prepend"===a&&(this.prependData=null)}}),b.defaults=a.extend({},a.fn.editabletypes.abstractinput.defaults,{source:null,prepend:!1,sourceError:"Error when loading list",sourceCache:!0,sourceOptions:null}),a.fn.editabletypes.list=b}(window.jQuery),function(a){"use strict";var b=function(a){this.init("text",a,b.defaults)};a.fn.editableutils.inherit(b,a.fn.editabletypes.abstractinput),a.extend(b.prototype,{render:function(){this.renderClear(),this.setClass(),this.setAttr("placeholder")},activate:function(){this.$input.is(":visible")&&(this.$input.focus(),a.fn.editableutils.setCursorPosition(this.$input.get(0),this.$input.val().length),this.toggleClear&&this.toggleClear())},renderClear:function(){this.options.clear&&(this.$clear=a('<span class="editable-clear-x"></span>'),this.$input.after(this.$clear).css("padding-right",24).keyup(a.proxy(function(b){if(!~a.inArray(b.keyCode,[40,38,9,13,27])){clearTimeout(this.t);var c=this;this.t=setTimeout(function(){c.toggleClear(b)},100)}},this)).parent().css("position","relative"),this.$clear.click(a.proxy(this.clear,this)))},postrender:function(){},toggleClear:function(){if(this.$clear){var a=this.$input.val().length,b=this.$clear.is(":visible");a&&!b&&this.$clear.show(),!a&&b&&this.$clear.hide()}},clear:function(){this.$clear.hide(),this.$input.val("").focus()}}),b.defaults=a.extend({},a.fn.editabletypes.abstractinput.defaults,{tpl:'<input type="text">',placeholder:null,clear:!0}),a.fn.editabletypes.text=b}(window.jQuery),function(a){"use strict";var b=function(a){this.init("textarea",a,b.defaults)};a.fn.editableutils.inherit(b,a.fn.editabletypes.abstractinput),a.extend(b.prototype,{render:function(){this.setClass(),this.setAttr("placeholder"),this.setAttr("rows"),this.$input.keydown(function(b){b.ctrlKey&&13===b.which&&a(this).closest("form").submit()})},activate:function(){a.fn.editabletypes.text.prototype.activate.call(this)}}),b.defaults=a.extend({},a.fn.editabletypes.abstractinput.defaults,{tpl:"<textarea></textarea>",inputclass:"input-large",placeholder:null,rows:7}),a.fn.editabletypes.textarea=b}(window.jQuery),function(a){"use strict";var b=function(a){this.init("select",a,b.defaults)};a.fn.editableutils.inherit(b,a.fn.editabletypes.list),a.extend(b.prototype,{renderList:function(){this.$input.empty();var b=function(c,d){var e;if(a.isArray(d))for(var f=0;f<d.length;f++)e={},d[f].children?(e.label=d[f].text,c.append(b(a("<optgroup>",e),d[f].children))):(e.value=d[f].value,d[f].disabled&&(e.disabled=!0),c.append(a("<option>",e).text(d[f].text)));return c};b(this.$input,this.sourceData),this.setClass(),this.$input.on("keydown.editable",function(b){13===b.which&&a(this).closest("form").submit()})},value2htmlFinal:function(b,c){var d="",e=a.fn.editableutils.itemsByValue(b,this.sourceData);e.length&&(d=e[0].text),a.fn.editabletypes.abstractinput.prototype.value2html.call(this,d,c)},autosubmit:function(){this.$input.off("keydown.editable").on("change.editable",function(){a(this).closest("form").submit()})}}),b.defaults=a.extend({},a.fn.editabletypes.list.defaults,{tpl:"<select></select>"}),a.fn.editabletypes.select=b}(window.jQuery),function(a){"use strict";var b=function(a){this.init("checklist",a,b.defaults)};a.fn.editableutils.inherit(b,a.fn.editabletypes.list),a.extend(b.prototype,{renderList:function(){var b;if(this.$tpl.empty(),a.isArray(this.sourceData)){for(var c=0;c<this.sourceData.length;c++)b=a("<label>").append(a("<input>",{type:"checkbox",value:this.sourceData[c].value})).append(a("<span>").text(" "+this.sourceData[c].text)),a("<div>").append(b).appendTo(this.$tpl);this.$input=this.$tpl.find('input[type="checkbox"]'),this.setClass()}},value2str:function(b){return a.isArray(b)?b.sort().join(a.trim(this.options.separator)):""},str2value:function(b){var c,d=null;return"string"==typeof b&&b.length?(c=new RegExp("\\s*"+a.trim(this.options.separator)+"\\s*"),d=b.split(c)):d=a.isArray(b)?b:[b],d},value2input:function(b){this.$input.prop("checked",!1),a.isArray(b)&&b.length&&this.$input.each(function(c,d){var e=a(d);a.each(b,function(a,b){e.val()==b&&e.prop("checked",!0)})})},input2value:function(){var b=[];return this.$input.filter(":checked").each(function(c,d){b.push(a(d).val())}),b},value2htmlFinal:function(b,c){var d=[],e=a.fn.editableutils.itemsByValue(b,this.sourceData),f=this.options.escape;e.length?(a.each(e,function(b,c){var e=f?a.fn.editableutils.escape(c.text):c.text;d.push(e)}),a(c).html(d.join("<br>"))):a(c).empty()},activate:function(){this.$input.first().focus()},autosubmit:function(){this.$input.on("keydown",function(b){13===b.which&&a(this).closest("form").submit()})}}),b.defaults=a.extend({},a.fn.editabletypes.list.defaults,{tpl:'<div class="editable-checklist"></div>',inputclass:null,separator:","}),a.fn.editabletypes.checklist=b}(window.jQuery),function(a){"use strict";var b=function(a){this.init("password",a,b.defaults)};a.fn.editableutils.inherit(b,a.fn.editabletypes.text),a.extend(b.prototype,{value2html:function(b,c){b?a(c).text("[hidden]"):a(c).empty()},html2value:function(){return null}}),b.defaults=a.extend({},a.fn.editabletypes.text.defaults,{tpl:'<input type="password">'}),a.fn.editabletypes.password=b}(window.jQuery),function(a){"use strict";var b=function(a){this.init("email",a,b.defaults)};a.fn.editableutils.inherit(b,a.fn.editabletypes.text),b.defaults=a.extend({},a.fn.editabletypes.text.defaults,{tpl:'<input type="email">'}),a.fn.editabletypes.email=b}(window.jQuery),function(a){"use strict";var b=function(a){this.init("url",a,b.defaults)};a.fn.editableutils.inherit(b,a.fn.editabletypes.text),b.defaults=a.extend({},a.fn.editabletypes.text.defaults,{tpl:'<input type="url">'}),a.fn.editabletypes.url=b}(window.jQuery),function(a){"use strict";var b=function(a){this.init("tel",a,b.defaults)};a.fn.editableutils.inherit(b,a.fn.editabletypes.text),b.defaults=a.extend({},a.fn.editabletypes.text.defaults,{tpl:'<input type="tel">'}),a.fn.editabletypes.tel=b}(window.jQuery),function(a){"use strict";var b=function(a){this.init("number",a,b.defaults)};a.fn.editableutils.inherit(b,a.fn.editabletypes.text),a.extend(b.prototype,{render:function(){b.superclass.render.call(this),this.setAttr("min"),this.setAttr("max"),this.setAttr("step")},postrender:function(){this.$clear&&this.$clear.css({right:24})}}),b.defaults=a.extend({},a.fn.editabletypes.text.defaults,{tpl:'<input type="number">',inputclass:"input-mini",min:null,max:null,step:null}),a.fn.editabletypes.number=b}(window.jQuery),function(a){"use strict";
var b=function(a){this.init("range",a,b.defaults)};a.fn.editableutils.inherit(b,a.fn.editabletypes.number),a.extend(b.prototype,{render:function(){this.$input=this.$tpl.filter("input"),this.setClass(),this.setAttr("min"),this.setAttr("max"),this.setAttr("step"),this.$input.on("input",function(){a(this).siblings("output").text(a(this).val())})},activate:function(){this.$input.focus()}}),b.defaults=a.extend({},a.fn.editabletypes.number.defaults,{tpl:'<input type="range"><output style="width: 30px; display: inline-block"></output>',inputclass:"input-medium"}),a.fn.editabletypes.range=b}(window.jQuery),function(a){"use strict";var b=function(a){this.init("time",a,b.defaults)};a.fn.editableutils.inherit(b,a.fn.editabletypes.abstractinput),a.extend(b.prototype,{render:function(){this.setClass()}}),b.defaults=a.extend({},a.fn.editabletypes.abstractinput.defaults,{tpl:'<input type="time">'}),a.fn.editabletypes.time=b}(window.jQuery),function(a){"use strict";var b=function(c){if(this.init("select2",c,b.defaults),c.select2=c.select2||{},this.sourceData=null,c.placeholder&&(c.select2.placeholder=c.placeholder),!c.select2.tags&&c.source){var d=c.source;a.isFunction(c.source)&&(d=c.source.call(c.scope)),"string"==typeof d?(c.select2.ajax=c.select2.ajax||{},c.select2.ajax.data||(c.select2.ajax.data=function(a){return{query:a}}),c.select2.ajax.results||(c.select2.ajax.results=function(a){return{results:a}}),c.select2.ajax.url=d):(this.sourceData=this.convertSource(d),c.select2.data=this.sourceData)}if(this.options.select2=a.extend({},b.defaults.select2,c.select2),this.isMultiple=this.options.select2.tags||this.options.select2.multiple,this.isRemote="ajax"in this.options.select2,this.idFunc=this.options.select2.id,"function"!=typeof this.idFunc){var e=this.idFunc||"id";this.idFunc=function(a){return a[e]}}this.formatSelection=this.options.select2.formatSelection,"function"!=typeof this.formatSelection&&(this.formatSelection=function(a){return a.text})};a.fn.editableutils.inherit(b,a.fn.editabletypes.abstractinput),a.extend(b.prototype,{render:function(){this.setClass(),this.isRemote&&this.$input.on("select2-loaded",a.proxy(function(a){this.sourceData=a.items.results},this)),this.isMultiple&&this.$input.on("change",function(){a(this).closest("form").parent().triggerHandler("resize")})},value2html:function(c,d){var e,f="",g=this;this.options.select2.tags?e=c:this.sourceData&&(e=a.fn.editableutils.itemsByValue(c,this.sourceData,this.idFunc)),a.isArray(e)?(f=[],a.each(e,function(a,b){f.push(b&&"object"==typeof b?g.formatSelection(b):b)})):e&&(f=g.formatSelection(e)),f=a.isArray(f)?f.join(this.options.viewseparator):f,b.superclass.value2html.call(this,f,d)},html2value:function(a){return this.options.select2.tags?this.str2value(a,this.options.viewseparator):null},value2input:function(b){if(a.isArray(b)&&(b=b.join(this.getSeparator())),this.$input.data("select2")?this.$input.val(b).trigger("change",!0):(this.$input.val(b),this.$input.select2(this.options.select2)),this.isRemote&&!this.isMultiple&&!this.options.select2.initSelection){var c=this.options.select2.id,d=this.options.select2.formatSelection;if(!c&&!d){var e=a(this.options.scope);if(!e.data("editable").isEmpty){var f={id:b,text:e.text()};this.$input.select2("data",f)}}}},input2value:function(){return this.$input.select2("val")},str2value:function(b,c){if("string"!=typeof b||!this.isMultiple)return b;c=c||this.getSeparator();var d,e,f;if(null===b||b.length<1)return null;for(d=b.split(c),e=0,f=d.length;f>e;e+=1)d[e]=a.trim(d[e]);return d},autosubmit:function(){this.$input.on("change",function(b,c){c||a(this).closest("form").submit()})},getSeparator:function(){return this.options.select2.separator||a.fn.select2.defaults.separator},convertSource:function(b){if(a.isArray(b)&&b.length&&void 0!==b[0].value)for(var c=0;c<b.length;c++)void 0!==b[c].value&&(b[c].id=b[c].value,delete b[c].value);return b},destroy:function(){this.$input.data("select2")&&this.$input.select2("destroy")}}),b.defaults=a.extend({},a.fn.editabletypes.abstractinput.defaults,{tpl:'<input type="hidden">',select2:null,placeholder:null,source:null,viewseparator:", "}),a.fn.editabletypes.select2=b}(window.jQuery),function(a){var b=function(b,c){return this.$element=a(b),this.$element.is("input")?(this.options=a.extend({},a.fn.combodate.defaults,c,this.$element.data()),this.init(),void 0):(a.error("Combodate should be applied to INPUT element"),void 0)};b.prototype={constructor:b,init:function(){this.map={day:["D","date"],month:["M","month"],year:["Y","year"],hour:["[Hh]","hours"],minute:["m","minutes"],second:["s","seconds"],ampm:["[Aa]",""]},this.$widget=a('<span class="combodate"></span>').html(this.getTemplate()),this.initCombos(),this.$widget.on("change","select",a.proxy(function(b){this.$element.val(this.getValue()).change(),this.options.smartDays&&(a(b.target).is(".month")||a(b.target).is(".year"))&&this.fillCombo("day")},this)),this.$widget.find("select").css("width","auto"),this.$element.hide().after(this.$widget),this.setValue(this.$element.val()||this.options.value)},getTemplate:function(){var b=this.options.template;return a.each(this.map,function(a,c){c=c[0];var d=new RegExp(c+"+"),e=c.length>1?c.substring(1,2):c;b=b.replace(d,"{"+e+"}")}),b=b.replace(/ /g,"&nbsp;"),a.each(this.map,function(a,c){c=c[0];var d=c.length>1?c.substring(1,2):c;b=b.replace("{"+d+"}",'<select class="'+a+'"></select>')}),b},initCombos:function(){for(var a in this.map){var b=this.$widget.find("."+a);this["$"+a]=b.length?b:null,this.fillCombo(a)}},fillCombo:function(a){var b=this["$"+a];if(b){var c="fill"+a.charAt(0).toUpperCase()+a.slice(1),d=this[c](),e=b.val();b.empty();for(var f=0;f<d.length;f++)b.append('<option value="'+d[f][0]+'">'+d[f][1]+"</option>");b.val(e)}},fillCommon:function(a){var b,c=[];if("name"===this.options.firstItem){b=moment.relativeTime||moment.langData()._relativeTime;var d="function"==typeof b[a]?b[a](1,!0,a,!1):b[a];d=d.split(" ").reverse()[0],c.push(["",d])}else"empty"===this.options.firstItem&&c.push(["",""]);return c},fillDay:function(){var a,b,c=this.fillCommon("d"),d=-1!==this.options.template.indexOf("DD"),e=31;if(this.options.smartDays&&this.$month&&this.$year){var f=parseInt(this.$month.val(),10),g=parseInt(this.$year.val(),10);isNaN(f)||isNaN(g)||(e=moment([g,f]).daysInMonth())}for(b=1;e>=b;b++)a=d?this.leadZero(b):b,c.push([b,a]);return c},fillMonth:function(){var a,b,c=this.fillCommon("M"),d=-1!==this.options.template.indexOf("MMMM"),e=-1!==this.options.template.indexOf("MMM"),f=-1!==this.options.template.indexOf("MM");for(b=0;11>=b;b++)a=d?moment().date(1).month(b).format("MMMM"):e?moment().date(1).month(b).format("MMM"):f?this.leadZero(b+1):b+1,c.push([b,a]);return c},fillYear:function(){var a,b,c=[],d=-1!==this.options.template.indexOf("YYYY");for(b=this.options.maxYear;b>=this.options.minYear;b--)a=d?b:(b+"").substring(2),c[this.options.yearDescending?"push":"unshift"]([b,a]);return c=this.fillCommon("y").concat(c)},fillHour:function(){var a,b,c=this.fillCommon("h"),d=-1!==this.options.template.indexOf("h"),e=(-1!==this.options.template.indexOf("H"),-1!==this.options.template.toLowerCase().indexOf("hh")),f=d?1:0,g=d?12:23;for(b=f;g>=b;b++)a=e?this.leadZero(b):b,c.push([b,a]);return c},fillMinute:function(){var a,b,c=this.fillCommon("m"),d=-1!==this.options.template.indexOf("mm");for(b=0;59>=b;b+=this.options.minuteStep)a=d?this.leadZero(b):b,c.push([b,a]);return c},fillSecond:function(){var a,b,c=this.fillCommon("s"),d=-1!==this.options.template.indexOf("ss");for(b=0;59>=b;b+=this.options.secondStep)a=d?this.leadZero(b):b,c.push([b,a]);return c},fillAmpm:function(){var a=-1!==this.options.template.indexOf("a"),b=(-1!==this.options.template.indexOf("A"),[["am",a?"am":"AM"],["pm",a?"pm":"PM"]]);return b},getValue:function(b){var c,d={},e=this,f=!1;return a.each(this.map,function(a){if("ampm"!==a){var b="day"===a?1:0;return d[a]=e["$"+a]?parseInt(e["$"+a].val(),10):b,isNaN(d[a])?(f=!0,!1):void 0}}),f?"":(this.$ampm&&(d.hour=12===d.hour?"am"===this.$ampm.val()?0:12:"am"===this.$ampm.val()?d.hour:d.hour+12),c=moment([d.year,d.month,d.day,d.hour,d.minute,d.second]),this.highlight(c),b=void 0===b?this.options.format:b,null===b?c.isValid()?c:null:c.isValid()?c.format(b):"")},setValue:function(b){function c(b,c){var d={};return b.children("option").each(function(b,e){var f,g=a(e).attr("value");""!==g&&(f=Math.abs(g-c),("undefined"==typeof d.distance||f<d.distance)&&(d={value:g,distance:f}))}),d.value}if(b){var d="string"==typeof b?moment(b,this.options.format):moment(b),e=this,f={};d.isValid()&&(a.each(this.map,function(a,b){"ampm"!==a&&(f[a]=d[b[1]]())}),this.$ampm&&(f.hour>=12?(f.ampm="pm",f.hour>12&&(f.hour-=12)):(f.ampm="am",0===f.hour&&(f.hour=12))),a.each(f,function(a,b){e["$"+a]&&("minute"===a&&e.options.minuteStep>1&&e.options.roundTime&&(b=c(e["$"+a],b)),"second"===a&&e.options.secondStep>1&&e.options.roundTime&&(b=c(e["$"+a],b)),e["$"+a].val(b))}),this.options.smartDays&&this.fillCombo("day"),this.$element.val(d.format(this.options.format)).change())}},highlight:function(a){a.isValid()?this.options.errorClass?this.$widget.removeClass(this.options.errorClass):this.$widget.find("select").css("border-color",this.borderColor):this.options.errorClass?this.$widget.addClass(this.options.errorClass):(this.borderColor||(this.borderColor=this.$widget.find("select").css("border-color")),this.$widget.find("select").css("border-color","red"))},leadZero:function(a){return 9>=a?"0"+a:a},destroy:function(){this.$widget.remove(),this.$element.removeData("combodate").show()}},a.fn.combodate=function(c){var d,e=Array.apply(null,arguments);return e.shift(),"getValue"===c&&this.length&&(d=this.eq(0).data("combodate"))?d.getValue.apply(d,e):this.each(function(){var d=a(this),f=d.data("combodate"),g="object"==typeof c&&c;f||d.data("combodate",f=new b(this,g)),"string"==typeof c&&"function"==typeof f[c]&&f[c].apply(f,e)})},a.fn.combodate.defaults={format:"DD-MM-YYYY HH:mm",template:"D / MMM / YYYY H : mm",value:null,minYear:1970,maxYear:2015,yearDescending:!0,minuteStep:5,secondStep:1,firstItem:"empty",errorClass:null,roundTime:!0,smartDays:!1}}(window.jQuery),function(a){"use strict";var b=function(c){this.init("combodate",c,b.defaults),this.options.viewformat||(this.options.viewformat=this.options.format),c.combodate=a.fn.editableutils.tryParseJson(c.combodate,!0),this.options.combodate=a.extend({},b.defaults.combodate,c.combodate,{format:this.options.format,template:this.options.template})};a.fn.editableutils.inherit(b,a.fn.editabletypes.abstractinput),a.extend(b.prototype,{render:function(){this.$input.combodate(this.options.combodate),"bs3"===a.fn.editableform.engine&&this.$input.siblings().find("select").addClass("form-control"),this.options.inputclass&&this.$input.siblings().find("select").addClass(this.options.inputclass)},value2html:function(a,c){var d=a?a.format(this.options.viewformat):"";b.superclass.value2html.call(this,d,c)},html2value:function(a){return a?moment(a,this.options.viewformat):null},value2str:function(a){return a?a.format(this.options.format):""},str2value:function(a){return a?moment(a,this.options.format):null},value2submit:function(a){return this.value2str(a)},value2input:function(a){this.$input.combodate("setValue",a)},input2value:function(){return this.$input.combodate("getValue",null)},activate:function(){this.$input.siblings(".combodate").find("select").eq(0).focus()},autosubmit:function(){}}),b.defaults=a.extend({},a.fn.editabletypes.abstractinput.defaults,{tpl:'<input type="text">',inputclass:null,format:"YYYY-MM-DD",viewformat:null,template:"D / MMM / YYYY",combodate:null}),a.fn.editabletypes.combodate=b}(window.jQuery),function(a){"use strict";var b=a.fn.editableform.Constructor.prototype.initInput;a.extend(a.fn.editableform.Constructor.prototype,{initTemplate:function(){this.$form=a(a.fn.editableform.template),this.$form.find(".editable-error-block").addClass("help-block")},initInput:function(){b.apply(this);var c=null===this.input.options.inputclass||this.input.options.inputclass===!1,d="input-medium",e="text,select,textarea,password,email,url,tel,number,range,time".split(",");~a.inArray(this.input.type,e)&&c&&(this.input.options.inputclass=d,this.input.$input.addClass(d))}}),a.fn.editableform.buttons='<button type="submit" class="btn btn-primary editable-submit"><i class="icon-ok icon-white"></i></button><button type="button" class="btn editable-cancel"><i class="icon-remove"></i></button>',a.fn.editableform.errorGroupClass="error",a.fn.editableform.errorBlockClass=null,a.fn.editableform.engine="bs2"}(window.jQuery),function(a){"use strict";a.extend(a.fn.editableContainer.Popup.prototype,{containerName:"popover",innerCss:a.fn.popover&&a(a.fn.popover.defaults.template).find("p").length?".popover-content p":".popover-content",defaults:a.fn.popover.defaults,initContainer:function(){a.extend(this.containerOptions,{trigger:"manual",selector:!1,content:" ",template:this.defaults.template});var b;this.$element.data("template")&&(b=this.$element.data("template"),this.$element.removeData("template")),this.call(this.containerOptions),b&&this.$element.data("template",b)},innerShow:function(){this.call("show")},innerHide:function(){this.call("hide")},innerDestroy:function(){this.call("destroy")},setContainerOption:function(a,b){this.container().options[a]=b},setPosition:function(){!function(){var b,c,d,e,f,g,h,i,j,k,l=this.tip();switch(f="function"==typeof this.options.placement?this.options.placement.call(this,l[0],this.$element[0]):this.options.placement,b=/in/.test(f),l.removeClass("top right bottom left").css({top:0,left:0,display:"block"}),c=this.getPosition(b),d=l[0].offsetWidth,e=l[0].offsetHeight,f=b?f.split(" ")[1]:f,i={top:c.top+c.height,left:c.left+c.width/2-d/2},h={top:c.top-e,left:c.left+c.width/2-d/2},j={top:c.top+c.height/2-e/2,left:c.left-d},k={top:c.top+c.height/2-e/2,left:c.left+c.width},f){case"bottom":i.top+e>a(window).scrollTop()+a(window).height()&&(f=h.top>a(window).scrollTop()?"top":k.left+d<a(window).scrollLeft()+a(window).width()?"right":j.left>a(window).scrollLeft()?"left":"right");break;case"top":h.top<a(window).scrollTop()&&(f=i.top+e<a(window).scrollTop()+a(window).height()?"bottom":k.left+d<a(window).scrollLeft()+a(window).width()?"right":j.left>a(window).scrollLeft()?"left":"right");break;case"left":j.left<a(window).scrollLeft()&&(f=k.left+d<a(window).scrollLeft()+a(window).width()?"right":h.top>a(window).scrollTop()?"top":h.top>a(window).scrollTop()?"bottom":"right");break;case"right":k.left+d>a(window).scrollLeft()+a(window).width()&&(j.left>a(window).scrollLeft()?f="left":h.top>a(window).scrollTop()?f="top":h.top>a(window).scrollTop()&&(f="bottom"))}switch(f){case"bottom":g=i;break;case"top":g=h;break;case"left":g=j;break;case"right":g=k}l.offset(g).addClass(f).addClass("in")}.call(this.container())}})}(window.jQuery),function(a){function b(){return new Date(Date.UTC.apply(Date,arguments))}function c(b,c){var d,e=a(b).data(),f={},g=new RegExp("^"+c.toLowerCase()+"([A-Z])"),c=new RegExp("^"+c.toLowerCase());for(var h in e)c.test(h)&&(d=h.replace(g,function(a,b){return b.toLowerCase()}),f[d]=e[h]);return f}function d(b){var c={};if(k[b]||(b=b.split("-")[0],k[b])){var d=k[b];return a.each(j,function(a,b){b in d&&(c[b]=d[b])}),c}}var e=function(b,c){this._process_options(c),this.element=a(b),this.isInline=!1,this.isInput=this.element.is("input"),this.component=this.element.is(".date")?this.element.find(".add-on, .btn"):!1,this.hasInput=this.component&&this.element.find("input").length,this.component&&0===this.component.length&&(this.component=!1),this.picker=a(l.template),this._buildEvents(),this._attachEvents(),this.isInline?this.picker.addClass("datepicker-inline").appendTo(this.element):this.picker.addClass("datepicker-dropdown dropdown-menu"),this.o.rtl&&(this.picker.addClass("datepicker-rtl"),this.picker.find(".prev i, .next i").toggleClass("icon-arrow-left icon-arrow-right")),this.viewMode=this.o.startView,this.o.calendarWeeks&&this.picker.find("tfoot th.today").attr("colspan",function(a,b){return parseInt(b)+1}),this._allow_update=!1,this.setStartDate(this.o.startDate),this.setEndDate(this.o.endDate),this.setDaysOfWeekDisabled(this.o.daysOfWeekDisabled),this.fillDow(),this.fillMonths(),this._allow_update=!0,this.update(),this.showMode(),this.isInline&&this.show()};e.prototype={constructor:e,_process_options:function(b){this._o=a.extend({},this._o,b);var c=this.o=a.extend({},this._o),d=c.language;switch(k[d]||(d=d.split("-")[0],k[d]||(d=i.language)),c.language=d,c.startView){case 2:case"decade":c.startView=2;break;case 1:case"year":c.startView=1;break;default:c.startView=0}switch(c.minViewMode){case 1:case"months":c.minViewMode=1;break;case 2:case"years":c.minViewMode=2;break;default:c.minViewMode=0}c.startView=Math.max(c.startView,c.minViewMode),c.weekStart%=7,c.weekEnd=(c.weekStart+6)%7;var e=l.parseFormat(c.format);c.startDate!==-1/0&&(c.startDate=l.parseDate(c.startDate,e,c.language)),1/0!==c.endDate&&(c.endDate=l.parseDate(c.endDate,e,c.language)),c.daysOfWeekDisabled=c.daysOfWeekDisabled||[],a.isArray(c.daysOfWeekDisabled)||(c.daysOfWeekDisabled=c.daysOfWeekDisabled.split(/[,\s]*/)),c.daysOfWeekDisabled=a.map(c.daysOfWeekDisabled,function(a){return parseInt(a,10)})},_events:[],_secondaryEvents:[],_applyEvents:function(a){for(var b,c,d=0;d<a.length;d++)b=a[d][0],c=a[d][1],b.on(c)},_unapplyEvents:function(a){for(var b,c,d=0;d<a.length;d++)b=a[d][0],c=a[d][1],b.off(c)},_buildEvents:function(){this.isInput?this._events=[[this.element,{focus:a.proxy(this.show,this),keyup:a.proxy(this.update,this),keydown:a.proxy(this.keydown,this)}]]:this.component&&this.hasInput?this._events=[[this.element.find("input"),{focus:a.proxy(this.show,this),keyup:a.proxy(this.update,this),keydown:a.proxy(this.keydown,this)}],[this.component,{click:a.proxy(this.show,this)}]]:this.element.is("div")?this.isInline=!0:this._events=[[this.element,{click:a.proxy(this.show,this)}]],this._secondaryEvents=[[this.picker,{click:a.proxy(this.click,this)}],[a(window),{resize:a.proxy(this.place,this)}],[a(document),{mousedown:a.proxy(function(a){this.element.is(a.target)||this.element.find(a.target).size()||this.picker.is(a.target)||this.picker.find(a.target).size()||this.hide()},this)}]]},_attachEvents:function(){this._detachEvents(),this._applyEvents(this._events)},_detachEvents:function(){this._unapplyEvents(this._events)},_attachSecondaryEvents:function(){this._detachSecondaryEvents(),this._applyEvents(this._secondaryEvents)},_detachSecondaryEvents:function(){this._unapplyEvents(this._secondaryEvents)},_trigger:function(b,c){var d=c||this.date,e=new Date(d.getTime()+6e4*d.getTimezoneOffset());this.element.trigger({type:b,date:e,format:a.proxy(function(a){var b=a||this.o.format;return l.formatDate(d,b,this.o.language)},this)})},show:function(a){this.isInline||this.picker.appendTo("body"),this.picker.show(),this.height=this.component?this.component.outerHeight():this.element.outerHeight(),this.place(),this._attachSecondaryEvents(),a&&a.preventDefault(),this._trigger("show")},hide:function(){this.isInline||this.picker.is(":visible")&&(this.picker.hide().detach(),this._detachSecondaryEvents(),this.viewMode=this.o.startView,this.showMode(),this.o.forceParse&&(this.isInput&&this.element.val()||this.hasInput&&this.element.find("input").val())&&this.setValue(),this._trigger("hide"))},remove:function(){this.hide(),this._detachEvents(),this._detachSecondaryEvents(),this.picker.remove(),delete this.element.data().datepicker,this.isInput||delete this.element.data().date},getDate:function(){var a=this.getUTCDate();return new Date(a.getTime()+6e4*a.getTimezoneOffset())},getUTCDate:function(){return this.date},setDate:function(a){this.setUTCDate(new Date(a.getTime()-6e4*a.getTimezoneOffset()))},setUTCDate:function(a){this.date=a,this.setValue()},setValue:function(){var a=this.getFormattedDate();this.isInput?this.element.val(a):this.component&&this.element.find("input").val(a)},getFormattedDate:function(a){return void 0===a&&(a=this.o.format),l.formatDate(this.date,a,this.o.language)},setStartDate:function(a){this._process_options({startDate:a}),this.update(),this.updateNavArrows()},setEndDate:function(a){this._process_options({endDate:a}),this.update(),this.updateNavArrows()},setDaysOfWeekDisabled:function(a){this._process_options({daysOfWeekDisabled:a}),this.update(),this.updateNavArrows()},place:function(){if(!this.isInline){var b=parseInt(this.element.parents().filter(function(){return"auto"!=a(this).css("z-index")}).first().css("z-index"))+10,c=this.component?this.component.parent().offset():this.element.offset(),d=this.component?this.component.outerHeight(!0):this.element.outerHeight(!0);this.picker.css({top:c.top+d,left:c.left,zIndex:b})}},_allow_update:!0,update:function(){if(this._allow_update){var a,b=!1;arguments&&arguments.length&&("string"==typeof arguments[0]||arguments[0]instanceof Date)?(a=arguments[0],b=!0):(a=this.isInput?this.element.val():this.element.data("date")||this.element.find("input").val(),delete this.element.data().date),this.date=l.parseDate(a,this.o.format,this.o.language),b&&this.setValue(),this.viewDate=this.date<this.o.startDate?new Date(this.o.startDate):this.date>this.o.endDate?new Date(this.o.endDate):new Date(this.date),this.fill()}},fillDow:function(){var a=this.o.weekStart,b="<tr>";if(this.o.calendarWeeks){var c='<th class="cw">&nbsp;</th>';b+=c,this.picker.find(".datepicker-days thead tr:first-child").prepend(c)}for(;a<this.o.weekStart+7;)b+='<th class="dow">'+k[this.o.language].daysMin[a++%7]+"</th>";b+="</tr>",this.picker.find(".datepicker-days thead").append(b)},fillMonths:function(){for(var a="",b=0;12>b;)a+='<span class="month">'+k[this.o.language].monthsShort[b++]+"</span>";this.picker.find(".datepicker-months td").html(a)},setRange:function(b){b&&b.length?this.range=a.map(b,function(a){return a.valueOf()}):delete this.range,this.fill()},getClassNames:function(b){var c=[],d=this.viewDate.getUTCFullYear(),e=this.viewDate.getUTCMonth(),f=this.date.valueOf(),g=new Date;return b.getUTCFullYear()<d||b.getUTCFullYear()==d&&b.getUTCMonth()<e?c.push("old"):(b.getUTCFullYear()>d||b.getUTCFullYear()==d&&b.getUTCMonth()>e)&&c.push("new"),this.o.todayHighlight&&b.getUTCFullYear()==g.getFullYear()&&b.getUTCMonth()==g.getMonth()&&b.getUTCDate()==g.getDate()&&c.push("today"),f&&b.valueOf()==f&&c.push("active"),(b.valueOf()<this.o.startDate||b.valueOf()>this.o.endDate||-1!==a.inArray(b.getUTCDay(),this.o.daysOfWeekDisabled))&&c.push("disabled"),this.range&&(b>this.range[0]&&b<this.range[this.range.length-1]&&c.push("range"),-1!=a.inArray(b.valueOf(),this.range)&&c.push("selected")),c},fill:function(){var c,d=new Date(this.viewDate),e=d.getUTCFullYear(),f=d.getUTCMonth(),g=this.o.startDate!==-1/0?this.o.startDate.getUTCFullYear():-1/0,h=this.o.startDate!==-1/0?this.o.startDate.getUTCMonth():-1/0,i=1/0!==this.o.endDate?this.o.endDate.getUTCFullYear():1/0,j=1/0!==this.o.endDate?this.o.endDate.getUTCMonth():1/0;this.date&&this.date.valueOf(),this.picker.find(".datepicker-days thead th.datepicker-switch").text(k[this.o.language].months[f]+" "+e),this.picker.find("tfoot th.today").text(k[this.o.language].today).toggle(this.o.todayBtn!==!1),this.picker.find("tfoot th.clear").text(k[this.o.language].clear).toggle(this.o.clearBtn!==!1),this.updateNavArrows(),this.fillMonths();var m=b(e,f-1,28,0,0,0,0),n=l.getDaysInMonth(m.getUTCFullYear(),m.getUTCMonth());m.setUTCDate(n),m.setUTCDate(n-(m.getUTCDay()-this.o.weekStart+7)%7);var o=new Date(m);o.setUTCDate(o.getUTCDate()+42),o=o.valueOf();for(var p,q=[];m.valueOf()<o;){if(m.getUTCDay()==this.o.weekStart&&(q.push("<tr>"),this.o.calendarWeeks)){var r=new Date(+m+864e5*((this.o.weekStart-m.getUTCDay()-7)%7)),s=new Date(+r+864e5*((11-r.getUTCDay())%7)),t=new Date(+(t=b(s.getUTCFullYear(),0,1))+864e5*((11-t.getUTCDay())%7)),u=(s-t)/864e5/7+1;q.push('<td class="cw">'+u+"</td>")}p=this.getClassNames(m),p.push("day");var v=this.o.beforeShowDay(m);void 0===v?v={}:"boolean"==typeof v?v={enabled:v}:"string"==typeof v&&(v={classes:v}),v.enabled===!1&&p.push("disabled"),v.classes&&(p=p.concat(v.classes.split(/\s+/))),v.tooltip&&(c=v.tooltip),p=a.unique(p),q.push('<td class="'+p.join(" ")+'"'+(c?' title="'+c+'"':"")+">"+m.getUTCDate()+"</td>"),m.getUTCDay()==this.o.weekEnd&&q.push("</tr>"),m.setUTCDate(m.getUTCDate()+1)}this.picker.find(".datepicker-days tbody").empty().append(q.join(""));var w=this.date&&this.date.getUTCFullYear(),x=this.picker.find(".datepicker-months").find("th:eq(1)").text(e).end().find("span").removeClass("active");w&&w==e&&x.eq(this.date.getUTCMonth()).addClass("active"),(g>e||e>i)&&x.addClass("disabled"),e==g&&x.slice(0,h).addClass("disabled"),e==i&&x.slice(j+1).addClass("disabled"),q="",e=10*parseInt(e/10,10);var y=this.picker.find(".datepicker-years").find("th:eq(1)").text(e+"-"+(e+9)).end().find("td");e-=1;for(var z=-1;11>z;z++)q+='<span class="year'+(-1==z?" old":10==z?" new":"")+(w==e?" active":"")+(g>e||e>i?" disabled":"")+'">'+e+"</span>",e+=1;y.html(q)},updateNavArrows:function(){if(this._allow_update){var a=new Date(this.viewDate),b=a.getUTCFullYear(),c=a.getUTCMonth();switch(this.viewMode){case 0:this.o.startDate!==-1/0&&b<=this.o.startDate.getUTCFullYear()&&c<=this.o.startDate.getUTCMonth()?this.picker.find(".prev").css({visibility:"hidden"}):this.picker.find(".prev").css({visibility:"visible"}),1/0!==this.o.endDate&&b>=this.o.endDate.getUTCFullYear()&&c>=this.o.endDate.getUTCMonth()?this.picker.find(".next").css({visibility:"hidden"}):this.picker.find(".next").css({visibility:"visible"});break;case 1:case 2:this.o.startDate!==-1/0&&b<=this.o.startDate.getUTCFullYear()?this.picker.find(".prev").css({visibility:"hidden"}):this.picker.find(".prev").css({visibility:"visible"}),1/0!==this.o.endDate&&b>=this.o.endDate.getUTCFullYear()?this.picker.find(".next").css({visibility:"hidden"}):this.picker.find(".next").css({visibility:"visible"})}}},click:function(c){c.preventDefault();var d=a(c.target).closest("span, td, th");if(1==d.length)switch(d[0].nodeName.toLowerCase()){case"th":switch(d[0].className){case"datepicker-switch":this.showMode(1);break;case"prev":case"next":var e=l.modes[this.viewMode].navStep*("prev"==d[0].className?-1:1);switch(this.viewMode){case 0:this.viewDate=this.moveMonth(this.viewDate,e);break;case 1:case 2:this.viewDate=this.moveYear(this.viewDate,e)}this.fill();break;case"today":var f=new Date;f=b(f.getFullYear(),f.getMonth(),f.getDate(),0,0,0),this.showMode(-2);var g="linked"==this.o.todayBtn?null:"view";this._setDate(f,g);break;case"clear":var h;this.isInput?h=this.element:this.component&&(h=this.element.find("input")),h&&h.val("").change(),this._trigger("changeDate"),this.update(),this.o.autoclose&&this.hide()}break;case"span":if(!d.is(".disabled")){if(this.viewDate.setUTCDate(1),d.is(".month")){var i=1,j=d.parent().find("span").index(d),k=this.viewDate.getUTCFullYear();this.viewDate.setUTCMonth(j),this._trigger("changeMonth",this.viewDate),1===this.o.minViewMode&&this._setDate(b(k,j,i,0,0,0,0))}else{var k=parseInt(d.text(),10)||0,i=1,j=0;this.viewDate.setUTCFullYear(k),this._trigger("changeYear",this.viewDate),2===this.o.minViewMode&&this._setDate(b(k,j,i,0,0,0,0))}this.showMode(-1),this.fill()}break;case"td":if(d.is(".day")&&!d.is(".disabled")){var i=parseInt(d.text(),10)||1,k=this.viewDate.getUTCFullYear(),j=this.viewDate.getUTCMonth();d.is(".old")?0===j?(j=11,k-=1):j-=1:d.is(".new")&&(11==j?(j=0,k+=1):j+=1),this._setDate(b(k,j,i,0,0,0,0))}}},_setDate:function(a,b){b&&"date"!=b||(this.date=new Date(a)),b&&"view"!=b||(this.viewDate=new Date(a)),this.fill(),this.setValue(),this._trigger("changeDate");var c;this.isInput?c=this.element:this.component&&(c=this.element.find("input")),c&&(c.change(),!this.o.autoclose||b&&"date"!=b||this.hide())},moveMonth:function(a,b){if(!b)return a;var c,d,e=new Date(a.valueOf()),f=e.getUTCDate(),g=e.getUTCMonth(),h=Math.abs(b);if(b=b>0?1:-1,1==h)d=-1==b?function(){return e.getUTCMonth()==g}:function(){return e.getUTCMonth()!=c},c=g+b,e.setUTCMonth(c),(0>c||c>11)&&(c=(c+12)%12);else{for(var i=0;h>i;i++)e=this.moveMonth(e,b);c=e.getUTCMonth(),e.setUTCDate(f),d=function(){return c!=e.getUTCMonth()}}for(;d();)e.setUTCDate(--f),e.setUTCMonth(c);return e},moveYear:function(a,b){return this.moveMonth(a,12*b)},dateWithinRange:function(a){return a>=this.o.startDate&&a<=this.o.endDate},keydown:function(a){if(this.picker.is(":not(:visible)"))return 27==a.keyCode&&this.show(),void 0;var b,c,d,e=!1;switch(a.keyCode){case 27:this.hide(),a.preventDefault();break;case 37:case 39:if(!this.o.keyboardNavigation)break;b=37==a.keyCode?-1:1,a.ctrlKey?(c=this.moveYear(this.date,b),d=this.moveYear(this.viewDate,b)):a.shiftKey?(c=this.moveMonth(this.date,b),d=this.moveMonth(this.viewDate,b)):(c=new Date(this.date),c.setUTCDate(this.date.getUTCDate()+b),d=new Date(this.viewDate),d.setUTCDate(this.viewDate.getUTCDate()+b)),this.dateWithinRange(c)&&(this.date=c,this.viewDate=d,this.setValue(),this.update(),a.preventDefault(),e=!0);break;case 38:case 40:if(!this.o.keyboardNavigation)break;b=38==a.keyCode?-1:1,a.ctrlKey?(c=this.moveYear(this.date,b),d=this.moveYear(this.viewDate,b)):a.shiftKey?(c=this.moveMonth(this.date,b),d=this.moveMonth(this.viewDate,b)):(c=new Date(this.date),c.setUTCDate(this.date.getUTCDate()+7*b),d=new Date(this.viewDate),d.setUTCDate(this.viewDate.getUTCDate()+7*b)),this.dateWithinRange(c)&&(this.date=c,this.viewDate=d,this.setValue(),this.update(),a.preventDefault(),e=!0);break;case 13:this.hide(),a.preventDefault();break;case 9:this.hide()}if(e){this._trigger("changeDate");var f;this.isInput?f=this.element:this.component&&(f=this.element.find("input")),f&&f.change()}},showMode:function(a){a&&(this.viewMode=Math.max(this.o.minViewMode,Math.min(2,this.viewMode+a))),this.picker.find(">div").hide().filter(".datepicker-"+l.modes[this.viewMode].clsName).css("display","block"),this.updateNavArrows()}};var f=function(b,c){this.element=a(b),this.inputs=a.map(c.inputs,function(a){return a.jquery?a[0]:a}),delete c.inputs,a(this.inputs).datepicker(c).bind("changeDate",a.proxy(this.dateUpdated,this)),this.pickers=a.map(this.inputs,function(b){return a(b).data("datepicker")}),this.updateDates()};f.prototype={updateDates:function(){this.dates=a.map(this.pickers,function(a){return a.date}),this.updateRanges()},updateRanges:function(){var b=a.map(this.dates,function(a){return a.valueOf()});a.each(this.pickers,function(a,c){c.setRange(b)})},dateUpdated:function(b){var c=a(b.target).data("datepicker"),d=c.getUTCDate(),e=a.inArray(b.target,this.inputs),f=this.inputs.length;if(-1!=e){if(d<this.dates[e])for(;e>=0&&d<this.dates[e];)this.pickers[e--].setUTCDate(d);else if(d>this.dates[e])for(;f>e&&d>this.dates[e];)this.pickers[e++].setUTCDate(d);this.updateDates()}},remove:function(){a.map(this.pickers,function(a){a.remove()}),delete this.element.data().datepicker}};var g=a.fn.datepicker,h=a.fn.datepicker=function(b){var g=Array.apply(null,arguments);g.shift();var h;return this.each(function(){var j=a(this),k=j.data("datepicker"),l="object"==typeof b&&b;if(!k){var m=c(this,"date"),n=a.extend({},i,m,l),o=d(n.language),p=a.extend({},i,o,m,l);if(j.is(".input-daterange")||p.inputs){var q={inputs:p.inputs||j.find("input").toArray()};j.data("datepicker",k=new f(this,a.extend(p,q)))}else j.data("datepicker",k=new e(this,p))}return"string"==typeof b&&"function"==typeof k[b]&&(h=k[b].apply(k,g),void 0!==h)?!1:void 0}),void 0!==h?h:this},i=a.fn.datepicker.defaults={autoclose:!1,beforeShowDay:a.noop,calendarWeeks:!1,clearBtn:!1,daysOfWeekDisabled:[],endDate:1/0,forceParse:!0,format:"mm/dd/yyyy",keyboardNavigation:!0,language:"en",minViewMode:0,rtl:!1,startDate:-1/0,startView:0,todayBtn:!1,todayHighlight:!1,weekStart:0},j=a.fn.datepicker.locale_opts=["format","rtl","weekStart"];a.fn.datepicker.Constructor=e;var k=a.fn.datepicker.dates={en:{days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"],daysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat","Sun"],daysMin:["Su","Mo","Tu","We","Th","Fr","Sa","Su"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],today:"Today",clear:"Clear"}},l={modes:[{clsName:"days",navFnc:"Month",navStep:1},{clsName:"months",navFnc:"FullYear",navStep:1},{clsName:"years",navFnc:"FullYear",navStep:10}],isLeapYear:function(a){return 0===a%4&&0!==a%100||0===a%400
},getDaysInMonth:function(a,b){return[31,l.isLeapYear(a)?29:28,31,30,31,30,31,31,30,31,30,31][b]},validParts:/dd?|DD?|mm?|MM?|yy(?:yy)?/g,nonpunctuation:/[^ -\/:-@\[\u3400-\u9fff-`{-~\t\n\r]+/g,parseFormat:function(a){var b=a.replace(this.validParts,"\0").split("\0"),c=a.match(this.validParts);if(!b||!b.length||!c||0===c.length)throw new Error("Invalid date format.");return{separators:b,parts:c}},parseDate:function(c,d,f){if(c instanceof Date)return c;if("string"==typeof d&&(d=l.parseFormat(d)),/^[\-+]\d+[dmwy]([\s,]+[\-+]\d+[dmwy])*$/.test(c)){var g,h,i=/([\-+]\d+)([dmwy])/,j=c.match(/([\-+]\d+)([dmwy])/g);c=new Date;for(var m=0;m<j.length;m++)switch(g=i.exec(j[m]),h=parseInt(g[1]),g[2]){case"d":c.setUTCDate(c.getUTCDate()+h);break;case"m":c=e.prototype.moveMonth.call(e.prototype,c,h);break;case"w":c.setUTCDate(c.getUTCDate()+7*h);break;case"y":c=e.prototype.moveYear.call(e.prototype,c,h)}return b(c.getUTCFullYear(),c.getUTCMonth(),c.getUTCDate(),0,0,0)}var n,o,g,j=c&&c.match(this.nonpunctuation)||[],c=new Date,p={},q=["yyyy","yy","M","MM","m","mm","d","dd"],r={yyyy:function(a,b){return a.setUTCFullYear(b)},yy:function(a,b){return a.setUTCFullYear(2e3+b)},m:function(a,b){for(b-=1;0>b;)b+=12;for(b%=12,a.setUTCMonth(b);a.getUTCMonth()!=b;)a.setUTCDate(a.getUTCDate()-1);return a},d:function(a,b){return a.setUTCDate(b)}};r.M=r.MM=r.mm=r.m,r.dd=r.d,c=b(c.getFullYear(),c.getMonth(),c.getDate(),0,0,0);var s=d.parts.slice();if(j.length!=s.length&&(s=a(s).filter(function(b,c){return-1!==a.inArray(c,q)}).toArray()),j.length==s.length){for(var m=0,t=s.length;t>m;m++){if(n=parseInt(j[m],10),g=s[m],isNaN(n))switch(g){case"MM":o=a(k[f].months).filter(function(){var a=this.slice(0,j[m].length),b=j[m].slice(0,a.length);return a==b}),n=a.inArray(o[0],k[f].months)+1;break;case"M":o=a(k[f].monthsShort).filter(function(){var a=this.slice(0,j[m].length),b=j[m].slice(0,a.length);return a==b}),n=a.inArray(o[0],k[f].monthsShort)+1}p[g]=n}for(var u,m=0;m<q.length;m++)u=q[m],u in p&&!isNaN(p[u])&&r[u](c,p[u])}return c},formatDate:function(b,c,d){"string"==typeof c&&(c=l.parseFormat(c));var e={d:b.getUTCDate(),D:k[d].daysShort[b.getUTCDay()],DD:k[d].days[b.getUTCDay()],m:b.getUTCMonth()+1,M:k[d].monthsShort[b.getUTCMonth()],MM:k[d].months[b.getUTCMonth()],yy:b.getUTCFullYear().toString().substring(2),yyyy:b.getUTCFullYear()};e.dd=(e.d<10?"0":"")+e.d,e.mm=(e.m<10?"0":"")+e.m;for(var b=[],f=a.extend([],c.separators),g=0,h=c.parts.length;h>=g;g++)f.length&&b.push(f.shift()),b.push(e[c.parts[g]]);return b.join("")},headTemplate:'<thead><tr><th class="prev"><i class="icon-arrow-left"/></th><th colspan="5" class="datepicker-switch"></th><th class="next"><i class="icon-arrow-right"/></th></tr></thead>',contTemplate:'<tbody><tr><td colspan="7"></td></tr></tbody>',footTemplate:'<tfoot><tr><th colspan="7" class="today"></th></tr><tr><th colspan="7" class="clear"></th></tr></tfoot>'};l.template='<div class="datepicker"><div class="datepicker-days"><table class=" table-condensed">'+l.headTemplate+"<tbody></tbody>"+l.footTemplate+"</table>"+"</div>"+'<div class="datepicker-months">'+'<table class="table-condensed">'+l.headTemplate+l.contTemplate+l.footTemplate+"</table>"+"</div>"+'<div class="datepicker-years">'+'<table class="table-condensed">'+l.headTemplate+l.contTemplate+l.footTemplate+"</table>"+"</div>"+"</div>",a.fn.datepicker.DPGlobal=l,a.fn.datepicker.noConflict=function(){return a.fn.datepicker=g,this},a(document).on("focus.datepicker.data-api click.datepicker.data-api",'[data-provide="datepicker"]',function(b){var c=a(this);c.data("datepicker")||(b.preventDefault(),h.call(c,"show"))}),a(function(){h.call(a('[data-provide="datepicker-inline"]'))})}(window.jQuery),function(a){"use strict";a.fn.bdatepicker=a.fn.datepicker.noConflict(),a.fn.datepicker||(a.fn.datepicker=a.fn.bdatepicker);var b=function(a){this.init("date",a,b.defaults),this.initPicker(a,b.defaults)};a.fn.editableutils.inherit(b,a.fn.editabletypes.abstractinput),a.extend(b.prototype,{initPicker:function(b,c){this.options.viewformat||(this.options.viewformat=this.options.format),b.datepicker=a.fn.editableutils.tryParseJson(b.datepicker,!0),this.options.datepicker=a.extend({},c.datepicker,b.datepicker,{format:this.options.viewformat}),this.options.datepicker.language=this.options.datepicker.language||"en",this.dpg=a.fn.bdatepicker.DPGlobal,this.parsedFormat=this.dpg.parseFormat(this.options.format),this.parsedViewFormat=this.dpg.parseFormat(this.options.viewformat)},render:function(){this.$input.bdatepicker(this.options.datepicker),this.options.clear&&(this.$clear=a('<a href="#"></a>').html(this.options.clear).click(a.proxy(function(a){a.preventDefault(),a.stopPropagation(),this.clear()},this)),this.$tpl.parent().append(a('<div class="editable-clear">').append(this.$clear)))},value2html:function(a,c){var d=a?this.dpg.formatDate(a,this.parsedViewFormat,this.options.datepicker.language):"";b.superclass.value2html.call(this,d,c)},html2value:function(a){return this.parseDate(a,this.parsedViewFormat)},value2str:function(a){return a?this.dpg.formatDate(a,this.parsedFormat,this.options.datepicker.language):""},str2value:function(a){return this.parseDate(a,this.parsedFormat)},value2submit:function(a){return this.value2str(a)},value2input:function(a){this.$input.bdatepicker("update",a)},input2value:function(){return this.$input.data("datepicker").date},activate:function(){},clear:function(){this.$input.data("datepicker").date=null,this.$input.find(".active").removeClass("active"),this.options.showbuttons||this.$input.closest("form").submit()},autosubmit:function(){this.$input.on("mouseup",".day",function(b){if(!a(b.currentTarget).is(".old")&&!a(b.currentTarget).is(".new")){var c=a(this).closest("form");setTimeout(function(){c.submit()},200)}})},parseDate:function(a,b){var c,d=null;return a&&(d=this.dpg.parseDate(a,b,this.options.datepicker.language),"string"==typeof a&&(c=this.dpg.formatDate(d,b,this.options.datepicker.language),a!==c&&(d=null))),d}}),b.defaults=a.extend({},a.fn.editabletypes.abstractinput.defaults,{tpl:'<div class="editable-date well"></div>',inputclass:null,format:"yyyy-mm-dd",viewformat:null,datepicker:{weekStart:0,startView:0,minViewMode:0,autoclose:!1},clear:"&times; clear"}),a.fn.editabletypes.date=b}(window.jQuery),function(a){"use strict";var b=function(a){this.init("datefield",a,b.defaults),this.initPicker(a,b.defaults)};a.fn.editableutils.inherit(b,a.fn.editabletypes.date),a.extend(b.prototype,{render:function(){this.$input=this.$tpl.find("input"),this.setClass(),this.setAttr("placeholder"),this.$tpl.bdatepicker(this.options.datepicker),this.$input.off("focus keydown"),this.$input.keyup(a.proxy(function(){this.$tpl.removeData("date"),this.$tpl.bdatepicker("update")},this))},value2input:function(a){this.$input.val(a?this.dpg.formatDate(a,this.parsedViewFormat,this.options.datepicker.language):""),this.$tpl.bdatepicker("update")},input2value:function(){return this.html2value(this.$input.val())},activate:function(){a.fn.editabletypes.text.prototype.activate.call(this)},autosubmit:function(){}}),b.defaults=a.extend({},a.fn.editabletypes.date.defaults,{tpl:'<div class="input-append date"><input type="text"/><span class="add-on"><i class="icon-th"></i></span></div>',inputclass:"input-small",datepicker:{weekStart:0,startView:0,minViewMode:0,autoclose:!0}}),a.fn.editabletypes.datefield=b}(window.jQuery),function(a){"use strict";var b=function(a){this.init("datetime",a,b.defaults),this.initPicker(a,b.defaults)};a.fn.editableutils.inherit(b,a.fn.editabletypes.abstractinput),a.extend(b.prototype,{initPicker:function(b,c){this.options.viewformat||(this.options.viewformat=this.options.format),b.datetimepicker=a.fn.editableutils.tryParseJson(b.datetimepicker,!0),this.options.datetimepicker=a.extend({},c.datetimepicker,b.datetimepicker,{format:this.options.viewformat}),this.options.datetimepicker.language=this.options.datetimepicker.language||"en",this.dpg=a.fn.datetimepicker.DPGlobal,this.parsedFormat=this.dpg.parseFormat(this.options.format,this.options.formatType),this.parsedViewFormat=this.dpg.parseFormat(this.options.viewformat,this.options.formatType)},render:function(){this.$input.datetimepicker(this.options.datetimepicker),this.$input.on("changeMode",function(){var b=a(this).closest("form").parent();setTimeout(function(){b.triggerHandler("resize")},0)}),this.options.clear&&(this.$clear=a('<a href="#"></a>').html(this.options.clear).click(a.proxy(function(a){a.preventDefault(),a.stopPropagation(),this.clear()},this)),this.$tpl.parent().append(a('<div class="editable-clear">').append(this.$clear)))},value2html:function(a,c){var d=a?this.dpg.formatDate(this.toUTC(a),this.parsedViewFormat,this.options.datetimepicker.language,this.options.formatType):"";return c?(b.superclass.value2html.call(this,d,c),void 0):d},html2value:function(a){var b=this.parseDate(a,this.parsedViewFormat);return b?this.fromUTC(b):null},value2str:function(a){return a?this.dpg.formatDate(this.toUTC(a),this.parsedFormat,this.options.datetimepicker.language,this.options.formatType):""},str2value:function(a){var b=this.parseDate(a,this.parsedFormat);return b?this.fromUTC(b):null},value2submit:function(a){return this.value2str(a)},value2input:function(a){a&&this.$input.data("datetimepicker").setDate(a)},input2value:function(){var a=this.$input.data("datetimepicker");return a.date?a.getDate():null},activate:function(){},clear:function(){this.$input.data("datetimepicker").date=null,this.$input.find(".active").removeClass("active"),this.options.showbuttons||this.$input.closest("form").submit()},autosubmit:function(){this.$input.on("mouseup",".minute",function(){var b=a(this).closest("form");setTimeout(function(){b.submit()},200)})},toUTC:function(a){return a?new Date(a.valueOf()-6e4*a.getTimezoneOffset()):a},fromUTC:function(a){return a?new Date(a.valueOf()+6e4*a.getTimezoneOffset()):a},parseDate:function(a,b){var c,d=null;return a&&(d=this.dpg.parseDate(a,b,this.options.datetimepicker.language,this.options.formatType),"string"==typeof a&&(c=this.dpg.formatDate(d,b,this.options.datetimepicker.language,this.options.formatType),a!==c&&(d=null))),d}}),b.defaults=a.extend({},a.fn.editabletypes.abstractinput.defaults,{tpl:'<div class="editable-date well"></div>',inputclass:null,format:"yyyy-mm-dd hh:ii",formatType:"standard",viewformat:null,datetimepicker:{todayHighlight:!1,autoclose:!1},clear:"&times; clear"}),a.fn.editabletypes.datetime=b}(window.jQuery),function(a){"use strict";var b=function(a){this.init("datetimefield",a,b.defaults),this.initPicker(a,b.defaults)};a.fn.editableutils.inherit(b,a.fn.editabletypes.datetime),a.extend(b.prototype,{render:function(){this.$input=this.$tpl.find("input"),this.setClass(),this.setAttr("placeholder"),this.$tpl.datetimepicker(this.options.datetimepicker),this.$input.off("focus keydown"),this.$input.keyup(a.proxy(function(){this.$tpl.removeData("date"),this.$tpl.datetimepicker("update")},this))},value2input:function(a){this.$input.val(this.value2html(a)),this.$tpl.datetimepicker("update")},input2value:function(){return this.html2value(this.$input.val())},activate:function(){a.fn.editabletypes.text.prototype.activate.call(this)},autosubmit:function(){}}),b.defaults=a.extend({},a.fn.editabletypes.datetime.defaults,{tpl:'<div class="input-append date"><input type="text"/><span class="add-on"><i class="icon-th"></i></span></div>',inputclass:"input-medium",datetimepicker:{todayHighlight:!1,autoclose:!0}}),a.fn.editabletypes.datetimefield=b}(window.jQuery),function(a){"use strict";var b=function(c){this.init("typeahead",c,b.defaults),this.options.typeahead=a.extend({},b.defaults.typeahead,{matcher:this.matcher,sorter:this.sorter,highlighter:this.highlighter,updater:this.updater},c.typeahead)};a.fn.editableutils.inherit(b,a.fn.editabletypes.list),a.extend(b.prototype,{renderList:function(){this.$input=this.$tpl.is("input")?this.$tpl:this.$tpl.find('input[type="text"]'),this.options.typeahead.source=this.sourceData,this.$input.typeahead(this.options.typeahead);var b=this.$input.data("typeahead");b.render=a.proxy(this.typeaheadRender,b),b.select=a.proxy(this.typeaheadSelect,b),b.move=a.proxy(this.typeaheadMove,b),this.renderClear(),this.setClass(),this.setAttr("placeholder")},value2htmlFinal:function(b,c){if(this.getIsObjects()){var d=a.fn.editableutils.itemsByValue(b,this.sourceData);b=d.length?d[0].text:""}a.fn.editabletypes.abstractinput.prototype.value2html.call(this,b,c)},html2value:function(a){return a?a:null},value2input:function(b){if(this.getIsObjects()){var c=a.fn.editableutils.itemsByValue(b,this.sourceData);this.$input.data("value",b).val(c.length?c[0].text:"")}else this.$input.val(b)},input2value:function(){if(this.getIsObjects()){var b=this.$input.data("value"),c=a.fn.editableutils.itemsByValue(b,this.sourceData);return c.length&&c[0].text.toLowerCase()===this.$input.val().toLowerCase()?b:null}return this.$input.val()},getIsObjects:function(){if(void 0===this.isObjects){this.isObjects=!1;for(var a=0;a<this.sourceData.length;a++)if(this.sourceData[a].value!==this.sourceData[a].text){this.isObjects=!0;break}}return this.isObjects},activate:a.fn.editabletypes.text.prototype.activate,renderClear:a.fn.editabletypes.text.prototype.renderClear,postrender:a.fn.editabletypes.text.prototype.postrender,toggleClear:a.fn.editabletypes.text.prototype.toggleClear,clear:function(){a.fn.editabletypes.text.prototype.clear.call(this),this.$input.data("value","")},matcher:function(b){return a.fn.typeahead.Constructor.prototype.matcher.call(this,b.text)},sorter:function(a){for(var b,c,d=[],e=[],f=[];b=a.shift();)c=b.text,c.toLowerCase().indexOf(this.query.toLowerCase())?~c.indexOf(this.query)?e.push(b):f.push(b):d.push(b);return d.concat(e,f)},highlighter:function(b){return a.fn.typeahead.Constructor.prototype.highlighter.call(this,b.text)},updater:function(a){return this.$element.data("value",a.value),a.text},typeaheadRender:function(b){var c=this;return b=a(b).map(function(b,d){return b=a(c.options.item).data("item",d),b.find("a").html(c.highlighter(d)),b[0]}),this.options.autoSelect&&b.first().addClass("active"),this.$menu.html(b),this},typeaheadSelect:function(){var a=this.$menu.find(".active").data("item");return(this.options.autoSelect||a)&&this.$element.val(this.updater(a)).change(),this.hide()},typeaheadMove:function(a){if(this.shown){switch(a.keyCode){case 9:case 13:case 27:if(!this.$menu.find(".active").length)return;a.preventDefault();break;case 38:a.preventDefault(),this.prev();break;case 40:a.preventDefault(),this.next()}a.stopPropagation()}}}),b.defaults=a.extend({},a.fn.editabletypes.list.defaults,{tpl:'<input type="text">',typeahead:null,clear:!0}),a.fn.editabletypes.typeahead=b}(window.jQuery);
\ No newline at end of file
/*! X-editable - v1.5.1
* In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery
* http://github.com/vitalets/x-editable
* Copyright (c) 2013 Vitaliy Potapov; Licensed MIT */
!function(a){"use strict";var b=function(b,c){this.options=a.extend({},a.fn.editableform.defaults,c),this.$div=a(b),this.options.scope||(this.options.scope=this)};b.prototype={constructor:b,initInput:function(){this.input=this.options.input,this.value=this.input.str2value(this.options.value),this.input.prerender()},initTemplate:function(){this.$form=a(a.fn.editableform.template)},initButtons:function(){var b=this.$form.find(".editable-buttons");b.append(a.fn.editableform.buttons),"bottom"===this.options.showbuttons&&b.addClass("editable-buttons-bottom")},render:function(){this.$loading=a(a.fn.editableform.loading),this.$div.empty().append(this.$loading),this.initTemplate(),this.options.showbuttons?this.initButtons():this.$form.find(".editable-buttons").remove(),this.showLoading(),this.isSaving=!1,this.$div.triggerHandler("rendering"),this.initInput(),this.$form.find("div.editable-input").append(this.input.$tpl),this.$div.append(this.$form),a.when(this.input.render()).then(a.proxy(function(){if(this.options.showbuttons||this.input.autosubmit(),this.$form.find(".editable-cancel").click(a.proxy(this.cancel,this)),this.input.error)this.error(this.input.error),this.$form.find(".editable-submit").attr("disabled",!0),this.input.$input.attr("disabled",!0),this.$form.submit(function(a){a.preventDefault()});else{this.error(!1),this.input.$input.removeAttr("disabled"),this.$form.find(".editable-submit").removeAttr("disabled");var b=null===this.value||void 0===this.value||""===this.value?this.options.defaultValue:this.value;this.input.value2input(b),this.$form.submit(a.proxy(this.submit,this))}this.$div.triggerHandler("rendered"),this.showForm(),this.input.postrender&&this.input.postrender()},this))},cancel:function(){this.$div.triggerHandler("cancel")},showLoading:function(){var a,b;this.$form?(a=this.$form.outerWidth(),b=this.$form.outerHeight(),a&&this.$loading.width(a),b&&this.$loading.height(b),this.$form.hide()):(a=this.$loading.parent().width(),a&&this.$loading.width(a)),this.$loading.show()},showForm:function(a){this.$loading.hide(),this.$form.show(),a!==!1&&this.input.activate(),this.$div.triggerHandler("show")},error:function(b){var c,d=this.$form.find(".control-group"),e=this.$form.find(".editable-error-block");if(b===!1)d.removeClass(a.fn.editableform.errorGroupClass),e.removeClass(a.fn.editableform.errorBlockClass).empty().hide();else{if(b){c=(""+b).split("\n");for(var f=0;f<c.length;f++)c[f]=a("<div>").text(c[f]).html();b=c.join("<br>")}d.addClass(a.fn.editableform.errorGroupClass),e.addClass(a.fn.editableform.errorBlockClass).html(b).show()}},submit:function(b){b.stopPropagation(),b.preventDefault();var c=this.input.input2value(),d=this.validate(c);if("object"===a.type(d)&&void 0!==d.newValue){if(c=d.newValue,this.input.value2input(c),"string"==typeof d.msg)return this.error(d.msg),this.showForm(),void 0}else if(d)return this.error(d),this.showForm(),void 0;if(!this.options.savenochange&&this.input.value2str(c)==this.input.value2str(this.value))return this.$div.triggerHandler("nochange"),void 0;var e=this.input.value2submit(c);this.isSaving=!0,a.when(this.save(e)).done(a.proxy(function(a){this.isSaving=!1;var b="function"==typeof this.options.success?this.options.success.call(this.options.scope,a,c):null;return b===!1?(this.error(!1),this.showForm(!1),void 0):"string"==typeof b?(this.error(b),this.showForm(),void 0):(b&&"object"==typeof b&&b.hasOwnProperty("newValue")&&(c=b.newValue),this.error(!1),this.value=c,this.$div.triggerHandler("save",{newValue:c,submitValue:e,response:a}),void 0)},this)).fail(a.proxy(function(a){this.isSaving=!1;var b;b="function"==typeof this.options.error?this.options.error.call(this.options.scope,a,c):"string"==typeof a?a:a.responseText||a.statusText||"Unknown error!",this.error(b),this.showForm()},this))},save:function(b){this.options.pk=a.fn.editableutils.tryParseJson(this.options.pk,!0);var c,d="function"==typeof this.options.pk?this.options.pk.call(this.options.scope):this.options.pk,e=!!("function"==typeof this.options.url||this.options.url&&("always"===this.options.send||"auto"===this.options.send&&null!==d&&void 0!==d));return e?(this.showLoading(),c={name:this.options.name||"",value:b,pk:d},"function"==typeof this.options.params?c=this.options.params.call(this.options.scope,c):(this.options.params=a.fn.editableutils.tryParseJson(this.options.params,!0),a.extend(c,this.options.params)),"function"==typeof this.options.url?this.options.url.call(this.options.scope,c):a.ajax(a.extend({url:this.options.url,data:c,type:"POST"},this.options.ajaxOptions))):void 0},validate:function(a){return void 0===a&&(a=this.value),"function"==typeof this.options.validate?this.options.validate.call(this.options.scope,a):void 0},option:function(a,b){a in this.options&&(this.options[a]=b),"value"===a&&this.setValue(b)},setValue:function(a,b){this.value=b?this.input.str2value(a):a,this.$form&&this.$form.is(":visible")&&this.input.value2input(this.value)}},a.fn.editableform=function(c){var d=arguments;return this.each(function(){var e=a(this),f=e.data("editableform"),g="object"==typeof c&&c;f||e.data("editableform",f=new b(this,g)),"string"==typeof c&&f[c].apply(f,Array.prototype.slice.call(d,1))})},a.fn.editableform.Constructor=b,a.fn.editableform.defaults={type:"text",url:null,params:null,name:null,pk:null,value:null,defaultValue:null,send:"auto",validate:null,success:null,error:null,ajaxOptions:null,showbuttons:!0,scope:null,savenochange:!1},a.fn.editableform.template='<form class="form-inline editableform"><div class="control-group"><div><div class="editable-input"></div><div class="editable-buttons"></div></div><div class="editable-error-block"></div></div></form>',a.fn.editableform.loading='<div class="editableform-loading"></div>',a.fn.editableform.buttons='<button type="submit" class="editable-submit">ok</button><button type="button" class="editable-cancel">cancel</button>',a.fn.editableform.errorGroupClass=null,a.fn.editableform.errorBlockClass="editable-error",a.fn.editableform.engine="jquery"}(window.jQuery),function(a){"use strict";a.fn.editableutils={inherit:function(a,b){var c=function(){};c.prototype=b.prototype,a.prototype=new c,a.prototype.constructor=a,a.superclass=b.prototype},setCursorPosition:function(a,b){if(a.setSelectionRange)a.setSelectionRange(b,b);else if(a.createTextRange){var c=a.createTextRange();c.collapse(!0),c.moveEnd("character",b),c.moveStart("character",b),c.select()}},tryParseJson:function(a,b){if("string"==typeof a&&a.length&&a.match(/^[\{\[].*[\}\]]$/))if(b)try{a=new Function("return "+a)()}catch(c){}finally{return a}else a=new Function("return "+a)();return a},sliceObj:function(b,c,d){var e,f,g={};if(!a.isArray(c)||!c.length)return g;for(var h=0;h<c.length;h++)e=c[h],b.hasOwnProperty(e)&&(g[e]=b[e]),d!==!0&&(f=e.toLowerCase(),b.hasOwnProperty(f)&&(g[e]=b[f]));return g},getConfigData:function(b){var c={};return a.each(b.data(),function(a,b){("object"!=typeof b||b&&"object"==typeof b&&(b.constructor===Object||b.constructor===Array))&&(c[a]=b)}),c},objectKeys:function(a){if(Object.keys)return Object.keys(a);if(a!==Object(a))throw new TypeError("Object.keys called on a non-object");var b,c=[];for(b in a)Object.prototype.hasOwnProperty.call(a,b)&&c.push(b);return c},escape:function(b){return a("<div>").text(b).html()},itemsByValue:function(b,c,d){if(!c||null===b)return[];if("function"!=typeof d){var e=d||"value";d=function(a){return a[e]}}var f=a.isArray(b),g=[],h=this;return a.each(c,function(c,e){if(e.children)g=g.concat(h.itemsByValue(b,e.children,d));else if(f)a.grep(b,function(a){return a==(e&&"object"==typeof e?d(e):e)}).length&&g.push(e);else{var i=e&&"object"==typeof e?d(e):e;b==i&&g.push(e)}}),g},createInput:function(b){var c,d,e,f=b.type;return"date"===f&&("inline"===b.mode?a.fn.editabletypes.datefield?f="datefield":a.fn.editabletypes.dateuifield&&(f="dateuifield"):a.fn.editabletypes.date?f="date":a.fn.editabletypes.dateui&&(f="dateui"),"date"!==f||a.fn.editabletypes.date||(f="combodate")),"datetime"===f&&"inline"===b.mode&&(f="datetimefield"),"wysihtml5"!==f||a.fn.editabletypes[f]||(f="textarea"),"function"==typeof a.fn.editabletypes[f]?(c=a.fn.editabletypes[f],d=this.sliceObj(b,this.objectKeys(c.defaults)),e=new c(d)):(a.error("Unknown type: "+f),!1)},supportsTransitions:function(){var a=document.body||document.documentElement,b=a.style,c="transition",d=["Moz","Webkit","Khtml","O","ms"];if("string"==typeof b[c])return!0;c=c.charAt(0).toUpperCase()+c.substr(1);for(var e=0;e<d.length;e++)if("string"==typeof b[d[e]+c])return!0;return!1}}}(window.jQuery),function(a){"use strict";var b=function(a,b){this.init(a,b)},c=function(a,b){this.init(a,b)};b.prototype={containerName:null,containerDataName:null,innerCss:null,containerClass:"editable-container editable-popup",defaults:{},init:function(c,d){this.$element=a(c),this.options=a.extend({},a.fn.editableContainer.defaults,d),this.splitOptions(),this.formOptions.scope=this.$element[0],this.initContainer(),this.delayedHide=!1,this.$element.on("destroyed",a.proxy(function(){this.destroy()},this)),a(document).data("editable-handlers-attached")||(a(document).on("keyup.editable",function(b){27===b.which&&a(".editable-open").editableContainer("hide")}),a(document).on("click.editable",function(c){var d,e=a(c.target),f=[".editable-container",".ui-datepicker-header",".datepicker",".modal-backdrop",".bootstrap-wysihtml5-insert-image-modal",".bootstrap-wysihtml5-insert-link-modal"];if(a.contains(document.documentElement,c.target)&&!e.is(document)){for(d=0;d<f.length;d++)if(e.is(f[d])||e.parents(f[d]).length)return;b.prototype.closeOthers(c.target)}}),a(document).data("editable-handlers-attached",!0))},splitOptions:function(){if(this.containerOptions={},this.formOptions={},!a.fn[this.containerName])throw new Error(this.containerName+" not found. Have you included corresponding js file?");for(var b in this.options)b in this.defaults?this.containerOptions[b]=this.options[b]:this.formOptions[b]=this.options[b]},tip:function(){return this.container()?this.container().$tip:null},container:function(){var a;return this.containerDataName&&(a=this.$element.data(this.containerDataName))?a:a=this.$element.data(this.containerName)},call:function(){this.$element[this.containerName].apply(this.$element,arguments)},initContainer:function(){this.call(this.containerOptions)},renderForm:function(){this.$form.editableform(this.formOptions).on({save:a.proxy(this.save,this),nochange:a.proxy(function(){this.hide("nochange")},this),cancel:a.proxy(function(){this.hide("cancel")},this),show:a.proxy(function(){this.delayedHide?(this.hide(this.delayedHide.reason),this.delayedHide=!1):this.setPosition()},this),rendering:a.proxy(this.setPosition,this),resize:a.proxy(this.setPosition,this),rendered:a.proxy(function(){this.$element.triggerHandler("shown",a(this.options.scope).data("editable"))},this)}).editableform("render")},show:function(b){this.$element.addClass("editable-open"),b!==!1&&this.closeOthers(this.$element[0]),this.innerShow(),this.tip().addClass(this.containerClass),this.$form,this.$form=a("<div>"),this.tip().is(this.innerCss)?this.tip().append(this.$form):this.tip().find(this.innerCss).append(this.$form),this.renderForm()},hide:function(a){if(this.tip()&&this.tip().is(":visible")&&this.$element.hasClass("editable-open")){if(this.$form.data("editableform").isSaving)return this.delayedHide={reason:a},void 0;this.delayedHide=!1,this.$element.removeClass("editable-open"),this.innerHide(),this.$element.triggerHandler("hidden",a||"manual")}},innerShow:function(){},innerHide:function(){},toggle:function(a){this.container()&&this.tip()&&this.tip().is(":visible")?this.hide():this.show(a)},setPosition:function(){},save:function(a,b){this.$element.triggerHandler("save",b),this.hide("save")},option:function(a,b){this.options[a]=b,a in this.containerOptions?(this.containerOptions[a]=b,this.setContainerOption(a,b)):(this.formOptions[a]=b,this.$form&&this.$form.editableform("option",a,b))},setContainerOption:function(a,b){this.call("option",a,b)},destroy:function(){this.hide(),this.innerDestroy(),this.$element.off("destroyed"),this.$element.removeData("editableContainer")},innerDestroy:function(){},closeOthers:function(b){a(".editable-open").each(function(c,d){if(d!==b&&!a(d).find(b).length){var e=a(d),f=e.data("editableContainer");f&&("cancel"===f.options.onblur?e.data("editableContainer").hide("onblur"):"submit"===f.options.onblur&&e.data("editableContainer").tip().find("form").submit())}})},activate:function(){this.tip&&this.tip().is(":visible")&&this.$form&&this.$form.data("editableform").input.activate()}},a.fn.editableContainer=function(d){var e=arguments;return this.each(function(){var f=a(this),g="editableContainer",h=f.data(g),i="object"==typeof d&&d,j="inline"===i.mode?c:b;h||f.data(g,h=new j(this,i)),"string"==typeof d&&h[d].apply(h,Array.prototype.slice.call(e,1))})},a.fn.editableContainer.Popup=b,a.fn.editableContainer.Inline=c,a.fn.editableContainer.defaults={value:null,placement:"top",autohide:!0,onblur:"cancel",anim:!1,mode:"popup"},jQuery.event.special.destroyed={remove:function(a){a.handler&&a.handler()}}}(window.jQuery),function(a){"use strict";a.extend(a.fn.editableContainer.Inline.prototype,a.fn.editableContainer.Popup.prototype,{containerName:"editableform",innerCss:".editable-inline",containerClass:"editable-container editable-inline",initContainer:function(){this.$tip=a("<span></span>"),this.options.anim||(this.options.anim=0)},splitOptions:function(){this.containerOptions={},this.formOptions=this.options},tip:function(){return this.$tip},innerShow:function(){this.$element.hide(),this.tip().insertAfter(this.$element).show()},innerHide:function(){this.$tip.hide(this.options.anim,a.proxy(function(){this.$element.show(),this.innerDestroy()},this))},innerDestroy:function(){this.tip()&&this.tip().empty().remove()}})}(window.jQuery),function(a){"use strict";var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.editable.defaults,c,a.fn.editableutils.getConfigData(this.$element)),this.options.selector?this.initLive():this.init(),this.options.highlight&&!a.fn.editableutils.supportsTransitions()&&(this.options.highlight=!1)};b.prototype={constructor:b,init:function(){var b,c=!1;if(this.options.name=this.options.name||this.$element.attr("id"),this.options.scope=this.$element[0],this.input=a.fn.editableutils.createInput(this.options),this.input){switch(void 0===this.options.value||null===this.options.value?(this.value=this.input.html2value(a.trim(this.$element.html())),c=!0):(this.options.value=a.fn.editableutils.tryParseJson(this.options.value,!0),this.value="string"==typeof this.options.value?this.input.str2value(this.options.value):this.options.value),this.$element.addClass("editable"),"textarea"===this.input.type&&this.$element.addClass("editable-pre-wrapped"),"manual"!==this.options.toggle?(this.$element.addClass("editable-click"),this.$element.on(this.options.toggle+".editable",a.proxy(function(a){if(this.options.disabled||a.preventDefault(),"mouseenter"===this.options.toggle)this.show();else{var b="click"!==this.options.toggle;this.toggle(b)}},this))):this.$element.attr("tabindex",-1),"function"==typeof this.options.display&&(this.options.autotext="always"),this.options.autotext){case"always":b=!0;break;case"auto":b=!a.trim(this.$element.text()).length&&null!==this.value&&void 0!==this.value&&!c;break;default:b=!1}a.when(b?this.render():!0).then(a.proxy(function(){this.options.disabled?this.disable():this.enable(),this.$element.triggerHandler("init",this)},this))}},initLive:function(){var b=this.options.selector;this.options.selector=!1,this.options.autotext="never",this.$element.on(this.options.toggle+".editable",b,a.proxy(function(b){var c=a(b.target);c.data("editable")||(c.hasClass(this.options.emptyclass)&&c.empty(),c.editable(this.options).trigger(b))},this))},render:function(a){return this.options.display!==!1?this.input.value2htmlFinal?this.input.value2html(this.value,this.$element[0],this.options.display,a):"function"==typeof this.options.display?this.options.display.call(this.$element[0],this.value,a):this.input.value2html(this.value,this.$element[0]):void 0},enable:function(){this.options.disabled=!1,this.$element.removeClass("editable-disabled"),this.handleEmpty(this.isEmpty),"manual"!==this.options.toggle&&"-1"===this.$element.attr("tabindex")&&this.$element.removeAttr("tabindex")},disable:function(){this.options.disabled=!0,this.hide(),this.$element.addClass("editable-disabled"),this.handleEmpty(this.isEmpty),this.$element.attr("tabindex",-1)},toggleDisabled:function(){this.options.disabled?this.enable():this.disable()},option:function(b,c){return b&&"object"==typeof b?(a.each(b,a.proxy(function(b,c){this.option(a.trim(b),c)},this)),void 0):(this.options[b]=c,"disabled"===b?c?this.disable():this.enable():("value"===b&&this.setValue(c),this.container&&this.container.option(b,c),this.input.option&&this.input.option(b,c),void 0))},handleEmpty:function(b){this.options.display!==!1&&(this.isEmpty=void 0!==b?b:"function"==typeof this.input.isEmpty?this.input.isEmpty(this.$element):""===a.trim(this.$element.html()),this.options.disabled?this.isEmpty&&(this.$element.empty(),this.options.emptyclass&&this.$element.removeClass(this.options.emptyclass)):this.isEmpty?(this.$element.html(this.options.emptytext),this.options.emptyclass&&this.$element.addClass(this.options.emptyclass)):this.options.emptyclass&&this.$element.removeClass(this.options.emptyclass))},show:function(b){if(!this.options.disabled){if(this.container){if(this.container.tip().is(":visible"))return}else{var c=a.extend({},this.options,{value:this.value,input:this.input});this.$element.editableContainer(c),this.$element.on("save.internal",a.proxy(this.save,this)),this.container=this.$element.data("editableContainer")}this.container.show(b)}},hide:function(){this.container&&this.container.hide()},toggle:function(a){this.container&&this.container.tip().is(":visible")?this.hide():this.show(a)},save:function(a,b){if(this.options.unsavedclass){var c=!1;c=c||"function"==typeof this.options.url,c=c||this.options.display===!1,c=c||void 0!==b.response,c=c||this.options.savenochange&&this.input.value2str(this.value)!==this.input.value2str(b.newValue),c?this.$element.removeClass(this.options.unsavedclass):this.$element.addClass(this.options.unsavedclass)}if(this.options.highlight){var d=this.$element,e=d.css("background-color");d.css("background-color",this.options.highlight),setTimeout(function(){"transparent"===e&&(e=""),d.css("background-color",e),d.addClass("editable-bg-transition"),setTimeout(function(){d.removeClass("editable-bg-transition")},1700)},10)}this.setValue(b.newValue,!1,b.response)},validate:function(){return"function"==typeof this.options.validate?this.options.validate.call(this,this.value):void 0},setValue:function(b,c,d){this.value=c?this.input.str2value(b):b,this.container&&this.container.option("value",this.value),a.when(this.render(d)).then(a.proxy(function(){this.handleEmpty()},this))},activate:function(){this.container&&this.container.activate()},destroy:function(){this.disable(),this.container&&this.container.destroy(),this.input.destroy(),"manual"!==this.options.toggle&&(this.$element.removeClass("editable-click"),this.$element.off(this.options.toggle+".editable")),this.$element.off("save.internal"),this.$element.removeClass("editable editable-open editable-disabled"),this.$element.removeData("editable")}},a.fn.editable=function(c){var d={},e=arguments,f="editable";switch(c){case"validate":return this.each(function(){var b,c=a(this),e=c.data(f);e&&(b=e.validate())&&(d[e.options.name]=b)}),d;case"getValue":return 2===arguments.length&&arguments[1]===!0?d=this.eq(0).data(f).value:this.each(function(){var b=a(this),c=b.data(f);c&&void 0!==c.value&&null!==c.value&&(d[c.options.name]=c.input.value2submit(c.value))}),d;case"submit":var g=arguments[1]||{},h=this,i=this.editable("validate");if(a.isEmptyObject(i)){var j={};if(1===h.length){var k=h.data("editable"),l={name:k.options.name||"",value:k.input.value2submit(k.value),pk:"function"==typeof k.options.pk?k.options.pk.call(k.options.scope):k.options.pk};"function"==typeof k.options.params?l=k.options.params.call(k.options.scope,l):(k.options.params=a.fn.editableutils.tryParseJson(k.options.params,!0),a.extend(l,k.options.params)),j={url:k.options.url,data:l,type:"POST"},g.success=g.success||k.options.success,g.error=g.error||k.options.error}else{var m=this.editable("getValue");j={url:g.url,data:m,type:"POST"}}j.success="function"==typeof g.success?function(a){g.success.call(h,a,g)}:a.noop,j.error="function"==typeof g.error?function(){g.error.apply(h,arguments)}:a.noop,g.ajaxOptions&&a.extend(j,g.ajaxOptions),g.data&&a.extend(j.data,g.data),a.ajax(j)}else"function"==typeof g.error&&g.error.call(h,i);return this}return this.each(function(){var d=a(this),g=d.data(f),h="object"==typeof c&&c;return h&&h.selector?(g=new b(this,h),void 0):(g||d.data(f,g=new b(this,h)),"string"==typeof c&&g[c].apply(g,Array.prototype.slice.call(e,1)),void 0)})},a.fn.editable.defaults={type:"text",disabled:!1,toggle:"click",emptytext:"Empty",autotext:"auto",value:null,display:null,emptyclass:"editable-empty",unsavedclass:"editable-unsaved",selector:null,highlight:"#FFFF80"}}(window.jQuery),function(a){"use strict";a.fn.editabletypes={};var b=function(){};b.prototype={init:function(b,c,d){this.type=b,this.options=a.extend({},d,c)},prerender:function(){this.$tpl=a(this.options.tpl),this.$input=this.$tpl,this.$clear=null,this.error=null},render:function(){},value2html:function(b,c){a(c)[this.options.escape?"text":"html"](a.trim(b))},html2value:function(b){return a("<div>").html(b).text()},value2str:function(a){return a},str2value:function(a){return a},value2submit:function(a){return a},value2input:function(a){this.$input.val(a)},input2value:function(){return this.$input.val()},activate:function(){this.$input.is(":visible")&&this.$input.focus()},clear:function(){this.$input.val(null)},escape:function(b){return a("<div>").text(b).html()},autosubmit:function(){},destroy:function(){},setClass:function(){this.options.inputclass&&this.$input.addClass(this.options.inputclass)},setAttr:function(a){void 0!==this.options[a]&&null!==this.options[a]&&this.$input.attr(a,this.options[a])},option:function(a,b){this.options[a]=b}},b.defaults={tpl:"",inputclass:null,escape:!0,scope:null,showbuttons:!0},a.extend(a.fn.editabletypes,{abstractinput:b})}(window.jQuery),function(a){"use strict";var b=function(){};a.fn.editableutils.inherit(b,a.fn.editabletypes.abstractinput),a.extend(b.prototype,{render:function(){var b=a.Deferred();return this.error=null,this.onSourceReady(function(){this.renderList(),b.resolve()},function(){this.error=this.options.sourceError,b.resolve()}),b.promise()},html2value:function(){return null},value2html:function(b,c,d,e){var f=a.Deferred(),g=function(){"function"==typeof d?d.call(c,b,this.sourceData,e):this.value2htmlFinal(b,c),f.resolve()};return null===b?g.call(this):this.onSourceReady(g,function(){f.resolve()}),f.promise()},onSourceReady:function(b,c){var d;if(a.isFunction(this.options.source)?(d=this.options.source.call(this.options.scope),this.sourceData=null):d=this.options.source,this.options.sourceCache&&a.isArray(this.sourceData))return b.call(this),void 0;try{d=a.fn.editableutils.tryParseJson(d,!1)}catch(e){return c.call(this),void 0}if("string"==typeof d){if(this.options.sourceCache){var f,g=d;if(a(document).data(g)||a(document).data(g,{}),f=a(document).data(g),f.loading===!1&&f.sourceData)return this.sourceData=f.sourceData,this.doPrepend(),b.call(this),void 0;if(f.loading===!0)return f.callbacks.push(a.proxy(function(){this.sourceData=f.sourceData,this.doPrepend(),b.call(this)},this)),f.err_callbacks.push(a.proxy(c,this)),void 0;f.loading=!0,f.callbacks=[],f.err_callbacks=[]}var h=a.extend({url:d,type:"get",cache:!1,dataType:"json",success:a.proxy(function(d){f&&(f.loading=!1),this.sourceData=this.makeArray(d),a.isArray(this.sourceData)?(f&&(f.sourceData=this.sourceData,a.each(f.callbacks,function(){this.call()})),this.doPrepend(),b.call(this)):(c.call(this),f&&a.each(f.err_callbacks,function(){this.call()}))},this),error:a.proxy(function(){c.call(this),f&&(f.loading=!1,a.each(f.err_callbacks,function(){this.call()}))},this)},this.options.sourceOptions);a.ajax(h)}else this.sourceData=this.makeArray(d),a.isArray(this.sourceData)?(this.doPrepend(),b.call(this)):c.call(this)},doPrepend:function(){null!==this.options.prepend&&void 0!==this.options.prepend&&(a.isArray(this.prependData)||(a.isFunction(this.options.prepend)&&(this.options.prepend=this.options.prepend.call(this.options.scope)),this.options.prepend=a.fn.editableutils.tryParseJson(this.options.prepend,!0),"string"==typeof this.options.prepend&&(this.options.prepend={"":this.options.prepend}),this.prependData=this.makeArray(this.options.prepend)),a.isArray(this.prependData)&&a.isArray(this.sourceData)&&(this.sourceData=this.prependData.concat(this.sourceData)))},renderList:function(){},value2htmlFinal:function(){},makeArray:function(b){var c,d,e,f,g=[];if(!b||"string"==typeof b)return null;if(a.isArray(b)){f=function(a,b){return d={value:a,text:b},c++>=2?!1:void 0};for(var h=0;h<b.length;h++)e=b[h],"object"==typeof e?(c=0,a.each(e,f),1===c?g.push(d):c>1&&(e.children&&(e.children=this.makeArray(e.children)),g.push(e))):g.push({value:e,text:e})}else a.each(b,function(a,b){g.push({value:a,text:b})});return g},option:function(a,b){this.options[a]=b,"source"===a&&(this.sourceData=null),"prepend"===a&&(this.prependData=null)}}),b.defaults=a.extend({},a.fn.editabletypes.abstractinput.defaults,{source:null,prepend:!1,sourceError:"Error when loading list",sourceCache:!0,sourceOptions:null}),a.fn.editabletypes.list=b}(window.jQuery),function(a){"use strict";var b=function(a){this.init("text",a,b.defaults)};a.fn.editableutils.inherit(b,a.fn.editabletypes.abstractinput),a.extend(b.prototype,{render:function(){this.renderClear(),this.setClass(),this.setAttr("placeholder")},activate:function(){this.$input.is(":visible")&&(this.$input.focus(),a.fn.editableutils.setCursorPosition(this.$input.get(0),this.$input.val().length),this.toggleClear&&this.toggleClear())},renderClear:function(){this.options.clear&&(this.$clear=a('<span class="editable-clear-x"></span>'),this.$input.after(this.$clear).css("padding-right",24).keyup(a.proxy(function(b){if(!~a.inArray(b.keyCode,[40,38,9,13,27])){clearTimeout(this.t);var c=this;this.t=setTimeout(function(){c.toggleClear(b)},100)}},this)).parent().css("position","relative"),this.$clear.click(a.proxy(this.clear,this)))},postrender:function(){},toggleClear:function(){if(this.$clear){var a=this.$input.val().length,b=this.$clear.is(":visible");a&&!b&&this.$clear.show(),!a&&b&&this.$clear.hide()}},clear:function(){this.$clear.hide(),this.$input.val("").focus()}}),b.defaults=a.extend({},a.fn.editabletypes.abstractinput.defaults,{tpl:'<input type="text">',placeholder:null,clear:!0}),a.fn.editabletypes.text=b}(window.jQuery),function(a){"use strict";var b=function(a){this.init("textarea",a,b.defaults)};a.fn.editableutils.inherit(b,a.fn.editabletypes.abstractinput),a.extend(b.prototype,{render:function(){this.setClass(),this.setAttr("placeholder"),this.setAttr("rows"),this.$input.keydown(function(b){b.ctrlKey&&13===b.which&&a(this).closest("form").submit()})},activate:function(){a.fn.editabletypes.text.prototype.activate.call(this)}}),b.defaults=a.extend({},a.fn.editabletypes.abstractinput.defaults,{tpl:"<textarea></textarea>",inputclass:"input-large",placeholder:null,rows:7}),a.fn.editabletypes.textarea=b}(window.jQuery),function(a){"use strict";var b=function(a){this.init("select",a,b.defaults)};a.fn.editableutils.inherit(b,a.fn.editabletypes.list),a.extend(b.prototype,{renderList:function(){this.$input.empty();var b=function(c,d){var e;if(a.isArray(d))for(var f=0;f<d.length;f++)e={},d[f].children?(e.label=d[f].text,c.append(b(a("<optgroup>",e),d[f].children))):(e.value=d[f].value,d[f].disabled&&(e.disabled=!0),c.append(a("<option>",e).text(d[f].text)));return c};b(this.$input,this.sourceData),this.setClass(),this.$input.on("keydown.editable",function(b){13===b.which&&a(this).closest("form").submit()})},value2htmlFinal:function(b,c){var d="",e=a.fn.editableutils.itemsByValue(b,this.sourceData);e.length&&(d=e[0].text),a.fn.editabletypes.abstractinput.prototype.value2html.call(this,d,c)},autosubmit:function(){this.$input.off("keydown.editable").on("change.editable",function(){a(this).closest("form").submit()})}}),b.defaults=a.extend({},a.fn.editabletypes.list.defaults,{tpl:"<select></select>"}),a.fn.editabletypes.select=b}(window.jQuery),function(a){"use strict";var b=function(a){this.init("checklist",a,b.defaults)};a.fn.editableutils.inherit(b,a.fn.editabletypes.list),a.extend(b.prototype,{renderList:function(){var b;if(this.$tpl.empty(),a.isArray(this.sourceData)){for(var c=0;c<this.sourceData.length;c++)b=a("<label>").append(a("<input>",{type:"checkbox",value:this.sourceData[c].value})).append(a("<span>").text(" "+this.sourceData[c].text)),a("<div>").append(b).appendTo(this.$tpl);this.$input=this.$tpl.find('input[type="checkbox"]'),this.setClass()}},value2str:function(b){return a.isArray(b)?b.sort().join(a.trim(this.options.separator)):""},str2value:function(b){var c,d=null;return"string"==typeof b&&b.length?(c=new RegExp("\\s*"+a.trim(this.options.separator)+"\\s*"),d=b.split(c)):d=a.isArray(b)?b:[b],d},value2input:function(b){this.$input.prop("checked",!1),a.isArray(b)&&b.length&&this.$input.each(function(c,d){var e=a(d);a.each(b,function(a,b){e.val()==b&&e.prop("checked",!0)})})},input2value:function(){var b=[];return this.$input.filter(":checked").each(function(c,d){b.push(a(d).val())}),b},value2htmlFinal:function(b,c){var d=[],e=a.fn.editableutils.itemsByValue(b,this.sourceData),f=this.options.escape;e.length?(a.each(e,function(b,c){var e=f?a.fn.editableutils.escape(c.text):c.text;d.push(e)}),a(c).html(d.join("<br>"))):a(c).empty()},activate:function(){this.$input.first().focus()},autosubmit:function(){this.$input.on("keydown",function(b){13===b.which&&a(this).closest("form").submit()})}}),b.defaults=a.extend({},a.fn.editabletypes.list.defaults,{tpl:'<div class="editable-checklist"></div>',inputclass:null,separator:","}),a.fn.editabletypes.checklist=b}(window.jQuery),function(a){"use strict";var b=function(a){this.init("password",a,b.defaults)};a.fn.editableutils.inherit(b,a.fn.editabletypes.text),a.extend(b.prototype,{value2html:function(b,c){b?a(c).text("[hidden]"):a(c).empty()},html2value:function(){return null}}),b.defaults=a.extend({},a.fn.editabletypes.text.defaults,{tpl:'<input type="password">'}),a.fn.editabletypes.password=b}(window.jQuery),function(a){"use strict";var b=function(a){this.init("email",a,b.defaults)};a.fn.editableutils.inherit(b,a.fn.editabletypes.text),b.defaults=a.extend({},a.fn.editabletypes.text.defaults,{tpl:'<input type="email">'}),a.fn.editabletypes.email=b}(window.jQuery),function(a){"use strict";var b=function(a){this.init("url",a,b.defaults)};a.fn.editableutils.inherit(b,a.fn.editabletypes.text),b.defaults=a.extend({},a.fn.editabletypes.text.defaults,{tpl:'<input type="url">'}),a.fn.editabletypes.url=b}(window.jQuery),function(a){"use strict";var b=function(a){this.init("tel",a,b.defaults)};a.fn.editableutils.inherit(b,a.fn.editabletypes.text),b.defaults=a.extend({},a.fn.editabletypes.text.defaults,{tpl:'<input type="tel">'}),a.fn.editabletypes.tel=b}(window.jQuery),function(a){"use strict";var b=function(a){this.init("number",a,b.defaults)};a.fn.editableutils.inherit(b,a.fn.editabletypes.text),a.extend(b.prototype,{render:function(){b.superclass.render.call(this),this.setAttr("min"),this.setAttr("max"),this.setAttr("step")},postrender:function(){this.$clear&&this.$clear.css({right:24})}}),b.defaults=a.extend({},a.fn.editabletypes.text.defaults,{tpl:'<input type="number">',inputclass:"input-mini",min:null,max:null,step:null}),a.fn.editabletypes.number=b}(window.jQuery),function(a){"use strict";
var b=function(a){this.init("range",a,b.defaults)};a.fn.editableutils.inherit(b,a.fn.editabletypes.number),a.extend(b.prototype,{render:function(){this.$input=this.$tpl.filter("input"),this.setClass(),this.setAttr("min"),this.setAttr("max"),this.setAttr("step"),this.$input.on("input",function(){a(this).siblings("output").text(a(this).val())})},activate:function(){this.$input.focus()}}),b.defaults=a.extend({},a.fn.editabletypes.number.defaults,{tpl:'<input type="range"><output style="width: 30px; display: inline-block"></output>',inputclass:"input-medium"}),a.fn.editabletypes.range=b}(window.jQuery),function(a){"use strict";var b=function(a){this.init("time",a,b.defaults)};a.fn.editableutils.inherit(b,a.fn.editabletypes.abstractinput),a.extend(b.prototype,{render:function(){this.setClass()}}),b.defaults=a.extend({},a.fn.editabletypes.abstractinput.defaults,{tpl:'<input type="time">'}),a.fn.editabletypes.time=b}(window.jQuery),function(a){"use strict";var b=function(c){if(this.init("select2",c,b.defaults),c.select2=c.select2||{},this.sourceData=null,c.placeholder&&(c.select2.placeholder=c.placeholder),!c.select2.tags&&c.source){var d=c.source;a.isFunction(c.source)&&(d=c.source.call(c.scope)),"string"==typeof d?(c.select2.ajax=c.select2.ajax||{},c.select2.ajax.data||(c.select2.ajax.data=function(a){return{query:a}}),c.select2.ajax.results||(c.select2.ajax.results=function(a){return{results:a}}),c.select2.ajax.url=d):(this.sourceData=this.convertSource(d),c.select2.data=this.sourceData)}if(this.options.select2=a.extend({},b.defaults.select2,c.select2),this.isMultiple=this.options.select2.tags||this.options.select2.multiple,this.isRemote="ajax"in this.options.select2,this.idFunc=this.options.select2.id,"function"!=typeof this.idFunc){var e=this.idFunc||"id";this.idFunc=function(a){return a[e]}}this.formatSelection=this.options.select2.formatSelection,"function"!=typeof this.formatSelection&&(this.formatSelection=function(a){return a.text})};a.fn.editableutils.inherit(b,a.fn.editabletypes.abstractinput),a.extend(b.prototype,{render:function(){this.setClass(),this.isRemote&&this.$input.on("select2-loaded",a.proxy(function(a){this.sourceData=a.items.results},this)),this.isMultiple&&this.$input.on("change",function(){a(this).closest("form").parent().triggerHandler("resize")})},value2html:function(c,d){var e,f="",g=this;this.options.select2.tags?e=c:this.sourceData&&(e=a.fn.editableutils.itemsByValue(c,this.sourceData,this.idFunc)),a.isArray(e)?(f=[],a.each(e,function(a,b){f.push(b&&"object"==typeof b?g.formatSelection(b):b)})):e&&(f=g.formatSelection(e)),f=a.isArray(f)?f.join(this.options.viewseparator):f,b.superclass.value2html.call(this,f,d)},html2value:function(a){return this.options.select2.tags?this.str2value(a,this.options.viewseparator):null},value2input:function(b){if(a.isArray(b)&&(b=b.join(this.getSeparator())),this.$input.data("select2")?this.$input.val(b).trigger("change",!0):(this.$input.val(b),this.$input.select2(this.options.select2)),this.isRemote&&!this.isMultiple&&!this.options.select2.initSelection){var c=this.options.select2.id,d=this.options.select2.formatSelection;if(!c&&!d){var e=a(this.options.scope);if(!e.data("editable").isEmpty){var f={id:b,text:e.text()};this.$input.select2("data",f)}}}},input2value:function(){return this.$input.select2("val")},str2value:function(b,c){if("string"!=typeof b||!this.isMultiple)return b;c=c||this.getSeparator();var d,e,f;if(null===b||b.length<1)return null;for(d=b.split(c),e=0,f=d.length;f>e;e+=1)d[e]=a.trim(d[e]);return d},autosubmit:function(){this.$input.on("change",function(b,c){c||a(this).closest("form").submit()})},getSeparator:function(){return this.options.select2.separator||a.fn.select2.defaults.separator},convertSource:function(b){if(a.isArray(b)&&b.length&&void 0!==b[0].value)for(var c=0;c<b.length;c++)void 0!==b[c].value&&(b[c].id=b[c].value,delete b[c].value);return b},destroy:function(){this.$input.data("select2")&&this.$input.select2("destroy")}}),b.defaults=a.extend({},a.fn.editabletypes.abstractinput.defaults,{tpl:'<input type="hidden">',select2:null,placeholder:null,source:null,viewseparator:", "}),a.fn.editabletypes.select2=b}(window.jQuery),function(a){var b=function(b,c){return this.$element=a(b),this.$element.is("input")?(this.options=a.extend({},a.fn.combodate.defaults,c,this.$element.data()),this.init(),void 0):(a.error("Combodate should be applied to INPUT element"),void 0)};b.prototype={constructor:b,init:function(){this.map={day:["D","date"],month:["M","month"],year:["Y","year"],hour:["[Hh]","hours"],minute:["m","minutes"],second:["s","seconds"],ampm:["[Aa]",""]},this.$widget=a('<span class="combodate"></span>').html(this.getTemplate()),this.initCombos(),this.$widget.on("change","select",a.proxy(function(b){this.$element.val(this.getValue()).change(),this.options.smartDays&&(a(b.target).is(".month")||a(b.target).is(".year"))&&this.fillCombo("day")},this)),this.$widget.find("select").css("width","auto"),this.$element.hide().after(this.$widget),this.setValue(this.$element.val()||this.options.value)},getTemplate:function(){var b=this.options.template;return a.each(this.map,function(a,c){c=c[0];var d=new RegExp(c+"+"),e=c.length>1?c.substring(1,2):c;b=b.replace(d,"{"+e+"}")}),b=b.replace(/ /g,"&nbsp;"),a.each(this.map,function(a,c){c=c[0];var d=c.length>1?c.substring(1,2):c;b=b.replace("{"+d+"}",'<select class="'+a+'"></select>')}),b},initCombos:function(){for(var a in this.map){var b=this.$widget.find("."+a);this["$"+a]=b.length?b:null,this.fillCombo(a)}},fillCombo:function(a){var b=this["$"+a];if(b){var c="fill"+a.charAt(0).toUpperCase()+a.slice(1),d=this[c](),e=b.val();b.empty();for(var f=0;f<d.length;f++)b.append('<option value="'+d[f][0]+'">'+d[f][1]+"</option>");b.val(e)}},fillCommon:function(a){var b,c=[];if("name"===this.options.firstItem){b=moment.relativeTime||moment.langData()._relativeTime;var d="function"==typeof b[a]?b[a](1,!0,a,!1):b[a];d=d.split(" ").reverse()[0],c.push(["",d])}else"empty"===this.options.firstItem&&c.push(["",""]);return c},fillDay:function(){var a,b,c=this.fillCommon("d"),d=-1!==this.options.template.indexOf("DD"),e=31;if(this.options.smartDays&&this.$month&&this.$year){var f=parseInt(this.$month.val(),10),g=parseInt(this.$year.val(),10);isNaN(f)||isNaN(g)||(e=moment([g,f]).daysInMonth())}for(b=1;e>=b;b++)a=d?this.leadZero(b):b,c.push([b,a]);return c},fillMonth:function(){var a,b,c=this.fillCommon("M"),d=-1!==this.options.template.indexOf("MMMM"),e=-1!==this.options.template.indexOf("MMM"),f=-1!==this.options.template.indexOf("MM");for(b=0;11>=b;b++)a=d?moment().date(1).month(b).format("MMMM"):e?moment().date(1).month(b).format("MMM"):f?this.leadZero(b+1):b+1,c.push([b,a]);return c},fillYear:function(){var a,b,c=[],d=-1!==this.options.template.indexOf("YYYY");for(b=this.options.maxYear;b>=this.options.minYear;b--)a=d?b:(b+"").substring(2),c[this.options.yearDescending?"push":"unshift"]([b,a]);return c=this.fillCommon("y").concat(c)},fillHour:function(){var a,b,c=this.fillCommon("h"),d=-1!==this.options.template.indexOf("h"),e=(-1!==this.options.template.indexOf("H"),-1!==this.options.template.toLowerCase().indexOf("hh")),f=d?1:0,g=d?12:23;for(b=f;g>=b;b++)a=e?this.leadZero(b):b,c.push([b,a]);return c},fillMinute:function(){var a,b,c=this.fillCommon("m"),d=-1!==this.options.template.indexOf("mm");for(b=0;59>=b;b+=this.options.minuteStep)a=d?this.leadZero(b):b,c.push([b,a]);return c},fillSecond:function(){var a,b,c=this.fillCommon("s"),d=-1!==this.options.template.indexOf("ss");for(b=0;59>=b;b+=this.options.secondStep)a=d?this.leadZero(b):b,c.push([b,a]);return c},fillAmpm:function(){var a=-1!==this.options.template.indexOf("a"),b=(-1!==this.options.template.indexOf("A"),[["am",a?"am":"AM"],["pm",a?"pm":"PM"]]);return b},getValue:function(b){var c,d={},e=this,f=!1;return a.each(this.map,function(a){if("ampm"!==a){var b="day"===a?1:0;return d[a]=e["$"+a]?parseInt(e["$"+a].val(),10):b,isNaN(d[a])?(f=!0,!1):void 0}}),f?"":(this.$ampm&&(d.hour=12===d.hour?"am"===this.$ampm.val()?0:12:"am"===this.$ampm.val()?d.hour:d.hour+12),c=moment([d.year,d.month,d.day,d.hour,d.minute,d.second]),this.highlight(c),b=void 0===b?this.options.format:b,null===b?c.isValid()?c:null:c.isValid()?c.format(b):"")},setValue:function(b){function c(b,c){var d={};return b.children("option").each(function(b,e){var f,g=a(e).attr("value");""!==g&&(f=Math.abs(g-c),("undefined"==typeof d.distance||f<d.distance)&&(d={value:g,distance:f}))}),d.value}if(b){var d="string"==typeof b?moment(b,this.options.format):moment(b),e=this,f={};d.isValid()&&(a.each(this.map,function(a,b){"ampm"!==a&&(f[a]=d[b[1]]())}),this.$ampm&&(f.hour>=12?(f.ampm="pm",f.hour>12&&(f.hour-=12)):(f.ampm="am",0===f.hour&&(f.hour=12))),a.each(f,function(a,b){e["$"+a]&&("minute"===a&&e.options.minuteStep>1&&e.options.roundTime&&(b=c(e["$"+a],b)),"second"===a&&e.options.secondStep>1&&e.options.roundTime&&(b=c(e["$"+a],b)),e["$"+a].val(b))}),this.options.smartDays&&this.fillCombo("day"),this.$element.val(d.format(this.options.format)).change())}},highlight:function(a){a.isValid()?this.options.errorClass?this.$widget.removeClass(this.options.errorClass):this.$widget.find("select").css("border-color",this.borderColor):this.options.errorClass?this.$widget.addClass(this.options.errorClass):(this.borderColor||(this.borderColor=this.$widget.find("select").css("border-color")),this.$widget.find("select").css("border-color","red"))},leadZero:function(a){return 9>=a?"0"+a:a},destroy:function(){this.$widget.remove(),this.$element.removeData("combodate").show()}},a.fn.combodate=function(c){var d,e=Array.apply(null,arguments);return e.shift(),"getValue"===c&&this.length&&(d=this.eq(0).data("combodate"))?d.getValue.apply(d,e):this.each(function(){var d=a(this),f=d.data("combodate"),g="object"==typeof c&&c;f||d.data("combodate",f=new b(this,g)),"string"==typeof c&&"function"==typeof f[c]&&f[c].apply(f,e)})},a.fn.combodate.defaults={format:"DD-MM-YYYY HH:mm",template:"D / MMM / YYYY H : mm",value:null,minYear:1970,maxYear:2015,yearDescending:!0,minuteStep:5,secondStep:1,firstItem:"empty",errorClass:null,roundTime:!0,smartDays:!1}}(window.jQuery),function(a){"use strict";var b=function(c){this.init("combodate",c,b.defaults),this.options.viewformat||(this.options.viewformat=this.options.format),c.combodate=a.fn.editableutils.tryParseJson(c.combodate,!0),this.options.combodate=a.extend({},b.defaults.combodate,c.combodate,{format:this.options.format,template:this.options.template})};a.fn.editableutils.inherit(b,a.fn.editabletypes.abstractinput),a.extend(b.prototype,{render:function(){this.$input.combodate(this.options.combodate),"bs3"===a.fn.editableform.engine&&this.$input.siblings().find("select").addClass("form-control"),this.options.inputclass&&this.$input.siblings().find("select").addClass(this.options.inputclass)},value2html:function(a,c){var d=a?a.format(this.options.viewformat):"";b.superclass.value2html.call(this,d,c)},html2value:function(a){return a?moment(a,this.options.viewformat):null},value2str:function(a){return a?a.format(this.options.format):""},str2value:function(a){return a?moment(a,this.options.format):null},value2submit:function(a){return this.value2str(a)},value2input:function(a){this.$input.combodate("setValue",a)},input2value:function(){return this.$input.combodate("getValue",null)},activate:function(){this.$input.siblings(".combodate").find("select").eq(0).focus()},autosubmit:function(){}}),b.defaults=a.extend({},a.fn.editabletypes.abstractinput.defaults,{tpl:'<input type="text">',inputclass:null,format:"YYYY-MM-DD",viewformat:null,template:"D / MMM / YYYY",combodate:null}),a.fn.editabletypes.combodate=b}(window.jQuery),function(a){"use strict";var b=a.fn.editableform.Constructor.prototype.initInput;a.extend(a.fn.editableform.Constructor.prototype,{initTemplate:function(){this.$form=a(a.fn.editableform.template),this.$form.find(".control-group").addClass("form-group"),this.$form.find(".editable-error-block").addClass("help-block")},initInput:function(){b.apply(this);var c=null===this.input.options.inputclass||this.input.options.inputclass===!1,d="input-sm",e="text,select,textarea,password,email,url,tel,number,range,time,typeaheadjs".split(",");~a.inArray(this.input.type,e)&&(this.input.$input.addClass("form-control"),c&&(this.input.options.inputclass=d,this.input.$input.addClass(d)));for(var f=this.$form.find(".editable-buttons"),g=c?[d]:this.input.options.inputclass.split(" "),h=0;h<g.length;h++)"input-lg"===g[h].toLowerCase()&&f.find("button").removeClass("btn-sm").addClass("btn-lg")}}),a.fn.editableform.buttons='<button type="submit" class="btn btn-primary btn-sm editable-submit"><i class="glyphicon glyphicon-ok"></i></button><button type="button" class="btn btn-default btn-sm editable-cancel"><i class="glyphicon glyphicon-remove"></i></button>',a.fn.editableform.errorGroupClass="has-error",a.fn.editableform.errorBlockClass=null,a.fn.editableform.engine="bs3"}(window.jQuery),function(a){"use strict";a.extend(a.fn.editableContainer.Popup.prototype,{containerName:"popover",containerDataName:"bs.popover",innerCss:".popover-content",defaults:a.fn.popover.Constructor.DEFAULTS,initContainer:function(){a.extend(this.containerOptions,{trigger:"manual",selector:!1,content:" ",template:this.defaults.template});var b;this.$element.data("template")&&(b=this.$element.data("template"),this.$element.removeData("template")),this.call(this.containerOptions),b&&this.$element.data("template",b)},innerShow:function(){this.call("show")},innerHide:function(){this.call("hide")},innerDestroy:function(){this.call("destroy")},setContainerOption:function(a,b){this.container().options[a]=b},setPosition:function(){!function(){var a=this.tip(),b="function"==typeof this.options.placement?this.options.placement.call(this,a[0],this.$element[0]):this.options.placement,c=/\s?auto?\s?/i,d=c.test(b);d&&(b=b.replace(c,"")||"top");var e=this.getPosition(),f=a[0].offsetWidth,g=a[0].offsetHeight;if(d){var h=this.$element.parent(),i=b,j=document.documentElement.scrollTop||document.body.scrollTop,k="body"==this.options.container?window.innerWidth:h.outerWidth(),l="body"==this.options.container?window.innerHeight:h.outerHeight(),m="body"==this.options.container?0:h.offset().left;b="bottom"==b&&e.top+e.height+g-j>l?"top":"top"==b&&e.top-j-g<0?"bottom":"right"==b&&e.right+f>k?"left":"left"==b&&e.left-f<m?"right":b,a.removeClass(i).addClass(b)}var n=this.getCalculatedOffset(b,e,f,g);this.applyPlacement(n,b)}.call(this.container())}})}(window.jQuery),function(a){function b(){return new Date(Date.UTC.apply(Date,arguments))}function c(b,c){var d,e=a(b).data(),f={},g=new RegExp("^"+c.toLowerCase()+"([A-Z])"),c=new RegExp("^"+c.toLowerCase());for(var h in e)c.test(h)&&(d=h.replace(g,function(a,b){return b.toLowerCase()}),f[d]=e[h]);return f}function d(b){var c={};if(k[b]||(b=b.split("-")[0],k[b])){var d=k[b];return a.each(j,function(a,b){b in d&&(c[b]=d[b])}),c}}var e=function(b,c){this._process_options(c),this.element=a(b),this.isInline=!1,this.isInput=this.element.is("input"),this.component=this.element.is(".date")?this.element.find(".add-on, .btn"):!1,this.hasInput=this.component&&this.element.find("input").length,this.component&&0===this.component.length&&(this.component=!1),this.picker=a(l.template),this._buildEvents(),this._attachEvents(),this.isInline?this.picker.addClass("datepicker-inline").appendTo(this.element):this.picker.addClass("datepicker-dropdown dropdown-menu"),this.o.rtl&&(this.picker.addClass("datepicker-rtl"),this.picker.find(".prev i, .next i").toggleClass("icon-arrow-left icon-arrow-right")),this.viewMode=this.o.startView,this.o.calendarWeeks&&this.picker.find("tfoot th.today").attr("colspan",function(a,b){return parseInt(b)+1}),this._allow_update=!1,this.setStartDate(this.o.startDate),this.setEndDate(this.o.endDate),this.setDaysOfWeekDisabled(this.o.daysOfWeekDisabled),this.fillDow(),this.fillMonths(),this._allow_update=!0,this.update(),this.showMode(),this.isInline&&this.show()};e.prototype={constructor:e,_process_options:function(b){this._o=a.extend({},this._o,b);var c=this.o=a.extend({},this._o),d=c.language;switch(k[d]||(d=d.split("-")[0],k[d]||(d=i.language)),c.language=d,c.startView){case 2:case"decade":c.startView=2;break;case 1:case"year":c.startView=1;break;default:c.startView=0}switch(c.minViewMode){case 1:case"months":c.minViewMode=1;break;case 2:case"years":c.minViewMode=2;break;default:c.minViewMode=0}c.startView=Math.max(c.startView,c.minViewMode),c.weekStart%=7,c.weekEnd=(c.weekStart+6)%7;var e=l.parseFormat(c.format);c.startDate!==-1/0&&(c.startDate=l.parseDate(c.startDate,e,c.language)),1/0!==c.endDate&&(c.endDate=l.parseDate(c.endDate,e,c.language)),c.daysOfWeekDisabled=c.daysOfWeekDisabled||[],a.isArray(c.daysOfWeekDisabled)||(c.daysOfWeekDisabled=c.daysOfWeekDisabled.split(/[,\s]*/)),c.daysOfWeekDisabled=a.map(c.daysOfWeekDisabled,function(a){return parseInt(a,10)})},_events:[],_secondaryEvents:[],_applyEvents:function(a){for(var b,c,d=0;d<a.length;d++)b=a[d][0],c=a[d][1],b.on(c)},_unapplyEvents:function(a){for(var b,c,d=0;d<a.length;d++)b=a[d][0],c=a[d][1],b.off(c)},_buildEvents:function(){this.isInput?this._events=[[this.element,{focus:a.proxy(this.show,this),keyup:a.proxy(this.update,this),keydown:a.proxy(this.keydown,this)}]]:this.component&&this.hasInput?this._events=[[this.element.find("input"),{focus:a.proxy(this.show,this),keyup:a.proxy(this.update,this),keydown:a.proxy(this.keydown,this)}],[this.component,{click:a.proxy(this.show,this)}]]:this.element.is("div")?this.isInline=!0:this._events=[[this.element,{click:a.proxy(this.show,this)}]],this._secondaryEvents=[[this.picker,{click:a.proxy(this.click,this)}],[a(window),{resize:a.proxy(this.place,this)}],[a(document),{mousedown:a.proxy(function(a){this.element.is(a.target)||this.element.find(a.target).size()||this.picker.is(a.target)||this.picker.find(a.target).size()||this.hide()},this)}]]},_attachEvents:function(){this._detachEvents(),this._applyEvents(this._events)},_detachEvents:function(){this._unapplyEvents(this._events)},_attachSecondaryEvents:function(){this._detachSecondaryEvents(),this._applyEvents(this._secondaryEvents)},_detachSecondaryEvents:function(){this._unapplyEvents(this._secondaryEvents)},_trigger:function(b,c){var d=c||this.date,e=new Date(d.getTime()+6e4*d.getTimezoneOffset());this.element.trigger({type:b,date:e,format:a.proxy(function(a){var b=a||this.o.format;return l.formatDate(d,b,this.o.language)},this)})},show:function(a){this.isInline||this.picker.appendTo("body"),this.picker.show(),this.height=this.component?this.component.outerHeight():this.element.outerHeight(),this.place(),this._attachSecondaryEvents(),a&&a.preventDefault(),this._trigger("show")},hide:function(){this.isInline||this.picker.is(":visible")&&(this.picker.hide().detach(),this._detachSecondaryEvents(),this.viewMode=this.o.startView,this.showMode(),this.o.forceParse&&(this.isInput&&this.element.val()||this.hasInput&&this.element.find("input").val())&&this.setValue(),this._trigger("hide"))},remove:function(){this.hide(),this._detachEvents(),this._detachSecondaryEvents(),this.picker.remove(),delete this.element.data().datepicker,this.isInput||delete this.element.data().date},getDate:function(){var a=this.getUTCDate();return new Date(a.getTime()+6e4*a.getTimezoneOffset())},getUTCDate:function(){return this.date},setDate:function(a){this.setUTCDate(new Date(a.getTime()-6e4*a.getTimezoneOffset()))},setUTCDate:function(a){this.date=a,this.setValue()},setValue:function(){var a=this.getFormattedDate();this.isInput?this.element.val(a):this.component&&this.element.find("input").val(a)},getFormattedDate:function(a){return void 0===a&&(a=this.o.format),l.formatDate(this.date,a,this.o.language)},setStartDate:function(a){this._process_options({startDate:a}),this.update(),this.updateNavArrows()},setEndDate:function(a){this._process_options({endDate:a}),this.update(),this.updateNavArrows()},setDaysOfWeekDisabled:function(a){this._process_options({daysOfWeekDisabled:a}),this.update(),this.updateNavArrows()},place:function(){if(!this.isInline){var b=parseInt(this.element.parents().filter(function(){return"auto"!=a(this).css("z-index")}).first().css("z-index"))+10,c=this.component?this.component.parent().offset():this.element.offset(),d=this.component?this.component.outerHeight(!0):this.element.outerHeight(!0);this.picker.css({top:c.top+d,left:c.left,zIndex:b})}},_allow_update:!0,update:function(){if(this._allow_update){var a,b=!1;arguments&&arguments.length&&("string"==typeof arguments[0]||arguments[0]instanceof Date)?(a=arguments[0],b=!0):(a=this.isInput?this.element.val():this.element.data("date")||this.element.find("input").val(),delete this.element.data().date),this.date=l.parseDate(a,this.o.format,this.o.language),b&&this.setValue(),this.viewDate=this.date<this.o.startDate?new Date(this.o.startDate):this.date>this.o.endDate?new Date(this.o.endDate):new Date(this.date),this.fill()}},fillDow:function(){var a=this.o.weekStart,b="<tr>";if(this.o.calendarWeeks){var c='<th class="cw">&nbsp;</th>';b+=c,this.picker.find(".datepicker-days thead tr:first-child").prepend(c)}for(;a<this.o.weekStart+7;)b+='<th class="dow">'+k[this.o.language].daysMin[a++%7]+"</th>";b+="</tr>",this.picker.find(".datepicker-days thead").append(b)},fillMonths:function(){for(var a="",b=0;12>b;)a+='<span class="month">'+k[this.o.language].monthsShort[b++]+"</span>";this.picker.find(".datepicker-months td").html(a)},setRange:function(b){b&&b.length?this.range=a.map(b,function(a){return a.valueOf()}):delete this.range,this.fill()},getClassNames:function(b){var c=[],d=this.viewDate.getUTCFullYear(),e=this.viewDate.getUTCMonth(),f=this.date.valueOf(),g=new Date;return b.getUTCFullYear()<d||b.getUTCFullYear()==d&&b.getUTCMonth()<e?c.push("old"):(b.getUTCFullYear()>d||b.getUTCFullYear()==d&&b.getUTCMonth()>e)&&c.push("new"),this.o.todayHighlight&&b.getUTCFullYear()==g.getFullYear()&&b.getUTCMonth()==g.getMonth()&&b.getUTCDate()==g.getDate()&&c.push("today"),f&&b.valueOf()==f&&c.push("active"),(b.valueOf()<this.o.startDate||b.valueOf()>this.o.endDate||-1!==a.inArray(b.getUTCDay(),this.o.daysOfWeekDisabled))&&c.push("disabled"),this.range&&(b>this.range[0]&&b<this.range[this.range.length-1]&&c.push("range"),-1!=a.inArray(b.valueOf(),this.range)&&c.push("selected")),c},fill:function(){var c,d=new Date(this.viewDate),e=d.getUTCFullYear(),f=d.getUTCMonth(),g=this.o.startDate!==-1/0?this.o.startDate.getUTCFullYear():-1/0,h=this.o.startDate!==-1/0?this.o.startDate.getUTCMonth():-1/0,i=1/0!==this.o.endDate?this.o.endDate.getUTCFullYear():1/0,j=1/0!==this.o.endDate?this.o.endDate.getUTCMonth():1/0;this.date&&this.date.valueOf(),this.picker.find(".datepicker-days thead th.datepicker-switch").text(k[this.o.language].months[f]+" "+e),this.picker.find("tfoot th.today").text(k[this.o.language].today).toggle(this.o.todayBtn!==!1),this.picker.find("tfoot th.clear").text(k[this.o.language].clear).toggle(this.o.clearBtn!==!1),this.updateNavArrows(),this.fillMonths();var m=b(e,f-1,28,0,0,0,0),n=l.getDaysInMonth(m.getUTCFullYear(),m.getUTCMonth());m.setUTCDate(n),m.setUTCDate(n-(m.getUTCDay()-this.o.weekStart+7)%7);var o=new Date(m);o.setUTCDate(o.getUTCDate()+42),o=o.valueOf();for(var p,q=[];m.valueOf()<o;){if(m.getUTCDay()==this.o.weekStart&&(q.push("<tr>"),this.o.calendarWeeks)){var r=new Date(+m+864e5*((this.o.weekStart-m.getUTCDay()-7)%7)),s=new Date(+r+864e5*((11-r.getUTCDay())%7)),t=new Date(+(t=b(s.getUTCFullYear(),0,1))+864e5*((11-t.getUTCDay())%7)),u=(s-t)/864e5/7+1;q.push('<td class="cw">'+u+"</td>")}p=this.getClassNames(m),p.push("day");var v=this.o.beforeShowDay(m);void 0===v?v={}:"boolean"==typeof v?v={enabled:v}:"string"==typeof v&&(v={classes:v}),v.enabled===!1&&p.push("disabled"),v.classes&&(p=p.concat(v.classes.split(/\s+/))),v.tooltip&&(c=v.tooltip),p=a.unique(p),q.push('<td class="'+p.join(" ")+'"'+(c?' title="'+c+'"':"")+">"+m.getUTCDate()+"</td>"),m.getUTCDay()==this.o.weekEnd&&q.push("</tr>"),m.setUTCDate(m.getUTCDate()+1)}this.picker.find(".datepicker-days tbody").empty().append(q.join(""));var w=this.date&&this.date.getUTCFullYear(),x=this.picker.find(".datepicker-months").find("th:eq(1)").text(e).end().find("span").removeClass("active");w&&w==e&&x.eq(this.date.getUTCMonth()).addClass("active"),(g>e||e>i)&&x.addClass("disabled"),e==g&&x.slice(0,h).addClass("disabled"),e==i&&x.slice(j+1).addClass("disabled"),q="",e=10*parseInt(e/10,10);var y=this.picker.find(".datepicker-years").find("th:eq(1)").text(e+"-"+(e+9)).end().find("td");e-=1;for(var z=-1;11>z;z++)q+='<span class="year'+(-1==z?" old":10==z?" new":"")+(w==e?" active":"")+(g>e||e>i?" disabled":"")+'">'+e+"</span>",e+=1;y.html(q)},updateNavArrows:function(){if(this._allow_update){var a=new Date(this.viewDate),b=a.getUTCFullYear(),c=a.getUTCMonth();switch(this.viewMode){case 0:this.o.startDate!==-1/0&&b<=this.o.startDate.getUTCFullYear()&&c<=this.o.startDate.getUTCMonth()?this.picker.find(".prev").css({visibility:"hidden"}):this.picker.find(".prev").css({visibility:"visible"}),1/0!==this.o.endDate&&b>=this.o.endDate.getUTCFullYear()&&c>=this.o.endDate.getUTCMonth()?this.picker.find(".next").css({visibility:"hidden"}):this.picker.find(".next").css({visibility:"visible"});break;case 1:case 2:this.o.startDate!==-1/0&&b<=this.o.startDate.getUTCFullYear()?this.picker.find(".prev").css({visibility:"hidden"}):this.picker.find(".prev").css({visibility:"visible"}),1/0!==this.o.endDate&&b>=this.o.endDate.getUTCFullYear()?this.picker.find(".next").css({visibility:"hidden"}):this.picker.find(".next").css({visibility:"visible"})}}},click:function(c){c.preventDefault();var d=a(c.target).closest("span, td, th");if(1==d.length)switch(d[0].nodeName.toLowerCase()){case"th":switch(d[0].className){case"datepicker-switch":this.showMode(1);break;case"prev":case"next":var e=l.modes[this.viewMode].navStep*("prev"==d[0].className?-1:1);switch(this.viewMode){case 0:this.viewDate=this.moveMonth(this.viewDate,e);break;case 1:case 2:this.viewDate=this.moveYear(this.viewDate,e)}this.fill();break;case"today":var f=new Date;f=b(f.getFullYear(),f.getMonth(),f.getDate(),0,0,0),this.showMode(-2);var g="linked"==this.o.todayBtn?null:"view";this._setDate(f,g);break;case"clear":var h;this.isInput?h=this.element:this.component&&(h=this.element.find("input")),h&&h.val("").change(),this._trigger("changeDate"),this.update(),this.o.autoclose&&this.hide()}break;case"span":if(!d.is(".disabled")){if(this.viewDate.setUTCDate(1),d.is(".month")){var i=1,j=d.parent().find("span").index(d),k=this.viewDate.getUTCFullYear();this.viewDate.setUTCMonth(j),this._trigger("changeMonth",this.viewDate),1===this.o.minViewMode&&this._setDate(b(k,j,i,0,0,0,0))}else{var k=parseInt(d.text(),10)||0,i=1,j=0;this.viewDate.setUTCFullYear(k),this._trigger("changeYear",this.viewDate),2===this.o.minViewMode&&this._setDate(b(k,j,i,0,0,0,0))}this.showMode(-1),this.fill()}break;case"td":if(d.is(".day")&&!d.is(".disabled")){var i=parseInt(d.text(),10)||1,k=this.viewDate.getUTCFullYear(),j=this.viewDate.getUTCMonth();d.is(".old")?0===j?(j=11,k-=1):j-=1:d.is(".new")&&(11==j?(j=0,k+=1):j+=1),this._setDate(b(k,j,i,0,0,0,0))}}},_setDate:function(a,b){b&&"date"!=b||(this.date=new Date(a)),b&&"view"!=b||(this.viewDate=new Date(a)),this.fill(),this.setValue(),this._trigger("changeDate");var c;this.isInput?c=this.element:this.component&&(c=this.element.find("input")),c&&(c.change(),!this.o.autoclose||b&&"date"!=b||this.hide())},moveMonth:function(a,b){if(!b)return a;var c,d,e=new Date(a.valueOf()),f=e.getUTCDate(),g=e.getUTCMonth(),h=Math.abs(b);if(b=b>0?1:-1,1==h)d=-1==b?function(){return e.getUTCMonth()==g}:function(){return e.getUTCMonth()!=c},c=g+b,e.setUTCMonth(c),(0>c||c>11)&&(c=(c+12)%12);else{for(var i=0;h>i;i++)e=this.moveMonth(e,b);c=e.getUTCMonth(),e.setUTCDate(f),d=function(){return c!=e.getUTCMonth()}}for(;d();)e.setUTCDate(--f),e.setUTCMonth(c);return e},moveYear:function(a,b){return this.moveMonth(a,12*b)},dateWithinRange:function(a){return a>=this.o.startDate&&a<=this.o.endDate},keydown:function(a){if(this.picker.is(":not(:visible)"))return 27==a.keyCode&&this.show(),void 0;var b,c,d,e=!1;switch(a.keyCode){case 27:this.hide(),a.preventDefault();break;case 37:case 39:if(!this.o.keyboardNavigation)break;b=37==a.keyCode?-1:1,a.ctrlKey?(c=this.moveYear(this.date,b),d=this.moveYear(this.viewDate,b)):a.shiftKey?(c=this.moveMonth(this.date,b),d=this.moveMonth(this.viewDate,b)):(c=new Date(this.date),c.setUTCDate(this.date.getUTCDate()+b),d=new Date(this.viewDate),d.setUTCDate(this.viewDate.getUTCDate()+b)),this.dateWithinRange(c)&&(this.date=c,this.viewDate=d,this.setValue(),this.update(),a.preventDefault(),e=!0);break;case 38:case 40:if(!this.o.keyboardNavigation)break;b=38==a.keyCode?-1:1,a.ctrlKey?(c=this.moveYear(this.date,b),d=this.moveYear(this.viewDate,b)):a.shiftKey?(c=this.moveMonth(this.date,b),d=this.moveMonth(this.viewDate,b)):(c=new Date(this.date),c.setUTCDate(this.date.getUTCDate()+7*b),d=new Date(this.viewDate),d.setUTCDate(this.viewDate.getUTCDate()+7*b)),this.dateWithinRange(c)&&(this.date=c,this.viewDate=d,this.setValue(),this.update(),a.preventDefault(),e=!0);break;case 13:this.hide(),a.preventDefault();break;case 9:this.hide()}if(e){this._trigger("changeDate");var f;this.isInput?f=this.element:this.component&&(f=this.element.find("input")),f&&f.change()}},showMode:function(a){a&&(this.viewMode=Math.max(this.o.minViewMode,Math.min(2,this.viewMode+a))),this.picker.find(">div").hide().filter(".datepicker-"+l.modes[this.viewMode].clsName).css("display","block"),this.updateNavArrows()}};var f=function(b,c){this.element=a(b),this.inputs=a.map(c.inputs,function(a){return a.jquery?a[0]:a}),delete c.inputs,a(this.inputs).datepicker(c).bind("changeDate",a.proxy(this.dateUpdated,this)),this.pickers=a.map(this.inputs,function(b){return a(b).data("datepicker")}),this.updateDates()};f.prototype={updateDates:function(){this.dates=a.map(this.pickers,function(a){return a.date}),this.updateRanges()},updateRanges:function(){var b=a.map(this.dates,function(a){return a.valueOf()});a.each(this.pickers,function(a,c){c.setRange(b)})},dateUpdated:function(b){var c=a(b.target).data("datepicker"),d=c.getUTCDate(),e=a.inArray(b.target,this.inputs),f=this.inputs.length;if(-1!=e){if(d<this.dates[e])for(;e>=0&&d<this.dates[e];)this.pickers[e--].setUTCDate(d);else if(d>this.dates[e])for(;f>e&&d>this.dates[e];)this.pickers[e++].setUTCDate(d);this.updateDates()}},remove:function(){a.map(this.pickers,function(a){a.remove()}),delete this.element.data().datepicker}};var g=a.fn.datepicker,h=a.fn.datepicker=function(b){var g=Array.apply(null,arguments);g.shift();var h;return this.each(function(){var j=a(this),k=j.data("datepicker"),l="object"==typeof b&&b;if(!k){var m=c(this,"date"),n=a.extend({},i,m,l),o=d(n.language),p=a.extend({},i,o,m,l);if(j.is(".input-daterange")||p.inputs){var q={inputs:p.inputs||j.find("input").toArray()};j.data("datepicker",k=new f(this,a.extend(p,q)))}else j.data("datepicker",k=new e(this,p))}return"string"==typeof b&&"function"==typeof k[b]&&(h=k[b].apply(k,g),void 0!==h)?!1:void 0}),void 0!==h?h:this},i=a.fn.datepicker.defaults={autoclose:!1,beforeShowDay:a.noop,calendarWeeks:!1,clearBtn:!1,daysOfWeekDisabled:[],endDate:1/0,forceParse:!0,format:"mm/dd/yyyy",keyboardNavigation:!0,language:"en",minViewMode:0,rtl:!1,startDate:-1/0,startView:0,todayBtn:!1,todayHighlight:!1,weekStart:0},j=a.fn.datepicker.locale_opts=["format","rtl","weekStart"];a.fn.datepicker.Constructor=e;var k=a.fn.datepicker.dates={en:{days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"],daysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat","Sun"],daysMin:["Su","Mo","Tu","We","Th","Fr","Sa","Su"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],today:"Today",clear:"Clear"}},l={modes:[{clsName:"days",navFnc:"Month",navStep:1},{clsName:"months",navFnc:"FullYear",navStep:1},{clsName:"years",navFnc:"FullYear",navStep:10}],isLeapYear:function(a){return 0===a%4&&0!==a%100||0===a%400
},getDaysInMonth:function(a,b){return[31,l.isLeapYear(a)?29:28,31,30,31,30,31,31,30,31,30,31][b]},validParts:/dd?|DD?|mm?|MM?|yy(?:yy)?/g,nonpunctuation:/[^ -\/:-@\[\u3400-\u9fff-`{-~\t\n\r]+/g,parseFormat:function(a){var b=a.replace(this.validParts,"\0").split("\0"),c=a.match(this.validParts);if(!b||!b.length||!c||0===c.length)throw new Error("Invalid date format.");return{separators:b,parts:c}},parseDate:function(c,d,f){if(c instanceof Date)return c;if("string"==typeof d&&(d=l.parseFormat(d)),/^[\-+]\d+[dmwy]([\s,]+[\-+]\d+[dmwy])*$/.test(c)){var g,h,i=/([\-+]\d+)([dmwy])/,j=c.match(/([\-+]\d+)([dmwy])/g);c=new Date;for(var m=0;m<j.length;m++)switch(g=i.exec(j[m]),h=parseInt(g[1]),g[2]){case"d":c.setUTCDate(c.getUTCDate()+h);break;case"m":c=e.prototype.moveMonth.call(e.prototype,c,h);break;case"w":c.setUTCDate(c.getUTCDate()+7*h);break;case"y":c=e.prototype.moveYear.call(e.prototype,c,h)}return b(c.getUTCFullYear(),c.getUTCMonth(),c.getUTCDate(),0,0,0)}var n,o,g,j=c&&c.match(this.nonpunctuation)||[],c=new Date,p={},q=["yyyy","yy","M","MM","m","mm","d","dd"],r={yyyy:function(a,b){return a.setUTCFullYear(b)},yy:function(a,b){return a.setUTCFullYear(2e3+b)},m:function(a,b){for(b-=1;0>b;)b+=12;for(b%=12,a.setUTCMonth(b);a.getUTCMonth()!=b;)a.setUTCDate(a.getUTCDate()-1);return a},d:function(a,b){return a.setUTCDate(b)}};r.M=r.MM=r.mm=r.m,r.dd=r.d,c=b(c.getFullYear(),c.getMonth(),c.getDate(),0,0,0);var s=d.parts.slice();if(j.length!=s.length&&(s=a(s).filter(function(b,c){return-1!==a.inArray(c,q)}).toArray()),j.length==s.length){for(var m=0,t=s.length;t>m;m++){if(n=parseInt(j[m],10),g=s[m],isNaN(n))switch(g){case"MM":o=a(k[f].months).filter(function(){var a=this.slice(0,j[m].length),b=j[m].slice(0,a.length);return a==b}),n=a.inArray(o[0],k[f].months)+1;break;case"M":o=a(k[f].monthsShort).filter(function(){var a=this.slice(0,j[m].length),b=j[m].slice(0,a.length);return a==b}),n=a.inArray(o[0],k[f].monthsShort)+1}p[g]=n}for(var u,m=0;m<q.length;m++)u=q[m],u in p&&!isNaN(p[u])&&r[u](c,p[u])}return c},formatDate:function(b,c,d){"string"==typeof c&&(c=l.parseFormat(c));var e={d:b.getUTCDate(),D:k[d].daysShort[b.getUTCDay()],DD:k[d].days[b.getUTCDay()],m:b.getUTCMonth()+1,M:k[d].monthsShort[b.getUTCMonth()],MM:k[d].months[b.getUTCMonth()],yy:b.getUTCFullYear().toString().substring(2),yyyy:b.getUTCFullYear()};e.dd=(e.d<10?"0":"")+e.d,e.mm=(e.m<10?"0":"")+e.m;for(var b=[],f=a.extend([],c.separators),g=0,h=c.parts.length;h>=g;g++)f.length&&b.push(f.shift()),b.push(e[c.parts[g]]);return b.join("")},headTemplate:'<thead><tr><th class="prev"><i class="icon-arrow-left"/></th><th colspan="5" class="datepicker-switch"></th><th class="next"><i class="icon-arrow-right"/></th></tr></thead>',contTemplate:'<tbody><tr><td colspan="7"></td></tr></tbody>',footTemplate:'<tfoot><tr><th colspan="7" class="today"></th></tr><tr><th colspan="7" class="clear"></th></tr></tfoot>'};l.template='<div class="datepicker"><div class="datepicker-days"><table class=" table-condensed">'+l.headTemplate+"<tbody></tbody>"+l.footTemplate+"</table>"+"</div>"+'<div class="datepicker-months">'+'<table class="table-condensed">'+l.headTemplate+l.contTemplate+l.footTemplate+"</table>"+"</div>"+'<div class="datepicker-years">'+'<table class="table-condensed">'+l.headTemplate+l.contTemplate+l.footTemplate+"</table>"+"</div>"+"</div>",a.fn.datepicker.DPGlobal=l,a.fn.datepicker.noConflict=function(){return a.fn.datepicker=g,this},a(document).on("focus.datepicker.data-api click.datepicker.data-api",'[data-provide="datepicker"]',function(b){var c=a(this);c.data("datepicker")||(b.preventDefault(),h.call(c,"show"))}),a(function(){h.call(a('[data-provide="datepicker-inline"]'))})}(window.jQuery),function(a){"use strict";a.fn.bdatepicker=a.fn.datepicker.noConflict(),a.fn.datepicker||(a.fn.datepicker=a.fn.bdatepicker);var b=function(a){this.init("date",a,b.defaults),this.initPicker(a,b.defaults)};a.fn.editableutils.inherit(b,a.fn.editabletypes.abstractinput),a.extend(b.prototype,{initPicker:function(b,c){this.options.viewformat||(this.options.viewformat=this.options.format),b.datepicker=a.fn.editableutils.tryParseJson(b.datepicker,!0),this.options.datepicker=a.extend({},c.datepicker,b.datepicker,{format:this.options.viewformat}),this.options.datepicker.language=this.options.datepicker.language||"en",this.dpg=a.fn.bdatepicker.DPGlobal,this.parsedFormat=this.dpg.parseFormat(this.options.format),this.parsedViewFormat=this.dpg.parseFormat(this.options.viewformat)},render:function(){this.$input.bdatepicker(this.options.datepicker),this.options.clear&&(this.$clear=a('<a href="#"></a>').html(this.options.clear).click(a.proxy(function(a){a.preventDefault(),a.stopPropagation(),this.clear()},this)),this.$tpl.parent().append(a('<div class="editable-clear">').append(this.$clear)))},value2html:function(a,c){var d=a?this.dpg.formatDate(a,this.parsedViewFormat,this.options.datepicker.language):"";b.superclass.value2html.call(this,d,c)},html2value:function(a){return this.parseDate(a,this.parsedViewFormat)},value2str:function(a){return a?this.dpg.formatDate(a,this.parsedFormat,this.options.datepicker.language):""},str2value:function(a){return this.parseDate(a,this.parsedFormat)},value2submit:function(a){return this.value2str(a)},value2input:function(a){this.$input.bdatepicker("update",a)},input2value:function(){return this.$input.data("datepicker").date},activate:function(){},clear:function(){this.$input.data("datepicker").date=null,this.$input.find(".active").removeClass("active"),this.options.showbuttons||this.$input.closest("form").submit()},autosubmit:function(){this.$input.on("mouseup",".day",function(b){if(!a(b.currentTarget).is(".old")&&!a(b.currentTarget).is(".new")){var c=a(this).closest("form");setTimeout(function(){c.submit()},200)}})},parseDate:function(a,b){var c,d=null;return a&&(d=this.dpg.parseDate(a,b,this.options.datepicker.language),"string"==typeof a&&(c=this.dpg.formatDate(d,b,this.options.datepicker.language),a!==c&&(d=null))),d}}),b.defaults=a.extend({},a.fn.editabletypes.abstractinput.defaults,{tpl:'<div class="editable-date well"></div>',inputclass:null,format:"yyyy-mm-dd",viewformat:null,datepicker:{weekStart:0,startView:0,minViewMode:0,autoclose:!1},clear:"&times; clear"}),a.fn.editabletypes.date=b}(window.jQuery),function(a){"use strict";var b=function(a){this.init("datefield",a,b.defaults),this.initPicker(a,b.defaults)};a.fn.editableutils.inherit(b,a.fn.editabletypes.date),a.extend(b.prototype,{render:function(){this.$input=this.$tpl.find("input"),this.setClass(),this.setAttr("placeholder"),this.$tpl.bdatepicker(this.options.datepicker),this.$input.off("focus keydown"),this.$input.keyup(a.proxy(function(){this.$tpl.removeData("date"),this.$tpl.bdatepicker("update")},this))},value2input:function(a){this.$input.val(a?this.dpg.formatDate(a,this.parsedViewFormat,this.options.datepicker.language):""),this.$tpl.bdatepicker("update")},input2value:function(){return this.html2value(this.$input.val())},activate:function(){a.fn.editabletypes.text.prototype.activate.call(this)},autosubmit:function(){}}),b.defaults=a.extend({},a.fn.editabletypes.date.defaults,{tpl:'<div class="input-append date"><input type="text"/><span class="add-on"><i class="icon-th"></i></span></div>',inputclass:"input-small",datepicker:{weekStart:0,startView:0,minViewMode:0,autoclose:!0}}),a.fn.editabletypes.datefield=b}(window.jQuery),function(a){"use strict";var b=function(a){this.init("datetime",a,b.defaults),this.initPicker(a,b.defaults)};a.fn.editableutils.inherit(b,a.fn.editabletypes.abstractinput),a.extend(b.prototype,{initPicker:function(b,c){this.options.viewformat||(this.options.viewformat=this.options.format),b.datetimepicker=a.fn.editableutils.tryParseJson(b.datetimepicker,!0),this.options.datetimepicker=a.extend({},c.datetimepicker,b.datetimepicker,{format:this.options.viewformat}),this.options.datetimepicker.language=this.options.datetimepicker.language||"en",this.dpg=a.fn.datetimepicker.DPGlobal,this.parsedFormat=this.dpg.parseFormat(this.options.format,this.options.formatType),this.parsedViewFormat=this.dpg.parseFormat(this.options.viewformat,this.options.formatType)},render:function(){this.$input.datetimepicker(this.options.datetimepicker),this.$input.on("changeMode",function(){var b=a(this).closest("form").parent();setTimeout(function(){b.triggerHandler("resize")},0)}),this.options.clear&&(this.$clear=a('<a href="#"></a>').html(this.options.clear).click(a.proxy(function(a){a.preventDefault(),a.stopPropagation(),this.clear()},this)),this.$tpl.parent().append(a('<div class="editable-clear">').append(this.$clear)))},value2html:function(a,c){var d=a?this.dpg.formatDate(this.toUTC(a),this.parsedViewFormat,this.options.datetimepicker.language,this.options.formatType):"";return c?(b.superclass.value2html.call(this,d,c),void 0):d},html2value:function(a){var b=this.parseDate(a,this.parsedViewFormat);return b?this.fromUTC(b):null},value2str:function(a){return a?this.dpg.formatDate(this.toUTC(a),this.parsedFormat,this.options.datetimepicker.language,this.options.formatType):""},str2value:function(a){var b=this.parseDate(a,this.parsedFormat);return b?this.fromUTC(b):null},value2submit:function(a){return this.value2str(a)},value2input:function(a){a&&this.$input.data("datetimepicker").setDate(a)},input2value:function(){var a=this.$input.data("datetimepicker");return a.date?a.getDate():null},activate:function(){},clear:function(){this.$input.data("datetimepicker").date=null,this.$input.find(".active").removeClass("active"),this.options.showbuttons||this.$input.closest("form").submit()},autosubmit:function(){this.$input.on("mouseup",".minute",function(){var b=a(this).closest("form");setTimeout(function(){b.submit()},200)})},toUTC:function(a){return a?new Date(a.valueOf()-6e4*a.getTimezoneOffset()):a},fromUTC:function(a){return a?new Date(a.valueOf()+6e4*a.getTimezoneOffset()):a},parseDate:function(a,b){var c,d=null;return a&&(d=this.dpg.parseDate(a,b,this.options.datetimepicker.language,this.options.formatType),"string"==typeof a&&(c=this.dpg.formatDate(d,b,this.options.datetimepicker.language,this.options.formatType),a!==c&&(d=null))),d}}),b.defaults=a.extend({},a.fn.editabletypes.abstractinput.defaults,{tpl:'<div class="editable-date well"></div>',inputclass:null,format:"yyyy-mm-dd hh:ii",formatType:"standard",viewformat:null,datetimepicker:{todayHighlight:!1,autoclose:!1},clear:"&times; clear"}),a.fn.editabletypes.datetime=b}(window.jQuery),function(a){"use strict";var b=function(a){this.init("datetimefield",a,b.defaults),this.initPicker(a,b.defaults)};a.fn.editableutils.inherit(b,a.fn.editabletypes.datetime),a.extend(b.prototype,{render:function(){this.$input=this.$tpl.find("input"),this.setClass(),this.setAttr("placeholder"),this.$tpl.datetimepicker(this.options.datetimepicker),this.$input.off("focus keydown"),this.$input.keyup(a.proxy(function(){this.$tpl.removeData("date"),this.$tpl.datetimepicker("update")},this))},value2input:function(a){this.$input.val(this.value2html(a)),this.$tpl.datetimepicker("update")},input2value:function(){return this.html2value(this.$input.val())},activate:function(){a.fn.editabletypes.text.prototype.activate.call(this)},autosubmit:function(){}}),b.defaults=a.extend({},a.fn.editabletypes.datetime.defaults,{tpl:'<div class="input-append date"><input type="text"/><span class="add-on"><i class="icon-th"></i></span></div>',inputclass:"input-medium",datetimepicker:{todayHighlight:!1,autoclose:!0}}),a.fn.editabletypes.datetimefield=b}(window.jQuery);
\ No newline at end of file
...@@ -13,6 +13,11 @@ ...@@ -13,6 +13,11 @@
<link href="{{ admin_static.url(filename='bootstrap/bootstrap2/css/bootstrap.css') }}" rel="stylesheet"> <link href="{{ admin_static.url(filename='bootstrap/bootstrap2/css/bootstrap.css') }}" rel="stylesheet">
<link href="{{ admin_static.url(filename='bootstrap/bootstrap2/css/bootstrap-responsive.css') }}" rel="stylesheet"> <link href="{{ admin_static.url(filename='bootstrap/bootstrap2/css/bootstrap-responsive.css') }}" rel="stylesheet">
<link href="{{ admin_static.url(filename='admin/css/bootstrap2/admin.css') }}" rel="stylesheet"> <link href="{{ admin_static.url(filename='admin/css/bootstrap2/admin.css') }}" rel="stylesheet">
<style>
body {
padding-top: 4px;
}
</style>
{% endblock %} {% endblock %}
{% block head %} {% block head %}
{% endblock %} {% endblock %}
......
...@@ -32,8 +32,8 @@ ...@@ -32,8 +32,8 @@
</th> </th>
{% endif %} {% endif %}
<th class="span1">&nbsp;</th> <th class="span1">&nbsp;</th>
<th>Name</th> <th>{{ _gettext('Name') }}</th>
<th>Size</th> <th>{{ _gettext('Size') }}</th>
{% endblock %} {% endblock %}
</tr> </tr>
</thead> </thead>
...@@ -58,7 +58,8 @@ ...@@ -58,7 +58,8 @@
{% if is_dir %} {% if is_dir %}
{% if name != '..' and admin_view.can_delete_dirs %} {% if name != '..' and admin_view.can_delete_dirs %}
<form class="icon" method="POST" action="{{ get_url('.delete') }}"> <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) }}')"> <button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\' recursively?', name=name) }}')">
<i class="icon-remove"></i> <i class="icon-remove"></i>
</button> </button>
...@@ -66,7 +67,8 @@ ...@@ -66,7 +67,8 @@
{% endif %} {% endif %}
{% else %} {% else %}
<form class="icon" method="POST" action="{{ get_url('.delete') }}"> <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) }}')"> <button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\'?', name=name) }}')">
<i class="icon-remove"></i> <i class="icon-remove"></i>
</button> </button>
......
{% macro menu_icon(item) -%} {% macro menu_icon(item) -%}
{% set icon_type = item.get_icon_type() %} {% set icon_type = item.get_icon_type() %}
{% if icon_type %} {%- if icon_type %}
{% set icon_value = item.get_icon_value() %} {% set icon_value = item.get_icon_value() %}
{% if icon_type == 'glyph' %} {% if icon_type == 'glyph' %}
<i class="{{ icon_value }}"></i> <i class="{{ icon_value }}"></i>
...@@ -13,45 +13,43 @@ ...@@ -13,45 +13,43 @@
{%- endmacro %} {%- endmacro %}
{% macro menu() %} {% macro menu() %}
{% for item in admin_view.admin.menu() %} {%- for item in admin_view.admin.menu() %}
{% if item.is_category() %} {%- if item.is_category() -%}
{% set children = item.get_children() %} {% set children = item.get_children() %}
{% if children %} {%- if children %}
{% set class_name = item.get_class_name() %} {% 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 %}"> <li class="active dropdown{% if class_name %} {{class_name}}{% endif %}">
{% else %} {% else -%}
<li class="dropdown{% if class_name %} {{class_name}}{% endif %}"> <li class="dropdown{% if class_name %} {{class_name}}{% endif %}">
{% endif %} {%- endif %}
<a class="dropdown-toggle" data-toggle="dropdown" href="javascript:void(0)"> <a class="dropdown-toggle" data-toggle="dropdown" href="javascript:void(0)">{{ menu_icon(item) }}{{ item.name }}<b class="caret"></b></a>
{{ menu_icon(item) }}{{ item.name }}<b class="caret"></b>
</a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
{% for child in children %} {%- for child in children -%}
{% set class_name = child.get_class_name() %} {% 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 %}"> <li class="active{% if class_name %} {{class_name}}{% endif %}">
{% else %} {% else %}
<li{% if class_name %} class="{{class_name}}"{% endif %}> <li{% if class_name %} class="{{class_name}}"{% endif %}>
{% endif %} {%- endif %}
<a href="{{ child.get_url() }}">{{ menu_icon(child) }}{{ child.name }}</a> <a href="{{ child.get_url() }}">{{ menu_icon(child) }}{{ child.name }}</a>
</li> </li>
{% endfor %} {%- endfor %}
</ul> </ul>
</li> </li>
{% endif %} {% endif %}
{% else %} {%- else %}
{% if item.is_accessible() and item.is_visible() %} {%- if item.is_accessible() and item.is_visible() -%}
{% set class_name = item.get_class_name() %} {% 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 %}"> <li class="active{% if class_name %} {{class_name}}{% endif %}">
{% else %} {%- else %}
<li{% if class_name %} class="{{class_name}}"{% endif %}> <li{% if class_name %} class="{{class_name}}"{% endif %}>
{% endif %} {%- endif %}
<a href="{{ item.get_url() }}">{{ menu_icon(item) }}{{ item.name }}</a> <a href="{{ item.get_url() }}">{{ menu_icon(item) }}{{ item.name }}</a>
</li> </li>
{% endif %} {%- endif -%}
{% endif %} {% endif -%}
{% endfor %} {% endfor %}
{% endmacro %} {% endmacro %}
......
...@@ -143,7 +143,7 @@ ...@@ -143,7 +143,7 @@
{% endmacro %} {% endmacro %}
{% macro form_tag(form=None) %} {% macro form_tag(form=None) %}
<form action="" method="POST" class="form-horizontal" enctype="multipart/form-data"> <form action="" method="POST" class="admin-form form-horizontal" enctype="multipart/form-data">
<fieldset> <fieldset>
{{ caller() }} {{ caller() }}
</fieldset> </fieldset>
...@@ -158,7 +158,7 @@ ...@@ -158,7 +158,7 @@
{{ extra }} {{ extra }}
{% endif %} {% endif %}
{% if cancel_url %} {% if cancel_url %}
<a href="{{ cancel_url }}" class="btn btn-large">{{ _gettext('Cancel') }}</a> <a href="{{ cancel_url }}" class="btn btn-large btn-danger">{{ _gettext('Cancel') }}</a>
{% endif %} {% endif %}
</div> </div>
</div> </div>
...@@ -178,6 +178,9 @@ ...@@ -178,6 +178,9 @@
<link href="{{ admin_static.url(filename='vendor/leaflet/leaflet.css') }}" rel="stylesheet"> <link href="{{ admin_static.url(filename='vendor/leaflet/leaflet.css') }}" rel="stylesheet">
<link href="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.css') }}" rel="stylesheet"> <link href="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.css') }}" rel="stylesheet">
{% endif %} {% endif %}
{% if editable_columns %}
<link href="{{ admin_static.url(filename='vendor/x-editable/css/bootstrap2-editable-1.5.1.css') }}" rel="stylesheet">
{% endif %}
{% endmacro %} {% endmacro %}
{% macro form_js() %} {% macro form_js() %}
...@@ -189,5 +192,8 @@ ...@@ -189,5 +192,8 @@
<script src="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.js') }}"></script> <script src="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.js') }}"></script>
{% endif %} {% endif %}
<script src="{{ admin_static.url(filename='vendor/bootstrap-daterangepicker/daterangepicker.js') }}"></script> <script src="{{ admin_static.url(filename='vendor/bootstrap-daterangepicker/daterangepicker.js') }}"></script>
{% if editable_columns %}
<script src="{{ admin_static.url(filename='vendor/x-editable/js/bootstrap2-editable-1.5.1.min.js') }}"></script>
{% endif %}
<script src="{{ admin_static.url(filename='admin/js/form-1.0.0.js') }}"></script> <script src="{{ admin_static.url(filename='admin/js/form-1.0.0.js') }}"></script>
{% endmacro %} {% endmacro %}
{% macro render_inline_fields(field, template, render, check=None) %} {% 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"> <div class="inline-field-list">
{% for subfield in field %} {% for subfield in field %}
<div id="{{ subfield.id }}" class="inline-field"> <div id="{{ subfield.id }}" class="inline-field well well-small">
{%- if not check or check(subfield) %} {%- if not check or check(subfield) %}
<legend>
{{ field.label.text }} #{{ loop.index }}
<div class="pull-right">
{% if subfield.get_pk and subfield.get_pk() %} {% if subfield.get_pk and subfield.get_pk() %}
<div class="inline-field-control">
<input type="checkbox" name="del-{{ subfield.id }}" id="del-{{ subfield.id }}" /> <input type="checkbox" name="del-{{ subfield.id }}" id="del-{{ subfield.id }}" />
<label for="del-{{ subfield.id }}" style="display: inline">{{ _gettext('Delete?') }}</label> <label for="del-{{ subfield.id }}" style="display: inline">{{ _gettext('Delete?') }}</label>
</div>
{% else %} {% else %}
<div class="inline-field-control">
<a href="javascript:void(0)" class="inline-remove-field"><i class="icon-remove"></i></a> <a href="javascript:void(0)" class="inline-remove-field"><i class="icon-remove"></i></a>
</div>
{% endif %} {% endif %}
</div>
</legend>
{%- endif -%} {%- endif -%}
{{ render(subfield) }} {{ render(subfield) }}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{# template for new inline form fields #}
<div class="inline-field-template hide"> <div class="inline-field-template hide">
{% filter forceescape %} {% filter forceescape %}
<div class="inline-field"> <div class="inline-field well well-small">
<div class="inline-field-control"> <legend>
New {{ field.label.text }}
<div class="pull-right">
<a href="javascript:void(0)" class="inline-remove-field"><i class="icon-remove"></i></a> <a href="javascript:void(0)" class="inline-remove-field"><i class="icon-remove"></i></a>
</div> </div>
</legend>
{{ render(template) }} {{ render(template) }}
</div> </div>
{% endfilter %} {% endfilter %}
</div> </div>
<a id="{{ field.id }}-button" href="javascript:void(0)" class="btn" onclick="faForm.addInlineField(this, '{{ field.id }}');">{{ _gettext('Add') }} {{ field.label.text }}</a> <a id="{{ field.id }}-button" href="javascript:void(0)" class="btn" onclick="faForm.addInlineField(this, '{{ field.id }}');">{{ _gettext('Add') }} {{ field.label.text }}</a>
</div> </div>
{% endmacro %} {% endmacro %}
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
{% block body %} {% block body %}
{% block model_menu_bar %} {% block model_menu_bar %}
<ul class="nav nav-tabs"> <ul class="nav nav-tabs actions-nav">
<li class="active"> <li class="active">
<a href="javascript:void(0)">{{ _gettext('List') }} ({{ count }})</a> <a href="javascript:void(0)">{{ _gettext('List') }} ({{ count }})</a>
</li> </li>
...@@ -107,10 +107,10 @@ ...@@ -107,10 +107,10 @@
</a> </a>
{%- endif -%} {%- endif -%}
{%- if admin_view.can_delete -%} {%- if admin_view.can_delete -%}
<form class="icon" method="POST" action="{{ get_url('.delete_view', id=get_pk_value(row), url=return_url) }}"> <form class="icon" method="POST" action="{{ get_url('.delete_view') }}">
{% if csrf_token %} {{ delete_form.id(value=get_pk_value(row)) }}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/> {{ delete_form.url(value=return_url) }}
{% endif %} {{ delete_form.csrf_token }}
<button onclick="return confirm('{{ _gettext('Are you sure you want to delete this record?') }}');" title="{{ _gettext('Delete record') }}"> <button onclick="return confirm('{{ _gettext('Are you sure you want to delete this record?') }}');" title="{{ _gettext('Delete record') }}">
<i class="icon-trash"></i> <i class="icon-trash"></i>
</button> </button>
...@@ -119,8 +119,17 @@ ...@@ -119,8 +119,17 @@
{% endblock %} {% endblock %}
</td> </td>
{% endblock %} {% endblock %}
{% for c, name in list_columns %} {% for c, name in list_columns %}
{% if admin_view.is_editable(c) %}
{% if form.csrf_token %}
<td>{{ form[c](pk=get_pk_value(row), value=get_value(row, c), csrf=form.csrf_token._value()) }}</td>
{% else %}
<td>{{ form[c](pk=get_pk_value(row), value=get_value(row, c)) }}</td>
{% endif %}
{% else %}
<td>{{ get_value(row, c) }}</td> <td>{{ get_value(row, c) }}</td>
{% endif %}
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}
</tr> </tr>
......
...@@ -15,6 +15,11 @@ ...@@ -15,6 +15,11 @@
<link href="{{ admin_static.url(filename='bootstrap/bootstrap3/css/bootstrap.min.css') }}" rel="stylesheet"> <link href="{{ admin_static.url(filename='bootstrap/bootstrap3/css/bootstrap.min.css') }}" rel="stylesheet">
<link href="{{ admin_static.url(filename='bootstrap/bootstrap3/css/bootstrap-theme.min.css') }}" rel="stylesheet"> <link href="{{ admin_static.url(filename='bootstrap/bootstrap3/css/bootstrap-theme.min.css') }}" rel="stylesheet">
<link href="{{ admin_static.url(filename='admin/css/bootstrap3/admin.css') }}" rel="stylesheet"> <link href="{{ admin_static.url(filename='admin/css/bootstrap3/admin.css') }}" rel="stylesheet">
<style>
body {
padding-top: 4px;
}
</style>
{% endblock %} {% endblock %}
{% block head %} {% block head %}
{% endblock %} {% endblock %}
......
...@@ -32,8 +32,8 @@ ...@@ -32,8 +32,8 @@
</th> </th>
{% endif %} {% endif %}
<th class="col-md-1">&nbsp;</th> <th class="col-md-1">&nbsp;</th>
<th>Name</th> <th>{{ _gettext('Name') }}</th>
<th>Size</th> <th>{{ _gettext('Size') }}</th>
{% endblock %} {% endblock %}
</tr> </tr>
</thead> </thead>
...@@ -58,7 +58,8 @@ ...@@ -58,7 +58,8 @@
{% if is_dir %} {% if is_dir %}
{% if name != '..' and admin_view.can_delete_dirs %} {% if name != '..' and admin_view.can_delete_dirs %}
<form class="icon" method="POST" action="{{ get_url('.delete') }}"> <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) }}')"> <button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\' recursively?', name=name) }}')">
<i class="glyphicon glyphicon-remove"></i> <i class="glyphicon glyphicon-remove"></i>
</button> </button>
...@@ -66,7 +67,8 @@ ...@@ -66,7 +67,8 @@
{% endif %} {% endif %}
{% else %} {% else %}
<form class="icon" method="POST" action="{{ get_url('.delete') }}"> <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) }}')"> <button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\'?', name=name) }}')">
<i class="glyphicon glyphicon-trash"></i> <i class="glyphicon glyphicon-trash"></i>
</button> </button>
......
{% macro menu_icon(item) -%} {% macro menu_icon(item) -%}
{% set icon_type = item.get_icon_type() %} {% set icon_type = item.get_icon_type() %}
{% if icon_type %} {%- if icon_type %}
{% set icon_value = item.get_icon_value() %} {% set icon_value = item.get_icon_value() %}
{% if icon_type == 'glyph' %} {% if icon_type == 'glyph' %}
<i class="glyphicon {{ icon_value }}"></i> <i class="glyphicon {{ icon_value }}"></i>
...@@ -13,42 +13,43 @@ ...@@ -13,42 +13,43 @@
{%- endmacro %} {%- endmacro %}
{% macro menu() %} {% macro menu() %}
{% for item in admin_view.admin.menu() %} {%- for item in admin_view.admin.menu() %}
{% if item.is_category() %} {%- if item.is_category() -%}
{% set children = item.get_children() %} {% set children = item.get_children() %}
{% if children %} {%- if children %}
{% if item.is_active(admin_view) %} {% set class_name = item.get_class_name() %}
{%- if item.is_active(admin_view) %}
<li class="active dropdown{% if class_name %} {{class_name}}{% endif %}"> <li class="active dropdown{% if class_name %} {{class_name}}{% endif %}">
{% else %} {% else -%}
<li class="dropdown{% if class_name %} {{class_name}}{% endif %}"> <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> <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"> <ul class="dropdown-menu">
{% for child in children %} {%- for child in children -%}
{% set class_name = child.get_class_name() %} {% 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 %}> <li class="active{% if class_name %} {{class_name}}{% endif %}">
{% else %} {% else %}
<li{% if class_name %} class="{{class_name}}"{% endif %}> <li{% if class_name %} class="{{class_name}}"{% endif %}>
{% endif %} {%- endif %}
<a href="{{ child.get_url() }}">{{ menu_icon(child) }}{{ child.name }}</a> <a href="{{ child.get_url() }}">{{ menu_icon(child) }}{{ child.name }}</a>
</li> </li>
{% endfor %} {%- endfor %}
</ul> </ul>
</li> </li>
{% endif %} {% endif %}
{% else %} {%- else %}
{% if item.is_accessible() and item.is_visible() %} {%- if item.is_accessible() and item.is_visible() -%}
{% set class_name = item.get_class_name() %} {% 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 %}> <li class="active{% if class_name %} {{class_name}}{% endif %}">
{% else %} {%- else %}
<li{% if class_name %} class="{{class_name}}"{% endif %}> <li{% if class_name %} class="{{class_name}}"{% endif %}>
{% endif %} {%- endif %}
<a href="{{ item.get_url() }}">{{ menu_icon(item) }}{{ item.name }}</a> <a href="{{ item.get_url() }}">{{ menu_icon(item) }}{{ item.name }}</a>
</li> </li>
{% endif %} {%- endif -%}
{% endif %} {% endif -%}
{% endfor %} {% endfor %}
{% endmacro %} {% endmacro %}
......
...@@ -88,10 +88,10 @@ ...@@ -88,10 +88,10 @@
<div class="{{ kwargs.get('column_class', 'col-md-10') }}"> <div class="{{ kwargs.get('column_class', 'col-md-10') }}">
{% set _dummy = kwargs.setdefault('class', 'form-control') %} {% set _dummy = kwargs.setdefault('class', 'form-control') %}
{{ field(**kwargs)|safe }} {{ field(**kwargs)|safe }}
</div>
{% if field.description %} {% if field.description %}
<span class="help-block">{{ field.description }}</span> <p class="help-block">{{ field.description }}</p>
{% endif %} {% endif %}
</div>
{% if direct_error %} {% if direct_error %}
<ul{% if direct_error %} class="input-errors"{% endif %}> <ul{% if direct_error %} class="input-errors"{% endif %}>
{% for e in field.errors if e is string %} {% for e in field.errors if e is string %}
...@@ -138,12 +138,13 @@ ...@@ -138,12 +138,13 @@
{% endmacro %} {% endmacro %}
{% macro form_tag(form=None, action=None) %} {% macro form_tag(form=None, action=None) %}
<form action="{{ action or '' }}" method="POST" role="form" class="form-horizontal" enctype="multipart/form-data"> <form action="{{ action or '' }}" method="POST" role="form" class="admin-form form-horizontal" enctype="multipart/form-data">
{{ caller() }} {{ caller() }}
</form> </form>
{% endmacro %} {% endmacro %}
{% macro render_form_buttons(cancel_url, extra=None) %} {% macro render_form_buttons(cancel_url, extra=None) %}
<hr>
<div class="form-group"> <div class="form-group">
<div class="col-md-offset-2 col-md-10 submit-row"> <div class="col-md-offset-2 col-md-10 submit-row">
<input type="submit" class="btn btn-primary" value="{{ _gettext('Submit') }}" /> <input type="submit" class="btn btn-primary" value="{{ _gettext('Submit') }}" />
...@@ -151,7 +152,7 @@ ...@@ -151,7 +152,7 @@
{{ extra }} {{ extra }}
{% endif %} {% endif %}
{% if cancel_url %} {% if cancel_url %}
<a href="{{ cancel_url }}" class="btn btn-cancel" role="button">{{ _gettext('Cancel') }}</a> <a href="{{ cancel_url }}" class="btn btn-danger" role="button">{{ _gettext('Cancel') }}</a>
{% endif %} {% endif %}
</div> </div>
</div> </div>
...@@ -172,6 +173,9 @@ ...@@ -172,6 +173,9 @@
<link href="{{ admin_static.url(filename='vendor/leaflet/leaflet.css') }}" rel="stylesheet"> <link href="{{ admin_static.url(filename='vendor/leaflet/leaflet.css') }}" rel="stylesheet">
<link href="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.css') }}" rel="stylesheet"> <link href="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.css') }}" rel="stylesheet">
{% endif %} {% endif %}
{% if editable_columns %}
<link href="{{ admin_static.url(filename='vendor/x-editable/css/bootstrap3-editable-1.5.1.css') }}" rel="stylesheet">
{% endif %}
{% endmacro %} {% endmacro %}
{% macro form_js() %} {% macro form_js() %}
...@@ -183,5 +187,8 @@ ...@@ -183,5 +187,8 @@
<script src="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.js') }}"></script> <script src="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.js') }}"></script>
{% endif %} {% endif %}
<script src="{{ admin_static.url(filename='vendor/bootstrap-daterangepicker/daterangepicker.js') }}"></script> <script src="{{ admin_static.url(filename='vendor/bootstrap-daterangepicker/daterangepicker.js') }}"></script>
{% if editable_columns %}
<script src="{{ admin_static.url(filename='vendor/x-editable/js/bootstrap3-editable-1.5.1.min.js') }}"></script>
{% endif %}
<script src="{{ admin_static.url(filename='admin/js/form-1.0.0.js') }}"></script> <script src="{{ admin_static.url(filename='admin/js/form-1.0.0.js') }}"></script>
{% endmacro %} {% endmacro %}
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
{% import 'admin/lib.html' as lib with context %} {% import 'admin/lib.html' as lib with context %}
{% macro extra() %} {% macro extra() %}
<input name="_continue_editing" type="submit" class="btn" value="{{ _gettext('Save and Continue') }}" /> <input name="_continue_editing" type="submit" class="btn btn-default" value="{{ _gettext('Save and Continue') }}" />
{% endmacro %} {% endmacro %}
{% block head %} {% block head %}
......
{% macro render_inline_fields(field, template, render, check=None) %} {% 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"> <div class="inline-field-list">
{% for subfield in field %} {% for subfield in field %}
<div id="{{ subfield.id }}" class="inline-field well"> <div id="{{ subfield.id }}" class="inline-field well well-sm">
{%- if not check or check(subfield) %} {%- if not check or check(subfield) %}
<legend>
<small>
{{ field.label.text }} #{{ loop.index }}
<div class="pull-right">
{% if subfield.get_pk and subfield.get_pk() %} {% if subfield.get_pk and subfield.get_pk() %}
<div class="inline-field-control">
<input type="checkbox" name="del-{{ subfield.id }}" id="del-{{ subfield.id }}" /> <input type="checkbox" name="del-{{ subfield.id }}" id="del-{{ subfield.id }}" />
<label for="del-{{ subfield.id }}" style="display: inline">{{ _gettext('Delete?') }}</label> <label for="del-{{ subfield.id }}" style="display: inline">{{ _gettext('Delete?') }}</label>
</div>
{% else %} {% else %}
<div class="inline-field-control">
<a href="javascript:void(0)" class="inline-remove-field"><i class="glyphicon glyphicon-remove"></i></a> <a href="javascript:void(0)" class="inline-remove-field"><i class="glyphicon glyphicon-remove"></i></a>
</div>
{% endif %} {% endif %}
</div>
</small>
</legend>
<div class='clearfix'></div>
{%- endif -%} {%- endif -%}
{{ render(subfield) }} {{ render(subfield) }}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{# template for new inline form fields #}
<div class="inline-field-template hide"> <div class="inline-field-template hide">
{% filter forceescape %} {% filter forceescape %}
<div class="inline-field panel panel-info"> <div class="inline-field well well-sm">
<div class="inline-field-control"> <legend>
<small>New {{ field.label.text }}</small>
<div class="pull-right">
<a href="javascript:void(0)" class="inline-remove-field"><span class="glyphicon glyphicon-remove"></span></a> <a href="javascript:void(0)" class="inline-remove-field"><span class="glyphicon glyphicon-remove"></span></a>
</div> </div>
</legend>
<div class='clearfix'></div>
{{ render(template) }} {{ render(template) }}
</div> </div>
{% endfilter %} {% endfilter %}
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
<div class="pull-right"> <div class="pull-right">
<button type="submit" class="btn btn-primary" style="display: none">{{ _gettext('Apply') }}</button> <button type="submit" class="btn btn-primary" style="display: none">{{ _gettext('Apply') }}</button>
{% if active_filters %} {% if active_filters %}
<a href="{{ clear_search_url }}" class="btn btn-link">{{ _gettext('Reset Filters') }}</a> <a href="{{ clear_search_url }}" class="btn btn-default">{{ _gettext('Reset Filters') }}</a>
{% endif %} {% endif %}
</div> </div>
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
<div class="clearfix"></div> <div class="clearfix"></div>
{% endmacro %} {% endmacro %}
{% macro search_form(input_class="span2") %} {% macro search_form(input_class="col-md-2") %}
<form method="GET" action="{{ return_url }}" class="navbar-form navbar-left" role="search"> <form method="GET" action="{{ return_url }}" class="navbar-form navbar-left" role="search">
{% if sort_column is not none %} {% if sort_column is not none %}
<input type="hidden" name="sort" value="{{ sort_column }}"> <input type="hidden" name="sort" value="{{ sort_column }}">
...@@ -34,11 +34,9 @@ ...@@ -34,11 +34,9 @@
<input type="hidden" name="desc" value="{{ sort_desc }}"> <input type="hidden" name="desc" value="{{ sort_desc }}">
{% endif %} {% endif %}
{% if search %} {% if search %}
<div class="input-append form-group"> <div class="input-group">
<input type="text" name="search" value="{{ search }}" class="{{ input_class }} form-control" placeholder="{{ _gettext('Search') }}"> <input type="text" name="search" value="{{ search }}" class="{{ input_class }} form-control" placeholder="{{ _gettext('Search') }}">
<a href="{{ clear_search_url }}" class="clear add-on"> <a href="{{ clear_search_url }}" class="input-group-addon clear"><span class="glyphicon glyphicon-remove"></span></a>
<span class="glyphicon glyphicon-remove"></span>
</a>
</div> </div>
{% else %} {% else %}
<div class="form-group"> <div class="form-group">
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
{% block body %} {% block body %}
{% block model_menu_bar %} {% block model_menu_bar %}
<ul class="nav nav-tabs"> <ul class="nav nav-tabs actions-nav">
<li class="active"> <li class="active">
<a href="javascript:void(0)">{{ _gettext('List') }} ({{ count }})</a> <a href="javascript:void(0)">{{ _gettext('List') }} ({{ count }})</a>
</li> </li>
...@@ -107,10 +107,10 @@ ...@@ -107,10 +107,10 @@
</a> </a>
{%- endif -%} {%- endif -%}
{%- if admin_view.can_delete -%} {%- if admin_view.can_delete -%}
<form class="icon" method="POST" action="{{ get_url('.delete_view', id=get_pk_value(row), url=return_url) }}"> <form class="icon" method="POST" action="{{ get_url('.delete_view') }}">
{% if csrf_token %} {{ delete_form.id(value=get_pk_value(row)) }}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/> {{ delete_form.url(value=return_url) }}
{% endif %} {{ delete_form.csrf_token }}
<button onclick="return confirm('{{ _gettext('Are you sure you want to delete this record?') }}');" title="Delete record"> <button onclick="return confirm('{{ _gettext('Are you sure you want to delete this record?') }}');" title="Delete record">
<span class="glyphicon glyphicon-trash"></span> <span class="glyphicon glyphicon-trash"></span>
</button> </button>
...@@ -120,7 +120,15 @@ ...@@ -120,7 +120,15 @@
</td> </td>
{% endblock %} {% endblock %}
{% for c, name in list_columns %} {% for c, name in list_columns %}
{% if admin_view.is_editable(c) %}
{% if form.csrf_token %}
<td>{{ form[c](pk=get_pk_value(row), value=get_value(row, c), csrf=form.csrf_token._value()) }}</td>
{% else %}
<td>{{ form[c](pk=get_pk_value(row), value=get_value(row, c)) }}</td>
{% endif %}
{% else %}
<td>{{ get_value(row, c) }}</td> <td>{{ get_value(row, c) }}</td>
{% endif %}
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}
</tr> </tr>
......
from nose.tools import eq_, ok_
import os.path as op import os.path as op
from nose.tools import eq_, ok_
from flask.ext.admin.contrib import fileadmin from flask.ext.admin.contrib import fileadmin
from . import setup from . import setup
try:
from StringIO import StringIO
except ImportError:
from io import StringIO
def create_view(): def create_view():
app, admin = setup() app, admin = setup()
class MyFileAdmin(fileadmin.FileAdmin):
editable_extensions = ('txt',)
path = op.join(op.dirname(__file__), 'files') path = op.join(op.dirname(__file__), 'files')
view = fileadmin.FileAdmin(path, '/files/', name='Files') view = MyFileAdmin(path, '/files/', name='Files')
admin.add_view(view) admin.add_view(view)
return app, admin, view return app, admin, view
...@@ -21,8 +30,104 @@ def test_file_admin(): ...@@ -21,8 +30,104 @@ def test_file_admin():
client = app.test_client() 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) eq_(rv.status_code, 200)
ok_('dummy.txt' in rv.data.decode('utf-8')) 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'))
...@@ -2,7 +2,8 @@ from __future__ import unicode_literals ...@@ -2,7 +2,8 @@ from __future__ import unicode_literals
from nose.tools import eq_, ok_ from nose.tools import eq_, ok_
from flask.ext.admin.contrib.geoa import ModelView from flask.ext.admin.contrib.geoa import ModelView
from flask.ext.admin.contrib.geoa.sqltypes import Geometry from geoalchemy2 import Geometry
from geoalchemy2.shape import to_shape
from flask.ext.admin.contrib.geoa.fields import GeoJSONField from flask.ext.admin.contrib.geoa.fields import GeoJSONField
from . import setup from . import setup
...@@ -66,48 +67,49 @@ def test_model(): ...@@ -66,48 +67,49 @@ def test_model():
model = db.session.query(GeoModel).first() model = db.session.query(GeoModel).first()
eq_(model.name, "test1") eq_(model.name, "test1")
eq_(model.point.geom_type, "Point") eq_(to_shape(model.point).geom_type, "Point")
eq_(list(model.point.coords), [(125.8, 10.0)]) eq_(list(to_shape(model.point).coords), [(125.8, 10.0)])
eq_(model.line.geom_type, "LineString") eq_(to_shape(model.line).geom_type, "LineString")
eq_(list(model.line.coords), [(50.2345, 94.2), (50.21, 94.87)]) eq_(list(to_shape(model.line).coords), [(50.2345, 94.2), (50.21, 94.87)])
eq_(model.polygon.geom_type, "Polygon") eq_(to_shape(model.polygon).geom_type, "Polygon")
eq_(list(model.polygon.exterior.coords), eq_(list(to_shape(model.polygon).exterior.coords),
[(100.0, 0.0), (101.0, 0.0), (101.0, 1.0), (100.0, 1.0), (100.0, 0.0)]) [(100.0, 0.0), (101.0, 0.0), (101.0, 1.0), (100.0, 1.0), (100.0, 0.0)])
eq_(model.multi.geom_type, "MultiPoint") eq_(to_shape(model.multi).geom_type, "MultiPoint")
eq_(len(model.multi.geoms), 2) eq_(len(to_shape(model.multi).geoms), 2)
eq_(list(model.multi.geoms[0].coords), [(100.0, 0.0)]) eq_(list(to_shape(model.multi).geoms[0].coords), [(100.0, 0.0)])
eq_(list(model.multi.geoms[1].coords), [(101.0, 1.0)]) eq_(list(to_shape(model.multi).geoms[1].coords), [(101.0, 1.0)])
rv = client.get('/admin/geomodel/') rv = client.get('/admin/geomodel/')
eq_(rv.status_code, 200) eq_(rv.status_code, 200)
point_opt_1 = '>{"type": "Point", "coordinates": [125.8, 10.0]}</textarea>' point_opt_1 = '>{"type": "Point", "coordinates": [125.8, 10.0]}</textarea>'
point_opt_2 = '>{"coordinates": [125.8, 10.0], "type": "Point"}</textarea>' point_opt_2 = '>{"coordinates": [125.8, 10.0], "type": "Point"}</textarea>'
point_opt_3 = '>{"type":"Point","coordinates":[125.8,10]}</textarea>'
html = rv.data.decode('utf-8') html = rv.data.decode('utf-8')
ok_(point_opt_1 in html or point_opt_2 in html, html) ok_(point_opt_1 in html or point_opt_2 in html or point_opt_3 in html, html)
url = '/admin/geomodel/edit/?id=%s' % model.id url = '/admin/geomodel/edit/?id=%s' % model.id
rv = client.get(url) rv = client.get(url)
eq_(rv.status_code, 200) eq_(rv.status_code, 200)
rv = client.post(url, data={ #rv = client.post(url, data={
"name": "edited", # "name": "edited",
"point": '{"type": "Point", "coordinates": [99.9, 10.5]}', # "point": '{"type": "Point", "coordinates": [99.9, 10.5]}',
"line": '', # set to NULL in the database # "line": '', # set to NULL in the database
}) #})
eq_(rv.status_code, 302) #eq_(rv.status_code, 302)
#
model = db.session.query(GeoModel).first() #model = db.session.query(GeoModel).first()
eq_(model.name, "edited") #eq_(model.name, "edited")
eq_(model.point.geom_type, "Point") #eq_(to_shape(model.point).geom_type, "Point")
eq_(list(model.point.coords), [(99.9, 10.5)]) #eq_(list(to_shape(model.point).coords), [(99.9, 10.5)])
eq_(model.line, None) #eq_(to_shape(model.line), None)
eq_(model.polygon.geom_type, "Polygon") #eq_(to_shape(model.polygon).geom_type, "Polygon")
eq_(list(model.polygon.exterior.coords), #eq_(list(to_shape(model.polygon).exterior.coords),
[(100.0, 0.0), (101.0, 0.0), (101.0, 1.0), (100.0, 1.0), (100.0, 0.0)]) # [(100.0, 0.0), (101.0, 0.0), (101.0, 1.0), (100.0, 1.0), (100.0, 0.0)])
eq_(model.multi.geom_type, "MultiPoint") #eq_(to_shape(model.multi).geom_type, "MultiPoint")
eq_(len(model.multi.geoms), 2) #eq_(len(to_shape(model.multi).geoms), 2)
eq_(list(model.multi.geoms[0].coords), [(100.0, 0.0)]) #eq_(list(to_shape(model.multi).geoms[0].coords), [(100.0, 0.0)])
eq_(list(model.multi.geoms[1].coords), [(101.0, 1.0)]) #eq_(list(to_shape(model.multi).geoms[1].coords), [(101.0, 1.0)])
url = '/admin/geomodel/delete/?id=%s' % model.id url = '/admin/geomodel/delete/?id=%s' % model.id
rv = client.post(url) rv = client.post(url)
......
...@@ -52,6 +52,22 @@ def create_models(db): ...@@ -52,6 +52,22 @@ def create_models(db):
return Model1, Model2 return Model1, Model2
def fill_db(Model1, Model2):
Model1('test1_val_1', 'test2_val_1').save()
Model1('test1_val_2', 'test2_val_2').save()
Model1('test1_val_3', 'test2_val_3').save()
Model1('test1_val_4', 'test2_val_4').save()
Model1(None, 'empty_obj').save()
Model2('string_field_val_1', None, None).save()
Model2('string_field_val_2', None, None).save()
Model2('string_field_val_3', 5000, 25.9).save()
Model2('string_field_val_4', 9000, 75.5).save()
Model1('datetime_obj1', datetime_field=datetime(2014,4,3,1,9,0)).save()
Model1('datetime_obj2', datetime_field=datetime(2013,3,2,0,8,0)).save()
def test_model(): def test_model():
app, db, admin = setup() app, db, admin = setup()
...@@ -124,25 +140,86 @@ def test_model(): ...@@ -124,25 +140,86 @@ def test_model():
eq_(rv.status_code, 302) eq_(rv.status_code, 302)
eq_(Model1.objects.count(), 0) eq_(Model1.objects.count(), 0)
def test_column_filters():
def test_column_editable_list():
app, db, admin = setup() app, db, admin = setup()
Model1, Model2 = create_models(db) Model1, Model2 = create_models(db)
# fill DB with values view = CustomModelView(Model1,
Model1('test1_val_1', 'test2_val_1').save() column_editable_list=[
Model1('test1_val_2', 'test2_val_2').save() 'test1', 'datetime_field'])
Model1('test1_val_3', 'test2_val_3').save() admin.add_view(view)
Model1('test1_val_4', 'test2_val_4').save()
Model1(None, 'empty_obj').save()
Model2('string_field_val_1', None, None).save() fill_db(Model1, Model2)
Model2('string_field_val_2', None, None).save()
Model2('string_field_val_3', 5000, 25.9).save()
Model2('string_field_val_4', 9000, 75.5).save()
Model1('datetime_obj1', datetime_field=datetime(2014,4,3,1,9,0)).save() client = app.test_client()
Model1('datetime_obj2', datetime_field=datetime(2013,3,2,0,8,0)).save()
# Test in-line edit field rendering
rv = client.get('/admin/model1/')
data = rv.data.decode('utf-8')
ok_('data-role="x-editable"' in data)
# Form - Test basic in-line edit functionality
obj1 = Model1.objects.get(test1 = 'test1_val_3')
rv = client.post('/admin/model1/ajax/update/', data={
'test1-' + str(obj1.id): 'change-success-1',
})
data = rv.data.decode('utf-8')
ok_('Record was successfully saved.' == data)
# confirm the value has changed
rv = client.get('/admin/model1/')
data = rv.data.decode('utf-8')
ok_('change-success-1' in data)
# Test validation error
obj2 = Model1.objects.get(test1 = 'datetime_obj1')
rv = client.post('/admin/model1/ajax/update/', data={
'datetime_field-' + str(obj2.id): 'problematic-input',
})
eq_(rv.status_code, 500)
# Test invalid primary key
rv = client.post('/admin/model1/ajax/update/', data={
'test1-1000': 'problematic-input',
})
data = rv.data.decode('utf-8')
eq_(rv.status_code, 500)
# Test editing column not in column_editable_list
rv = client.post('/admin/model1/ajax/update/', data={
'test2-1': 'problematic-input',
})
data = rv.data.decode('utf-8')
eq_(rv.status_code, 500)
# Test in-line editing for relations
view = CustomModelView(Model2,
column_editable_list=[
'model1'])
admin.add_view(view)
obj3 = Model2.objects.get(string_field = 'string_field_val_1')
rv = client.post('/admin/model2/ajax/update/', data={
'model1-' + str(obj3.id): str(obj1.id),
})
data = rv.data.decode('utf-8')
ok_('Record was successfully saved.' == data)
# confirm the value has changed
rv = client.get('/admin/model2/')
data = rv.data.decode('utf-8')
ok_('test1_val_1' in data)
def test_column_filters():
app, db, admin = setup()
Model1, Model2 = create_models(db)
# fill DB with values
fill_db(Model1, Model2)
# Test string filter # Test string filter
view = CustomModelView(Model1, column_filters=['test1']) view = CustomModelView(Model1, column_filters=['test1'])
......
...@@ -59,7 +59,9 @@ def create_models(db): ...@@ -59,7 +59,9 @@ def create_models(db):
datetime_field = peewee.DateTimeField(null=True) datetime_field = peewee.DateTimeField(null=True)
def __str__(self): def __str__(self):
return self.test1 # "or ''" fixes error when loading choices for relation field:
# TypeError: coercing to Unicode: need string or buffer, NoneType found
return self.test1 or ''
class Model2(BaseModel): class Model2(BaseModel):
def __init__(self, char_field=None, int_field=None, float_field=None, def __init__(self, char_field=None, int_field=None, float_field=None,
...@@ -76,12 +78,35 @@ def create_models(db): ...@@ -76,12 +78,35 @@ def create_models(db):
float_field = peewee.FloatField(null=True) float_field = peewee.FloatField(null=True)
bool_field = peewee.BooleanField() bool_field = peewee.BooleanField()
# Relation
model1 = peewee.ForeignKeyField(Model1, null=True)
Model1.create_table() Model1.create_table()
Model2.create_table() Model2.create_table()
return Model1, Model2 return Model1, Model2
def fill_db(Model1, Model2):
Model1('test1_val_1', 'test2_val_1').save()
Model1('test1_val_2', 'test2_val_2').save()
Model1('test1_val_3', 'test2_val_3').save()
Model1('test1_val_4', 'test2_val_4').save()
Model1(None, 'empty_obj').save()
Model2('char_field_val_1', None, None).save()
Model2('char_field_val_2', None, None).save()
Model2('char_field_val_3', 5000, 25.9).save()
Model2('char_field_val_4', 9000, 75.5).save()
Model1('date_obj1', date_field=date(2014,11,17)).save()
Model1('date_obj2', date_field=date(2013,10,16)).save()
Model1('timeonly_obj1', timeonly_field=time(11,10,9)).save()
Model1('timeonly_obj2', timeonly_field=time(10,9,8)).save()
Model1('datetime_obj1', datetime_field=datetime(2014,4,3,1,9,0)).save()
Model1('datetime_obj2', datetime_field=datetime(2013,3,2,0,8,0)).save()
def test_model(): def test_model():
app, db, admin = setup() app, db, admin = setup()
Model1, Model2 = create_models(db) Model1, Model2 = create_models(db)
...@@ -153,29 +178,82 @@ def test_model(): ...@@ -153,29 +178,82 @@ def test_model():
eq_(rv.status_code, 302) eq_(rv.status_code, 302)
eq_(Model1.select().count(), 0) eq_(Model1.select().count(), 0)
def test_column_filters():
def test_column_editable_list():
app, db, admin = setup() app, db, admin = setup()
Model1, Model2 = create_models(db) Model1, Model2 = create_models(db)
# fill DB with values view = CustomModelView(Model1,
Model1('test1_val_1', 'test2_val_1').save() column_editable_list=[
Model1('test1_val_2', 'test2_val_2').save() 'test1', 'enum_field'])
Model1('test1_val_3', 'test2_val_3').save() admin.add_view(view)
Model1('test1_val_4', 'test2_val_4').save()
Model1(None, 'empty_obj').save()
Model2('char_field_val_1', None, None).save() fill_db(Model1, Model2)
Model2('char_field_val_2', None, None).save()
Model2('char_field_val_3', 5000, 25.9).save()
Model2('char_field_val_4', 9000, 75.5).save()
Model1('date_obj1', date_field=date(2014,11,17)).save() client = app.test_client()
Model1('date_obj2', date_field=date(2013,10,16)).save()
Model1('timeonly_obj1', timeonly_field=time(11,10,9)).save() # Test in-line edit field rendering
Model1('timeonly_obj2', timeonly_field=time(10,9,8)).save() rv = client.get('/admin/model1/')
Model1('datetime_obj1', datetime_field=datetime(2014,4,3,1,9,0)).save() data = rv.data.decode('utf-8')
Model1('datetime_obj2', datetime_field=datetime(2013,3,2,0,8,0)).save() ok_('data-role="x-editable"' in data)
# Form - Test basic in-line edit functionality
rv = client.post('/admin/model1/ajax/update/', data={
'test1-1': 'change-success-1',
})
data = rv.data.decode('utf-8')
ok_('Record was successfully saved.' == data)
# ensure the value has changed
rv = client.get('/admin/model1/')
data = rv.data.decode('utf-8')
ok_('change-success-1' in data)
# Test validation error
rv = client.post('/admin/model1/ajax/update/', data={
'enum_field-1': 'problematic-input',
})
eq_(rv.status_code, 500)
# Test invalid primary key
rv = client.post('/admin/model1/ajax/update/', data={
'test1-1000': 'problematic-input',
})
data = rv.data.decode('utf-8')
eq_(rv.status_code, 500)
# Test editing column not in column_editable_list
rv = client.post('/admin/model1/ajax/update/', data={
'test2-1': 'problematic-input',
})
data = rv.data.decode('utf-8')
eq_(rv.status_code, 500)
# Test in-line editing for relations
view = CustomModelView(Model2,
column_editable_list=[
'model1'])
admin.add_view(view)
rv = client.post('/admin/model2/ajax/update/', data={
'model1-1': '3',
})
data = rv.data.decode('utf-8')
ok_('Record was successfully saved.' == data)
# confirm the value has changed
rv = client.get('/admin/model2/')
data = rv.data.decode('utf-8')
ok_('test1_val_3' in data)
def test_column_filters():
app, db, admin = setup()
Model1, Model2 = create_models(db)
fill_db(Model1, Model2)
# Test string filter # Test string filter
view = CustomModelView(Model1, column_filters=['test1']) view = CustomModelView(Model1, column_filters=['test1'])
......
...@@ -5,7 +5,7 @@ from wtforms import fields ...@@ -5,7 +5,7 @@ from wtforms import fields
from flask.ext.admin import form from flask.ext.admin import form
from flask.ext.admin._compat import as_unicode from flask.ext.admin._compat import as_unicode
from flask.ext.admin._compat import iteritems from flask.ext.admin._compat import iteritems
from flask.ext.admin.contrib.sqla import ModelView from flask.ext.admin.contrib.sqla import ModelView, filters
from flask.ext.babelex import Babel from flask.ext.babelex import Babel
from . import setup from . import setup
...@@ -81,6 +81,39 @@ def create_models(db): ...@@ -81,6 +81,39 @@ def create_models(db):
return Model1, Model2 return Model1, Model2
def fill_db(db, Model1, Model2):
model1_obj1 = Model1('test1_val_1', 'test2_val_1', bool_field=True)
model1_obj2 = Model1('test1_val_2', 'test2_val_2')
model1_obj3 = Model1('test1_val_3', 'test2_val_3')
model1_obj4 = Model1('test1_val_4', 'test2_val_4')
model2_obj1 = Model2('test2_val_1', model1=model1_obj1, float_field=None)
model2_obj2 = Model2('test2_val_2', model1=model1_obj2, float_field=None)
model2_obj3 = Model2('test2_val_3', int_field=5000, float_field=25.9)
model2_obj4 = Model2('test2_val_4', int_field=9000, float_field=75.5)
date_obj1 = Model1('date_obj1', date_field=date(2014,11,17))
date_obj2 = Model1('date_obj2', date_field=date(2013,10,16))
timeonly_obj1 = Model1('timeonly_obj1', time_field=time(11,10,9))
timeonly_obj2 = Model1('timeonly_obj2', time_field=time(10,9,8))
datetime_obj1 = Model1('datetime_obj1', datetime_field=datetime(2014,4,3,1,9,0))
datetime_obj2 = Model1('datetime_obj2', datetime_field=datetime(2013,3,2,0,8,0))
enum_obj1 = Model1('enum_obj1', enum_field="model1_v1")
enum_obj2 = Model1('enum_obj2', enum_field="model1_v2")
empty_obj = Model1(test2="empty_obj")
db.session.add_all([
model1_obj1, model1_obj2, model1_obj3, model1_obj4,
model2_obj1, model2_obj2, model2_obj3, model2_obj4,
date_obj1, timeonly_obj1, datetime_obj1,
date_obj2, timeonly_obj2, datetime_obj2,
enum_obj1, enum_obj2, empty_obj
])
db.session.commit()
def test_model(): def test_model():
app, db, admin = setup() app, db, admin = setup()
Model1, Model2 = create_models(db) Model1, Model2 = create_models(db)
...@@ -121,7 +154,9 @@ def test_model(): ...@@ -121,7 +154,9 @@ def test_model():
eq_(rv.status_code, 200) eq_(rv.status_code, 200)
rv = client.post('/admin/model1/new/', rv = client.post('/admin/model1/new/',
data=dict(test1='test1large', test2='test2')) data=dict(test1='test1large',
test2='test2',
time_field=time(0,0,0)))
eq_(rv.status_code, 302) eq_(rv.status_code, 302)
model = db.session.query(Model1).first() model = db.session.query(Model1).first()
...@@ -138,6 +173,9 @@ def test_model(): ...@@ -138,6 +173,9 @@ def test_model():
rv = client.get(url) rv = client.get(url)
eq_(rv.status_code, 200) eq_(rv.status_code, 200)
# verify that midnight does not show as blank
ok_(u'00:00:00' in rv.data.decode('utf-8'))
rv = client.post(url, rv = client.post(url,
data=dict(test1='test1small', test2='test2large')) data=dict(test1='test1small', test2='test2large'))
eq_(rv.status_code, 302) eq_(rv.status_code, 302)
...@@ -215,27 +253,32 @@ def test_column_searchable_list(): ...@@ -215,27 +253,32 @@ def test_column_searchable_list():
Model1, Model2 = create_models(db) Model1, Model2 = create_models(db)
view = CustomModelView(Model1, db.session, view = CustomModelView(Model2, db.session,
column_searchable_list=['test1', 'test2']) column_searchable_list=['string_field', 'int_field'])
admin.add_view(view) admin.add_view(view)
eq_(view._search_supported, True) eq_(view._search_supported, True)
eq_(len(view._search_fields), 2) eq_(len(view._search_fields), 2)
ok_(isinstance(view._search_fields[0], db.Column)) ok_(isinstance(view._search_fields[0], db.Column))
ok_(isinstance(view._search_fields[1], db.Column)) ok_(isinstance(view._search_fields[1], db.Column))
eq_(view._search_fields[0].name, 'test1') eq_(view._search_fields[0].name, 'string_field')
eq_(view._search_fields[1].name, 'test2') eq_(view._search_fields[1].name, 'int_field')
db.session.add(Model1('model1')) db.session.add(Model2('model1-test', 5000))
db.session.add(Model1('model2')) db.session.add(Model2('model2-test', 9000))
db.session.commit() db.session.commit()
client = app.test_client() client = app.test_client()
rv = client.get('/admin/model1/?search=model1') rv = client.get('/admin/model2/?search=model1')
data = rv.data.decode('utf-8') data = rv.data.decode('utf-8')
ok_('model1' in data) ok_('model1-test' in data)
ok_('model2' not in data) ok_('model2-test' not in data)
rv = client.get('/admin/model2/?search=9000')
data = rv.data.decode('utf-8')
ok_('model1-test' not in data)
ok_('model2-test' in data)
def test_complex_searchable_list(): def test_complex_searchable_list():
...@@ -247,18 +290,31 @@ def test_complex_searchable_list(): ...@@ -247,18 +290,31 @@ def test_complex_searchable_list():
column_searchable_list=['model1.test1']) column_searchable_list=['model1.test1'])
admin.add_view(view) admin.add_view(view)
m1 = Model1('model1') m1 = Model1('model1-test1-val')
m2 = Model1('model1-test2-val')
db.session.add(m1) db.session.add(m1)
db.session.add(Model2('model2', model1=m1)) db.session.add(m2)
db.session.add(Model2('model3')) db.session.add(Model2('model2-test1-val', model1=m1))
db.session.add(Model2('model2-test2-val', model1=m2))
db.session.commit() db.session.commit()
client = app.test_client() client = app.test_client()
rv = client.get('/admin/model2/?search=model1') # test relation string - 'model1.test1'
rv = client.get('/admin/model2/?search=model1-test1')
data = rv.data.decode('utf-8') data = rv.data.decode('utf-8')
ok_('model1' in data) ok_('model2-test1-val' in data)
ok_('model3' not in data) ok_('model2-test2-val' not in data)
view2 = CustomModelView(Model1, db.session,
column_searchable_list=[Model2.string_field])
admin.add_view(view2)
# test relation object - Model2.string_field
rv = client.get('/admin/model1/?search=model2-test1')
data = rv.data.decode('utf-8')
ok_('model1-test1-val' in data)
ok_('model1-test2-val' not in data)
def test_complex_searchable_list_missing_children(): def test_complex_searchable_list_missing_children():
...@@ -281,6 +337,75 @@ def test_complex_searchable_list_missing_children(): ...@@ -281,6 +337,75 @@ def test_complex_searchable_list_missing_children():
ok_('magic string' in data) ok_('magic string' in data)
def test_column_editable_list():
app, db, admin = setup()
Model1, Model2 = create_models(db)
view = CustomModelView(Model1, db.session,
column_editable_list=[
'test1', 'enum_field'])
admin.add_view(view)
fill_db(db, Model1, Model2)
client = app.test_client()
# Test in-line edit field rendering
rv = client.get('/admin/model1/')
data = rv.data.decode('utf-8')
ok_('data-role="x-editable"' in data)
# Form - Test basic in-line edit functionality
rv = client.post('/admin/model1/ajax/update/', data={
'test1-1': 'change-success-1',
})
data = rv.data.decode('utf-8')
ok_('Record was successfully saved.' == data)
# ensure the value has changed
rv = client.get('/admin/model1/')
data = rv.data.decode('utf-8')
ok_('change-success-1' in data)
# Test validation error
rv = client.post('/admin/model1/ajax/update/', data={
'enum_field-1': 'problematic-input',
})
eq_(rv.status_code, 500)
# Test invalid primary key
rv = client.post('/admin/model1/ajax/update/', data={
'test1-1000': 'problematic-input',
})
data = rv.data.decode('utf-8')
eq_(rv.status_code, 500)
# Test editing column not in column_editable_list
rv = client.post('/admin/model1/ajax/update/', data={
'test2-1': 'problematic-input',
})
data = rv.data.decode('utf-8')
eq_(rv.status_code, 500)
# Test in-line editing for relations
view = CustomModelView(Model2, db.session,
column_editable_list=[
'model1'])
admin.add_view(view)
rv = client.post('/admin/model2/ajax/update/', data={
'model1-1': '3',
})
data = rv.data.decode('utf-8')
ok_('Record was successfully saved.' == data)
# confirm the value has changed
rv = client.get('/admin/model2/')
data = rv.data.decode('utf-8')
ok_('test1_val_3' in data)
def test_column_filters(): def test_column_filters():
app, db, admin = setup() app, db, admin = setup()
...@@ -378,37 +503,17 @@ def test_column_filters(): ...@@ -378,37 +503,17 @@ def test_column_filters():
(1, 'not equal'), (1, 'not equal'),
]) ])
# Fill DB # Test column_labels on filters
model1_obj1 = Model1('test1_val_1', 'test2_val_1', bool_field=True) view = CustomModelView(Model2, db.session,
model1_obj2 = Model1('test1_val_2', 'test2_val_2') column_filters=['model1.bool_field', 'string_field'],
model1_obj3 = Model1('test1_val_3', 'test2_val_3') column_labels={
model1_obj4 = Model1('test1_val_4', 'test2_val_4') 'model1.bool_field': 'Test Filter #1',
'string_field': 'Test Filter #2',
model2_obj1 = Model2('test2_val_1', model1=model1_obj1, float_field=None) })
model2_obj2 = Model2('test2_val_2', model1=model1_obj1, float_field=None)
model2_obj3 = Model2('test2_val_3', int_field=5000, float_field=25.9)
model2_obj4 = Model2('test2_val_4', int_field=9000, float_field=75.5)
date_obj1 = Model1('date_obj1', date_field=date(2014,11,17))
date_obj2 = Model1('date_obj2', date_field=date(2013,10,16))
timeonly_obj1 = Model1('timeonly_obj1', time_field=time(11,10,9))
timeonly_obj2 = Model1('timeonly_obj2', time_field=time(10,9,8))
datetime_obj1 = Model1('datetime_obj1', datetime_field=datetime(2014,4,3,1,9,0))
datetime_obj2 = Model1('datetime_obj2', datetime_field=datetime(2013,3,2,0,8,0))
enum_obj1 = Model1('enum_obj1', enum_field="model1_v1") eq_(list(view._filter_groups.keys()), [u'Test Filter #1', u'Test Filter #2'])
enum_obj2 = Model1('enum_obj2', enum_field="model1_v2")
empty_obj = Model1(test2="empty_obj") fill_db(db, Model1, Model2)
db.session.add_all([
model1_obj1, model1_obj2, model1_obj3, model1_obj4,
model2_obj1, model2_obj2, model2_obj3, model2_obj4,
date_obj1, timeonly_obj1, datetime_obj1,
date_obj2, timeonly_obj2, datetime_obj2,
enum_obj1, enum_obj2, empty_obj
])
db.session.commit()
client = app.test_client() client = app.test_client()
...@@ -705,7 +810,7 @@ def test_column_filters(): ...@@ -705,7 +810,7 @@ def test_column_filters():
eq_(rv.status_code, 200) eq_(rv.status_code, 200)
data = rv.data.decode('utf-8') data = rv.data.decode('utf-8')
ok_('test2_val_1' in data) ok_('test2_val_1' in data)
ok_('test2_val_2' in data) ok_('test2_val_2' not in data)
ok_('test2_val_3' not in data) ok_('test2_val_3' not in data)
ok_('test2_val_4' not in data) ok_('test2_val_4' not in data)
...@@ -989,6 +1094,18 @@ def test_column_filters(): ...@@ -989,6 +1094,18 @@ def test_column_filters():
ok_('enum_obj1' not in data) ok_('enum_obj1' not in data)
ok_('enum_obj2' not in data) ok_('enum_obj2' not in data)
# Test single custom filter on relation
view = CustomModelView(Model2, db.session,
column_filters = [
filters.FilterEqual(Model1.test1, "Test1")
], endpoint='_relation_test')
admin.add_view(view)
rv = client.get('/admin/_relation_test/?flt1_0=test1_val_1')
data = rv.data.decode('utf-8')
ok_('test1_val_1' in data)
ok_('test1_val_2' not in data)
def test_url_args(): def test_url_args():
app, db, admin = setup() app, db, admin = setup()
...@@ -1244,6 +1361,45 @@ def test_default_sort(): ...@@ -1244,6 +1361,45 @@ def test_default_sort():
eq_(data[2].test1, 'c') eq_(data[2].test1, 'c')
def test_complex_sort():
app, db, admin = setup()
M1, M2 = create_models(db)
m1 = M1('b')
db.session.add(m1)
db.session.add(M2('c', model1=m1))
m2 = M1('a')
db.session.add(m2)
db.session.add(M2('c', model1=m2))
db.session.commit()
# test sorting on relation string - 'model1.test1'
view = CustomModelView(M2, db.session,
column_list = ['string_field', 'model1.test1'],
column_sortable_list = ['model1.test1'])
admin.add_view(view)
client = app.test_client()
rv = client.get('/admin/model2/?sort=1')
eq_(rv.status_code, 200)
# test sorting on relation object - M2.string_field
view2 = CustomModelView(M1, db.session,
column_list = ['model2.string_field'],
column_sortable_list = [M2.string_field])
admin.add_view(view2)
client = app.test_client()
rv = client.get('/admin/model1/?sort=1')
eq_(rv.status_code, 200)
data = rv.data.decode('utf-8')
ok_('Sort by' in data)
def test_default_complex_sort(): def test_default_complex_sort():
app, db, admin = setup() app, db, admin = setup()
M1, M2 = create_models(db) M1, M2 = create_models(db)
...@@ -1267,6 +1423,7 @@ def test_default_complex_sort(): ...@@ -1267,6 +1423,7 @@ def test_default_complex_sort():
eq_(data[0].model1.test1, 'a') eq_(data[0].model1.test1, 'a')
eq_(data[1].model1.test1, 'b') eq_(data[1].model1.test1, 'b')
def test_extra_fields(): def test_extra_fields():
app, db, admin = setup() app, db, admin = setup()
......
from nose.tools import ok_, eq_, raises from nose.tools import ok_, eq_, raises
from flask import Flask, request, abort from flask import Flask, request, abort, url_for
from flask.views import MethodView from flask.views import MethodView
from flask.ext.admin import base from flask.ext.admin import base
...@@ -133,6 +133,11 @@ def test_admin_customizations(): ...@@ -133,6 +133,11 @@ def test_admin_customizations():
rv = client.get('/foobar/') rv = client.get('/foobar/')
eq_(rv.status_code, 200) eq_(rv.status_code, 200)
# test custom static_url_path
with app.test_request_context('/'):
rv = client.get(url_for('admin.static', filename='bootstrap/bootstrap2/css/bootstrap.css'))
eq_(rv.status_code, 200)
def test_baseview_registration(): def test_baseview_registration():
admin = base.Admin() admin = base.Admin()
...@@ -360,6 +365,11 @@ def test_root_mount(): ...@@ -360,6 +365,11 @@ def test_root_mount():
rv = client.get('/mockview/') rv = client.get('/mockview/')
eq_(rv.data, b'Success!') eq_(rv.data, b'Success!')
# test static files when url='/'
with app.test_request_context('/'):
rv = client.get(url_for('admin.static', filename='bootstrap/bootstrap2/css/bootstrap.css'))
eq_(rv.status_code, 200)
def test_menu_links(): def test_menu_links():
app = Flask(__name__) app = Flask(__name__)
......
import wtforms
from nose.tools import eq_, ok_ from nose.tools import eq_, ok_
from flask import Flask from flask import Flask, session
from werkzeug.wsgi import DispatcherMiddleware from werkzeug.wsgi import DispatcherMiddleware
from werkzeug.test import Client from werkzeug.test import Client
...@@ -12,6 +14,14 @@ from flask.ext.admin._compat import iteritems, itervalues ...@@ -12,6 +14,14 @@ from flask.ext.admin._compat import iteritems, itervalues
from flask.ext.admin.model import base, filters from flask.ext.admin.model import base, filters
def wtforms2_and_up(func):
"""Decorator for skipping test if wtforms <2
"""
if int(wtforms.__version__[0]) < 2:
func.__test__ = False
return func
class Model(object): class Model(object):
def __init__(self, id=None, c1=1, c2=2, c3=3): def __init__(self, id=None, c1=1, c2=2, c3=3):
self.id = id self.id = id
...@@ -329,6 +339,104 @@ def test_form(): ...@@ -329,6 +339,104 @@ def test_form():
pass pass
@wtforms2_and_up
def test_csrf():
from datetime import timedelta
from wtforms.csrf.session import SessionCSRF
from wtforms.meta import DefaultMeta
# BaseForm w/ CSRF
class SecureForm(form.BaseForm):
class Meta(DefaultMeta):
csrf = True
csrf_class = SessionCSRF
csrf_secret = b'EPj00jpfj8Gx1SjnyLxwBBSQfnQ9DJYe0Ym'
csrf_time_limit = timedelta(minutes=20)
@property
def csrf_context(self):
return session
class SecureModelView(MockModelView):
form_base_class = SecureForm
def scaffold_form(self):
return SecureForm
def get_csrf_token(data):
data = data.split('name="csrf_token" type="hidden" value="')[1]
token = data.split('"')[0]
return token
app, admin = setup()
view = SecureModelView(Model, endpoint='secure')
admin.add_view(view)
client = app.test_client()
################
# create_view
################
rv = client.get('/admin/secure/new/')
eq_(rv.status_code, 200)
ok_(u'name="csrf_token"' in rv.data.decode('utf-8'))
csrf_token = get_csrf_token(rv.data.decode('utf-8'))
# Create without CSRF token
rv = client.post('/admin/secure/new/', data=dict(name='test1'))
eq_(rv.status_code, 200)
# Create with CSRF token
rv = client.post('/admin/secure/new/', data=dict(name='test1',
csrf_token=csrf_token))
eq_(rv.status_code, 302)
###############
# edit_view
###############
rv = client.get('/admin/secure/edit/?url=%2Fadmin%2Fsecure%2F&id=1')
eq_(rv.status_code, 200)
ok_(u'name="csrf_token"' in rv.data.decode('utf-8'))
csrf_token = get_csrf_token(rv.data.decode('utf-8'))
# Edit without CSRF token
rv = client.post('/admin/secure/edit/?url=%2Fadmin%2Fsecure%2F&id=1',
data=dict(name='test1'))
eq_(rv.status_code, 200)
# Edit with CSRF token
rv = client.post('/admin/secure/edit/?url=%2Fadmin%2Fsecure%2F&id=1',
data=dict(name='test1', csrf_token=csrf_token))
eq_(rv.status_code, 302)
################
# delete_view
################
rv = client.get('/admin/secure/')
eq_(rv.status_code, 200)
ok_(u'name="csrf_token"' in rv.data.decode('utf-8'))
csrf_token = get_csrf_token(rv.data.decode('utf-8'))
# Delete without CSRF token, test validation errors
rv = client.post('/admin/secure/delete/',
data=dict(id="1", url="/admin/secure/"), follow_redirects=True)
eq_(rv.status_code, 200)
ok_(u'Record was successfully deleted.' not in rv.data.decode('utf-8'))
ok_(u'Failed to delete record.' in rv.data.decode('utf-8'))
# Delete with CSRF token
rv = client.post('/admin/secure/delete/',
data=dict(id="1", url="/admin/secure/", csrf_token=csrf_token),
follow_redirects=True)
eq_(rv.status_code, 200)
ok_(u'Record was successfully deleted.' in rv.data.decode('utf-8'))
def test_custom_form(): def test_custom_form():
app, admin = setup() app, admin = setup()
......
...@@ -101,6 +101,12 @@ def get_dict_attr(obj, attr, default=None): ...@@ -101,6 +101,12 @@ def get_dict_attr(obj, attr, default=None):
return default return default
def escape(value):
return (as_unicode(value)
.replace(CHAR_ESCAPE, CHAR_ESCAPE + CHAR_ESCAPE)
.replace(CHAR_SEPARATOR, CHAR_ESCAPE + CHAR_SEPARATOR))
def iterencode(iter): def iterencode(iter):
""" """
Encode enumerable as compact string representation. Encode enumerable as compact string representation.
......
# Translations template for Flask-Admin.
# Copyright (C) 2014 ORGANIZATION
# This file is distributed under the same license as the Flask-Admin
# project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
#
msgid ""
msgstr ""
"Project-Id-Version: Flask-Admin 1.0.9\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2014-12-18 08:30+0100\n"
"PO-Revision-Date: 2014-12-18 09:01+0100\n"
"Last-Translator: Eduard Carreras <ecarreras@gisce.net>\n"
"Language-Team: ca_ES\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 1.3\n"
"X-Generator: Poedit 1.5.4\n"
"Language: ca_ES\n"
#: ../flask_admin/base.py:419
msgid "Home"
msgstr "Home"
#: ../flask_admin/contrib/fileadmin.py:34
msgid "Invalid directory name"
msgstr "Nom del directori no vàlid"
#: ../flask_admin/contrib/fileadmin.py:42
msgid "File to upload"
msgstr "Arxiu per carregar"
#: ../flask_admin/contrib/fileadmin.py:51
msgid "File required."
msgstr "Requereix Arxiu."
#: ../flask_admin/contrib/fileadmin.py:56
msgid "Invalid file type."
msgstr "Tipus de fitxer no vàlid."
#: ../flask_admin/contrib/fileadmin.py:60
msgid "Content"
msgstr "Contingut"
#: ../flask_admin/contrib/fileadmin.py:427
#, python-format
msgid "File \"%(name)s\" already exists."
msgstr "L'Arxiu \"%(name)s\" ja existeix."
#: ../flask_admin/contrib/fileadmin.py:446
#: ../flask_admin/contrib/fileadmin.py:512
#: ../flask_admin/contrib/fileadmin.py:565
#: ../flask_admin/contrib/fileadmin.py:602
#: ../flask_admin/contrib/fileadmin.py:645
#: ../flask_admin/contrib/fileadmin.py:693
msgid "Permission denied."
msgstr "Permis denegat"
#: ../flask_admin/contrib/fileadmin.py:508
msgid "File uploading is disabled."
msgstr "La pujada de fitxers està desactivada."
#: ../flask_admin/contrib/fileadmin.py:521
#, python-format
msgid "Failed to save file: %(error)s"
msgstr "Error en desar el fitxer: %(error)s"
#: ../flask_admin/contrib/fileadmin.py:561
msgid "Directory creation is disabled."
msgstr "Creació directory està desactivada."
#: ../flask_admin/contrib/fileadmin.py:576
#, python-format
msgid "Failed to create directory: %(error)s"
msgstr "No s'ha pogut crear el directori: %(error)s"
#: ../flask_admin/contrib/fileadmin.py:598
msgid "Deletion is disabled."
msgstr "Eliminar està desactivat."
#: ../flask_admin/contrib/fileadmin.py:607
msgid "Directory deletion is disabled."
msgstr "La opció eliminar directori està deshabilitada"
#: ../flask_admin/contrib/fileadmin.py:613
#, python-format
msgid "Directory \"%(path)s\" was successfully deleted."
msgstr "El directori \"%(path)s\" s'ha eliminat correctament."
#: ../flask_admin/contrib/fileadmin.py:615
#, python-format
msgid "Failed to delete directory: %(error)s"
msgstr "Error en esborrar el directori: %(error)s"
#: ../flask_admin/contrib/fileadmin.py:620
#: ../flask_admin/contrib/fileadmin.py:759
#, python-format
msgid "File \"%(name)s\" was successfully deleted."
msgstr "L'arxiu \"%(name)s\" ha estat eliminat correctament "
#: ../flask_admin/contrib/fileadmin.py:622
#: ../flask_admin/contrib/fileadmin.py:761
#, python-format
msgid "Failed to delete file: %(name)s"
msgstr "Error en eliminar fitxer: %(name)s"
#: ../flask_admin/contrib/fileadmin.py:641
msgid "Renaming is disabled."
msgstr "La opció canvi de nom està deshabilitada"
#: ../flask_admin/contrib/fileadmin.py:649
msgid "Path does not exist."
msgstr "La ruta no extisteix"
#: ../flask_admin/contrib/fileadmin.py:661
#, python-format
msgid "Successfully renamed \"%(src)s\" to \"%(dst)s\""
msgstr "Èxit en reanomenar \"%(src)s\" per \"%(dst)s\""
#: ../flask_admin/contrib/fileadmin.py:664
#, python-format
msgid "Failed to rename: %(error)s"
msgstr "Error en canviar el nom de: %(error)s"
#: ../flask_admin/contrib/fileadmin.py:709
#, python-format
msgid "Error saving changes to %(name)s."
msgstr "Error en desar canvis a %(name)s."
#: ../flask_admin/contrib/fileadmin.py:713
#, python-format
msgid "Changes to %(name)s saved successfully."
msgstr "Canvis a %(name)s guardat correctament "."
#: ../flask_admin/contrib/fileadmin.py:720
#, python-format
msgid "Error reading %(name)s."
msgstr "Error llegint %(name)s."
#: ../flask_admin/contrib/fileadmin.py:723
#: ../flask_admin/contrib/fileadmin.py:732
#, python-format
msgid "Unexpected error while reading from %(name)s"
msgstr "Error inesperat en llegir des de %(name)s"
#: ../flask_admin/contrib/fileadmin.py:729
#, python-format
msgid "Cannot edit %(name)s."
msgstr "No es pot editar %(name)s."
#: ../flask_admin/contrib/fileadmin.py:746
#: ../flask_admin/contrib/mongoengine/view.py:599
#: ../flask_admin/contrib/peewee/view.py:406
#: ../flask_admin/contrib/pymongo/view.py:343
#: ../flask_admin/contrib/sqla/view.py:932
msgid "Delete"
msgstr "Eliminar"
#: ../flask_admin/contrib/fileadmin.py:747
msgid "Are you sure you want to delete these files?"
msgstr "Esteu segur que voleu eliminar aquests arxius?"
#: ../flask_admin/contrib/fileadmin.py:750
msgid "File deletion is disabled."
msgstr "Eliminació de fitxer inhabilitada."
#: ../flask_admin/contrib/fileadmin.py:763
msgid "Edit"
msgstr "Editar"
#: ../flask_admin/contrib/rediscli.py:125
msgid "Cli: Invalid command."
msgstr "Cli: comanda no vàlid"
#: ../flask_admin/contrib/geoa/fields.py:29
msgid "Invalid JSON"
msgstr "JSON no vàlid"
#: ../flask_admin/contrib/mongoengine/filters.py:38
#: ../flask_admin/contrib/peewee/filters.py:38
#: ../flask_admin/contrib/pymongo/filters.py:38
#: ../flask_admin/contrib/sqla/filters.py:38
msgid "equals"
msgstr "igual"
#: ../flask_admin/contrib/mongoengine/filters.py:47
#: ../flask_admin/contrib/peewee/filters.py:46
#: ../flask_admin/contrib/pymongo/filters.py:47
#: ../flask_admin/contrib/sqla/filters.py:46
msgid "not equal"
msgstr "no és igual"
#: ../flask_admin/contrib/mongoengine/filters.py:57
#: ../flask_admin/contrib/peewee/filters.py:55
#: ../flask_admin/contrib/pymongo/filters.py:57
#: ../flask_admin/contrib/sqla/filters.py:55
msgid "contains"
msgstr "te"
#: ../flask_admin/contrib/mongoengine/filters.py:67
#: ../flask_admin/contrib/peewee/filters.py:64
#: ../flask_admin/contrib/pymongo/filters.py:67
#: ../flask_admin/contrib/sqla/filters.py:64
msgid "not contains"
msgstr "no te"
#: ../flask_admin/contrib/mongoengine/filters.py:76
#: ../flask_admin/contrib/peewee/filters.py:72
#: ../flask_admin/contrib/pymongo/filters.py:80
#: ../flask_admin/contrib/sqla/filters.py:72
msgid "greater than"
msgstr "més que"
#: ../flask_admin/contrib/mongoengine/filters.py:85
#: ../flask_admin/contrib/peewee/filters.py:80
#: ../flask_admin/contrib/pymongo/filters.py:93
#: ../flask_admin/contrib/sqla/filters.py:80
msgid "smaller than"
msgstr "menor que"
#: ../flask_admin/contrib/mongoengine/filters.py:97
#: ../flask_admin/contrib/peewee/filters.py:91
#: ../flask_admin/contrib/sqla/filters.py:91
msgid "empty"
msgstr "buit"
#: ../flask_admin/contrib/mongoengine/filters.py:112
#: ../flask_admin/contrib/peewee/filters.py:105
#: ../flask_admin/contrib/sqla/filters.py:105
msgid "in list"
msgstr "en la llista"
#: ../flask_admin/contrib/mongoengine/filters.py:121
#: ../flask_admin/contrib/peewee/filters.py:114
#: ../flask_admin/contrib/sqla/filters.py:114
msgid "not in list"
msgstr "no en la llista"
#: ../flask_admin/contrib/mongoengine/filters.py:221
#: ../flask_admin/contrib/peewee/filters.py:208
#: ../flask_admin/contrib/peewee/filters.py:245
#: ../flask_admin/contrib/peewee/filters.py:282
#: ../flask_admin/contrib/sqla/filters.py:209
#: ../flask_admin/contrib/sqla/filters.py:246
#: ../flask_admin/contrib/sqla/filters.py:283
msgid "not between"
msgstr "no entre"
#: ../flask_admin/contrib/mongoengine/view.py:493
#, python-format
msgid "Failed to get model. %(error)s"
msgstr "No s'ha pogut obtenir el model. %(error)s"
#: ../flask_admin/contrib/mongoengine/view.py:512
#: ../flask_admin/contrib/peewee/view.py:357
#: ../flask_admin/contrib/pymongo/view.py:278
#: ../flask_admin/contrib/sqla/view.py:864
#, python-format
msgid "Failed to create record. %(error)s"
msgstr "Error en eliminar el registre. %(error)s"
#: ../flask_admin/contrib/mongoengine/view.py:538
#: ../flask_admin/contrib/peewee/view.py:376
#: ../flask_admin/contrib/pymongo/view.py:303
#: ../flask_admin/contrib/sqla/view.py:890
#, python-format
msgid "Failed to update record. %(error)s"
msgstr "Error en actualitzar el registre. %(error)s"
#: ../flask_admin/contrib/mongoengine/view.py:562
#: ../flask_admin/contrib/peewee/view.py:392
#: ../flask_admin/contrib/pymongo/view.py:329
#: ../flask_admin/contrib/sqla/view.py:916
#, python-format
msgid "Failed to delete record. %(error)s"
msgstr "Error en eliminar el registre. %(error)s"
#: ../flask_admin/contrib/mongoengine/view.py:600
#: ../flask_admin/contrib/peewee/view.py:407
#: ../flask_admin/contrib/pymongo/view.py:344
#: ../flask_admin/contrib/sqla/view.py:933
msgid "Are you sure you want to delete selected records?"
msgstr "¿Segur que vols esborrar els registres seleccionats?"
#: ../flask_admin/contrib/mongoengine/view.py:609
#: ../flask_admin/contrib/peewee/view.py:423
#: ../flask_admin/contrib/pymongo/view.py:354
#: ../flask_admin/contrib/sqla/view.py:949
#, python-format
msgid "Record was successfully deleted."
msgid_plural "%(count)s records were successfully deleted."
msgstr[0] "El registre he sigut eliminat correctament."
msgstr[1] "%(count)s registres s'han eliminat amb èxit."
#: ../flask_admin/contrib/mongoengine/view.py:615
#: ../flask_admin/contrib/peewee/view.py:429
#: ../flask_admin/contrib/pymongo/view.py:359
#: ../flask_admin/contrib/sqla/view.py:957
#, python-format
msgid "Failed to delete records. %(error)s"
msgstr "Error en eliminar registres. %(error)s"
#: ../flask_admin/contrib/sqla/fields.py:123
#: ../flask_admin/contrib/sqla/fields.py:173
#: ../flask_admin/contrib/sqla/fields.py:178
#: ../flask_admin/model/fields.py:167 ../flask_admin/model/fields.py:216
msgid "Not a valid choice"
msgstr "Selecció no valida"
#: ../flask_admin/contrib/sqla/validators.py:42
msgid "Already exists."
msgstr "Ja existeixen"
#: ../flask_admin/contrib/sqla/validators.py:60
#, python-format
msgid "At least %d item is required"
msgid_plural "At least %d items are required"
msgstr[0] "Es requereix almenys %d registre"
msgstr[1] "Es requereixen almenys %d registres"
#: ../flask_admin/contrib/sqla/view.py:843
#, python-format
msgid "Integrity error. %(message)s"
msgstr "Error d'integritat. %(message)s"
#: ../flask_admin/form/fields.py:92
msgid "Invalid time format"
msgstr "Format d'hora no vàlida"
#: ../flask_admin/form/fields.py:138
msgid "Invalid Choice: could not coerce"
msgstr "Opció no vàlida: no es pot realitxar"
#: ../flask_admin/form/upload.py:187
msgid "Invalid file extension"
msgstr "Extensió de fitxer no vàlid"
#: ../flask_admin/model/base.py:1103
msgid "There are no items in the table."
msgstr "No hi ha registres a la taula."
#: ../flask_admin/model/base.py:1127
#, python-format
msgid "Invalid Filter Value: %(value)s"
msgstr "Valor de filtre no vàlid"
#: ../flask_admin/model/base.py:1339
msgid "Record was successfully created."
msgstr "Registre ha estat creat correctament."
#: ../flask_admin/model/base.py:1376
msgid "Record was successfully saved."
msgstr "El registre ha estat guardat amb èxit "
#: ../flask_admin/model/filters.py:99
msgid "Yes"
msgstr "Si"
#: ../flask_admin/model/filters.py:100
msgid "No"
msgstr "No"
#: ../flask_admin/model/filters.py:162 ../flask_admin/model/filters.py:202
#: ../flask_admin/model/filters.py:247
msgid "between"
msgstr "entre"
#: ../flask_admin/templates/bootstrap2/admin/actions.html:4
#: ../flask_admin/templates/bootstrap3/admin/actions.html:4
msgid "With selected"
msgstr "Amb la selecció"
#: ../flask_admin/templates/bootstrap2/admin/lib.html:156
#: ../flask_admin/templates/bootstrap3/admin/lib.html:149
msgid "Submit"
msgstr "Enviar"
#: ../flask_admin/templates/bootstrap2/admin/lib.html:161
#: ../flask_admin/templates/bootstrap3/admin/lib.html:154
msgid "Cancel"
msgstr "Cancel·lar"
#: ../flask_admin/templates/bootstrap2/admin/file/edit.html:5
#: ../flask_admin/templates/bootstrap3/admin/file/edit.html:5
#, python-format
msgid "You are editing %(path)s"
msgstr "Esteu editant %(path)s"
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:9
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:9
msgid "Root"
msgstr "Arrel"
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:62
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:62
#, python-format
msgid "Are you sure you want to delete \\'%(name)s\\' recursively?"
msgstr "Esteu segur que voleu eliminar \\'%(name)s\\' recursivament"
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:70
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:70
#, python-format
msgid "Are you sure you want to delete \\'%(name)s\\'?"
msgstr "Esteu segur que voleu eliminar \\'%(name)s\\'?"
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:105
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:105
msgid "Upload File"
msgstr "Carrega un fitxer"
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:110
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:110
msgid "Create Directory"
msgstr "Crear un directori"
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:127
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:127
msgid "Please select at least one file."
msgstr "Seleccionar almenys un fitxer"
#: ../flask_admin/templates/bootstrap2/admin/file/rename.html:5
#: ../flask_admin/templates/bootstrap3/admin/file/rename.html:5
#, python-format
msgid "Please provide new name for %(name)s"
msgstr "Proporciona un nou nom per %(name)"
#: ../flask_admin/templates/bootstrap2/admin/model/create.html:5
#: ../flask_admin/templates/bootstrap3/admin/model/create.html:5
msgid "Save and Add"
msgstr "Guarda i Afegeix"
#: ../flask_admin/templates/bootstrap2/admin/model/create.html:16
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:16
#: ../flask_admin/templates/bootstrap3/admin/model/create.html:17
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:16
msgid "List"
msgstr "LLista"
#: ../flask_admin/templates/bootstrap2/admin/model/create.html:19
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:20
#: ../flask_admin/templates/bootstrap3/admin/model/create.html:20
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:20
msgid "Create"
msgstr "Crear"
#: ../flask_admin/templates/bootstrap2/admin/model/edit.html:5
#: ../flask_admin/templates/bootstrap3/admin/model/edit.html:5
msgid "Save and Continue"
msgstr "Guarda i Continua"
#: ../flask_admin/templates/bootstrap2/admin/model/inline_list_base.html:10
#: ../flask_admin/templates/bootstrap3/admin/model/inline_list_base.html:10
msgid "Delete?"
msgstr "Esborrar?"
#: ../flask_admin/templates/bootstrap2/admin/model/inline_list_base.html:33
#: ../flask_admin/templates/bootstrap3/admin/model/inline_list_base.html:33
msgid "Add"
msgstr "Afegir"
#: ../flask_admin/templates/bootstrap2/admin/model/layout.html:3
#: ../flask_admin/templates/bootstrap3/admin/model/layout.html:3
msgid "Add Filter"
msgstr "Afegir filtre"
#: ../flask_admin/templates/bootstrap2/admin/model/layout.html:17
#: ../flask_admin/templates/bootstrap3/admin/model/layout.html:17
msgid "Apply"
msgstr "Aplicar"
#: ../flask_admin/templates/bootstrap2/admin/model/layout.html:19
#: ../flask_admin/templates/bootstrap3/admin/model/layout.html:19
msgid "Reset Filters"
msgstr "restabliment de filtre"
#: ../flask_admin/templates/bootstrap2/admin/model/layout.html:38
#: ../flask_admin/templates/bootstrap2/admin/model/layout.html:45
#: ../flask_admin/templates/bootstrap3/admin/model/layout.html:38
#: ../flask_admin/templates/bootstrap3/admin/model/layout.html:45
msgid "Search"
msgstr "Cercar"
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:20
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:20
msgid "Create new record"
msgstr "Crea un nou registre"
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:56
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:56
msgid "Select all records"
msgstr "Seleccionar tots els registres"
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:67
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:76
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:67
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:76
#, python-format
msgid "Sort by %(name)s"
msgstr "Ordena per %(name)s"
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:98
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:98
msgid "Select record"
msgstr "Seleccionar registre"
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:105
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:105
msgid "Edit record"
msgstr "Editar registre"
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:114
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:114
msgid "Are you sure you want to delete this record?"
msgstr "Esteu segurs que voleu eliminar el registre?"
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:114
msgid "Delete record"
msgstr "Borrar registre"
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:150
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:150
msgid "Please select at least one record."
msgstr "Seleccionar almenys un registre."
...@@ -8,371 +8,517 @@ msgid "" ...@@ -8,371 +8,517 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Flask-Admin VERSION\n" "Project-Id-Version: Flask-Admin VERSION\n"
"Report-Msgid-Bugs-To: hamid.rs90{in}gmail\n" "Report-Msgid-Bugs-To: hamid.rs90{in}gmail\n"
"POT-Creation-Date: 2013-04-01 12:40+0430\n" "POT-Creation-Date: 2014-12-28 03:33-0600\n"
"PO-Revision-Date: 2013-04-02 01:13+0430\n" "PO-Revision-Date: 2013-04-02 01:13+0430\n"
"Last-Translator: hamid <hamid.rs90{in}gmail>\n" "Last-Translator: Mohammad Taha Jahangir\n"
"Language-Team: \n" "Language-Team: \n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " "Plural-Forms: nplurals=2; plural=(n != 1)\n"
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 0.9.6\n" "Generated-By: Babel 1.3\n"
#: ../flask_admin/base.py:310 #: ../flask_admin/base.py:419
msgid "Home" msgid "Home"
msgstr "خانه" msgstr "خانه"
#: ../flask_admin/form.py:83 #: ../flask_admin/contrib/fileadmin.py:34
msgid "Invalid time format"
msgstr "قالب زمان اشتباه است"
#: ../flask_admin/form.py:145
msgid "Invalid Choice: could not coerce"
msgstr "انتخاب اشتباه"
#: ../flask_admin/contrib/fileadmin.py:33
msgid "Invalid directory name" msgid "Invalid directory name"
msgstr "نام پوشه نادرست است" msgstr "نام پوشه نادرست است"
#: ../flask_admin/contrib/fileadmin.py:41 #: ../flask_admin/contrib/fileadmin.py:42
msgid "File to upload" msgid "File to upload"
msgstr "فایلی که باید آپلود شود" msgstr "فایلی که باید آپلود شود"
#: ../flask_admin/contrib/fileadmin.py:50 #: ../flask_admin/contrib/fileadmin.py:51
msgid "File required." msgid "File required."
msgstr "فایل لازم است" msgstr "فایل لازم است"
#: ../flask_admin/contrib/fileadmin.py:55 #: ../flask_admin/contrib/fileadmin.py:56
msgid "Invalid file type." msgid "Invalid file type."
msgstr "نوع فایل غیرمجاز است" msgstr "نوع فایل غیرمجاز است"
#: ../flask_admin/contrib/fileadmin.py:59 #: ../flask_admin/contrib/fileadmin.py:60
msgid "Content" msgid "Content"
msgstr "محتوا" msgstr "محتوا"
#: ../flask_admin/contrib/fileadmin.py:475 #: ../flask_admin/contrib/fileadmin.py:427
msgid "File uploading is disabled."
msgstr "آپلود فایل غیر فعال است"
#: ../flask_admin/contrib/fileadmin.py:484
#, python-format #, python-format
msgid "File \"%(name)s\" already exists." msgid "File \"%(name)s\" already exists."
msgstr "فایل \"%(name)s\" قبلا وجود داشته است" msgstr "فایل \"%(name)s\" قبلا وجود داشته است"
#: ../flask_admin/contrib/fileadmin.py:492 #: ../flask_admin/contrib/fileadmin.py:446
#: ../flask_admin/contrib/fileadmin.py:512
#: ../flask_admin/contrib/fileadmin.py:565
#: ../flask_admin/contrib/fileadmin.py:602
#: ../flask_admin/contrib/fileadmin.py:645
#: ../flask_admin/contrib/fileadmin.py:693
msgid "Permission denied."
msgstr "خطای دسترسی"
#: ../flask_admin/contrib/fileadmin.py:508
msgid "File uploading is disabled."
msgstr "آپلود فایل غیر فعال است"
#: ../flask_admin/contrib/fileadmin.py:521
#, python-format #, python-format
msgid "Failed to save file: %(error)s" msgid "Failed to save file: %(error)s"
msgstr "ذخیره ی فایل بامشکل روبرو شد %(error)s" msgstr "ذخیره ی فایل بامشکل روبرو شد %(error)s"
#: ../flask_admin/contrib/fileadmin.py:511 #: ../flask_admin/contrib/fileadmin.py:561
msgid "Directory creation is disabled." msgid "Directory creation is disabled."
msgstr "ساخت پوشه غیر فعال است" msgstr "ساخت پوشه غیر فعال است"
#: ../flask_admin/contrib/fileadmin.py:522 #: ../flask_admin/contrib/fileadmin.py:576
#, python-format #, python-format
msgid "Failed to create directory: %(error)s" msgid "Failed to create directory: %(error)s"
msgstr "ساخت پوشه ی با مشکل روبرو شد %(error)s" msgstr "ساخت پوشه ی با مشکل روبرو شد %(error)s"
#: ../flask_admin/contrib/fileadmin.py:544 #: ../flask_admin/contrib/fileadmin.py:598
msgid "Deletion is disabled." msgid "Deletion is disabled."
msgstr "حذف غیر فعال است" msgstr "حذف غیر فعال است"
#: ../flask_admin/contrib/fileadmin.py:549 #: ../flask_admin/contrib/fileadmin.py:607
msgid "Directory deletion is disabled." msgid "Directory deletion is disabled."
msgstr "حذف پوشه غیر فعال است" msgstr "حذف پوشه غیر فعال است"
#: ../flask_admin/contrib/fileadmin.py:555 #: ../flask_admin/contrib/fileadmin.py:613
#, python-format #, python-format
msgid "Directory \"%s\" was successfully deleted." msgid "Directory \"%(path)s\" was successfully deleted."
msgstr "پوشه ی \"%s\" با موفقیت حذف شد" msgstr "پوشه %(path)s با موفقیت حذف شد"
#: ../flask_admin/contrib/fileadmin.py:557 #: ../flask_admin/contrib/fileadmin.py:615
#, python-format #, python-format
msgid "Failed to delete directory: %(error)s" msgid "Failed to delete directory: %(error)s"
msgstr "حذف پوشه ی با مشکل روبرو شد %(error)s" msgstr "حذف پوشه ی با مشکل روبرو شد %(error)s"
#: ../flask_admin/contrib/fileadmin.py:562 #: ../flask_admin/contrib/fileadmin.py:620
#: ../flask_admin/contrib/fileadmin.py:685 #: ../flask_admin/contrib/fileadmin.py:759
#, python-format #, python-format
msgid "File \"%(name)s\" was successfully deleted." msgid "File \"%(name)s\" was successfully deleted."
msgstr "فایل \"%(name)s\" با موفقیت حذف شد" msgstr "فایل \"%(name)s\" با موفقیت حذف شد"
#: ../flask_admin/contrib/fileadmin.py:564 #: ../flask_admin/contrib/fileadmin.py:622
#: ../flask_admin/contrib/fileadmin.py:687 #: ../flask_admin/contrib/fileadmin.py:761
#, python-format #, python-format
msgid "Failed to delete file: %(name)s" msgid "Failed to delete file: %(name)s"
msgstr "حذف فایل%(name)s با مشکل روبرو شد" msgstr "حذف فایل%(name)s با مشکل روبرو شد"
#: ../flask_admin/contrib/fileadmin.py:583 #: ../flask_admin/contrib/fileadmin.py:641
msgid "Renaming is disabled." msgid "Renaming is disabled."
msgstr "تغییر نام غیر فعال است" msgstr "تغییر نام غیر فعال است"
#: ../flask_admin/contrib/fileadmin.py:587 #: ../flask_admin/contrib/fileadmin.py:649
msgid "Path does not exist." msgid "Path does not exist."
msgstr "مسیر وجود ندارد" msgstr "مسیر وجود ندارد"
#: ../flask_admin/contrib/fileadmin.py:599 #: ../flask_admin/contrib/fileadmin.py:661
#, python-format #, python-format
msgid "Successfully renamed \"%(src)s\" to \"%(dst)s\"" msgid "Successfully renamed \"%(src)s\" to \"%(dst)s\""
msgstr "با موفقیت از\"%(src)s\" به \"%(dst)s\" تغییر نام داده شد" msgstr "با موفقیت از\"%(src)s\" به \"%(dst)s\" تغییر نام داده شد"
#: ../flask_admin/contrib/fileadmin.py:602 #: ../flask_admin/contrib/fileadmin.py:664
#, python-format #, python-format
msgid "Failed to rename: %(error)s" msgid "Failed to rename: %(error)s"
msgstr "تغییر نام با مشکل روبرو شد %(error)s" msgstr "تغییر نام با مشکل روبرو شد %(error)s"
#: ../flask_admin/contrib/fileadmin.py:640 #: ../flask_admin/contrib/fileadmin.py:709
#, python-format #, python-format
msgid "Error saving changes to %(name)s." msgid "Error saving changes to %(name)s."
msgstr "مشکل در ذخیره ی تغییرات %(name)s" msgstr "مشکل در ذخیره ی تغییرات %(name)s"
#: ../flask_admin/contrib/fileadmin.py:644 #: ../flask_admin/contrib/fileadmin.py:713
#, python-format #, python-format
msgid "Changes to %(name)s saved successfully." msgid "Changes to %(name)s saved successfully."
msgstr "تغییرات%(name)s با موفقیت صورت گرفت" msgstr "تغییرات%(name)s با موفقیت صورت گرفت"
#: ../flask_admin/contrib/fileadmin.py:651 #: ../flask_admin/contrib/fileadmin.py:720
#, python-format #, python-format
msgid "Error reading %(name)s." msgid "Error reading %(name)s."
msgstr "مشکل در خواندن %(name)s" msgstr "مشکل در خواندن %(name)s"
#: ../flask_admin/contrib/fileadmin.py:654 #: ../flask_admin/contrib/fileadmin.py:723
#: ../flask_admin/contrib/fileadmin.py:663 #: ../flask_admin/contrib/fileadmin.py:732
#, python-format #, python-format
msgid "Unexpected error while reading from %(name)s" msgid "Unexpected error while reading from %(name)s"
msgstr "خطای غیر منتظره در حال خواندن %(name)s" msgstr "خطای غیر منتظره در حال خواندن %(name)s"
#: ../flask_admin/contrib/fileadmin.py:660 #: ../flask_admin/contrib/fileadmin.py:729
#, python-format #, python-format
msgid "Cannot edit %(name)s." msgid "Cannot edit %(name)s."
msgstr "عدم توانایی در ویرایش %(name)s" msgstr "عدم توانایی در ویرایش %(name)s"
#: ../flask_admin/contrib/fileadmin.py:677 #: ../flask_admin/contrib/fileadmin.py:746
#: ../flask_admin/contrib/mongoengine/view.py:389 #: ../flask_admin/contrib/mongoengine/view.py:599
#: ../flask_admin/contrib/peewee/view.py:375 #: ../flask_admin/contrib/peewee/view.py:406
#: ../flask_admin/contrib/pymongo/view.py:326 #: ../flask_admin/contrib/pymongo/view.py:343
#: ../flask_admin/contrib/sqla/view.py:787 #: ../flask_admin/contrib/sqla/view.py:932
msgid "Delete" msgid "Delete"
msgstr "حذف" msgstr "حذف"
#: ../flask_admin/contrib/fileadmin.py:678 #: ../flask_admin/contrib/fileadmin.py:747
msgid "Are you sure you want to delete these files?" msgid "Are you sure you want to delete these files?"
msgstr "آیا از حذف این فایل ها اطمینان دارید؟" msgstr "آیا از حذف این فایل ها اطمینان دارید؟"
#: ../flask_admin/contrib/fileadmin.py:689 #: ../flask_admin/contrib/fileadmin.py:750
msgid "File deletion is disabled."
msgstr "امکان حذف فایل وجود ندارد"
#: ../flask_admin/contrib/fileadmin.py:763
msgid "Edit" msgid "Edit"
msgstr "ویرایش" msgstr "ویرایش"
#: ../flask_admin/contrib/mongoengine/filters.py:36 #: ../flask_admin/contrib/rediscli.py:125
#: ../flask_admin/contrib/peewee/filters.py:35 msgid "Cli: Invalid command."
msgstr "Cli: دستور نامعتبر است."
#: ../flask_admin/contrib/geoa/fields.py:29
msgid "Invalid JSON"
msgstr "JSON نامعتبر است"
#: ../flask_admin/contrib/mongoengine/filters.py:38
#: ../flask_admin/contrib/peewee/filters.py:38
#: ../flask_admin/contrib/pymongo/filters.py:38 #: ../flask_admin/contrib/pymongo/filters.py:38
#: ../flask_admin/contrib/sqla/filters.py:36 #: ../flask_admin/contrib/sqla/filters.py:38
msgid "equals" msgid "equals"
msgstr "برابر با" msgstr "برابر با"
#: ../flask_admin/contrib/mongoengine/filters.py:45 #: ../flask_admin/contrib/mongoengine/filters.py:47
#: ../flask_admin/contrib/peewee/filters.py:43 #: ../flask_admin/contrib/peewee/filters.py:46
#: ../flask_admin/contrib/pymongo/filters.py:47 #: ../flask_admin/contrib/pymongo/filters.py:47
#: ../flask_admin/contrib/sqla/filters.py:44 #: ../flask_admin/contrib/sqla/filters.py:46
msgid "not equal" msgid "not equal"
msgstr "برابر نیست با" msgstr "برابر نیست با"
#: ../flask_admin/contrib/mongoengine/filters.py:55 #: ../flask_admin/contrib/mongoengine/filters.py:57
#: ../flask_admin/contrib/peewee/filters.py:52 #: ../flask_admin/contrib/peewee/filters.py:55
#: ../flask_admin/contrib/pymongo/filters.py:57 #: ../flask_admin/contrib/pymongo/filters.py:57
#: ../flask_admin/contrib/sqla/filters.py:53 #: ../flask_admin/contrib/sqla/filters.py:55
msgid "contains" msgid "contains"
msgstr "محتوی" msgstr "محتوی"
#: ../flask_admin/contrib/mongoengine/filters.py:65 #: ../flask_admin/contrib/mongoengine/filters.py:67
#: ../flask_admin/contrib/peewee/filters.py:61 #: ../flask_admin/contrib/peewee/filters.py:64
#: ../flask_admin/contrib/pymongo/filters.py:67 #: ../flask_admin/contrib/pymongo/filters.py:67
#: ../flask_admin/contrib/sqla/filters.py:62 #: ../flask_admin/contrib/sqla/filters.py:64
msgid "not contains" msgid "not contains"
msgstr "محتوی نیست" msgstr "محتوی نیست"
#: ../flask_admin/contrib/mongoengine/filters.py:74 #: ../flask_admin/contrib/mongoengine/filters.py:76
#: ../flask_admin/contrib/peewee/filters.py:69 #: ../flask_admin/contrib/peewee/filters.py:72
#: ../flask_admin/contrib/pymongo/filters.py:76 #: ../flask_admin/contrib/pymongo/filters.py:80
#: ../flask_admin/contrib/sqla/filters.py:70 #: ../flask_admin/contrib/sqla/filters.py:72
msgid "greater than" msgid "greater than"
msgstr "بزرگتر از" msgstr "بزرگتر از"
#: ../flask_admin/contrib/mongoengine/filters.py:83 #: ../flask_admin/contrib/mongoengine/filters.py:85
#: ../flask_admin/contrib/peewee/filters.py:77 #: ../flask_admin/contrib/peewee/filters.py:80
#: ../flask_admin/contrib/pymongo/filters.py:85 #: ../flask_admin/contrib/pymongo/filters.py:93
#: ../flask_admin/contrib/sqla/filters.py:78 #: ../flask_admin/contrib/sqla/filters.py:80
msgid "smaller than" msgid "smaller than"
msgstr "کوچکتر از" msgstr "کوچکتر از"
#: ../flask_admin/contrib/mongoengine/view.py:338 #: ../flask_admin/contrib/mongoengine/filters.py:97
#: ../flask_admin/contrib/peewee/view.py:337 #: ../flask_admin/contrib/peewee/filters.py:91
#: ../flask_admin/contrib/pymongo/view.py:268 #: ../flask_admin/contrib/sqla/filters.py:91
#: ../flask_admin/contrib/sqla/view.py:734 msgid "empty"
msgstr "خالی باشد"
#: ../flask_admin/contrib/mongoengine/filters.py:112
#: ../flask_admin/contrib/peewee/filters.py:105
#: ../flask_admin/contrib/sqla/filters.py:105
msgid "in list"
msgstr "در این لیست باشد"
#: ../flask_admin/contrib/mongoengine/filters.py:121
#: ../flask_admin/contrib/peewee/filters.py:114
#: ../flask_admin/contrib/sqla/filters.py:114
msgid "not in list"
msgstr "در این لیست نباشد"
#: ../flask_admin/contrib/mongoengine/filters.py:221
#: ../flask_admin/contrib/peewee/filters.py:208
#: ../flask_admin/contrib/peewee/filters.py:245
#: ../flask_admin/contrib/peewee/filters.py:282
#: ../flask_admin/contrib/sqla/filters.py:209
#: ../flask_admin/contrib/sqla/filters.py:246
#: ../flask_admin/contrib/sqla/filters.py:283
msgid "not between"
msgstr "بین این دو نباشد"
#: ../flask_admin/contrib/mongoengine/view.py:493
#, python-format
msgid "Failed to get model. %(error)s"
msgstr "خواندن مدل با مشکل روبرو شد. %(error)s"
#: ../flask_admin/contrib/mongoengine/view.py:512
#: ../flask_admin/contrib/peewee/view.py:357
#: ../flask_admin/contrib/pymongo/view.py:278
#: ../flask_admin/contrib/sqla/view.py:864
#, python-format #, python-format
msgid "Failed to create record. %(error)s" msgid "Failed to create record. %(error)s"
msgstr "ساخت مدل با مشکل روبرو شد %(error)s" msgstr "ساخت مدل با مشکل روبرو شد %(error)s"
#: ../flask_admin/contrib/mongoengine/view.py:358 #: ../flask_admin/contrib/mongoengine/view.py:538
#: ../flask_admin/contrib/peewee/view.py:352 #: ../flask_admin/contrib/peewee/view.py:376
#: ../flask_admin/contrib/pymongo/view.py:290 #: ../flask_admin/contrib/pymongo/view.py:303
#: ../flask_admin/contrib/sqla/view.py:754 #: ../flask_admin/contrib/sqla/view.py:890
#, python-format #, python-format
msgid "Failed to update record. %(error)s" msgid "Failed to update record. %(error)s"
msgstr "بروزرسانی مدل با مشکل روبرو شد%(error)s" msgstr "بروزرسانی مدل با مشکل روبرو شد%(error)s"
#: ../flask_admin/contrib/mongoengine/view.py:375 #: ../flask_admin/contrib/mongoengine/view.py:562
#: ../flask_admin/contrib/peewee/view.py:362 #: ../flask_admin/contrib/peewee/view.py:392
#: ../flask_admin/contrib/pymongo/view.py:312 #: ../flask_admin/contrib/pymongo/view.py:329
#: ../flask_admin/contrib/sqla/view.py:773 #: ../flask_admin/contrib/sqla/view.py:916
#, python-format #, python-format
msgid "Failed to delete record. %(error)s" msgid "Failed to delete record. %(error)s"
msgstr "حذف مدل با مشکل روبرو شد %(error)s" msgstr "حذف مدل با مشکل روبرو شد %(error)s"
#: ../flask_admin/contrib/mongoengine/view.py:390 #: ../flask_admin/contrib/mongoengine/view.py:600
#: ../flask_admin/contrib/peewee/view.py:376 #: ../flask_admin/contrib/peewee/view.py:407
#: ../flask_admin/contrib/pymongo/view.py:327 #: ../flask_admin/contrib/pymongo/view.py:344
#: ../flask_admin/contrib/sqla/view.py:788 #: ../flask_admin/contrib/sqla/view.py:933
msgid "Are you sure you want to delete selected records?" msgid "Are you sure you want to delete selected records?"
msgstr "آیا از خذف مدل ها اطمینان دارید" msgstr "آیا از حذف مدل ها اطمینان دارید"
#: ../flask_admin/contrib/mongoengine/view.py:400 #: ../flask_admin/contrib/mongoengine/view.py:609
#: ../flask_admin/contrib/peewee/view.py:392 #: ../flask_admin/contrib/peewee/view.py:423
#: ../flask_admin/contrib/pymongo/view.py:337 #: ../flask_admin/contrib/pymongo/view.py:354
#: ../flask_admin/contrib/sqla/view.py:806 #: ../flask_admin/contrib/sqla/view.py:949
#, python-format #, python-format
msgid "Record was successfully deleted." msgid "Record was successfully deleted."
msgid_plural "%(count)s records were successfully deleted." msgid_plural "%(count)s records were successfully deleted."
msgstr[0] "مدل با موفقیت حذف شد" msgstr[0] "مدل با موفقیت حذف شد"
msgstr[1] "%(count)s مدل با موفقیت خذف شدند" msgstr[1] "%(count)s مدل با موفقیت حذف شد."
msgstr[2] ""
#: ../flask_admin/contrib/mongoengine/view.py:405 #: ../flask_admin/contrib/mongoengine/view.py:615
#: ../flask_admin/contrib/peewee/view.py:397 #: ../flask_admin/contrib/peewee/view.py:429
#: ../flask_admin/contrib/pymongo/view.py:342 #: ../flask_admin/contrib/pymongo/view.py:359
#: ../flask_admin/contrib/sqla/view.py:811 #: ../flask_admin/contrib/sqla/view.py:957
#, python-format #, python-format
msgid "Failed to delete records. %(error)s" msgid "Failed to delete records. %(error)s"
msgstr "حذف مدل ها با مشکل روبرو شد %(error)s" msgstr "حذف مدل ها با مشکل روبرو شد %(error)s"
#: ../flask_admin/contrib/sqla/fields.py:125 #: ../flask_admin/contrib/sqla/fields.py:123
#: ../flask_admin/contrib/sqla/fields.py:175 #: ../flask_admin/contrib/sqla/fields.py:173
#: ../flask_admin/contrib/sqla/fields.py:180 #: ../flask_admin/contrib/sqla/fields.py:178 ../flask_admin/model/fields.py:167
#: ../flask_admin/model/fields.py:216
msgid "Not a valid choice" msgid "Not a valid choice"
msgstr "انتخاب مناسبی نیست" msgstr "انتخاب مناسبی نیست"
#: ../flask_admin/contrib/sqla/validators.py:33 #: ../flask_admin/contrib/sqla/validators.py:42
msgid "Already exists." msgid "Already exists."
msgstr "قبلا وجود داشته است" msgstr "قبلا وجود داشته است"
#: ../flask_admin/model/base.py:947 #: ../flask_admin/contrib/sqla/validators.py:60
#, python-format
msgid "At least %d item is required"
msgid_plural "At least %d items are required"
msgstr[0] "حداقل یک مورد لازم است"
msgstr[1] "حداقل %d مورد لازم است"
#: ../flask_admin/contrib/sqla/view.py:843
#, python-format
msgid "Integrity error. %(message)s"
msgstr "خطای یکپارچگی داده. %(message)s"
#: ../flask_admin/form/fields.py:92
msgid "Invalid time format"
msgstr "قالب زمان اشتباه است"
#: ../flask_admin/form/fields.py:138
msgid "Invalid Choice: could not coerce"
msgstr "انتخاب اشتباه"
#: ../flask_admin/form/upload.py:187
msgid "Invalid file extension"
msgstr "پسوند فایل غیرمجاز است"
#: ../flask_admin/model/base.py:1103
msgid "There are no items in the table."
msgstr "هیچ موردی در این جدول وجود ندارد."
#: ../flask_admin/model/base.py:1127
#, python-format
msgid "Invalid Filter Value: %(value)s"
msgstr "مقدار فیلتر معتبر نیست: %(value)s"
#: ../flask_admin/model/base.py:1339
msgid "Record was successfully created." msgid "Record was successfully created."
msgstr "مدل با موفقیت ساخته شد" msgstr "مدل با موفقیت ساخته شد"
#: ../flask_admin/model/filters.py:85 #: ../flask_admin/model/base.py:1376
msgid "Record was successfully saved."
msgstr "مدل با موفقیت ذخیره شد"
#: ../flask_admin/model/filters.py:99
msgid "Yes" msgid "Yes"
msgstr "بله" msgstr "بله"
#: ../flask_admin/model/filters.py:86 #: ../flask_admin/model/filters.py:100
msgid "No" msgid "No"
msgstr "خیر" msgstr "خیر"
#: ../flask_admin/templates/admin/actions.html:2 #: ../flask_admin/model/filters.py:162 ../flask_admin/model/filters.py:202
#: ../flask_admin/model/filters.py:247
msgid "between"
msgstr "بین این دو باشد"
#: ../flask_admin/templates/bootstrap2/admin/actions.html:4
#: ../flask_admin/templates/bootstrap3/admin/actions.html:4
msgid "With selected" msgid "With selected"
msgstr "با انتخاب شده ها" msgstr "با انتخاب شده ها"
#: ../flask_admin/templates/admin/lib.html:130 #: ../flask_admin/templates/bootstrap2/admin/lib.html:156
#: ../flask_admin/templates/bootstrap3/admin/lib.html:149
msgid "Submit" msgid "Submit"
msgstr "اعمال" msgstr "اعمال"
#: ../flask_admin/templates/admin/lib.html:135 #: ../flask_admin/templates/bootstrap2/admin/lib.html:161
#: ../flask_admin/templates/bootstrap3/admin/lib.html:154
msgid "Cancel" msgid "Cancel"
msgstr "لغو" msgstr "لغو"
#: ../flask_admin/templates/admin/file/edit.html:5 #: ../flask_admin/templates/bootstrap2/admin/file/edit.html:5
#: ../flask_admin/templates/bootstrap3/admin/file/edit.html:5
#, python-format #, python-format
msgid "You are editing %(path)s" msgid "You are editing %(path)s"
msgstr "شما در حال ویرایش %(path)s هستید" msgstr "شما در حال ویرایش %(path)s هستید"
#: ../flask_admin/templates/admin/file/list.html:8 #: ../flask_admin/templates/bootstrap2/admin/file/list.html:9
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:9
msgid "Root" msgid "Root"
msgstr "ریشه" msgstr "ریشه"
#: ../flask_admin/templates/admin/file/list.html:55 #: ../flask_admin/templates/bootstrap2/admin/file/list.html:62
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:62
#, python-format #, python-format
msgid "Are you sure you want to delete \\'%(name)s\\' recursively?" msgid "Are you sure you want to delete \\'%(name)s\\' recursively?"
msgstr "آیا از حذف محتویات \\'%(name)s\\' اطمینان دارید؟" msgstr "آیا از حذف محتویات \\'%(name)s\\' اطمینان دارید؟"
#: ../flask_admin/templates/admin/file/list.html:63 #: ../flask_admin/templates/bootstrap2/admin/file/list.html:70
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:70
#, python-format #, python-format
msgid "Are you sure you want to delete \\'%(name)s\\'?" msgid "Are you sure you want to delete \\'%(name)s\\'?"
msgstr "آیا از حذف \\'%(name)s\\'اطمینان دارید؟" msgstr "آیا از حذف \\'%(name)s\\'اطمینان دارید؟"
#: ../flask_admin/templates/admin/file/list.html:90 #: ../flask_admin/templates/bootstrap2/admin/file/list.html:105
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:105
msgid "Upload File" msgid "Upload File"
msgstr "آپلود فایل" msgstr "آپلود فایل"
#: ../flask_admin/templates/admin/file/list.html:95 #: ../flask_admin/templates/bootstrap2/admin/file/list.html:110
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:110
msgid "Create Directory" msgid "Create Directory"
msgstr "ایجاد پوشه" msgstr "ایجاد پوشه"
#: ../flask_admin/templates/admin/file/list.html:109 #: ../flask_admin/templates/bootstrap2/admin/file/list.html:127
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:127
msgid "Please select at least one file." msgid "Please select at least one file."
msgstr "حداقل یک فایل انتخاب کنید" msgstr "حداقل یک فایل انتخاب کنید"
#: ../flask_admin/templates/admin/file/rename.html:5 #: ../flask_admin/templates/bootstrap2/admin/file/rename.html:5
#: ../flask_admin/templates/bootstrap3/admin/file/rename.html:5
#, python-format #, python-format
msgid "Please provide new name for %(name)s" msgid "Please provide new name for %(name)s"
msgstr "لطفا یک نام جدید%(name)s برای وارد کنید" msgstr "لطفا یک نام جدید%(name)s برای وارد کنید"
#: ../flask_admin/templates/admin/model/create.html:5 #: ../flask_admin/templates/bootstrap2/admin/model/create.html:5
#: ../flask_admin/templates/bootstrap3/admin/model/create.html:5
msgid "Save and Add" msgid "Save and Add"
msgstr "ذخیره و اضافه" msgstr "ذخیره و اضافه"
#: ../flask_admin/templates/admin/model/create.html:17 #: ../flask_admin/templates/bootstrap2/admin/model/create.html:16
#: ../flask_admin/templates/admin/model/list.html:16 #: ../flask_admin/templates/bootstrap2/admin/model/list.html:16
#: ../flask_admin/templates/bootstrap3/admin/model/create.html:17
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:16
msgid "List" msgid "List"
msgstr "لیست" msgstr "لیست"
#: ../flask_admin/templates/admin/model/create.html:20 #: ../flask_admin/templates/bootstrap2/admin/model/create.html:19
#: ../flask_admin/templates/admin/model/list.html:20 #: ../flask_admin/templates/bootstrap2/admin/model/list.html:20
#: ../flask_admin/templates/bootstrap3/admin/model/create.html:20
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:20
msgid "Create" msgid "Create"
msgstr "ایجاد" msgstr "ایجاد"
#: ../flask_admin/templates/admin/model/inline_list_base.html:18 #: ../flask_admin/templates/bootstrap2/admin/model/edit.html:5
#: ../flask_admin/templates/bootstrap3/admin/model/edit.html:5
msgid "Save and Continue"
msgstr "ذخیره و ادامه"
#: ../flask_admin/templates/bootstrap2/admin/model/inline_list_base.html:10
#: ../flask_admin/templates/bootstrap3/admin/model/inline_list_base.html:10
msgid "Delete?" msgid "Delete?"
msgstr "حذف؟" msgstr "حذف؟"
#: ../flask_admin/templates/admin/model/inline_list_base.html:26 #: ../flask_admin/templates/bootstrap2/admin/model/inline_list_base.html:33
#: ../flask_admin/templates/bootstrap3/admin/model/inline_list_base.html:33
msgid "Add" msgid "Add"
msgstr "اضافه" msgstr "اضافه"
#: ../flask_admin/templates/admin/model/layout.html:3 #: ../flask_admin/templates/bootstrap2/admin/model/layout.html:3
#: ../flask_admin/templates/bootstrap3/admin/model/layout.html:3
msgid "Add Filter" msgid "Add Filter"
msgstr "اضافه کردن فیلتر" msgstr "اضافه کردن فیلتر"
#: ../flask_admin/templates/admin/model/layout.html:17 #: ../flask_admin/templates/bootstrap2/admin/model/layout.html:17
#: ../flask_admin/templates/bootstrap3/admin/model/layout.html:17
msgid "Apply" msgid "Apply"
msgstr "اعمال" msgstr "اعمال"
#: ../flask_admin/templates/admin/model/layout.html:19 #: ../flask_admin/templates/bootstrap2/admin/model/layout.html:19
#: ../flask_admin/templates/bootstrap3/admin/model/layout.html:19
msgid "Reset Filters" msgid "Reset Filters"
msgstr "برگرداندن فیلتر ها به حالت اول" msgstr "برگرداندن فیلتر ها به حالت اول"
#: ../flask_admin/templates/admin/model/layout.html:28 #: ../flask_admin/templates/bootstrap2/admin/model/layout.html:38
msgid "Remove Filter" #: ../flask_admin/templates/bootstrap2/admin/model/layout.html:45
msgstr "حذف فیلتر" #: ../flask_admin/templates/bootstrap3/admin/model/layout.html:38
#: ../flask_admin/templates/bootstrap3/admin/model/layout.html:45
#: ../flask_admin/templates/admin/model/layout.html:67
msgid "Search" msgid "Search"
msgstr "جستجو" msgstr "جستجو"
#: ../flask_admin/templates/admin/model/list.html:108 #: ../flask_admin/templates/bootstrap2/admin/model/list.html:20
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:20
msgid "Create new record"
msgstr "ایجاد رکورد جدید"
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:56
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:56
msgid "Select all records"
msgstr "انتخاب همه رکوردها"
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:67
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:76
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:67
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:76
#, python-format
msgid "Sort by %(name)s"
msgstr "مرتب سازی بر اساس %(name)s"
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:98
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:98
msgid "Select record"
msgstr "انتخاب رکورد"
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:105
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:105
msgid "Edit record"
msgstr "ویرایش رکورد"
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:114
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:114
msgid "Are you sure you want to delete this record?" msgid "Are you sure you want to delete this record?"
msgstr "آیا از حذف این موارد اطمینان دارید؟" msgstr "آیا از حذف این موارد اطمینان دارید؟"
#: ../flask_admin/templates/admin/model/list.html:133 #: ../flask_admin/templates/bootstrap2/admin/model/list.html:114
msgid "Delete record"
msgstr "حذف رکورد"
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:150
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:150
msgid "Please select at least one record." msgid "Please select at least one record."
msgstr "حداقل یک پوشه انتخاب کنید" msgstr "حداقل یک پوشه انتخاب کنید"
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