소스 검색

initial commit

Nanak Tattyrek 10 년 전
커밋
9322171d75
100개의 변경된 파일8523개의 추가작업 그리고 0개의 파일을 삭제
  1. BIN
      src/.DS_Store
  2. 83 0
      src/index.py
  3. 663 0
      src/static/css/bootstrap-editable.css
  4. 347 0
      src/static/css/bootstrap-theme.css
  5. 0 0
      src/static/css/bootstrap-theme.css.map
  6. 6 0
      src/static/css/bootstrap-theme.min.css
  7. 768 0
      src/static/css/bootstrap.css
  8. 0 0
      src/static/css/bootstrap.css.map
  9. 6 0
      src/static/css/bootstrap.min.css
  10. 158 0
      src/static/css/carousel.css
  11. 23 0
      src/static/css/editablegrid.css
  12. BIN
      src/static/img/down.png
  13. BIN
      src/static/img/up.png
  14. 2244 0
      src/static/js/editablegrid.js
  15. 428 0
      src/static/js/editablegrid_charts.js
  16. 379 0
      src/static/js/editablegrid_charts_ofc.js
  17. 408 0
      src/static/js/editablegrid_editors.js
  18. 292 0
      src/static/js/editablegrid_renderers.js
  19. 774 0
      src/static/js/editablegrid_utils.js
  20. 74 0
      src/static/js/editablegrid_validators.js
  21. 1 0
      src/static/js/jquery-1.11.1.min.js
  22. 17 0
      src/templates/delete.html
  23. 187 0
      src/templates/index.html
  24. 170 0
      src/templates/index_old.html
  25. 29 0
      src/templates/insert.html
  26. 1 0
      src/templates/ok.txt
  27. 33 0
      src/templates/update.html
  28. BIN
      venv/.DS_Store
  29. 1 0
      venv/.Python
  30. 80 0
      venv/bin/activate
  31. 42 0
      venv/bin/activate.csh
  32. 74 0
      venv/bin/activate.fish
  33. 34 0
      venv/bin/activate_this.py
  34. 11 0
      venv/bin/easy_install
  35. 11 0
      venv/bin/easy_install-2.7
  36. 11 0
      venv/bin/pip
  37. 11 0
      venv/bin/pip2
  38. 11 0
      venv/bin/pip2.7
  39. BIN
      venv/bin/python
  40. 1 0
      venv/bin/python2
  41. 1 0
      venv/bin/python2.7
  42. 1 0
      venv/include/python2.7
  43. 1 0
      venv/lib/python2.7/UserDict.py
  44. BIN
      venv/lib/python2.7/UserDict.pyc
  45. 1 0
      venv/lib/python2.7/_abcoll.py
  46. BIN
      venv/lib/python2.7/_abcoll.pyc
  47. 1 0
      venv/lib/python2.7/_weakrefset.py
  48. BIN
      venv/lib/python2.7/_weakrefset.pyc
  49. 1 0
      venv/lib/python2.7/abc.py
  50. BIN
      venv/lib/python2.7/abc.pyc
  51. 1 0
      venv/lib/python2.7/codecs.py
  52. BIN
      venv/lib/python2.7/codecs.pyc
  53. 1 0
      venv/lib/python2.7/config
  54. 1 0
      venv/lib/python2.7/copy_reg.py
  55. BIN
      venv/lib/python2.7/copy_reg.pyc
  56. 101 0
      venv/lib/python2.7/distutils/__init__.py
  57. BIN
      venv/lib/python2.7/distutils/__init__.pyc
  58. 6 0
      venv/lib/python2.7/distutils/distutils.cfg
  59. 1 0
      venv/lib/python2.7/encodings
  60. 1 0
      venv/lib/python2.7/fnmatch.py
  61. BIN
      venv/lib/python2.7/fnmatch.pyc
  62. 1 0
      venv/lib/python2.7/genericpath.py
  63. BIN
      venv/lib/python2.7/genericpath.pyc
  64. 1 0
      venv/lib/python2.7/lib-dynload
  65. 1 0
      venv/lib/python2.7/linecache.py
  66. BIN
      venv/lib/python2.7/linecache.pyc
  67. 1 0
      venv/lib/python2.7/locale.py
  68. BIN
      venv/lib/python2.7/locale.pyc
  69. 0 0
      venv/lib/python2.7/no-global-site-packages.txt
  70. 1 0
      venv/lib/python2.7/ntpath.py
  71. 1 0
      venv/lib/python2.7/orig-prefix.txt
  72. 1 0
      venv/lib/python2.7/os.py
  73. BIN
      venv/lib/python2.7/os.pyc
  74. 1 0
      venv/lib/python2.7/posixpath.py
  75. BIN
      venv/lib/python2.7/posixpath.pyc
  76. 1 0
      venv/lib/python2.7/re.py
  77. BIN
      venv/lib/python2.7/re.pyc
  78. 58 0
      venv/lib/python2.7/site-packages/Flask-0.10.1-py2.7.egg-info/PKG-INFO
  79. 238 0
      venv/lib/python2.7/site-packages/Flask-0.10.1-py2.7.egg-info/SOURCES.txt
  80. 1 0
      venv/lib/python2.7/site-packages/Flask-0.10.1-py2.7.egg-info/dependency_links.txt
  81. 148 0
      venv/lib/python2.7/site-packages/Flask-0.10.1-py2.7.egg-info/installed-files.txt
  82. 1 0
      venv/lib/python2.7/site-packages/Flask-0.10.1-py2.7.egg-info/not-zip-safe
  83. 3 0
      venv/lib/python2.7/site-packages/Flask-0.10.1-py2.7.egg-info/requires.txt
  84. 1 0
      venv/lib/python2.7/site-packages/Flask-0.10.1-py2.7.egg-info/top_level.txt
  85. 55 0
      venv/lib/python2.7/site-packages/Jinja2-2.7.3-py2.7.egg-info/PKG-INFO
  86. 126 0
      venv/lib/python2.7/site-packages/Jinja2-2.7.3-py2.7.egg-info/SOURCES.txt
  87. 1 0
      venv/lib/python2.7/site-packages/Jinja2-2.7.3-py2.7.egg-info/dependency_links.txt
  88. 4 0
      venv/lib/python2.7/site-packages/Jinja2-2.7.3-py2.7.egg-info/entry_points.txt
  89. 92 0
      venv/lib/python2.7/site-packages/Jinja2-2.7.3-py2.7.egg-info/installed-files.txt
  90. 1 0
      venv/lib/python2.7/site-packages/Jinja2-2.7.3-py2.7.egg-info/not-zip-safe
  91. 4 0
      venv/lib/python2.7/site-packages/Jinja2-2.7.3-py2.7.egg-info/requires.txt
  92. 1 0
      venv/lib/python2.7/site-packages/Jinja2-2.7.3-py2.7.egg-info/top_level.txt
  93. 119 0
      venv/lib/python2.7/site-packages/MarkupSafe-0.23-py2.7.egg-info/PKG-INFO
  94. 17 0
      venv/lib/python2.7/site-packages/MarkupSafe-0.23-py2.7.egg-info/SOURCES.txt
  95. 1 0
      venv/lib/python2.7/site-packages/MarkupSafe-0.23-py2.7.egg-info/dependency_links.txt
  96. 18 0
      venv/lib/python2.7/site-packages/MarkupSafe-0.23-py2.7.egg-info/installed-files.txt
  97. 1 0
      venv/lib/python2.7/site-packages/MarkupSafe-0.23-py2.7.egg-info/not-zip-safe
  98. 1 0
      venv/lib/python2.7/site-packages/MarkupSafe-0.23-py2.7.egg-info/top_level.txt
  99. 54 0
      venv/lib/python2.7/site-packages/Werkzeug-0.10.4.dist-info/DESCRIPTION.rst
  100. 73 0
      venv/lib/python2.7/site-packages/Werkzeug-0.10.4.dist-info/METADATA

BIN
src/.DS_Store


+ 83 - 0
src/index.py

@@ -0,0 +1,83 @@
+from flask import Flask, render_template, request, redirect
+import os
+from pymongo import MongoClient
+from bson.objectid import ObjectId
+
+
+def connect():
+    connection = MongoClient("localhost", 27017)
+    handle = connection["dpTapeDB"]
+    handle.authenticate("gui", "GahN7chi")
+    return handle
+
+app = Flask(__name__)
+handle = connect()
+
+
+@app.route("/index", methods=['GET'])
+@app.route("/", methods=['GET'])
+def index():
+    results = [x for x in handle.digilst.find()[0:5]]
+    return render_template('index.html', results=results)
+
+
+@app.route("/insert", methods=['GET'])
+def insert():
+    return render_template("insert.html")
+
+
+@app.route("/update", methods=['GET'])
+def update():
+    return render_template("update.html")
+
+
+@app.route("/delete", methods=['GET'])
+def delete():
+    return render_template("delete.html")
+
+
+@app.route("/write_insert", methods=['POST'])
+def write_insert():
+    date = request.form.get("date")
+    date2 = request.form.get("date2")
+    titel = request.form.get("titel")
+    languages = request.form.get("languages")
+    country = request.form.get("country")
+    city = request.form.get("city")
+    place = request.form.get("place")
+    category = request.form.get("category")
+    duration = request.form.get("duration")
+    location = request.form.get("location")
+    format = request.form.get("format")
+    note = request.form.get("note")
+    status = request.form.get("status")
+
+    handle.digilst.insert({"Date": date, "Date2": date2,
+                           "Titel": titel, "Languages": languages,
+                           "Country": country, "City": city, "Place": place,
+                           "Category": category, "Duration": duration,
+                           "Location": location, "Format": format,
+                           "Note": note, "Status": status})
+    return redirect("/insert")
+
+
+@app.route("/write_update", methods=['POST'])
+def write_update():
+    mongoid = ObjectId(request.form.get("mongoid"))
+    field = request.form.get("field")
+    value = request.form.get("value")
+    handle.digilst.update({"_id": mongoid}, {"$set": {field: value}}, upsert=False)
+    return render_template("ok.txt")
+
+
+@app.route("/write_delete", methods=['POST'])
+def write_delete():
+    mongoid = ObjectId(request.form.get("mongoid"))
+    handle.digilst.remove({"_id": mongoid})
+    return redirect("/delete")
+
+
+if __name__ == '__main__':
+    port = int(os.environ.get('PORT', 5000))
+
+    app.run(host='0.0.0.0', port=port, debug=True)

+ 663 - 0
src/static/css/bootstrap-editable.css

@@ -0,0 +1,663 @@
+/*! 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;
+}

+ 347 - 0
src/static/css/bootstrap-theme.css

@@ -0,0 +1,347 @@
+/*!
+ * Bootstrap v3.1.1 (http://getbootstrap.com)
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ */
+
+.btn-default,
+.btn-primary,
+.btn-success,
+.btn-info,
+.btn-warning,
+.btn-danger {
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, .2);
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
+          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
+}
+.btn-default:active,
+.btn-primary:active,
+.btn-success:active,
+.btn-info:active,
+.btn-warning:active,
+.btn-danger:active,
+.btn-default.active,
+.btn-primary.active,
+.btn-success.active,
+.btn-info.active,
+.btn-warning.active,
+.btn-danger.active {
+  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
+          box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
+}
+.btn:active,
+.btn.active {
+  background-image: none;
+}
+.btn-default {
+  text-shadow: 0 1px 0 #fff;
+  background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%);
+  background-image:         linear-gradient(to bottom, #fff 0%, #e0e0e0 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-color: #dbdbdb;
+  border-color: #ccc;
+}
+.btn-default:hover,
+.btn-default:focus {
+  background-color: #e0e0e0;
+  background-position: 0 -15px;
+}
+.btn-default:active,
+.btn-default.active {
+  background-color: #e0e0e0;
+  border-color: #dbdbdb;
+}
+.btn-primary {
+  background-image: -webkit-linear-gradient(top, #428bca 0%, #2d6ca2 100%);
+  background-image:         linear-gradient(to bottom, #428bca 0%, #2d6ca2 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-color: #2b669a;
+}
+.btn-primary:hover,
+.btn-primary:focus {
+  background-color: #2d6ca2;
+  background-position: 0 -15px;
+}
+.btn-primary:active,
+.btn-primary.active {
+  background-color: #2d6ca2;
+  border-color: #2b669a;
+}
+.btn-success {
+  background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);
+  background-image:         linear-gradient(to bottom, #5cb85c 0%, #419641 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-color: #3e8f3e;
+}
+.btn-success:hover,
+.btn-success:focus {
+  background-color: #419641;
+  background-position: 0 -15px;
+}
+.btn-success:active,
+.btn-success.active {
+  background-color: #419641;
+  border-color: #3e8f3e;
+}
+.btn-info {
+  background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
+  background-image:         linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-color: #28a4c9;
+}
+.btn-info:hover,
+.btn-info:focus {
+  background-color: #2aabd2;
+  background-position: 0 -15px;
+}
+.btn-info:active,
+.btn-info.active {
+  background-color: #2aabd2;
+  border-color: #28a4c9;
+}
+.btn-warning {
+  background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
+  background-image:         linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-color: #e38d13;
+}
+.btn-warning:hover,
+.btn-warning:focus {
+  background-color: #eb9316;
+  background-position: 0 -15px;
+}
+.btn-warning:active,
+.btn-warning.active {
+  background-color: #eb9316;
+  border-color: #e38d13;
+}
+.btn-danger {
+  background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
+  background-image:         linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-color: #b92c28;
+}
+.btn-danger:hover,
+.btn-danger:focus {
+  background-color: #c12e2a;
+  background-position: 0 -15px;
+}
+.btn-danger:active,
+.btn-danger.active {
+  background-color: #c12e2a;
+  border-color: #b92c28;
+}
+.thumbnail,
+.img-thumbnail {
+  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
+          box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
+}
+.dropdown-menu > li > a:hover,
+.dropdown-menu > li > a:focus {
+  background-color: #e8e8e8;
+  background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
+  background-image:         linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
+  background-repeat: repeat-x;
+}
+.dropdown-menu > .active > a,
+.dropdown-menu > .active > a:hover,
+.dropdown-menu > .active > a:focus {
+  background-color: #357ebd;
+  background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%);
+  background-image:         linear-gradient(to bottom, #428bca 0%, #357ebd 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);
+  background-repeat: repeat-x;
+}
+.navbar-default {
+  background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%);
+  background-image:         linear-gradient(to bottom, #fff 0%, #f8f8f8 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-radius: 4px;
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
+          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
+}
+.navbar-default .navbar-nav > .active > a {
+  background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f3f3f3 100%);
+  background-image:         linear-gradient(to bottom, #ebebeb 0%, #f3f3f3 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0);
+  background-repeat: repeat-x;
+  -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
+          box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
+}
+.navbar-brand,
+.navbar-nav > li > a {
+  text-shadow: 0 1px 0 rgba(255, 255, 255, .25);
+}
+.navbar-inverse {
+  background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%);
+  background-image:         linear-gradient(to bottom, #3c3c3c 0%, #222 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+}
+.navbar-inverse .navbar-nav > .active > a {
+  background-image: -webkit-linear-gradient(top, #222 0%, #282828 100%);
+  background-image:         linear-gradient(to bottom, #222 0%, #282828 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0);
+  background-repeat: repeat-x;
+  -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
+          box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
+}
+.navbar-inverse .navbar-brand,
+.navbar-inverse .navbar-nav > li > a {
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, .25);
+}
+.navbar-static-top,
+.navbar-fixed-top,
+.navbar-fixed-bottom {
+  border-radius: 0;
+}
+.alert {
+  text-shadow: 0 1px 0 rgba(255, 255, 255, .2);
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
+          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
+}
+.alert-success {
+  background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
+  background-image:         linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);
+  background-repeat: repeat-x;
+  border-color: #b2dba1;
+}
+.alert-info {
+  background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
+  background-image:         linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);
+  background-repeat: repeat-x;
+  border-color: #9acfea;
+}
+.alert-warning {
+  background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
+  background-image:         linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);
+  background-repeat: repeat-x;
+  border-color: #f5e79e;
+}
+.alert-danger {
+  background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
+  background-image:         linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);
+  background-repeat: repeat-x;
+  border-color: #dca7a7;
+}
+.progress {
+  background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
+  background-image:         linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);
+  background-repeat: repeat-x;
+}
+.progress-bar {
+  background-image: -webkit-linear-gradient(top, #428bca 0%, #3071a9 100%);
+  background-image:         linear-gradient(to bottom, #428bca 0%, #3071a9 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0);
+  background-repeat: repeat-x;
+}
+.progress-bar-success {
+  background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);
+  background-image:         linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);
+  background-repeat: repeat-x;
+}
+.progress-bar-info {
+  background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
+  background-image:         linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);
+  background-repeat: repeat-x;
+}
+.progress-bar-warning {
+  background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
+  background-image:         linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);
+  background-repeat: repeat-x;
+}
+.progress-bar-danger {
+  background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);
+  background-image:         linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);
+  background-repeat: repeat-x;
+}
+.list-group {
+  border-radius: 4px;
+  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
+          box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
+}
+.list-group-item.active,
+.list-group-item.active:hover,
+.list-group-item.active:focus {
+  text-shadow: 0 -1px 0 #3071a9;
+  background-image: -webkit-linear-gradient(top, #428bca 0%, #3278b3 100%);
+  background-image:         linear-gradient(to bottom, #428bca 0%, #3278b3 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0);
+  background-repeat: repeat-x;
+  border-color: #3278b3;
+}
+.panel {
+  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
+          box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
+}
+.panel-default > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
+  background-image:         linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
+  background-repeat: repeat-x;
+}
+.panel-primary > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%);
+  background-image:         linear-gradient(to bottom, #428bca 0%, #357ebd 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);
+  background-repeat: repeat-x;
+}
+.panel-success > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
+  background-image:         linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);
+  background-repeat: repeat-x;
+}
+.panel-info > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
+  background-image:         linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);
+  background-repeat: repeat-x;
+}
+.panel-warning > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
+  background-image:         linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);
+  background-repeat: repeat-x;
+}
+.panel-danger > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
+  background-image:         linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);
+  background-repeat: repeat-x;
+}
+.well {
+  background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
+  background-image:         linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);
+  background-repeat: repeat-x;
+  border-color: #dcdcdc;
+  -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
+          box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
+}
+/*# sourceMappingURL=bootstrap-theme.css.map */

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
src/static/css/bootstrap-theme.css.map


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 6 - 0
src/static/css/bootstrap-theme.min.css


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 768 - 0
src/static/css/bootstrap.css


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
src/static/css/bootstrap.css.map


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 6 - 0
src/static/css/bootstrap.min.css


+ 158 - 0
src/static/css/carousel.css

@@ -0,0 +1,158 @@
+/* GLOBAL STYLES
+-------------------------------------------------- */
+/* Padding below the footer and lighter body text */
+
+body {
+  padding-bottom: 40px;
+  color: #5a5a5a;
+}
+
+
+
+/* CUSTOMIZE THE NAVBAR
+-------------------------------------------------- */
+
+/* Special class on .container surrounding .navbar, used for positioning it into place. */
+.navbar-wrapper {
+  position: absolute;
+  top: 0;
+  right: 0;
+  left: 0;
+  z-index: 20;
+}
+
+/* Flip around the padding for proper display in narrow viewports */
+.navbar-wrapper .container {
+  padding-right: 0;
+  padding-left: 0;
+}
+.navbar-wrapper .navbar {
+  padding-right: 15px;
+  padding-left: 15px;
+}
+
+
+/* CUSTOMIZE THE CAROUSEL
+-------------------------------------------------- */
+html, body {
+	height:100%;
+	margin:0;
+	padding:0;
+}
+.carousel, .item, .active {
+	height:100%;
+}
+.carousel-inner {
+	height:100%;
+}
+.carousel {
+	margin-bottom: 60px;
+}
+.carousel-caption {
+	z-index: 10;
+}
+.carousel .item {
+	background-color: #FFFFFF;
+}
+.carousel .carousel-inner .bg {
+	background-repeat:no-repeat;
+	background-size:cover;
+}
+.carousel .carousel-inner .bg1 {
+	background-image:url(../images/birds.jpg);
+	background-position: center top; 
+}
+.carousel .carousel-inner .bg2 {
+	background-image:url(../images/birds.jpg);
+	background-position: center center; 
+}
+.carousel .carousel-inner .bg3 {
+	background-image:url(../images/birds.jpg);
+	background-position: center bottom; 
+}
+
+/* MARKETING CONTENT
+-------------------------------------------------- */
+
+/* Pad the edges of the mobile views a bit */
+.marketing {
+  padding-right: 15px;
+  padding-left: 15px;
+}
+
+/* Center align the text within the three columns below the carousel */
+.marketing .col-lg-4 {
+  margin-bottom: 20px;
+  text-align: center;
+}
+.marketing h2 {
+  font-weight: normal;
+}
+.marketing .col-lg-4 p {
+  margin-right: 10px;
+  margin-left: 10px;
+}
+
+
+/* Featurettes
+------------------------- */
+
+.featurette-divider {
+  margin: 80px 0; /* Space out the Bootstrap <hr> more */
+}
+
+/* Thin out the marketing headings */
+.featurette-heading {
+  font-weight: 300;
+  line-height: 1;
+  letter-spacing: -1px;
+}
+
+
+
+/* RESPONSIVE CSS
+-------------------------------------------------- */
+
+@media (min-width: 768px) {
+
+  /* Remove the edge padding needed for mobile */
+  .marketing {
+    padding-right: 0;
+    padding-left: 0;
+  }
+
+  /* Navbar positioning foo */
+  .navbar-wrapper {
+    margin-top: 20px;
+  }
+  .navbar-wrapper .container {
+    padding-right: 15px;
+    padding-left:  15px;
+  }
+  .navbar-wrapper .navbar {
+    padding-right: 0;
+    padding-left:  0;
+  }
+
+  /* The navbar becomes detached from the top, so we round the corners */
+  .navbar-wrapper .navbar {
+    border-radius: 4px;
+  }
+
+  /* Bump up size of carousel content */
+  .carousel-caption p {
+    margin-bottom: 20px;
+    font-size: 21px;
+    line-height: 1.4;
+  }
+
+  .featurette-heading {
+    font-size: 50px;
+  }
+}
+
+@media (min-width: 992px) {
+  .featurette-heading {
+    margin-top: 120px;
+  }
+}

+ 23 - 0
src/static/css/editablegrid.css

@@ -0,0 +1,23 @@
+td.number {
+	text-align: right;
+	font-weight: bold;
+	padding-right: 5px;
+	white-space: nowrap;
+}
+
+td.number.nan {
+	font-weight: normal;
+	color: #AAA;
+}
+
+th.number {
+	text-align: right;
+}
+
+td.boolean {
+	text-align: center;
+}
+
+th.boolean {
+	text-align: center;
+}

BIN
src/static/img/down.png


BIN
src/static/img/up.png


+ 2244 - 0
src/static/js/editablegrid.js

@@ -0,0 +1,2244 @@
+if (typeof _$ == 'undefined') {
+	function _$(elementId) { return document.getElementById(elementId); }
+}
+
+/**
+ * Creates a new column
+ * @constructor
+ * @class Represents a column in the editable grid
+ * @param {Object} config
+ */
+function Column(config)
+{
+	// default properties
+	var props = {
+			name: "",
+			label: "",
+			editable: true,
+			renderable: true,
+			datatype: "string",
+			unit: null,
+			precision: -1, // means that all decimals are displayed
+			nansymbol: '',
+			decimal_point: ',',
+			thousands_separator: '.',
+			unit_before_number: false,
+			bar: true, // is the column to be displayed in a bar chart ? relevant only for numerical columns 
+			hidden: false, // should the column be hidden by default
+			headerRenderer: null,
+			headerEditor: null,
+			cellRenderer: null,
+			cellEditor: null,
+			cellValidators: [],
+			enumProvider: null,
+			optionValues: null,
+			optionValuesForRender: null,
+			columnIndex: -1
+	};
+
+	// override default properties with the ones given
+	for (var p in props) this[p] = (typeof config == 'undefined' || typeof config[p] == 'undefined') ? props[p] : config[p];
+}
+
+Column.prototype.getOptionValuesForRender = function(rowIndex) { 
+	if (!this.enumProvider) {
+		console.log('getOptionValuesForRender called on column ' + this.name + ' but there is no EnumProvider');
+		return null;
+	}
+	var values = this.enumProvider.getOptionValuesForRender(this.editablegrid, this, rowIndex);
+	return values ? values : this.optionValuesForRender;
+};
+
+Column.prototype.getOptionValuesForEdit = function(rowIndex) { 
+	if (!this.enumProvider) {
+		console.log('getOptionValuesForEdit called on column ' + this.name + ' but there is no EnumProvider');
+		return null;
+	}
+	var values = this.enumProvider.getOptionValuesForEdit(this.editablegrid, this, rowIndex);
+	return values ? this.editablegrid._convertOptions(values) : this.optionValues;
+};
+
+Column.prototype.isValid = function(value) {
+	for (var i = 0; i < this.cellValidators.length; i++) if (!this.cellValidators[i].isValid(value)) return false;
+	return true;
+};
+
+Column.prototype.isNumerical = function() {
+	return this.datatype =='double' || this.datatype =='integer';
+};
+
+/**
+ * Creates a new enumeration provider 
+ * @constructor
+ * @class Base class for all enumeration providers
+ * @param {Object} config
+ */
+function EnumProvider(config)
+{
+	// default properties
+	this.getOptionValuesForRender = function(grid, column, rowIndex) { return null; };
+	this.getOptionValuesForEdit = function(grid, column, rowIndex) { return null; };
+
+	// override default properties with the ones given
+	for (var p in config) this[p] = config[p];
+}
+
+/**
+ * Creates a new EditableGrid.
+ * <p>You can specify here some configuration options (optional).
+ * <br/>You can also set these same configuration options afterwards.
+ * <p>These options are:
+ * <ul>
+ * <li>enableSort: enable sorting when clicking on column headers (default=true)</li>
+ * <li>doubleclick: use double click to edit cells (default=false)</li>
+ * <li>editmode: can be one of
+ * <ul>
+ * 		<li>absolute: cell editor comes over the cell (default)</li>
+ * 		<li>static: cell editor comes inside the cell</li>
+ * 		<li>fixed: cell editor comes in an external div</li>
+ * </ul>
+ * </li>
+ * <li>editorzoneid: used only when editmode is set to fixed, it is the id of the div to use for cell editors</li>
+ * <li>allowSimultaneousEdition: tells if several cells can be edited at the same time (default=false)<br/>
+ * Warning: on some Linux browsers (eg. Epiphany), a blur event is sent when the user clicks on a 'select' input to expand it.
+ * So practically, in these browsers you should set allowSimultaneousEdition to true if you want to use columns with option values and/or enum providers.
+ * This also used to happen in older versions of Google Chrome Linux but it has been fixed, so upgrade if needed.</li>
+ * <li>saveOnBlur: should be cells saved when clicking elsewhere ? (default=true)</li>
+ * <li>invalidClassName: CSS class to apply to text fields when the entered value is invalid (default="invalid")</li>
+ * <li>ignoreLastRow: ignore last row when sorting and charting the data (typically for a 'total' row)</li>
+ * <li>caption: text to use as the grid's caption</li>
+ * <li>dateFormat: EU or US (default="EU")</li>
+ * <li>shortMonthNames: list of month names (default=["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"])</li>
+ * <li>smartColorsBar: colors used for rendering (stacked) bar charts</li>
+ * <li>smartColorsPie: colors used for rendering pie charts</li>
+ * <li>pageSize: maximum number of rows displayed (0 means we don't want any pagination, which is the default)</li>
+ * <li>sortIconDown: icon used to show desc order</li>
+ * <li>sortIconUp:  icon used to show asc order</li>
+ * </ul>
+ * @constructor
+ * @class EditableGrid
+ */
+function EditableGrid(name, config) { if (name) this.init(name, config); }
+
+/**
+ * Default properties
+ */ 
+EditableGrid.prototype.enableSort = true;
+EditableGrid.prototype.enableStore = true;
+EditableGrid.prototype.doubleclick = false;
+EditableGrid.prototype.editmode = "absolute";
+EditableGrid.prototype.editorzoneid = "";
+EditableGrid.prototype.allowSimultaneousEdition = false;
+EditableGrid.prototype.saveOnBlur = true;
+EditableGrid.prototype.invalidClassName = "invalid";
+EditableGrid.prototype.ignoreLastRow = false;
+EditableGrid.prototype.caption = null;
+EditableGrid.prototype.dateFormat = "EU";
+EditableGrid.prototype.shortMonthNames = null;
+EditableGrid.prototype.smartColorsBar = ["#dc243c","#4040f6","#00f629","#efe100","#f93fb1","#6f8183","#111111"];
+EditableGrid.prototype.smartColorsPie = ["#FF0000","#00FF00","#0000FF","#FFD700","#FF00FF","#00FFFF","#800080"];
+EditableGrid.prototype.pageSize = 0; // client-side pagination, don't set this for server-side pagination!
+
+//server-side pagination, sorting and filtering
+EditableGrid.prototype.serverSide = false;
+EditableGrid.prototype.pageCount = 0;
+EditableGrid.prototype.totalRowCount = 0;
+EditableGrid.prototype.unfilteredRowCount = 0;
+EditableGrid.prototype.paginatorAttributes = null;
+EditableGrid.prototype.lastURL = null;
+
+EditableGrid.prototype.init = function (name, config)
+{
+	if (typeof name != "string" || (typeof config != "object" && typeof config != "undefined")) {
+		alert("The EditableGrid constructor takes two arguments:\n- name (string)\n- config (object)\n\nGot instead " + (typeof name) + " and " + (typeof config) + ".");
+	};
+
+	// override default properties with the ones given
+	if (typeof config != 'undefined') for (var p in config) this[p] = config[p];
+
+	this.Browser = {
+			IE:  !!(window.attachEvent && navigator.userAgent.indexOf('Opera') === -1),
+			Opera: navigator.userAgent.indexOf('Opera') > -1,
+			WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
+			Gecko: navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') === -1,
+			MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/)
+	};
+
+	if (typeof this.detectDir != 'function') {
+		var error = new Error();
+		alert("Who is calling me now ? " + error.stack);
+	}
+
+	// private data
+	this.name = name;
+	this.columns = [];
+	this.data = [];
+	this.dataUnfiltered = null; // non null means that data is filtered
+	this.xmlDoc = null;
+	this.sortedColumnName = -1;
+	this.sortDescending = false;
+	this.baseUrl = this.detectDir();
+	this.nbHeaderRows = 1;
+	this.lastSelectedRowIndex = -1;
+	this.currentPageIndex = 0;
+	this.currentFilter = null;
+	this.currentContainerid = null; 
+	this.currentClassName = null; 
+	this.currentTableid = null;
+
+	if (this.enableSort) {
+		this.sortUpImage = new Image();
+		if ( typeof config != "undefined" && typeof config['sortIconUp'] != "undefined" ) 
+			this.sortUpImage.src = config['sortIconUp'];
+		else
+			this.sortUpImage.src = this.baseUrl + "/images/bullet_arrow_up.png";
+
+
+		this.sortDownImage = new Image();
+		if ( typeof config != "undefined" && typeof config['sortIconDown'] != "undefined" ) 
+			this.sortDownImage.src = config['sortIconDown'];
+		else
+			this.sortDownImage.src = this.baseUrl + "/images/bullet_arrow_down.png";
+	}
+
+	// restore stored parameters, or use default values if nothing stored
+	this.currentPageIndex = this.localisset('pageIndex') ? parseInt(this.localget('pageIndex')) : 0;
+	this.sortedColumnName = this.localisset('sortColumnIndexOrName') ? this.localget('sortColumnIndexOrName') : -1;
+	this.sortDescending = this.localisset('sortColumnIndexOrName') && this.localisset('sortDescending') ? this.localget('sortDescending') == 'true' : false;
+	this.currentFilter = this.localisset('filter') ? this.localget('filter') : null;
+};
+
+/**
+ * Callback functions
+ */
+
+EditableGrid.prototype.tableLoaded = function() {};
+EditableGrid.prototype.chartRendered = function() {};
+EditableGrid.prototype.tableRendered = function(containerid, className, tableid) {};
+EditableGrid.prototype.tableSorted = function(columnIndex, descending) {};
+EditableGrid.prototype.tableFiltered = function() {};
+EditableGrid.prototype.modelChanged = function(rowIndex, columnIndex, oldValue, newValue, row) {};
+EditableGrid.prototype.rowSelected = function(oldRowIndex, newRowIndex) {};
+EditableGrid.prototype.isHeaderEditable = function(rowIndex, columnIndex) { return false; };
+EditableGrid.prototype.isEditable =function(rowIndex, columnIndex) { return true; };
+EditableGrid.prototype.readonlyWarning = function() {};
+/** Notifies that a row has been deleted */
+EditableGrid.prototype.rowRemoved = function(oldRowIndex, rowId) {};
+
+/**
+ * Load metadata and/or data from an XML url
+ * The callback "tableLoaded" is called when loading is complete.
+ */
+EditableGrid.prototype.loadXML = function(url, callback, dataOnly)
+{
+	this.lastURL = url; 
+	var self = this;
+
+	// IE
+	if (window.ActiveXObject) 
+	{
+		this.xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
+		this.xmlDoc.onreadystatechange = function() {
+			if (self.xmlDoc.readyState == 4) {
+				self.processXML();
+				self._callback('xml', callback);
+			}
+		};
+		this.xmlDoc.load(this._addUrlParameters(url, dataOnly));
+	}
+
+	// generic Ajax
+	else if (window.XMLHttpRequest) 
+	{
+		this.xmlDoc = new XMLHttpRequest();
+		this.xmlDoc.onreadystatechange = function () {
+			if (this.readyState == 4) {
+				self.xmlDoc = this.responseXML;
+				if (!self.xmlDoc) { console.error("Could not load XML from url '" + url + "'"); return false; }
+				self.processXML();
+				self._callback('xml', callback);
+			}
+		};
+		this.xmlDoc.open("GET", this._addUrlParameters(url, dataOnly), true);
+		this.xmlDoc.send("");
+	}
+
+	// Firefox (and some other browsers) 
+	else if (document.implementation && document.implementation.createDocument) 
+	{
+		this.xmlDoc = document.implementation.createDocument("", "", null);
+		this.xmlDoc.onload = function() {
+			self.processXML();
+			self._callback('xml', callback);
+		};
+		this.xmlDoc.load(this._addUrlParameters(url, dataOnly));
+	}
+
+	// should never happen
+	else { 
+		alert("Cannot load a XML url with this browser!"); 
+		return false;
+	}
+
+	return true;
+};
+
+/**
+ * Load metadata and/or data from an XML string
+ * No callback "tableLoaded" is called since this is a synchronous operation.
+ * 
+ * Contributed by Tim Consolazio of Tcoz Tech Services, tcoz@tcoz.com
+ * http://tcoztechwire.blogspot.com/2012/04/setxmlfromstring-extension-for.html
+ */
+EditableGrid.prototype.loadXMLFromString = function(xml)
+{
+	if (window.DOMParser) {
+		var parser = new DOMParser();
+		this.xmlDoc = parser.parseFromString(xml, "application/xml");
+	}
+	else {
+		this.xmlDoc = new ActiveXObject("Microsoft.XMLDOM"); // IE
+		this.xmlDoc.async = "false";
+		this.xmlDoc.loadXML(xml);
+	}
+
+	this.processXML();
+};
+
+/**
+ * Process the XML content
+ * @private
+ */
+EditableGrid.prototype.processXML = function()
+{
+	with (this) {
+
+		// clear model and pointer to current table
+		this.data = [];
+		this.dataUnfiltered = null;
+		this.table = null;
+
+		// load metadata (only one tag <metadata> --> metadata[0])
+		var metadata = xmlDoc.getElementsByTagName("metadata");
+		if (metadata && metadata.length >= 1) {
+
+			this.columns = [];
+			var columnDeclarations = metadata[0].getElementsByTagName("column");
+			for (var i = 0; i < columnDeclarations.length; i++) {
+
+				// get column type
+				var col = columnDeclarations[i];
+				var datatype = col.getAttribute("datatype");
+
+				// get enumerated values if any
+				var optionValuesForRender = null;
+				var optionValues = null;
+				var enumValues = col.getElementsByTagName("values");
+				if (enumValues.length > 0) {
+					optionValues = [];
+					optionValuesForRender = {};
+
+					var enumGroups = enumValues[0].getElementsByTagName("group");
+					if (enumGroups.length > 0) {
+						for (var g = 0; g < enumGroups.length; g++) {
+							var groupOptionValues = [];
+							enumValues = enumGroups[g].getElementsByTagName("value");
+							for (var v = 0; v < enumValues.length; v++) {
+								var _value = enumValues[v].getAttribute("value");
+								var _label = enumValues[v].firstChild ? enumValues[v].firstChild.nodeValue : "";
+								optionValuesForRender[_value] = _label; 
+								groupOptionValues.push({ value: _value, label: _label });
+							}
+							optionValues.push({ label: enumGroups[g].getAttribute("label"), values: groupOptionValues});
+						}
+					}
+					else {
+						enumValues = enumValues[0].getElementsByTagName("value");
+						for (var v = 0; v < enumValues.length; v++) {
+							var _value = enumValues[v].getAttribute("value");
+							var _label = enumValues[v].firstChild ? enumValues[v].firstChild.nodeValue : "";
+							optionValuesForRender[_value] = _label; 
+							optionValues.push({ value: _value, label: _label });
+						}
+					}
+				}
+
+				// create new column           
+				columns.push(new Column({
+					name: col.getAttribute("name"),
+					label: (typeof col.getAttribute("label") == 'string' ? col.getAttribute("label") : col.getAttribute("name")),
+					datatype: (col.getAttribute("datatype") ? col.getAttribute("datatype") : "string"),
+					editable: col.getAttribute("editable") == "true",
+					bar: (col.getAttribute("bar") ? col.getAttribute("bar") == "true" : true),
+					hidden: (col.getAttribute("hidden") ? col.getAttribute("hidden") == "true" : false),
+					optionValuesForRender: optionValuesForRender,
+					optionValues: optionValues
+				}));
+			}
+
+			// process columns
+			processColumns();
+		}
+
+		// load server-side pagination data
+		var paginator = xmlDoc.getElementsByTagName("paginator");
+		if (paginator && paginator.length >= 1) {
+			this.paginatorAttributes = null; // TODO: paginator[0].getAllAttributesAsPOJO;
+			this.pageCount = paginator[0].getAttribute('pagecount');
+			this.totalRowCount = paginator[0].getAttribute('totalrowcount');
+			this.unfilteredRowCount = paginator[0].getAttribute('unfilteredrowcount');
+		}
+
+		// if no row id is provided, we create one since we need one
+		var defaultRowId = 1;
+
+		// load content
+		var rows = xmlDoc.getElementsByTagName("row");
+		for (var i = 0; i < rows.length; i++) 
+		{
+			// get all defined cell values
+			var cellValues = {};
+			var cols = rows[i].getElementsByTagName("column");
+			for (var j = 0; j < cols.length; j++) {
+				var colname = cols[j].getAttribute("name");
+				if (!colname) {
+					if (j >= columns.length) console.error("You defined too many columns for row " + (i+1));
+					else colname = columns[j].name; 
+				}
+				cellValues[colname] = cols[j].firstChild ? cols[j].firstChild.nodeValue : "";
+			}
+
+			// for each row we keep the orginal index, the id and all other attributes that may have been set in the XML
+			var rowData = { visible: true, originalIndex: i, id: rows[i].getAttribute("id") ? rows[i].getAttribute("id") : defaultRowId++ };  
+			for (var attrIndex = 0; attrIndex < rows[i].attributes.length; attrIndex++) {
+				var node = rows[i].attributes.item(attrIndex);
+				if (node.nodeName != "id") rowData[node.nodeName] = node.nodeValue; 
+			}
+
+			// get column values for this rows
+			rowData.columns = [];
+			for (var c = 0; c < columns.length; c++) {
+				var cellValue = columns[c].name in cellValues ? cellValues[columns[c].name] : "";
+				rowData.columns.push(getTypedValue(c, cellValue));
+			}
+
+			// add row data in our model
+			data.push(rowData);
+		}
+	}
+
+	return true;
+};
+
+/**
+ * Load metadata and/or data from a JSON url
+ * The callback "tableLoaded" is called when loading is complete.
+ */
+EditableGrid.prototype.loadJSON = function(url, callback, dataOnly)
+{
+	this.lastURL = url; 
+	var self = this;
+
+	// should never happen
+	if (!window.XMLHttpRequest) {
+		alert("Cannot load a JSON url with this browser!"); 
+		return false;
+	}
+
+	var ajaxRequest = new XMLHttpRequest();
+	ajaxRequest.onreadystatechange = function () {
+		if (this.readyState == 4) {
+			if (!this.responseText) { console.error("Could not load JSON from url '" + url + "'"); return false; }
+			if (!self.processJSON(this.responseText)) { console.error("Invalid JSON data obtained from url '" + url + "'"); return false; }
+			self._callback('json', callback);
+		}
+	};
+
+	ajaxRequest.open("GET", this._addUrlParameters(url, dataOnly), true);
+	ajaxRequest.send("");
+
+	return true;
+};
+
+EditableGrid.prototype._addUrlParameters = function(baseUrl, dataOnly)
+{
+	// we add a dummy timestamp parameter to avoid getting an old version from the browser's cache
+	var sep = baseUrl.indexOf('?') >= 0 ? '&' : '?'; 
+	baseUrl += sep + (new Date().getTime());
+
+	if (!this.serverSide) return baseUrl;
+
+	// add pagination, filtering and sorting parameters to the base url
+	return baseUrl
+	+ "&page=" + (this.currentPageIndex + 1)
+	+ "&filter=" + (this.currentFilter ? encodeURIComponent(this.currentFilter) : "")
+	+ "&sort=" + (this.sortedColumnName && this.sortedColumnName != -1 ? encodeURIComponent(this.sortedColumnName) : "")
+	+ "&asc=" + (this.sortDescending ? 0 : 1)
+	+ (dataOnly ? '&data_only=1' : '');
+};
+
+EditableGrid.prototype._callback = function(type, callback)
+{
+	if (callback) callback.call(this); 
+	else {
+
+		if (this.serverSide) {
+
+			// deferred refreshGrid: first load the updated data from the server then call the original refreshGrid
+			this.refreshGrid = function(baseUrl) {
+				var callback = function() { EditableGrid.prototype.refreshGrid.call(this); };
+				var load = type == 'xml' ? this.loadXML : this.loadJSON;
+				load.call(this, baseUrl || this.lastURL, callback, true);
+			};
+		}
+
+		this.tableLoaded();
+	}
+};
+
+/**
+ * Load metadata and/or data from a JSON string
+ * No callback "tableLoaded" is called since this is a synchronous operation.
+ */
+EditableGrid.prototype.loadJSONFromString = function(json)
+{
+	return this.processJSON(json);
+};
+
+/**
+ * Load metadata and/or data from a Javascript object
+ * No callback "tableLoaded" is called since this is a synchronous operation.
+ */
+EditableGrid.prototype.load = function(object)
+{
+	return this.processJSON(object);
+};
+
+/**
+ * Update and render data for given rows from a Javascript object
+ */
+EditableGrid.prototype.update = function(object)
+{
+	if (object.data) for (var i = 0; i < object.data.length; i++) 
+	{
+		var row = object.data[i];
+		if (!row.id || !row.values) continue;
+
+		// get row to update in our model
+		var rowIndex = this.getRowIndex(row.id);
+		var rowData = this.data[rowIndex];
+
+		// row values can be given as an array (same order as columns) or as an object (associative array)
+		if (Object.prototype.toString.call(row.values) !== '[object Array]' ) cellValues = row.values;
+		else {
+			cellValues = {};
+			for (var j = 0; j < row.values.length && j < this.columns.length; j++) cellValues[this.columns[j].name] = row.values[j];
+		}
+
+		// set all attributes that may have been set in the JSON
+		for (var attributeName in row) if (attributeName != "id" && attributeName != "values") rowData[attributeName] = row[attributeName];
+
+		// get column values for this rows
+		rowData.columns = [];
+		for (var c = 0; c < this.columns.length; c++) {
+			var cellValue = this.columns[c].name in cellValues ? cellValues[this.columns[c].name] : "";
+			rowData.columns.push(this.getTypedValue(c, cellValue));
+		}
+
+		// render row
+		var tr = this.getRow(rowIndex);
+		for (var j = 0; j < tr.cells.length && j < this.columns.length; j++)  if (this.columns[j].renderable) this.columns[j].cellRenderer._render(rowIndex, j, tr.cells[j], this.getValueAt(rowIndex,j));
+		this.tableRendered(this.currentContainerid, this.currentClassName, this.currentTableid);
+	}
+};
+
+/**
+ * Process the JSON content
+ * @private
+ */
+EditableGrid.prototype.processJSON = function(jsonData)
+{	
+	if (typeof jsonData == "string") jsonData = eval("(" + jsonData + ")");
+	if (!jsonData) return false;
+
+	// clear model and pointer to current table
+	this.data = [];
+	this.dataUnfiltered = null;
+	this.table = null;
+
+	// load metadata
+	if (jsonData.metadata) {
+
+		// create columns 
+		this.columns = [];
+		for (var c = 0; c < jsonData.metadata.length; c++) {
+			var columndata = jsonData.metadata[c];
+
+			var optionValues = columndata.values ? this._convertOptions(columndata.values) : null;
+			var optionValuesForRender = null;
+			if (optionValues) {
+
+				// build a fast lookup structure for rendering
+				var optionValuesForRender = {};
+				for (var optionIndex = 0; optionIndex < optionValues.length; optionIndex++) {
+					var optionValue = optionValues[optionIndex];
+					if (typeof optionValue.values == 'object') {
+						for (var groupOptionIndex = 0; groupOptionIndex < optionValue.values.length; groupOptionIndex++) {
+							var groupOptionValue = optionValue.values[groupOptionIndex];
+							optionValuesForRender[groupOptionValue.value] = groupOptionValue.label;
+						}
+					}
+					else optionValuesForRender[optionValue.value] = optionValue.label;
+				}
+			}
+
+			this.columns.push(new Column({
+				name: columndata.name,
+				label: (columndata.label ? columndata.label : columndata.name),
+				datatype: (columndata.datatype ? columndata.datatype : "string"),
+				editable: (columndata.editable ? true : false),
+				bar: (typeof columndata.bar == 'undefined' ? true : (columndata.bar || false)),
+				hidden: (typeof columndata.hidden == 'undefined' ? false : (columndata.hidden ? true : false)),
+				optionValuesForRender: optionValuesForRender,
+				optionValues: optionValues
+			}));
+		}
+
+		// process columns
+		this.processColumns();
+	}
+
+	// load server-side pagination data
+	if (jsonData.paginator) {
+		this.paginatorAttributes = jsonData.paginator;
+		this.pageCount = jsonData.paginator.pagecount;
+		this.totalRowCount = jsonData.paginator.totalrowcount;
+		this.unfilteredRowCount = jsonData.paginator.unfilteredrowcount;
+	}
+
+	// if no row id is provided, we create one since we need one
+	var defaultRowId = 1;
+
+	// load content
+	if (jsonData.data) for (var i = 0; i < jsonData.data.length; i++) 
+	{
+		var row = jsonData.data[i];
+		if (!row.values) continue;
+
+		// row values can be given as an array (same order as columns) or as an object (associative array)
+		if (Object.prototype.toString.call(row.values) !== '[object Array]' ) cellValues = row.values;
+		else {
+			cellValues = {};
+			for (var j = 0; j < row.values.length && j < this.columns.length; j++) cellValues[this.columns[j].name] = row.values[j];
+		}
+
+		// for each row we keep the orginal index, the id and all other attributes that may have been set in the JSON
+		var rowData = { visible: true, originalIndex: i, id: row.id ? row.id : defaultRowId++ };  
+		for (var attributeName in row) if (attributeName != "id" && attributeName != "values") rowData[attributeName] = row[attributeName];
+
+		// get column values for this rows
+		rowData.columns = [];
+		for (var c = 0; c < this.columns.length; c++) {
+			var cellValue = this.columns[c].name in cellValues ? cellValues[this.columns[c].name] : "";
+			rowData.columns.push(this.getTypedValue(c, cellValue));
+		}
+
+		// add row data in our model
+		this.data.push(rowData);
+	}
+
+	return true;
+};
+
+/**
+ * Process columns
+ * @private
+ */
+EditableGrid.prototype.processColumns = function()
+{
+	for (var columnIndex = 0; columnIndex < this.columns.length; columnIndex++) {
+
+		var column = this.columns[columnIndex];
+
+		// set column index and back pointer
+		column.columnIndex = columnIndex;
+		column.editablegrid = this;
+
+		// parse column type
+		this.parseColumnType(column);
+
+		// create suited enum provider if none given
+		if (!column.enumProvider) column.enumProvider = column.optionValues ? new EnumProvider() : null;
+
+		// create suited cell renderer if none given
+		if (!column.cellRenderer) this._createCellRenderer(column);
+		if (!column.headerRenderer) this._createHeaderRenderer(column);
+
+		// create suited cell editor if none given
+		if (!column.cellEditor) this._createCellEditor(column);  
+		if (!column.headerEditor) this._createHeaderEditor(column);
+
+		// add default cell validators based on the column type
+		this._addDefaultCellValidators(column);
+	}
+};
+
+/**
+ * Parse column type
+ * @private
+ */
+
+EditableGrid.prototype.parseColumnType = function(column)
+{
+	// reset
+	column.unit = null;
+	column.precision = -1;
+	column.decimal_point = ',';
+	column.thousands_separator = '.';
+	column.unit_before_number = false;
+	column.nansymbol = '';
+
+	// extract precision, unit and number format from type if 6 given
+	if (column.datatype.match(/(.*)\((.*),(.*),(.*),(.*),(.*),(.*)\)$/)) {
+		column.datatype = RegExp.$1;
+		column.unit = RegExp.$2;
+		column.precision = parseInt(RegExp.$3);
+		column.decimal_point = RegExp.$4;
+		column.thousands_separator = RegExp.$5;
+		column.unit_before_number = RegExp.$6;
+		column.nansymbol = RegExp.$7;
+
+		// trim should be done after fetching RegExp matches beacuse it itself uses a RegExp and causes interferences!
+		column.unit = column.unit.trim();
+		column.decimal_point = column.decimal_point.trim();
+		column.thousands_separator = column.thousands_separator.trim();
+		column.unit_before_number = column.unit_before_number.trim() == '1';
+		column.nansymbol = column.nansymbol.trim();
+	}
+
+	// extract precision, unit and number format from type if 5 given
+	else if (column.datatype.match(/(.*)\((.*),(.*),(.*),(.*),(.*)\)$/)) {
+		column.datatype = RegExp.$1;
+		column.unit = RegExp.$2;
+		column.precision = parseInt(RegExp.$3);
+		column.decimal_point = RegExp.$4;
+		column.thousands_separator = RegExp.$5;
+		column.unit_before_number = RegExp.$6;
+
+		// trim should be done after fetching RegExp matches beacuse it itself uses a RegExp and causes interferences!
+		column.unit = column.unit.trim();
+		column.decimal_point = column.decimal_point.trim();
+		column.thousands_separator = column.thousands_separator.trim();
+		column.unit_before_number = column.unit_before_number.trim() == '1';
+	}
+
+	// extract precision, unit and nansymbol from type if 3 given
+	else if (column.datatype.match(/(.*)\((.*),(.*),(.*)\)$/)) {
+		column.datatype = RegExp.$1;
+		column.unit = RegExp.$2.trim();
+		column.precision = parseInt(RegExp.$3);
+		column.nansymbol = RegExp.$4.trim();
+	}
+
+	// extract precision and unit from type if two given
+	else if (column.datatype.match(/(.*)\((.*),(.*)\)$/)) {
+		column.datatype = RegExp.$1.trim();
+		column.unit = RegExp.$2.trim();
+		column.precision = parseInt(RegExp.$3);
+	}
+
+	// extract precision or unit from type if any given
+	else if (column.datatype.match(/(.*)\((.*)\)$/)) {
+		column.datatype = RegExp.$1.trim();
+		var unit_or_precision = RegExp.$2.trim();
+		if (unit_or_precision.match(/^[0-9]*$/)) column.precision = parseInt(unit_or_precision);
+		else column.unit = unit_or_precision;
+	}
+
+	if (column.decimal_point == 'comma') column.decimal_point = ',';
+	if (column.decimal_point == 'dot') column.decimal_point = '.';
+	if (column.thousands_separator == 'comma') column.thousands_separator = ',';
+	if (column.thousands_separator == 'dot') column.thousands_separator = '.';
+
+	if (isNaN(column.precision)) column.precision = -1;
+	if (column.unit == '') column.unit = null;
+	if (column.nansymbol == '') column.nansymbol = null;
+};
+
+/**
+ * Get typed value
+ * @private
+ */
+
+EditableGrid.prototype.getTypedValue = function(columnIndex, cellValue) 
+{
+	if (cellValue === null) return cellValue;
+
+	var colType = this.getColumnType(columnIndex);
+	if (colType == 'boolean') cellValue = (cellValue && cellValue != 0 && cellValue != "false") ? true : false;
+	if (colType == 'integer') { cellValue = parseInt(cellValue, 10); } 
+	if (colType == 'double') { cellValue = parseFloat(cellValue); }
+	if (colType == 'string') { cellValue = "" + cellValue; }
+
+	return cellValue;
+};
+
+/**
+ * Attach to an existing HTML table.
+ * The second parameter can be used to give the column definitions.
+ * This parameter is left for compatibility, but is deprecated: you should now use "load" to setup the metadata.
+ */
+EditableGrid.prototype.attachToHTMLTable = function(_table, _columns)
+{
+	// clear model and pointer to current table
+	this.data = [];
+	this.dataUnfiltered = null;
+	this.table = null;
+
+	// process columns if given
+	if (_columns) {
+		this.columns = _columns;
+		for (var columnIndex = 0; columnIndex < this.columns.length; columnIndex++) this.columns[columnIndex].optionValues = this._convertOptions(this.columns[columnIndex].optionValues); // convert options from old format if needed
+		this.processColumns();
+	}
+
+	// get pointers to table components
+	this.table = typeof _table == 'string' ? _$(_table) : _table ;
+	if (!this.table) console.error("Invalid table given: " + _table);
+	this.tHead = this.table.tHead;
+	this.tBody = this.table.tBodies[0];
+
+	// create table body if needed
+	if (!this.tBody) {
+		this.tBody = document.createElement("TBODY");
+		this.table.insertBefore(this.tBody, this.table.firstChild);
+	}
+
+	// create table header if needed
+	if (!this.tHead) {
+		this.tHead = document.createElement("THEAD");
+		this.table.insertBefore(this.tHead, this.tBody);
+	}
+
+	// if header is empty use first body row as header
+	if (this.tHead.rows.length == 0 && this.tBody.rows.length > 0) 
+		this.tHead.appendChild(this.tBody.rows[0]);
+
+	// get number of rows in header
+	this.nbHeaderRows = this.tHead.rows.length;
+
+	// load header labels
+	var rows = this.tHead.rows;
+	for (var i = 0; i < rows.length; i++) {
+		var cols = rows[i].cells;
+		var columnIndexInModel = 0;
+		for (var j = 0; j < cols.length && columnIndexInModel < this.columns.length; j++) {
+			if (!this.columns[columnIndexInModel].label || this.columns[columnIndexInModel].label == this.columns[columnIndexInModel].name) this.columns[columnIndexInModel].label = cols[j].innerHTML;
+			var colspan = parseInt(cols[j].getAttribute("colspan"));
+			columnIndexInModel += colspan > 1 ? colspan : 1;
+		}
+	}
+
+	// load content
+	var rows = this.tBody.rows;
+	for (var i = 0; i < rows.length; i++) {
+		var rowData = [];
+		var cols = rows[i].cells;
+		for (var j = 0; j < cols.length && j < this.columns.length; j++) rowData.push(this.getTypedValue(j, cols[j].innerHTML));
+		this.data.push({ visible: true, originalIndex: i, id: rows[i].id, columns: rowData });
+		rows[i].rowId = rows[i].id;
+		rows[i].id = this._getRowDOMId(rows[i].id);
+	}
+};
+
+/**
+ * Creates a suitable cell renderer for the column
+ * @private
+ */
+EditableGrid.prototype._createCellRenderer = function(column)
+{
+	column.cellRenderer = 
+		column.enumProvider && column.datatype == "list" && typeof MultiselectCellRenderer != 'undefined' ? new MultiselectCellRenderer() :
+			column.enumProvider ? new EnumCellRenderer() :
+				column.datatype == "integer" || column.datatype == "double" ? new NumberCellRenderer() :
+					column.datatype == "boolean" ? new CheckboxCellRenderer() : 
+						column.datatype == "email" ? new EmailCellRenderer() : 
+							column.datatype == "website" || column.datatype == "url" ? new WebsiteCellRenderer() : 
+								column.datatype == "date" ? new DateCellRenderer() :
+									new CellRenderer();
+
+								// give access to the column from the cell renderer
+								if (column.cellRenderer) {
+									column.cellRenderer.editablegrid = this;
+									column.cellRenderer.column = column;
+								}
+};
+
+/**
+ * Creates a suitable header cell renderer for the column
+ * @private
+ */
+EditableGrid.prototype._createHeaderRenderer = function(column)
+{
+	column.headerRenderer = (this.enableSort && column.datatype != "html") ? new SortHeaderRenderer(column.name) : new CellRenderer();
+
+	// give access to the column from the header cell renderer
+	if (column.headerRenderer) {
+		column.headerRenderer.editablegrid = this;
+		column.headerRenderer.column = column;
+	}		
+};
+
+/**
+ * Creates a suitable cell editor for the column
+ * @private
+ */
+EditableGrid.prototype._createCellEditor = function(column)
+{
+	column.cellEditor = 
+		column.enumProvider && column.datatype == "list" && typeof MultiselectCellEditor != 'undefined' ? new MultiselectCellEditor() :
+			column.enumProvider ? new SelectCellEditor() :
+				column.datatype == "integer" || column.datatype == "double" ? new NumberCellEditor(column.datatype) :
+					column.datatype == "boolean" ? null :
+						column.datatype == "email" ? new TextCellEditor(column.precision) :
+							column.datatype == "website" || column.datatype == "url" ? new TextCellEditor(column.precision) :
+								column.datatype == "date" ? (typeof jQuery == 'undefined' || typeof jQuery.datepicker == 'undefined' ? new TextCellEditor(column.precision, 10) : new DateCellEditor({ fieldSize: column.precision, maxLength: 10 })) :
+									new TextCellEditor(column.precision);  
+
+								// give access to the column from the cell editor
+								if (column.cellEditor) {
+									column.cellEditor.editablegrid = this;
+									column.cellEditor.column = column;
+								}
+};
+
+/**
+ * Creates a suitable header cell editor for the column
+ * @private
+ */
+EditableGrid.prototype._createHeaderEditor = function(column)
+{
+	column.headerEditor =  new TextCellEditor();  
+
+	// give access to the column from the cell editor
+	if (column.headerEditor) {
+		column.headerEditor.editablegrid = this;
+		column.headerEditor.column = column;
+	}
+};
+
+/**
+ * Returns the number of rows
+ */
+EditableGrid.prototype.getRowCount = function()
+{
+	return this.data.length;
+};
+
+/**
+ * Returns the number of rows, not taking the filter into account if any
+ */
+EditableGrid.prototype.getUnfilteredRowCount = function()
+{
+	// given if server-side filtering is involved
+	if (this.unfilteredRowCount > 0) return this.unfilteredRowCount;
+
+	var _data = this.dataUnfiltered == null ? this.data : this.dataUnfiltered; 
+	return _data.length;
+};
+
+/**
+ * Returns the number of rows in all pages
+ */
+EditableGrid.prototype.getTotalRowCount = function()
+{
+	// different from getRowCount only is server-side pagination is involved
+	if (this.totalRowCount > 0) return this.totalRowCount;
+
+	return this.getRowCount();
+};
+
+/**
+ * Returns the number of columns
+ */
+EditableGrid.prototype.getColumnCount = function()
+{
+	return this.columns.length;
+};
+
+/**
+ * Returns true if the column exists
+ * @param {Object} columnIndexOrName index or name of the column
+ */
+EditableGrid.prototype.hasColumn = function(columnIndexOrName)
+{
+	return this.getColumnIndex(columnIndexOrName) >= 0;
+};
+
+/**
+ * Returns the column
+ * @param {Object} columnIndexOrName index or name of the column
+ */
+EditableGrid.prototype.getColumn = function(columnIndexOrName)
+{
+	var colIndex = this.getColumnIndex(columnIndexOrName);
+	if (colIndex < 0) { console.error("[getColumn] Column not found with index or name " + columnIndexOrName); return null; }
+	return this.columns[colIndex];
+};
+
+/**
+ * Returns the name of a column
+ * @param {Object} columnIndexOrName index or name of the column
+ */
+EditableGrid.prototype.getColumnName = function(columnIndexOrName)
+{
+	return this.getColumn(columnIndexOrName).name;
+};
+
+/**
+ * Returns the label of a column
+ * @param {Object} columnIndexOrName index or name of the column
+ */
+EditableGrid.prototype.getColumnLabel = function(columnIndexOrName)
+{
+	return this.getColumn(columnIndexOrName).label;
+};
+
+/**
+ * Returns the type of a column
+ * @param {Object} columnIndexOrName index or name of the column
+ */
+EditableGrid.prototype.getColumnType = function(columnIndexOrName)
+{
+	return this.getColumn(columnIndexOrName).datatype;
+};
+
+/**
+ * Returns the unit of a column
+ * @param {Object} columnIndexOrName index or name of the column
+ */
+EditableGrid.prototype.getColumnUnit = function(columnIndexOrName)
+{
+	return this.getColumn(columnIndexOrName).unit;
+};
+
+/**
+ * Returns the precision of a column
+ * @param {Object} columnIndexOrName index or name of the column
+ */
+EditableGrid.prototype.getColumnPrecision = function(columnIndexOrName)
+{
+	return this.getColumn(columnIndexOrName).precision;
+};
+
+/**
+ * Returns true if the column is to be displayed in a bar chart
+ * @param {Object} columnIndexOrName index or name of the column
+ */
+EditableGrid.prototype.isColumnBar = function(columnIndexOrName)
+{
+	var column = this.getColumn(columnIndexOrName);
+	return (column.bar && column.isNumerical());
+};
+
+/**
+ * Returns the stack of a column (for stacked bar charts)
+ * @param {Object} columnIndexOrName index or name of the column
+ */
+EditableGrid.prototype.getColumnStack = function(columnIndexOrName)
+{
+	var column = this.getColumn(columnIndexOrName);
+	return column.isNumerical() ? column.bar : '';
+};
+
+
+/**
+ * Returns true if the column is numerical (double or integer)
+ * @param {Object} columnIndexOrName index or name of the column
+ */
+EditableGrid.prototype.isColumnNumerical = function(columnIndexOrName)
+{
+	var column = this.getColumn(columnIndexOrName);
+	return column.isNumerical();;
+};
+
+/**
+ * Returns the value at the specified index
+ * @param {Integer} rowIndex
+ * @param {Integer} columnIndex
+ */
+EditableGrid.prototype.getValueAt = function(rowIndex, columnIndex)
+{
+	// check and get column
+	if (columnIndex < 0 || columnIndex >= this.columns.length) { console.error("[getValueAt] Invalid column index " + columnIndex); return null; }
+	var column = this.columns[columnIndex];
+
+	// get value in model
+	if (rowIndex < 0) return column.label;
+
+	if (typeof this.data[rowIndex] == 'undefined') { console.error("[getValueAt] Invalid row index " + rowIndex); return null; }
+	var rowData = this.data[rowIndex]['columns'];
+	return rowData ? rowData[columnIndex] : null;
+};
+
+/**
+ * Returns the display value (used for sorting and filtering) at the specified index
+ * @param {Integer} rowIndex
+ * @param {Integer} columnIndex
+ */
+EditableGrid.prototype.getDisplayValueAt = function(rowIndex, columnIndex)
+{
+	// use renderer to get the value that must be used for sorting and filtering
+	var value = this.getValueAt(rowIndex, columnIndex);
+	var renderer = rowIndex < 0 ? this.columns[columnIndex].headerRenderer : this.columns[columnIndex].cellRenderer;  
+	return renderer.getDisplayValue(rowIndex, value);
+};
+
+
+/**
+ * Sets the value at the specified index
+ * @param {Integer} rowIndex
+ * @param {Integer} columnIndex
+ * @param {Object} value
+ * @param {Boolean} render
+ */
+EditableGrid.prototype.setValueAt = function(rowIndex, columnIndex, value, render)
+{
+	if (typeof render == "undefined") render = true;
+	var previousValue = null;;
+
+	// check and get column
+	if (columnIndex < 0 || columnIndex >= this.columns.length) { console.error("[setValueAt] Invalid column index " + columnIndex); return null; }
+	var column = this.columns[columnIndex];
+
+	// set new value in model
+	if (rowIndex < 0) {
+		previousValue = column.label;
+		column.label = value;
+	}
+	else {
+
+		if (typeof this.data[rowIndex] == 'undefined') {
+			console.error('Invalid rowindex ' + rowIndex);
+			return null;
+		}
+
+		var rowData = this.data[rowIndex]['columns'];
+		previousValue = rowData[columnIndex];
+		if (rowData) rowData[columnIndex] = this.getTypedValue(columnIndex, value);
+	}
+
+	// render new value
+	if (render) {
+		var renderer = rowIndex < 0 ? column.headerRenderer : column.cellRenderer;
+		var cell = this.getCell(rowIndex, columnIndex);
+		if (cell) renderer._render(rowIndex, columnIndex, cell, value);
+	}
+
+	return previousValue;
+};
+
+/**
+ * Find column index from its name
+ * @param {Object} columnIndexOrName index or name of the column
+ */
+EditableGrid.prototype.getColumnIndex = function(columnIndexOrName)
+{
+	if (typeof columnIndexOrName == "undefined" || columnIndexOrName === "") return -1;
+
+	// TODO: problem because the name of a column could be a valid index, and we cannot make the distinction here!
+
+	// if columnIndexOrName is a number which is a valid index return it
+	if (!isNaN(columnIndexOrName) && columnIndexOrName >= 0 && columnIndexOrName < this.columns.length) return columnIndexOrName;
+
+	// otherwise search for the name
+	for (var c = 0; c < this.columns.length; c++) if (this.columns[c].name == columnIndexOrName) return c;
+
+	return -1;
+};
+
+/**
+ * Get HTML row object at given index
+ * @param {Integer} index of the row
+ */
+EditableGrid.prototype.getRow = function(rowIndex)
+{
+	if (rowIndex < 0) return this.tHead.rows[rowIndex + this.nbHeaderRows];
+	if (typeof this.data[rowIndex] == 'undefined') { console.error("[getRow] Invalid row index " + rowIndex); return null; }
+	return _$(this._getRowDOMId(this.data[rowIndex].id));
+};
+
+/**
+ * Get row id for given row index
+ * @param {Integer} index of the row
+ */
+EditableGrid.prototype.getRowId = function(rowIndex)
+{
+	return (rowIndex < 0 || rowIndex >= this.data.length) ? null : this.data[rowIndex]['id'];
+};
+
+/**
+ * Get index of row (in filtered data) with given id
+ * @param {Integer} rowId or HTML row object
+ */
+EditableGrid.prototype.getRowIndex = function(rowId) 
+{
+	rowId = typeof rowId == 'object' ? rowId.rowId : rowId;
+	for (var rowIndex = 0; rowIndex < this.data.length; rowIndex++) if (this.data[rowIndex].id == rowId) return rowIndex;
+	return -1; 
+};
+
+/**
+ * Get custom row attribute specified in XML
+ * @param {Integer} index of the row
+ * @param {String} name of the attribute
+ */
+EditableGrid.prototype.getRowAttribute = function(rowIndex, attributeName)
+{
+	if (typeof this.data[rowIndex] == 'undefined') {
+		console.error('Invalid rowindex ' + rowIndex);
+		return null;
+	}
+
+	return this.data[rowIndex][attributeName];
+};
+
+/**
+ * Set custom row attribute
+ * @param {Integer} index of the row
+ * @param {String} name of the attribute
+ * @param value of the attribute
+ */
+EditableGrid.prototype.setRowAttribute = function(rowIndex, attributeName, attributeValue)
+{
+	this.data[rowIndex][attributeName] = attributeValue;
+};
+
+/**
+ * Get Id of row in HTML DOM
+ * @private
+ */
+EditableGrid.prototype._getRowDOMId = function(rowId)
+{
+	return this.currentContainerid != null ? this.name + "_" + rowId : rowId;
+};
+
+/**
+ * Remove row with given id
+ * Deprecated: use remove(rowIndex) instead
+ * @param {Integer} rowId
+ */
+EditableGrid.prototype.removeRow = function(rowId)
+{
+	return this.remove(this.getRowIndex(rowId));
+};
+
+/**
+ * Remove row at given index
+ * @param {Integer} rowIndex
+ */
+EditableGrid.prototype.remove = function(rowIndex)
+{
+	var rowId = this.data[rowIndex].id;
+	var originalIndex = this.data[rowIndex].originalIndex;
+	var _data = this.dataUnfiltered == null ? this.data : this.dataUnfiltered; 
+
+	// delete row from DOM (needed for attach mode)
+	var tr = _$(this._getRowDOMId(rowId));
+	if (tr != null) this.tBody.removeChild(tr);
+
+	// update originalRowIndex
+	for (var r = 0; r < _data.length; r++) if (_data[r].originalIndex >= originalIndex) _data[r].originalIndex--;
+
+	// delete row from data
+	this.data.splice(rowIndex, 1);
+	if (this.dataUnfiltered != null) for (var r = 0; r < this.dataUnfiltered.length; r++) if (this.dataUnfiltered[r].id == rowId) { this.dataUnfiltered.splice(r, 1); break; }
+
+	// callback
+	this.rowRemoved(rowIndex,rowId);
+
+	// refresh grid
+	this.refreshGrid();
+};
+
+/**
+ * Return an associative array (column name => value) of values in row with given index 
+ * @param {Integer} rowIndex
+ */
+EditableGrid.prototype.getRowValues = function(rowIndex) 
+{
+	var rowValues = {};
+	for (var columnIndex = 0; columnIndex < this.getColumnCount(); columnIndex++) { 
+		rowValues[this.getColumnName(columnIndex)] = this.getValueAt(rowIndex, columnIndex);
+	}
+	return rowValues;
+};
+
+/**
+ * Append row with given id and data
+ * @param {Integer} rowId id of new row
+ * @param {Integer} columns
+ * @param {Boolean} dontSort
+ */
+EditableGrid.prototype.append = function(rowId, cellValues, rowAttributes, dontSort)
+{
+	return this.insertAfter(this.data.length - 1, rowId, cellValues, rowAttributes, dontSort);
+};
+
+/**
+ * Append row with given id and data
+ * Deprecated: use append instead
+ * @param {Integer} rowId id of new row
+ * @param {Integer} columns
+ * @param {Boolean} dontSort
+ */
+EditableGrid.prototype.addRow = function(rowId, cellValues, rowAttributes, dontSort)
+{
+	return this.append(rowId, cellValues, rowAttributes, dontSort);
+};
+
+/**
+ * Insert row with given id and data at given location
+ * We know rowIndex is valid, unless the table is empty
+ * @private
+ */
+EditableGrid.prototype._insert = function(rowIndex, offset, rowId, cellValues, rowAttributes, dontSort)
+{
+	var originalRowId = null;
+	var originalIndex = 0;
+	var _data = this.dataUnfiltered == null ? this.data : this.dataUnfiltered;
+
+	if (typeof this.data[rowIndex] != "undefined") {
+		originalRowId = this.data[rowIndex].id;
+		originalIndex = this.data[rowIndex].originalIndex + offset;
+	}
+
+	// append row in DOM (needed for attach mode)
+	if (this.currentContainerid == null) {
+		var tr = this.tBody.insertRow(rowIndex + offset);
+		tr.rowId = rowId;
+		tr.id = this._getRowDOMId(rowId);
+		for (var c = 0; c < this.columns.length; c++) tr.insertCell(c);
+	}
+
+	// build data for new row
+	var rowData = { visible: true, originalIndex: originalIndex, id: rowId };
+	if (rowAttributes) for (var attributeName in rowAttributes) rowData[attributeName] = rowAttributes[attributeName]; 
+	rowData.columns = [];
+	for (var c = 0; c < this.columns.length; c++) {
+		var cellValue = this.columns[c].name in cellValues ? cellValues[this.columns[c].name] : "";
+		rowData.columns.push(this.getTypedValue(c, cellValue));
+	}
+
+	// update originalRowIndex
+	for (var r = 0; r < _data.length; r++) if (_data[r].originalIndex >= originalIndex) _data[r].originalIndex++;
+
+	// append row in data
+	this.data.splice(rowIndex + offset, 0, rowData);
+	if (this.dataUnfiltered != null) {
+		if (originalRowId === null) this.dataUnfiltered.splice(rowIndex + offset, 0, rowData);
+		else for (var r = 0; r < this.dataUnfiltered.length; r++) if (this.dataUnfiltered[r].id == originalRowId) { this.dataUnfiltered.splice(r + offset, 0, rowData); break; }
+	}
+
+	// refresh grid
+	this.refreshGrid();
+
+	// sort and filter table
+	if (!dontSort) this.sort();
+	this.filter();
+};
+
+/**
+ * Insert row with given id and data before given row index
+ * @param {Integer} rowIndex index of row before which to insert new row
+ * @param {Integer} rowId id of new row
+ * @param {Integer} columns
+ * @param {Boolean} dontSort
+ */
+EditableGrid.prototype.insert = function(rowIndex, rowId, cellValues, rowAttributes, dontSort)
+{
+	if (rowIndex < 0) rowIndex = 0;
+	if (rowIndex >= this.data.length && this.data.length > 0) return this.insertAfter(this.data.length - 1, rowId, cellValues, rowAttributes, dontSort);
+	return this._insert(rowIndex, 0, rowId, cellValues, rowAttributes, dontSort);
+};
+
+/**
+ * Insert row with given id and data after given row index
+ * @param {Integer} rowIndex index of row after which to insert new row
+ * @param {Integer} rowId id of new row
+ * @param {Integer} columns
+ * @param {Boolean} dontSort
+ */
+EditableGrid.prototype.insertAfter = function(rowIndex, rowId, cellValues, rowAttributes, dontSort)
+{
+	if (rowIndex < 0) return this.insert(0, rowId, cellValues, rowAttributes, dontSort);
+	if (rowIndex >= this.data.length) rowIndex = this.data.length - 1; 
+	return this._insert(rowIndex, 1, rowId, cellValues, rowAttributes, dontSort);
+};
+
+/**
+ * Sets the column header cell renderer for the specified column index
+ * @param {Object} columnIndexOrName index or name of the column
+ * @param {CellRenderer} cellRenderer
+ */
+EditableGrid.prototype.setHeaderRenderer = function(columnIndexOrName, cellRenderer)
+{
+	var columnIndex = this.getColumnIndex(columnIndexOrName);
+	if (columnIndex < 0) console.error("[setHeaderRenderer] Invalid column: " + columnIndexOrName);
+	else {
+		var column = this.columns[columnIndex];
+		column.headerRenderer = (this.enableSort && column.datatype != "html") ? new SortHeaderRenderer(column.name, cellRenderer) : cellRenderer;
+
+		// give access to the column from the cell renderer
+		if (cellRenderer) {
+			if (this.enableSort && column.datatype != "html") {
+				column.headerRenderer.editablegrid = this;
+				column.headerRenderer.column = column;
+			}
+			cellRenderer.editablegrid = this;
+			cellRenderer.column = column;
+		}
+	}
+};
+
+/**
+ * Sets the cell renderer for the specified column index
+ * @param {Object} columnIndexOrName index or name of the column
+ * @param {CellRenderer} cellRenderer
+ */
+EditableGrid.prototype.setCellRenderer = function(columnIndexOrName, cellRenderer)
+{
+	var columnIndex = this.getColumnIndex(columnIndexOrName);
+	if (columnIndex < 0) console.error("[setCellRenderer] Invalid column: " + columnIndexOrName);
+	else {
+		var column = this.columns[columnIndex];
+		column.cellRenderer = cellRenderer;
+
+		// give access to the column from the cell renderer
+		if (cellRenderer) {
+			cellRenderer.editablegrid = this;
+			cellRenderer.column = column;
+		}
+	}
+};
+
+/**
+ * Sets the cell editor for the specified column index
+ * @param {Object} columnIndexOrName index or name of the column
+ * @param {CellEditor} cellEditor
+ */
+EditableGrid.prototype.setCellEditor = function(columnIndexOrName, cellEditor)
+{
+	var columnIndex = this.getColumnIndex(columnIndexOrName);
+	if (columnIndex < 0) console.error("[setCellEditor] Invalid column: " + columnIndexOrName);
+	else {
+		var column = this.columns[columnIndex];
+		column.cellEditor = cellEditor;
+
+		// give access to the column from the cell editor
+		if (cellEditor) {
+			cellEditor.editablegrid = this;
+			cellEditor.column = column;
+		}
+	}
+};
+
+/**
+ * Sets the header cell editor for the specified column index
+ * @param {Object} columnIndexOrName index or name of the column
+ * @param {CellEditor} cellEditor
+ */
+EditableGrid.prototype.setHeaderEditor = function(columnIndexOrName, cellEditor)
+{
+	var columnIndex = this.getColumnIndex(columnIndexOrName);
+	if (columnIndex < 0) console.error("[setHeaderEditor] Invalid column: " + columnIndexOrName);
+	else {
+		var column = this.columns[columnIndex];
+		column.headerEditor = cellEditor;
+
+		// give access to the column from the cell editor
+		if (cellEditor) {
+			cellEditor.editablegrid = this;
+			cellEditor.column = column;
+		}
+	}
+};
+
+/**
+ * Sets the enum provider for the specified column index
+ * @param {Object} columnIndexOrName index or name of the column
+ * @param {EnumProvider} enumProvider
+ */
+EditableGrid.prototype.setEnumProvider = function(columnIndexOrName, enumProvider)
+{
+	var columnIndex = this.getColumnIndex(columnIndexOrName);
+	if (columnIndex < 0) console.error("[setEnumProvider] Invalid column: " + columnIndexOrName);
+	else {
+		var hadProviderAlready = this.columns[columnIndex].enumProvider != null;
+		this.columns[columnIndex].enumProvider = enumProvider;
+
+		// if needed, we recreate the cell renderer and editor for this column
+		// if the column had an enum provider already, the render/editor previously created by default is ok already
+		// ... and we don't want to erase a custom renderer/editor that may have been set before calling setEnumProvider
+		if (!hadProviderAlready) {
+			this._createCellRenderer(this.columns[columnIndex]);
+			this._createCellEditor(this.columns[columnIndex]);
+		}
+	}
+};
+
+/**
+ * Clear all cell validators for the specified column index
+ * @param {Object} columnIndexOrName index or name of the column
+ */
+EditableGrid.prototype.clearCellValidators = function(columnIndexOrName)
+{
+	var columnIndex = this.getColumnIndex(columnIndexOrName);
+	if (columnIndex < 0) console.error("[clearCellValidators] Invalid column: " + columnIndexOrName);
+	else this.columns[columnIndex].cellValidators = [];
+};
+
+/**
+ * Adds default cell validators for the specified column index (according to the column type)
+ * @param {Object} columnIndexOrName index or name of the column
+ */
+EditableGrid.prototype.addDefaultCellValidators = function(columnIndexOrName)
+{
+	var columnIndex = this.getColumnIndex(columnIndexOrName);
+	if (columnIndex < 0) console.error("[addDefaultCellValidators] Invalid column: " + columnIndexOrName);
+	return this._addDefaultCellValidators(this.columns[columnIndex]);
+};
+
+/**
+ * Adds default cell validators for the specified column
+ * @private
+ */
+EditableGrid.prototype._addDefaultCellValidators = function(column)
+{
+	if (column.datatype == "integer" || column.datatype == "double") column.cellValidators.push(new NumberCellValidator(column.datatype));
+	else if (column.datatype == "email") column.cellValidators.push(new EmailCellValidator());
+	else if (column.datatype == "website" || column.datatype == "url") column.cellValidators.push(new WebsiteCellValidator());
+	else if (column.datatype == "date") column.cellValidators.push(new DateCellValidator(this));
+};
+
+/**
+ * Adds a cell validator for the specified column index
+ * @param {Object} columnIndexOrName index or name of the column
+ * @param {CellValidator} cellValidator
+ */
+EditableGrid.prototype.addCellValidator = function(columnIndexOrName, cellValidator)
+{
+	var columnIndex = this.getColumnIndex(columnIndexOrName);
+	if (columnIndex < 0) console.error("[addCellValidator] Invalid column: " + columnIndexOrName);
+	else this.columns[columnIndex].cellValidators.push(cellValidator);
+};
+
+/**
+ * Sets the table caption: set as null to remove
+ * @param columnIndexOrName
+ * @param caption
+ * @return
+ */
+EditableGrid.prototype.setCaption = function(caption)
+{
+	this.caption = caption;
+};
+
+/**
+ * Get cell element at given row and column
+ */
+EditableGrid.prototype.getCell = function(rowIndex, columnIndex)
+{
+	var row = this.getRow(rowIndex);
+	if (row == null) { console.error("[getCell] Invalid row index " + rowIndex); return null; }
+	return row.cells[columnIndex];
+};
+
+/**
+ * Get cell X position relative to the first non static offset parent
+ * @private
+ */
+EditableGrid.prototype.getCellX = function(oElement)
+{
+	var iReturnValue = 0;
+	while (oElement != null && this.isStatic(oElement)) try {
+		iReturnValue += oElement.offsetLeft;
+		oElement = oElement.offsetParent;
+	} catch(err) { oElement = null; }
+	return iReturnValue;
+};
+
+/**
+ * Get cell Y position relative to the first non static offset parent
+ * @private
+ */
+EditableGrid.prototype.getCellY = function(oElement)
+{
+	var iReturnValue = 0;
+	while (oElement != null && this.isStatic(oElement)) try {
+		iReturnValue += oElement.offsetTop;
+		oElement = oElement.offsetParent;
+	} catch(err) { oElement = null; }
+	return iReturnValue;
+};
+
+/**
+ * Get X scroll offset relative to the first non static offset parent
+ * @private
+ */
+EditableGrid.prototype.getScrollXOffset = function(oElement)
+{
+	var iReturnValue = 0;
+	while (oElement != null && typeof oElement.scrollLeft != 'undefined' && this.isStatic(oElement) && oElement != document.body) try {
+		iReturnValue += parseInt(oElement.scrollLeft);
+		oElement = oElement.parentNode;
+	} catch(err) { oElement = null; }
+	return iReturnValue;
+};
+
+/**
+ * Get Y scroll offset relative to the first non static offset parent
+ * @private
+ */
+EditableGrid.prototype.getScrollYOffset = function(oElement)
+{
+	var iReturnValue = 0;
+	while (oElement != null && typeof oElement.scrollTop != 'undefined' && this.isStatic(oElement) && oElement != document.body) try {
+		iReturnValue += parseInt(oElement.scrollTop);
+		oElement = oElement.parentNode;
+	} catch(err) { oElement = null; }
+	return iReturnValue;
+};
+
+/**
+ * Private
+ * @param containerid
+ * @param className
+ * @param tableid
+ * @return
+ */
+EditableGrid.prototype._rendergrid = function(containerid, className, tableid)
+{
+	with (this) {
+
+		lastSelectedRowIndex = -1;
+		_currentPageIndex = getCurrentPageIndex();
+
+		// if we are already attached to an existing table, just update the cell contents
+		if (typeof table != "undefined" && table != null) {
+
+			var _data = dataUnfiltered == null ? data : dataUnfiltered; 
+
+			// render headers
+			_renderHeaders();
+
+			// render content
+			var rows = tBody.rows;
+			var skipped = 0;
+			var displayed = 0;
+			var rowIndex = 0;
+
+			for (var i = 0; i < rows.length; i++) {
+
+				// filtering and pagination in attach mode means hiding rows
+				if (!_data[i].visible || (pageSize > 0 && displayed >= pageSize)) {
+					if (rows[i].style.display != 'none') {
+						rows[i].style.display = 'none';
+						rows[i].hidden_by_editablegrid = true;
+					}
+				}
+				else {
+					if (skipped < pageSize * _currentPageIndex) {
+						skipped++; 
+						if (rows[i].style.display != 'none') {
+							rows[i].style.display = 'none';
+							rows[i].hidden_by_editablegrid = true;
+						}
+					}
+					else {
+						displayed++;
+						var cols = rows[i].cells;
+						if (typeof rows[i].hidden_by_editablegrid != 'undefined' && rows[i].hidden_by_editablegrid) {
+							rows[i].style.display = '';
+							rows[i].hidden_by_editablegrid = false;
+						}
+						rows[i].rowId = getRowId(rowIndex);
+						rows[i].id = _getRowDOMId(rows[i].rowId);
+						for (var j = 0; j < cols.length && j < columns.length; j++) 
+							if (columns[j].renderable) columns[j].cellRenderer._render(rowIndex, j, cols[j], getValueAt(rowIndex,j));
+					}
+					rowIndex++;
+				}
+			}
+
+			// attach handler on click or double click 
+			table.editablegrid = this;
+			if (doubleclick) table.ondblclick = function(e) { this.editablegrid.mouseClicked(e); };
+			else table.onclick = function(e) { this.editablegrid.mouseClicked(e); }; 
+		}
+
+		// we must render a whole new table
+		else {
+
+			if (!containerid) return console.warn("The container ID not specified (renderGrid not called yet ?)");
+			if (!_$(containerid)) return console.error("Unable to get element [" + containerid + "]");
+
+			currentContainerid = containerid;
+			currentClassName = className;
+			currentTableid = tableid;
+
+			var startRowIndex = 0;
+			var endRowIndex = getRowCount();
+
+			// paginate if required
+			if (pageSize > 0) {
+				startRowIndex = _currentPageIndex * pageSize;
+				endRowIndex = Math.min(getRowCount(), startRowIndex + pageSize); 
+			}
+
+			// create editablegrid table and add it to our container 
+			this.table = document.createElement("table");
+			table.className = className || "editablegrid";          
+			if (typeof tableid != "undefined") table.id = tableid;
+			while (_$(containerid).hasChildNodes()) _$(containerid).removeChild(_$(containerid).firstChild);
+			_$(containerid).appendChild(table);
+
+			// create header
+			if (caption) {
+				var captionElement = document.createElement("CAPTION");
+				captionElement.innerHTML = this.caption;
+				table.appendChild(captionElement);
+			}
+
+			this.tHead = document.createElement("THEAD");
+			table.appendChild(tHead);
+			var trHeader = tHead.insertRow(0);
+			var columnCount = getColumnCount();
+			for (var c = 0; c < columnCount; c++) {
+				var headerCell = document.createElement("TH");
+				var td = trHeader.appendChild(headerCell);
+				columns[c].headerRenderer._render(-1, c, td, columns[c].label);
+			}
+
+			// create body and rows
+			this.tBody = document.createElement("TBODY");
+			table.appendChild(tBody);
+			var insertRowIndex = 0;
+			for (var i = startRowIndex; i < endRowIndex; i++) {
+				var tr = tBody.insertRow(insertRowIndex++);
+				tr.rowId = data[i]['id'];
+				tr.id = this._getRowDOMId(data[i]['id']);
+				for (j = 0; j < columnCount; j++) {
+
+					// create cell and render its content
+					var td = tr.insertCell(j);
+					columns[j].cellRenderer._render(i, j, td, getValueAt(i,j));
+				}
+			}
+
+			// attach handler on click or double click 
+			_$(containerid).editablegrid = this;
+			if (doubleclick) _$(containerid).ondblclick = function(e) { this.editablegrid.mouseClicked(e); };
+			else _$(containerid).onclick = function(e) { this.editablegrid.mouseClicked(e); }; 
+		}
+
+		// callback
+		tableRendered(containerid, className, tableid);
+	}
+};
+
+
+/**
+ * Renders the grid as an HTML table in the document
+ * @param {String} containerid 
+ * id of the div in which you wish to render the HTML table (this parameter is ignored if you used attachToHTMLTable)
+ * @param {String} className 
+ * CSS class name to be applied to the table (this parameter is ignored if you used attachToHTMLTable)
+ * @param {String} tableid
+ * ID to give to the table (this parameter is ignored if you used attachToHTMLTable)
+ * @see EditableGrid#attachToHTMLTable
+ * @see EditableGrid#loadXML
+ */
+EditableGrid.prototype.renderGrid = function(containerid, className, tableid)
+{
+	// actually render grid
+	this._rendergrid(containerid, className, tableid);
+
+	// if client side: sort and filter
+	if (!this.serverSide) {
+		this.sort() ;
+		this.filter();
+	}
+};
+
+
+/**
+ * Refreshes the grid
+ * @return
+ */
+EditableGrid.prototype.refreshGrid = function()
+{
+	if (this.currentContainerid != null) this.table = null; // if we are not in "attach mode", clear table to force a full re-render
+	this._rendergrid(this.currentContainerid, this.currentClassName, this.currentTableid);
+};
+
+/**
+ * Render all column headers 
+ * @private
+ */
+EditableGrid.prototype._renderHeaders = function() 
+{
+	with (this) {
+		var rows = tHead.rows;
+		for (var i = 0; i < 1 /*rows.length*/; i++) {
+			var rowData = [];
+			var cols = rows[i].cells;
+			var columnIndexInModel = 0;
+			for (var j = 0; j < cols.length && columnIndexInModel < columns.length; j++) {
+				columns[columnIndexInModel].headerRenderer._render(-1, columnIndexInModel, cols[j], columns[columnIndexInModel].label);
+				var colspan = parseInt(cols[j].getAttribute("colspan"));
+				columnIndexInModel += colspan > 1 ? colspan : 1;
+			}
+		}
+	}
+};
+
+/**
+ * Mouse click handler
+ * @param {Object} e
+ * @private
+ */
+EditableGrid.prototype.mouseClicked = function(e) 
+{
+	e = e || window.event;
+	with (this) {
+
+		// get row and column index from the clicked cell
+		var target = e.target || e.srcElement;
+
+		// go up parents to find a cell or a link under the clicked position
+		while (target) if (target.tagName == "A" || target.tagName == "TD" || target.tagName == "TH") break; else target = target.parentNode;
+		if (!target || !target.parentNode || !target.parentNode.parentNode || (target.parentNode.parentNode.tagName != "TBODY" && target.parentNode.parentNode.tagName != "THEAD") || target.isEditing) return;
+
+		// don't handle clicks on links
+		if (target.tagName == "A") return;
+
+		// get cell position in table
+		var rowIndex = getRowIndex(target.parentNode);
+		var columnIndex = target.cellIndex;
+
+		var column = columns[columnIndex];
+		if (column) {
+
+			// if another row has been selected: callback
+			if (rowIndex > -1 && rowIndex != lastSelectedRowIndex) {
+				rowSelected(lastSelectedRowIndex, rowIndex);				
+				lastSelectedRowIndex = rowIndex;
+			}
+
+			// edit current cell value
+			if (!column.editable) { readonlyWarning(column); }
+			else {
+				if (rowIndex < 0) { 
+					if (column.headerEditor && isHeaderEditable(rowIndex, columnIndex)) 
+						column.headerEditor.edit(rowIndex, columnIndex, target, column.label);
+				}
+				else if (column.cellEditor && isEditable(rowIndex, columnIndex))
+					column.cellEditor.edit(rowIndex, columnIndex, target, getValueAt(rowIndex, columnIndex));
+			}
+		}
+	}
+};
+
+
+/**
+ * Moves columns around (added by JRE)
+ * @param {array[strings]} an array of class names of the headers
+ * returns boolean based on success
+ */
+EditableGrid.prototype.sortColumns = function(headerArray){
+	with (this){
+		newColumns = [];
+		newColumnIndeces = [];
+
+		for (var i = 0; i < headerArray.length; i++) {
+
+			columnIndex = this.getColumnIndex(headerArray[i]);
+
+			if(columnIndex == -1){//a column could not be found. can't reorder anything or data may be lost
+				console.error("[sortColumns] Invalid column: " + columnIndex);
+				return false;
+			}
+
+			newColumns[i] = this.columns[columnIndex];
+			newColumnIndeces[i] = columnIndex;
+		}
+
+		//rearrance headers
+		this.columns = newColumns;
+
+		//need to rearrange all of the data elements as well
+		for (var i = 0; i < this.data.length; i++) {
+			var myData = this.data[i];
+			var myDataColumns = myData.columns;
+			var newDataColumns = [];
+
+			for (var j = 0; j < myDataColumns.length; j++) {
+				newIndex = newColumnIndeces[j];
+				newDataColumns[j] = myDataColumns[newIndex];
+			}
+
+			this.data[i].columns = newDataColumns;
+		}
+
+		return true;
+	}
+}
+
+/**
+ * Sort on a column
+ * @param {Object} columnIndexOrName index or name of the column
+ * @param {Boolean} descending
+ */
+EditableGrid.prototype.sort = function(columnIndexOrName, descending, backOnFirstPage)
+{
+	with (this) {
+
+		if (typeof columnIndexOrName  == 'undefined' && sortedColumnName === -1) {
+
+			// avoid a double render, but still send the expected callback
+			tableSorted(-1, sortDescending);
+			return true;
+		}
+
+		if (typeof columnIndexOrName  == 'undefined') columnIndexOrName = sortedColumnName;
+		if (typeof descending  == 'undefined') descending = sortDescending;
+
+		localset('sortColumnIndexOrName', columnIndexOrName);
+		localset('sortDescending', descending);
+
+		// if sorting is done on server-side, we are done here
+		if (serverSide) return backOnFirstPage ? setPageIndex(0) : refreshGrid();
+
+		var columnIndex = columnIndexOrName;
+		if (parseInt(columnIndex, 10) !== -1) {
+			columnIndex = this.getColumnIndex(columnIndexOrName);
+			if (columnIndex < 0) {
+				console.error("[sort] Invalid column: " + columnIndexOrName);
+				return false;
+			}
+		}
+
+		if (!enableSort) {
+			tableSorted(columnIndex, descending);
+			return;
+		}
+
+		// work on unfiltered data
+		var filterActive = dataUnfiltered != null; 
+		if (filterActive) data = dataUnfiltered;
+
+		var type = columnIndex < 0 ? "" : getColumnType(columnIndex);
+		var row_array = [];
+		var rowCount = getRowCount();
+		for (var i = 0; i < rowCount - (ignoreLastRow ? 1 : 0); i++) row_array.push([columnIndex < 0 ? null : getDisplayValueAt(i, columnIndex), i, data[i].originalIndex]);
+
+		var sort_function = type == "integer" || type == "double" ? sort_numeric : type == "boolean" ? sort_boolean : type == "date" ? sort_date : sort_alpha;
+		row_array.sort(columnIndex < 0 ? unsort : sort_stable(sort_function, descending));
+		if (ignoreLastRow) row_array.push([columnIndex < 0 ? null : getDisplayValueAt(rowCount - 1, columnIndex), rowCount - 1, data[rowCount - 1].originalIndex]);
+
+		// rebuild data using the new order
+		var _data = data;
+		data = [];
+		for (var i = 0; i < row_array.length; i++) data.push(_data[row_array[i][1]]);
+		delete row_array;
+
+		if (filterActive) {
+
+			// keep only visible rows in data
+			dataUnfiltered = data;
+			data = [];
+			for (var r = 0; r < rowCount; r++) if (dataUnfiltered[r].visible) data.push(dataUnfiltered[r]);
+		}
+
+		// refresh grid (back on first page if sort column has changed) and callback
+		if (backOnFirstPage) setPageIndex(0); else refreshGrid();
+		tableSorted(columnIndex, descending);
+		return true;
+	}
+};
+
+
+/**
+ * Filter the content of the table
+ * @param {String} filterString String string used to filter: all words must be found in the row
+ * @param {Array} cols Columns to sort.  If cols is not specified, the filter will be done on all columns
+ */
+EditableGrid.prototype.filter = function(filterString, cols)
+{
+	with (this) {
+
+		if (typeof filterString != 'undefined') {
+			this.currentFilter = filterString;
+			this.localset('filter', filterString);
+		}
+
+		// if filtering is done on server-side, we are done here
+		if (serverSide) return setPageIndex(0);
+
+		// un-filter if no or empty filter set
+		if (currentFilter == null || currentFilter == "") {
+			if (dataUnfiltered != null) {
+				data = dataUnfiltered;
+				dataUnfiltered = null;
+				for (var r = 0; r < getRowCount(); r++) data[r].visible = true;
+				setPageIndex(0);
+				tableFiltered();
+			}
+			return;
+		}		
+
+		var words = currentFilter.toLowerCase().split(" ");
+
+		// work on unfiltered data
+		if (dataUnfiltered != null) data = dataUnfiltered;
+
+		var rowCount = getRowCount();
+		var columnCount = typeof cols != 'undefined' ? cols.length  : getColumnCount();
+
+		for (var r = 0; r < rowCount; r++) {
+			var row = data[r];
+			row.visible = true;
+			var rowContent = ""; 
+
+			// add column values
+			for (var c = 0; c < columnCount; c++) {
+				if (getColumnType(c) == 'boolean') continue;
+				var displayValue = getDisplayValueAt(r, typeof cols != 'undefined'  ? cols[c] :  c);
+				var value = getValueAt(r, typeof cols != 'undefined'  ? cols[c] : c);
+				rowContent += displayValue + " " + (displayValue == value ? "" : value + " ");
+			}
+
+			// add attribute values
+			for (var attributeName in row) {
+				if (attributeName != "visible" && attributeName != "originalIndex" && attributeName != "columns") rowContent += row[attributeName];
+			}
+
+			// if row contents do not match one word in the filter, hide the row
+			for (var i = 0; i < words.length; i++) {
+				var word = words[i];
+				var match = false;
+
+				// a word starting with "!" means that we want a NON match
+				var invertMatch = word.startsWith("!");
+				if (invertMatch) word = word.substr(1);
+
+				// if word is of the form "colname/attributename=value" or "colname/attributename!=value", only this column/attribute is used
+				var colindex = -1;
+				var attributeName = null;
+				if (word.contains("!=")) {
+					var parts = word.split("!=");
+					colindex = getColumnIndex(parts[0]);
+					if (colindex >= 0) {
+						word = parts[1];
+						invertMatch = !invertMatch;
+					}
+					else if (typeof row[parts[0]] != 'undefined') {
+						attributeName = parts[0];
+						word = parts[1];
+						invertMatch = !invertMatch;
+					}
+				}
+				else if (word.contains("=")) {
+					var parts = word.split("=");
+					colindex = getColumnIndex(parts[0]);
+					if (colindex >= 0) word = parts[1];
+					else if (typeof row[parts[0]] != 'undefined') {
+						attributeName = parts[0];
+						word = parts[1];
+					}
+				}
+
+				// a word ending with "!" means that a column must match this word exactly
+				if (!word.endsWith("!")) {
+					if (colindex >= 0) match = (getValueAt(r, colindex) + ' ' + getDisplayValueAt(r, colindex)).trim().toLowerCase().indexOf(word) >= 0;
+					else if (attributeName !== null) match = (''+getRowAttribute(r, attributeName)).trim().toLowerCase().indexOf(word) >= 0;
+					else match = rowContent.toLowerCase().indexOf(word) >= 0; 
+				}
+				else {
+					word = word.substr(0, word.length - 1);
+					if (colindex >= 0) match = (''+getDisplayValueAt(r, colindex)).trim().toLowerCase() == word || (''+getValueAt(r, colindex)).trim().toLowerCase() == word; 
+					else if (attributeName !== null) match = (''+getRowAttribute(r, attributeName)).trim().toLowerCase() == word; 
+					else for (var c = 0; c < columnCount; c++) {
+						if (getColumnType(typeof cols != 'undefined'  ? cols[c] : c) == 'boolean') continue;
+						if ((''+getDisplayValueAt(r, typeof cols != 'undefined'  ? cols[c] : c)).trim().toLowerCase() == word || (''+getValueAt(r, typeof cols != 'undefined'  ? cols[c] : c)).trim().toLowerCase() == word) match = true;
+					}
+				}
+
+				if (invertMatch ? match : !match) {
+					data[r].visible = false;
+					break;
+				}
+			}
+		}
+
+		// keep only visible rows in data
+		dataUnfiltered = data;
+		data = [];
+		for (var r = 0; r < rowCount; r++) if (dataUnfiltered[r].visible) data.push(dataUnfiltered[r]);
+
+		// refresh grid (back on first page) and callback
+		setPageIndex(0);
+		tableFiltered();
+	}
+};
+
+
+
+/**
+ * Sets the page size(pageSize of 0 means no pagination)
+ * @param {Integer} pageSize Integer page size
+ */
+EditableGrid.prototype.setPageSize = function(pageSize)
+{
+	this.pageSize = parseInt(pageSize);
+	if (isNaN(this.pageSize)) this.pageSize = 0;
+	this.currentPageIndex = 0;
+	this.refreshGrid();
+};
+
+/**
+ * Returns the number of pages according to the current page size
+ */
+EditableGrid.prototype.getPageCount = function()
+{
+	if (this.getRowCount() == 0) return 0;
+	if (this.pageCount > 0) return this.pageCount; // server side pagination
+	else if (this.pageSize <= 0) { console.error("getPageCount: no or invalid page size defined (" + this.pageSize + ")"); return -1; }
+	return Math.ceil(this.getRowCount() / this.pageSize);
+};
+
+/**
+ * Returns the number of pages according to the current page size
+ */
+EditableGrid.prototype.getCurrentPageIndex = function()
+{
+	// if pagination is handled on the server side, pageSize will (must) be 0
+	if (this.pageSize <= 0 && !this.serverSide) return 0;
+
+	// if page index does not exist anymore, go to last page (without losing the information of the current page)
+	return Math.max(0, this.currentPageIndex >= this.getPageCount() ? this.getPageCount() - 1 : this.currentPageIndex);
+};
+
+/**
+ * Sets the current page (no effect if pageSize is 0)
+ * @param {Integer} pageIndex Integer page index
+ */
+EditableGrid.prototype.setPageIndex = function(pageIndex)
+{
+	this.currentPageIndex = pageIndex;
+	this.localset('pageIndex', pageIndex);
+	this.refreshGrid();
+};
+
+/**
+ * Go the previous page if we are not already on the first page
+ * @return
+ */
+EditableGrid.prototype.prevPage = function()
+{
+	if (this.canGoBack()) this.setPageIndex(this.getCurrentPageIndex() - 1);
+};
+
+/**
+ * Go the first page if we are not already on the first page
+ * @return
+ */
+EditableGrid.prototype.firstPage = function()
+{
+	if (this.canGoBack()) this.setPageIndex(0);
+};
+
+/**
+ * Go the next page if we are not already on the last page
+ * @return
+ */
+EditableGrid.prototype.nextPage = function()
+{
+	if (this.canGoForward()) this.setPageIndex(this.getCurrentPageIndex() + 1);
+};
+
+/**
+ * Go the last page if we are not already on the last page
+ * @return
+ */
+EditableGrid.prototype.lastPage = function()
+{
+	if (this.canGoForward()) this.setPageIndex(this.getPageCount() - 1);
+};
+
+/**
+ * Returns true if we are not already on the first page
+ * @return
+ */
+EditableGrid.prototype.canGoBack = function()
+{
+	return this.getCurrentPageIndex() > 0;
+};
+
+/**
+ * Returns true if we are not already on the last page
+ * @return
+ */
+EditableGrid.prototype.canGoForward = function()
+{
+	return this.getCurrentPageIndex() < this.getPageCount() - 1;
+};
+
+/**
+ * Returns an interval { startPageIndex: ..., endPageIndex: ... } so that a window of the given size is visible around the current page (hence the 'sliding').
+ * If pagination is not enabled this method displays an error and returns null.
+ * If pagination is enabled but there is only one page this function returns null (wihtout error).
+ * @param slidingWindowSize size of the visible window
+ * @return
+ */
+EditableGrid.prototype.getSlidingPageInterval = function(slidingWindowSize)
+{
+	var nbPages = this.getPageCount();
+	if (nbPages <= 1) return null;
+
+	var curPageIndex = this.getCurrentPageIndex();
+	var startPageIndex = Math.max(0, curPageIndex - Math.floor(slidingWindowSize/2));
+	var endPageIndex = Math.min(nbPages - 1, curPageIndex + Math.floor(slidingWindowSize/2));
+
+	if (endPageIndex - startPageIndex < slidingWindowSize) {
+		var diff = slidingWindowSize - (endPageIndex - startPageIndex + 1);
+		startPageIndex = Math.max(0, startPageIndex - diff);
+		endPageIndex = Math.min(nbPages - 1, endPageIndex + diff);
+	}
+
+	return { startPageIndex: startPageIndex, endPageIndex: endPageIndex };
+};
+
+/**
+ * Returns an array of page indices in the given interval.
+ * 
+ * @param interval
+ * The given interval must be an object with properties 'startPageIndex' and 'endPageIndex'.
+ * This interval may for example have been obtained with getCurrentPageInterval.
+ * 
+ * @param callback
+ * The given callback is applied to each page index before adding it to the result array.
+ * This callback is optional: if none given, the page index will be added as is to the array.
+ * If given , the callback will be called with two parameters: pageIndex (integer) and isCurrent (boolean).
+ * 
+ * @return
+ */
+EditableGrid.prototype.getPagesInInterval = function(interval, callback)
+{
+	var pages = [];
+	for (var p = interval.startPageIndex; p <= interval.endPageIndex; p++) {
+		pages.push(typeof callback == 'function' ? callback(p, p == this.getCurrentPageIndex()) : p);
+	}
+	return pages;
+};

+ 428 - 0
src/static/js/editablegrid_charts.js

@@ -0,0 +1,428 @@
+var EditableGrid_check_lib = null;
+
+EditableGrid.prototype.checkChartLib = function()
+{
+	try {
+		$('dummy').highcharts();
+	}
+	catch (e) {
+		alert('HighCharts library not loaded!');
+		return false;
+	}
+
+	return true;
+};
+
+EditableGrid.prototype.hex2rgba = function(hexColor, alpha)
+{
+	if (typeof alpha == 'undefined') alpha = 1.0;
+	var color = {red: parseInt(hexColor.substr(1,2),16), green: parseInt(hexColor.substr(3,2),16), blue:  parseInt(hexColor.substr(5,2),16)};
+	return 'rgba(' + color.red + ',' + color.green + ',' + color.blue + ',' + alpha + ')';
+};
+
+EditableGrid.prototype.getFormattedValue = function(columnIndex, value)
+{
+	try {
+
+		// let the renderer work on a dummy element
+		var element = document.createElement('div');
+		var renderer = this.getColumn(columnIndex).cellRenderer;
+		renderer._render(0, columnIndex, element, value);
+		return element.innerHTML;
+
+	} catch (ex) {
+		return value;
+	}
+};
+
+
+/**
+ * renderBarChart
+ * Render Highcharts column chart for the data contained in the table model
+ * @param divId
+ * @param title
+ * @param labelColumnIndexOrName
+ * @param options: legend (label of labelColumnIndexOrName), bgColor (transparent), alpha (0.9), limit (0), bar3d (true), rotateXLabels (0) 
+ * @return
+ */
+
+EditableGrid.prototype.renderBarChart = function(divId, title, labelColumnIndexOrName, options)
+{
+	var self = this;
+
+	// default options
+	this.legend = null;
+	this.bgColor = null; // transparent
+	this.alpha = 0.9;
+	this.limit = 0;
+	this.bar3d = false;
+	this.rotateXLabels = 0;
+
+	// used to find the rowindex from the pointIndex for drawing horizontal reference lines
+	var rowIndexByPoint = {};
+
+	with (this) {
+
+		if (EditableGrid_check_lib === null) EditableGrid_check_lib = checkChartLib();
+		if (!EditableGrid_check_lib) return false;
+
+		// override default options with the ones given
+		if (options) for (var p in options) this[p] = options[p];
+
+		// get useful values
+		labelColumnIndexOrName = labelColumnIndexOrName || 0;
+		var cLabel = getColumnIndex(labelColumnIndexOrName);
+		var columnCount = getColumnCount();
+		var rowCount = getRowCount() - (ignoreLastRow ? 1 : 0);
+		if (limit > 0 && rowCount > limit) rowCount = limit;
+
+		// base chart
+		var type = options['type'] || 'column';
+		var chart = {
+
+				chart: {
+					type: type,
+					backgroundColor: bgColor,
+					plotBackgroundColor: bgColor,
+					height: null, // auto
+					options3d: { 
+						enabled: bar3d
+					}
+				},
+
+				plotOptions: {
+					column: {
+						stacking: options['stacking'] || '',
+						groupPadding: 0.1,
+						pointPadding: 0.1,
+						borderWidth: 0
+					},
+					bar: {
+						stacking: options['stacking'] || '',
+						groupPadding: 0.1,
+						pointPadding: 0.1,
+						borderWidth: 0
+					},
+					line: {
+						step: 'center',
+						lineWidth: 4,
+						zIndex: 1000,
+						marker: { enabled: false, states: { hover: { enabled: false } } }
+					},
+					scatter: {
+						zIndex: 1000,
+						marker: { symbol: 'diamond', lineColor: null, lineWidth: 4, states: { hover: { enabled: false } } }
+					}
+				},
+
+				legend: {
+					align: 'center',
+					verticalAlign: (type == 'bar' ? 'top' : 'bottom'),
+					margin: (type == 'bar' ? 40 : 12),
+					floating: false
+				},
+
+				credits: {
+					enabled: false
+				},
+
+				title: {
+					text: title
+				},
+
+				tooltip: {
+					pointFormatter: function() { return this.series.name + '<b>: ' + self.getFormattedValue(this.series.options.colIndex, this.y) + '</b>'; }
+				}
+		};
+
+		// xaxis with legend and rotation
+		chart.xAxis = {
+				title: { text:  legend || getColumnLabel(labelColumnIndexOrName) }, 
+				labels: { rotation: rotateXLabels } 
+		};
+
+		// on category for each row
+		chart.xAxis.categories = []; 
+		for (var r = 0; r < rowCount; r++) {
+			if (getRowAttribute(r, "skip") == "1") continue;
+			var label = getRowAttribute(r, "barlabel"); // if there is a barlabel attribute, use it and ignore labelColumn
+			chart.xAxis.categories.push(label ? label : getValueAt(r, cLabel));
+		}
+
+		// one serie for each bar column
+		chart.series = [];
+		chart.yAxis = [];
+
+		for (var c = 0; c < columnCount; c++) {
+			if (!isColumnBar(c)) continue;
+
+			// get/create axis for column unit
+			var yAxisIndex = -1;
+			var unit = getColumnUnit(c);
+			for (var cy = 0; cy < chart.yAxis.length; cy++) if (chart.yAxis[cy].unit == unit) yAxisIndex = cy;
+			if (yAxisIndex < 0) {
+
+				// not found: create new axis with no title and unit
+				yAxisIndex = chart.yAxis.length;
+				chart.yAxis.push({ 
+					unit: unit, 
+					title: { text: "" },
+					labels: { format: '{value} ' + (unit ? unit : '') },
+					reversedStacks: (typeof options['reversedStacks'] == 'undefined' ? false : (!!options['reversedStacks']))
+				});
+			}
+
+			// serie's name, stack and color
+			var serie = { 
+					name: getColumnLabel(c), 
+					stack: getColumnStack(c),
+					yAxis: yAxisIndex, 
+					colIndex: c, // for pointFormatter
+					// let Highcharts handle smart colors
+					// color: hex2rgba(smartColorsBar[chart.series.length % smartColorsBar.length], alpha), 
+					data: []
+			};
+
+			/*
+			// test line serie combined with the bars or columns
+			if (turn_this_column_into_a_line_serie) {
+				serie.type = 'line';
+				serie.dashStyle= "ShortDot";
+				serie.color = 'gray';
+			}
+			 */
+
+			// data: one value per row
+			var maxValue = null;
+			for (var r = 0; r < rowCount; r++) {
+				if (getRowAttribute(r, "skip") == "1") continue;
+				var value = getValueAt(r,c);
+				rowIndexByPoint[serie.data.length] = r;
+				serie.data.push(value);
+
+				/*
+				if (maxValue === null || value > maxValue) maxValue = value;
+
+				// take reference columns into account for Y axis height
+				var reference_columns = self.getRowAttribute(r, 'reference_columns');
+				if (reference_columns) $.each(reference_columns, function(i, reference) {
+					var value = self.getValueAt(r, self.getColumnIndex(reference.column));
+					if (maxValue === null || value > maxValue) maxValue = value;
+				});
+				 */
+			}
+
+			// update Y max value
+			// if (typeof chart.yAxis[yAxisIndex].max == 'undefined' || maxValue > chart.yAxis[yAxisIndex].max) chart.yAxis[yAxisIndex].max = maxValue;
+
+			chart.series.push(serie);
+		}
+
+		// auto height based on number of bars
+		if (type == 'bar') {
+			var stacks = {};
+			chart.chart.height = 100; // margin
+			for (var s = 0; s < chart.series.length; s++) {
+				if (chart.series[s].stack in stacks) continue; // count height only once per stack
+				stacks[chart.series[s].stack] = true;
+				chart.chart.height += serie.data.length * 40;
+			}
+		}
+
+		// render chart
+		$('#' + divId).highcharts(chart, function (chart) {
+
+			var widthColumn = null;
+			if (chart.series[0]) $.each(chart.series[0].points, function(pointIndex, p) {
+
+				// check if reference_columns attribute is set on rows, otherwise break loop here
+				var rowIndex = rowIndexByPoint[pointIndex];
+				var reference_columns = self.getRowAttribute(rowIndex, 'reference_columns');
+				if (!reference_columns) return false;
+
+				// determine width of columns (only once)
+				if (widthColumn === null) {
+					var lastX = null;
+					widthColumn = 0;
+					$.each(chart.series[0].points, function(i, p) {
+						if (lastX) widthColumn = p.plotX - lastX;
+						lastX = p.plotX;
+					});
+				}
+
+				// for each reference column
+				$.each(reference_columns, function(i, reference) {
+
+					// get reference value
+					var reference_value = self.getValueAt(rowIndex, self.getColumnIndex(reference.column));
+
+					// get line style
+					var attributes = { 'stroke-width': 2, stroke: 'black', zIndex: 3 };
+					if (reference.style) for (var attr in reference.style) attributes[attr] = reference.style[attr];
+
+					// draw a line at Y = reference value
+					var yPosition = chart.yAxis[0].toPixels(reference_value);
+					var xPosition = chart.plotLeft + p.plotX - widthColumn / 2;
+					chart.renderer.path(['M', xPosition, yPosition, 'L', xPosition + widthColumn, yPosition]).attr(attributes).add();
+
+				});
+			});
+		});
+
+		if (options.noDataMessage) {
+			var container = $('#' + divId).highcharts();
+			if (chart.series.length == 0) container.showLoading(options.noDataMessage);
+		}
+	}
+};
+
+/**
+ * renderStackedBarChart
+ * Render open flash stacked bar chart for the data contained in the table model
+ * @param divId
+ * @param title
+ * @param labelColumnIndexOrName
+ * @param options: legend (label of labelColumnIndexOrName), bgColor (#ffffff), alpha (0.9), limit (0), rotateXLabels (0) 
+ * @return
+ */
+EditableGrid.prototype.renderStackedBarChart = function(divId, title, labelColumnIndexOrName, options)
+{
+	options = options || {};
+	options.stacking = 'normal';
+	return this.renderBarChart(divId, title, labelColumnIndexOrName, options);
+};
+
+/**
+ * renderPieChart
+ * @param divId
+ * @param title
+ * @param valueColumnIndexOrName
+ * @param labelColumnIndexOrName: if same as valueColumnIndexOrName, the chart will display the frequency of values in this column 
+ * @param options: startAngle (0), bgColor (transparent), alpha (0.9), limit (0), gradientFill (true) 
+ * @return
+ */
+EditableGrid.prototype.renderPieChart = function(divId, title, valueColumnIndexOrName, labelColumnIndexOrName, options) 
+{
+	// default options
+	this.startAngle = 0;
+	this.bgColor = null; // transparent
+	this.alpha = 0.9;
+	this.limit = 0;
+	this.pie3d = false,
+	this.gradientFill = true;
+
+	// override default options with the ones given
+	if (options) for (var p in options) this[p] = options[p];
+
+	with (this) {
+
+		if (EditableGrid_check_lib === null) EditableGrid_check_lib = checkChartLib();
+		if (!EditableGrid_check_lib) return false;
+
+		// useful values
+		labelColumnIndexOrName = labelColumnIndexOrName || 0;
+		title = (typeof title == 'undefined' || title === null) ? getColumnLabel(valueColumnIndexOrName) : title;
+		var cValue = getColumnIndex(valueColumnIndexOrName);
+		var cLabel = getColumnIndex(labelColumnIndexOrName);
+		var rowCount = getRowCount() - (ignoreLastRow ? 1 : 0);
+		if (limit > 0 && rowCount > limit) rowCount = limit;
+
+		// check the column is numerical
+		var type = getColumnType(valueColumnIndexOrName);
+		if (type != "double" && type != "integer" && cValue != cLabel) return false;
+
+		// base chart
+		var chart = {
+
+				chart: {
+					type: 'pie',
+					backgroundColor: bgColor,
+					plotBackgroundColor: bgColor,
+					plotBorderWidth: 0,
+					options3d: { 
+						enabled: pie3d,
+						alpha: 45
+					}
+
+				},
+
+				credits: {
+					enabled: false
+				},
+
+				title: {
+					text: title
+				},
+
+				tooltip: {
+					pointFormat: '{series.name}: <b>{point.percentage:.1f}%</b>'
+				},
+
+				plotOptions: {
+					pie: {
+						dataLabels: {
+							enabled: true,
+							format: cValue == cLabel ? '<b>{point.name}</b>' : '<b>{point.name}</b><br/>{point.formattedValue}'
+						},
+						startAngle: startAngle
+					}
+				}
+		};
+
+		chart.series = [];
+		var serie = { name: title, data: [] };
+		chart.series.push(serie);
+
+		if (cValue == cLabel) {
+
+			// frequency pie chart
+			var distinctValues = {}; 
+			for (var r = 0; r < rowCount; r++) {
+				if (getRowAttribute(r, "skip") == "1") continue;
+				var rowValue = getValueAt(r,cValue);
+				if (rowValue in distinctValues) distinctValues[rowValue]++;
+				else distinctValues[rowValue] = 1;
+			}
+
+			for (var value in distinctValues) {
+				var occurences = distinctValues[value];
+				serie.data.push({ 
+					y : occurences, 
+					name: value,
+					formattedValue: value
+					// let Highcharts handle smart colors
+					// color: hex2rgba(smartColorsBar[serie.data.length % smartColorsPie.length], alpha)
+				});
+			}
+			chart.series.push(serie);
+		}
+		else {
+
+			for (var r = 0; r < rowCount; r++) {
+				if (getRowAttribute(r, "skip") == "1") continue;
+				var value = getValueAt(r,cValue);
+				var label = getRowAttribute(r, "barlabel"); // if there is a barlabel attribute, use it and ignore labelColumn
+				if (value !== null && !isNaN(value)) serie.data.push({ 
+					y : value, 
+					name: (label ? label : getValueAt(r,cLabel)),
+					formattedValue: this.getFormattedValue(cValue, value)
+					// let Highcharts handle smart colors
+					// color: hex2rgba(smartColorsBar[serie.data.length % smartColorsPie.length], alpha)
+				});
+			}
+		}
+
+		$('#' + divId).highcharts(chart);
+		return serie.data.length;
+	}
+};
+
+/**
+ * clearChart
+ * @param divId
+ * @return
+ */
+EditableGrid.prototype.clearChart = function(divId) 
+{
+	$('#' + divId).html('');
+};

+ 379 - 0
src/static/js/editablegrid_charts_ofc.js

@@ -0,0 +1,379 @@
+var EditableGrid_pending_charts = {};
+
+function EditableGrid_loadChart(divId)
+{
+	var swf = findSWF(divId);
+	if (swf && typeof swf.load == "function") swf.load(JSON.stringify(EditableGrid_pending_charts[divId]));
+	else setTimeout("EditableGrid_loadChart('"+divId+"');", 100);
+}
+
+function EditableGrid_get_chart_data(divId) 
+{
+	setTimeout("EditableGrid_loadChart('"+divId+"');", 100);
+	return JSON.stringify(EditableGrid_pending_charts[divId]);
+}
+
+EditableGrid.prototype.checkChartLib_OFC = function()
+{
+	EditableGrid_check_lib = false;
+	if (typeof JSON.stringify == 'undefined') { alert('This method needs the JSON javascript library'); return false; }
+	else if (typeof findSWF == 'undefined') { alert('This method needs the open flash chart javascript library (findSWF)'); return false; }
+	else if (typeof ofc_chart == 'undefined') { alert('This method needs the open flash chart javascript library (ofc_chart)'); return false; }
+	else if (typeof swfobject == 'undefined') { alert('This method needs the swfobject javascript library'); return false; }
+	else return true;
+};
+
+/**
+ * renderBarChart
+ * Render open flash bar chart for the data contained in the table model
+ * @param divId
+ * @param title
+ * @param labelColumnIndexOrName
+ * @param options: legend (label of labelColumnIndexOrName), bgColor (#ffffff), alpha (0.9), limit (0), bar3d (true), rotateXLabels (0) 
+ * @return
+ */
+EditableGrid.prototype.renderBarChart_OFC = function(divId, title, labelColumnIndexOrName, options)
+{
+	with (this) {
+
+		if (EditableGrid_check_lib && !checkChartLib_OFC()) return false;
+
+		// default options
+		this.legend = null;
+		this.bgColor = "#ffffff";
+		this.alpha = 0.9;
+		this.limit = 0;
+		this.bar3d = true;
+		this.rotateXLabels = 0;
+
+		// override default options with the ones given
+		if (options) for (var p in options) this[p] = options[p];
+
+		labelColumnIndexOrName = labelColumnIndexOrName || 0;
+		var cLabel = getColumnIndex(labelColumnIndexOrName);
+
+		var chart = new ofc_chart();
+		chart.bg_colour = bgColor;
+		chart.set_title({text: title || '', style: "{font-size: 20px; color:#0000ff; font-family: Verdana; text-align: center;}"});
+
+		var columnCount = getColumnCount();
+		var rowCount = getRowCount() - (ignoreLastRow ? 1 : 0);
+		if (limit > 0 && rowCount > limit) rowCount = limit;
+
+		var maxvalue = 0;
+		for (var c = 0; c < columnCount; c++) {
+			if (!isColumnBar(c)) continue;
+			var bar = new ofc_element(bar3d ? "bar_3d" : "bar");
+			bar.alpha = alpha;
+			bar.colour = smartColorsBar[chart.elements.length % smartColorsBar.length];
+			bar.fill = "transparent";
+			bar.text = getColumnLabel(c);
+			for (var r = 0; r < rowCount; r++) {
+				if (getRowAttribute(r, "skip") == "1") continue;
+				var value = getValueAt(r,c);
+				if (value > maxvalue) maxvalue = value; 
+				bar.values.push(value);
+			}
+			chart.add_element(bar);
+		}
+
+		// round the y max value
+		var ymax = 10;
+		while (ymax < maxvalue) ymax *= 10;
+		var dec_step = ymax / 10;
+		while (ymax - dec_step > maxvalue) ymax -= dec_step;
+
+		var xLabels = [];
+		for (var r = 0; r < rowCount; r++) {
+			if (getRowAttribute(r, "skip") == "1") continue;
+			var label = getRowAttribute(r, "barlabel"); // if there is a barlabel attribute, use it and ignore labelColumn
+			xLabels.push(label ? label : getValueAt(r,cLabel));
+		}
+
+		chart.x_axis = {
+				stroke: 1,
+				tick_height:  10,
+				colour: "#E2E2E2",
+				"grid-colour": "#E2E2E2",
+				labels: { rotate: rotateXLabels, labels: xLabels },
+				"3d": 5
+		};
+
+		chart.y_axis = {
+				stroke: 4,
+				tick_length: 3,
+				colour: "#428BC7",
+				"grid-colour": "#E2E2E2",
+				offset: 0,
+				steps: ymax / 10.0,
+				max: ymax
+		};
+
+		// chart.num_decimals = 0;
+
+		chart.x_legend = {
+				text: legend || getColumnLabel(labelColumnIndexOrName),
+				style: "{font-size: 11px; color: #000033}"
+		};
+
+		chart.y_legend = {
+				text: "",
+				style: "{font-size: 11px; color: #000033}"
+		};
+
+		updateChart(divId, chart);
+	}
+};
+
+/**
+ * renderStackedBarChart
+ * Render open flash stacked bar chart for the data contained in the table model
+ * @param divId
+ * @param title
+ * @param labelColumnIndexOrName
+ * @param options: legend (label of labelColumnIndexOrName), bgColor (#ffffff), alpha (0.8), limit (0), rotateXLabels (0) 
+ * @return
+ */
+EditableGrid.prototype.renderStackedBarChart_OFC = function(divId, title, labelColumnIndexOrName, options)
+{
+	with (this) {
+
+		if (EditableGrid_check_lib && !checkChartLib_OFC()) return false;
+
+		// default options
+		this.legend = null;
+		this.bgColor = "#ffffff";
+		this.alpha = 0.8;
+		this.limit = 0;
+		this.rotateXLabels = 0;
+
+		// override default options with the ones given
+		if (options) for (var p in options) this[p] = options[p];
+
+		labelColumnIndexOrName = labelColumnIndexOrName || 0;
+		var cLabel = getColumnIndex(labelColumnIndexOrName);
+
+		var chart = new ofc_chart();
+		chart.bg_colour = bgColor;
+		chart.set_title({text: title || '', style: "{font-size: 20px; color:#0000ff; font-family: Verdana; text-align: center;}"});
+
+		var columnCount = getColumnCount();
+		var rowCount = getRowCount() - (ignoreLastRow ? 1 : 0);
+		if (limit > 0 && rowCount > limit) rowCount = limit;
+
+		var maxvalue = 0;
+		var bar = new ofc_element("bar_stack");
+		bar.alpha = alpha;
+		bar.colours = smartColorsBar;
+		bar.fill = "transparent";
+		bar.keys = [];
+
+		for (var c = 0; c < columnCount; c++) {
+			if (!isColumnBar(c)) continue;
+			bar.keys.push({ colour: smartColorsBar[bar.keys.length % smartColorsBar.length], text: getColumnLabel(c), "font-size": '13' });
+		}
+
+		for (var r = 0; r < rowCount; r++) {
+			if (getRowAttribute(r, "skip") == "1") continue;
+			var valueRow = [];
+			var valueStack = 0;
+			for (var c = 0; c < columnCount; c++) {
+				if (!isColumnBar(c)) continue;
+				var value = getValueAt(r,c);
+				value = isNaN(value) ? 0 : value;
+				valueStack += value;
+				valueRow.push(value);
+			}
+			if (valueStack > maxvalue) maxvalue = valueStack; 
+			bar.values.push(valueRow);
+		}
+
+		chart.add_element(bar);
+
+		// round the y max value
+		var ymax = 10;
+		while (ymax < maxvalue) ymax *= 10;
+		var dec_step = ymax / 10;
+		while (ymax - dec_step > maxvalue) ymax -= dec_step;
+
+		var xLabels = [];
+		for (var r = 0; r < rowCount; r++) {
+			if (getRowAttribute(r, "skip") == "1") continue;
+			xLabels.push("aa " + getValueAt(r,cLabel));
+		}
+
+		chart.x_axis = {
+				stroke: 1,
+				tick_height:  10,
+				colour: "#E2E2E2",
+				"grid-colour": "#E2E2E2",
+				labels: { rotate: rotateXLabels, labels: xLabels },
+				"3d": 5
+		};
+
+		chart.y_axis = {
+				stroke: 4,
+				tick_length: 3,
+				colour: "#428BC7",
+				"grid-colour": "#E2E2E2",
+				offset: 0,
+				steps: ymax / 10.0,
+				max: ymax
+		};
+
+		// chart.num_decimals = 0;
+
+		chart.x_legend = {
+				text: legend || getColumnLabel(labelColumnIndexOrName),
+				style: "{font-size: 11px; color: #000033}"
+		};
+
+		chart.y_legend = {
+				text: "",
+				style: "{font-size: 11px; color: #000033}"
+		};
+
+		updateChart(divId, chart);
+	}
+};
+
+/**
+ * renderPieChart
+ * @param divId
+ * @param title
+ * @param valueColumnIndexOrName
+ * @param labelColumnIndexOrName: if same as valueColumnIndexOrName, the chart will display the frequency of values in this column 
+ * @param options: startAngle (0), bgColor (#ffffff), alpha (0.5), limit (0), gradientFill (true) 
+ * @return
+ */
+EditableGrid.prototype.renderPieChart_OFC = function(divId, title, valueColumnIndexOrName, labelColumnIndexOrName, options) 
+{
+	with (this) {
+
+		if (EditableGrid_check_lib && !checkChartLib_OFC()) return false;
+
+		// default options
+		this.startAngle = 0;
+		this.bgColor = "#ffffff";
+		this.alpha = 0.5;
+		this.limit = 0;
+		this.gradientFill = true;
+
+		// override default options with the ones given
+		if (options) for (var p in options) this[p] = options[p];
+
+		var type = getColumnType(valueColumnIndexOrName);
+		if (type != "double" && type != "integer" && valueColumnIndexOrName != labelColumnIndexOrName) return;
+
+		labelColumnIndexOrName = labelColumnIndexOrName || 0;
+		title = (typeof title == 'undefined' || title === null) ? getColumnLabel(valueColumnIndexOrName) : title;
+
+		var cValue = getColumnIndex(valueColumnIndexOrName);
+		var cLabel = getColumnIndex(labelColumnIndexOrName);
+
+		var chart = new ofc_chart();
+		chart.bg_colour = bgColor;
+		chart.set_title({text: title, style: "{font-size: 20px; color:#0000ff; font-family: Verdana; text-align: center;}"});
+
+		var rowCount = getRowCount() - (ignoreLastRow ? 1 : 0);
+		if (limit > 0 && rowCount > limit) rowCount = limit;
+
+		var pie = new ofc_element("pie");
+		pie.colours = smartColorsPie;
+		pie.alpha = alpha;
+		pie['gradient-fill'] = gradientFill;
+
+		if (typeof startAngle != 'undefined' && startAngle !== null) pie['start-angle'] = startAngle;
+
+		if (valueColumnIndexOrName == labelColumnIndexOrName) {
+
+			// frequency pie chart
+			var distinctValues = {}; 
+			for (var r = 0; r < rowCount; r++) {
+				if (getRowAttribute(r, "skip") == "1") continue;
+				var rowValue = getValueAt(r,cValue);
+				if (rowValue in distinctValues) distinctValues[rowValue]++;
+				else distinctValues[rowValue] = 1;
+			}
+
+			for (var value in distinctValues) {
+				var occurences = distinctValues[value];
+				pie.values.push({value : occurences, label: value + ' (' + (100 * (occurences / rowCount)).toFixed(1) + '%)'});
+			}
+		}
+		else {
+
+			var total = 0; 
+			for (var r = 0; r < rowCount; r++) {
+				if (getRowAttribute(r, "skip") == "1") continue;
+				var rowValue = getValueAt(r,cValue);
+				total += isNaN(rowValue) ? 0 : rowValue;
+			}
+
+			for (var r = 0; r < rowCount; r++) {
+				if (getRowAttribute(r, "skip") == "1") continue;
+				var value = getValueAt(r,cValue);
+				var label = getValueAt(r,cLabel);
+				if (!isNaN(value)) pie.values.push({value : value, label: label + ' (' + (100 * (value / total)).toFixed(1) + '%)'});
+			}
+		}
+
+		chart.add_element(pie);
+
+		if (pie.values.length > 0) updateChart(divId, chart);
+		return pie.values.length;
+	}
+};
+
+/**
+ * updateChart
+ * @param divId
+ * @param chart
+ * @return
+ */
+EditableGrid.prototype.updateChart = function(divId, chart) 
+{
+	if (typeof this.ofcSwf == 'undefined' || !this.ofcSwf) {
+
+		// detect openflashchart swf location
+		this.ofcSwf = 'open-flash-chart.swf'; // defaults to current directory
+		var e = document.getElementsByTagName('script');
+		for (var i = 0; i < e.length; i++) {
+			var index = e[i].src.indexOf('openflashchart');
+			if (index != -1) {
+				this.ofcSwf = e[i].src.substr(0, index + 15) + this.ofcSwf;
+				break;
+			}
+		};
+	}
+
+	with (this) {
+
+		// reload or create new swf chart
+		var swf = findSWF(divId);
+		if (swf && typeof swf.load == "function") {
+			try { swf.load(JSON.stringify(chart)); }
+			catch (ex) { console.error(ex); }
+		}
+		else {
+			var div = _$(divId);
+			EditableGrid_pending_charts[divId] = chart;
+
+			// get chart dimensions
+			var w = parseInt(getStyle(div, 'width'));
+			var h = parseInt(getStyle(div, 'height'));
+			w = Math.max(isNaN(w)?0:w, div.offsetWidth);
+			h = Math.max(isNaN(h)?0:h, div.offsetHeight);
+
+			swfobject.embedSWF(this.ofcSwf, 
+					divId, 
+					"" + (w || 500), 
+					"" + (h || 200), 
+					"9.0.0", "expressInstall.swf", { "get-data": "EditableGrid_get_chart_data", "id": divId }, null, 
+					{ wmode: "Opaque", salign: "l", AllowScriptAccess:"always"}
+			);
+		}
+
+		chartRendered();
+	}
+};

+ 408 - 0
src/static/js/editablegrid_editors.js

@@ -0,0 +1,408 @@
+/**
+ * Abstract cell editor
+ * @constructor
+ * @class Base class for all cell editors
+ */
+
+function CellEditor(config) { this.init(config); }
+
+CellEditor.prototype.init = function(config) 
+{
+	// override default properties with the ones given
+	if (config) for (var p in config) this[p] = config[p];
+};
+
+CellEditor.prototype.edit = function(rowIndex, columnIndex, element, value) 
+{
+	// tag element and remember all the things we need to apply/cancel edition
+	element.isEditing = true;
+	element.rowIndex = rowIndex; 
+	element.columnIndex = columnIndex;
+
+	// call the specialized getEditor method
+	var editorInput = this.getEditor(element, value);
+	if (!editorInput) return false;
+
+	// give access to the cell editor and element from the editor widget
+	editorInput.element = element;
+	editorInput.celleditor = this;
+
+	// listen to pressed keys
+	// - tab does not work with onkeyup (it's too late)
+	// - on Safari escape does not work with onkeypress
+	// - with onkeydown everything is fine (but don't forget to return false)
+	editorInput.onkeydown = function(event) {
+
+		event = event || window.event;
+
+		// ENTER or TAB: apply value
+		if (event.keyCode == 13 || event.keyCode == 9) {
+
+			// backup onblur then remove it: it will be restored if editing could not be applied
+			this.onblur_backup = this.onblur; 
+			this.onblur = null;
+			if (this.celleditor.applyEditing(this.element, this.celleditor.getEditorValue(this)) === false) this.onblur = this.onblur_backup; 
+			return false;
+		}
+
+		// ESC: cancel editing
+		if (event.keyCode == 27) { 
+			this.onblur = null; 
+			this.celleditor.cancelEditing(this.element); 
+			return false; 
+		}
+	};
+
+	// if simultaneous edition is not allowed, we cancel edition when focus is lost
+	if (!this.editablegrid.allowSimultaneousEdition) editorInput.onblur = this.editablegrid.saveOnBlur ?
+			function(event) { 
+
+		// backup onblur then remove it: it will be restored if editing could not be applied
+		this.onblur_backup = this.onblur; 
+		this.onblur = null;
+		if (this.celleditor.applyEditing(this.element, this.celleditor.getEditorValue(this)) === false) this.onblur = this.onblur_backup; 
+	}
+	:
+		function(event) { 
+		this.onblur = null; 
+		this.celleditor.cancelEditing(this.element); 
+	};
+
+	// display the resulting editor widget
+	this.displayEditor(element, editorInput);
+
+	// give focus to the created editor
+	editorInput.focus();
+};
+
+CellEditor.prototype.getEditor = function(element, value) {
+	return null;
+};
+
+CellEditor.prototype.getEditorValue = function(editorInput) {
+	return editorInput.value;
+};
+
+CellEditor.prototype.formatValue = function(value) {
+	return value;
+};
+
+CellEditor.prototype.displayEditor = function(element, editorInput, adjustX, adjustY) 
+{
+	// use same font in input as in cell content
+	editorInput.style.fontFamily = this.editablegrid.getStyle(element, "fontFamily", "font-family"); 
+	editorInput.style.fontSize = this.editablegrid.getStyle(element, "fontSize", "font-size"); 
+
+	// static mode: add input field in the table cell
+	if (this.editablegrid.editmode == "static") {
+		while (element.hasChildNodes()) element.removeChild(element.firstChild);
+		element.appendChild(editorInput);
+	}
+
+	// absolute mode: add input field in absolute position over table cell, leaving current content
+	if (this.editablegrid.editmode == "absolute") {
+		element.appendChild(editorInput);
+		editorInput.style.position = "absolute";
+
+		// position editor input on the cell with the same padding as the actual cell content (and center vertically if vertical-align is set to "middle")
+		var paddingLeft = this.editablegrid.paddingLeft(element);
+		var paddingTop = this.editablegrid.paddingTop(element);
+
+		// find scroll offset
+		var offsetScrollX = this.editablegrid.getScrollXOffset(element);
+		var offsetScrollY = this.editablegrid.getScrollYOffset(element);
+
+		// position input
+		var vCenter = this.editablegrid.verticalAlign(element) == "middle" ? (element.offsetHeight - editorInput.offsetHeight) / 2 - paddingTop : 0;
+		editorInput.style.left = (this.editablegrid.getCellX(element) - offsetScrollX + paddingLeft + (adjustX ? adjustX : 0)) + "px";
+		editorInput.style.top = (this.editablegrid.getCellY(element) - offsetScrollY + paddingTop + vCenter + (adjustY ? adjustY : 0)) + "px";
+
+		// if number type: align field and its content to the right
+		if (this.column.datatype == 'integer' || this.column.datatype == 'double') {
+			var rightPadding = this.editablegrid.getCellX(element) - offsetScrollX + element.offsetWidth - (parseInt(editorInput.style.left) + editorInput.offsetWidth);
+			editorInput.style.left = (parseInt(editorInput.style.left) + rightPadding) + "px";
+			editorInput.style.textAlign = "right";
+		}
+	}
+
+	// fixed mode: don't show input field in the cell 
+	if (this.editablegrid.editmode == "fixed") {
+		var editorzone = _$(this.editablegrid.editorzoneid);
+		while (editorzone.hasChildNodes()) editorzone.removeChild(editorzone.firstChild);
+		editorzone.appendChild(editorInput);
+	}
+};
+
+CellEditor.prototype._clearEditor = function(element) 
+{
+	// untag element
+	element.isEditing = false;
+
+	// clear fixed editor zone if any
+	if (this.editablegrid.editmode == "fixed") {
+		var editorzone = _$(this.editablegrid.editorzoneid);
+		while (editorzone.hasChildNodes()) editorzone.removeChild(editorzone.firstChild);
+	}	
+};
+
+CellEditor.prototype.cancelEditing = function(element) 
+{
+	with (this) {
+
+		// check that the element is still being edited (otherwise onblur will be called on textfields that have been closed when we go to another tab in Firefox) 
+		if (element && element.isEditing) {
+
+			// render value before editon
+			var renderer = this == column.headerEditor ? column.headerRenderer : column.cellRenderer;
+			renderer._render(element.rowIndex, element.columnIndex, element, editablegrid.getValueAt(element.rowIndex, element.columnIndex));
+
+			_clearEditor(element);
+		}
+	}
+};
+
+CellEditor.prototype.applyEditing = function(element, newValue) 
+{
+	with (this) {
+
+		// check that the element is still being edited (otherwise onblur will be called on textfields that have been closed when we go to another tab in Firefox) 
+		if (element && element.isEditing) {
+
+			// do nothing if the value is rejected by at least one validator
+			if (!column.isValid(newValue)) return false;
+
+			// format the value before applying
+			var formattedValue = formatValue(newValue);
+
+			// update model and render cell (keeping previous value)
+			var previousValue = editablegrid.setValueAt(element.rowIndex, element.columnIndex, formattedValue);
+
+			// if the new value is different than the previous one, let the user handle the model change
+			var newValue = editablegrid.getValueAt(element.rowIndex, element.columnIndex);
+			if (!this.editablegrid.isSame(newValue, previousValue)) {
+				editablegrid.modelChanged(element.rowIndex, element.columnIndex, previousValue, newValue, editablegrid.getRow(element.rowIndex));
+			}
+
+			_clearEditor(element);	
+			return true;
+		}
+
+		return false;
+	}
+};
+
+/**
+ * Text cell editor
+ * @constructor
+ * @class Class to edit a cell with an HTML text input 
+ */
+
+function TextCellEditor(size, maxlen, config) { 
+	if (size) this.fieldSize = size; 
+	if (maxlen) this.maxLength = maxlen; 
+	if (config) this.init(config); 
+};
+
+TextCellEditor.prototype = new CellEditor();
+TextCellEditor.prototype.fieldSize = -1;
+TextCellEditor.prototype.maxLength = -1;
+TextCellEditor.prototype.autoHeight = true;
+
+TextCellEditor.prototype.editorValue = function(value) {
+	return value;
+};
+
+TextCellEditor.prototype.updateStyle = function(htmlInput)
+{
+	// change style for invalid values
+	if (this.column.isValid(this.getEditorValue(htmlInput))) this.editablegrid.removeClassName(htmlInput, this.editablegrid.invalidClassName);
+	else this.editablegrid.addClassName(htmlInput, this.editablegrid.invalidClassName);
+};
+
+TextCellEditor.prototype.getEditor = function(element, value)
+{
+	// create and initialize text field
+	var htmlInput = document.createElement("input"); 
+	htmlInput.setAttribute("type", "text");
+	if (this.maxLength > 0) htmlInput.setAttribute("maxlength", this.maxLength);
+
+	if (this.fieldSize > 0) htmlInput.setAttribute("size", this.fieldSize);
+	else htmlInput.style.width = this.editablegrid.autoWidth(element) + 'px'; // auto-adapt width to cell, if no length specified 
+
+	var autoHeight = this.editablegrid.autoHeight(element);
+	if (this.autoHeight) htmlInput.style.height = autoHeight + 'px'; // auto-adapt height to cell
+	htmlInput.value = this.editorValue(value);
+
+	// listen to keyup to check validity and update style of input field 
+	htmlInput.onkeyup = function(event) { this.celleditor.updateStyle(this); };
+
+	return htmlInput; 
+};
+
+TextCellEditor.prototype.displayEditor = function(element, htmlInput) 
+{
+	// call base method
+	CellEditor.prototype.displayEditor.call(this, element, htmlInput, -1 * this.editablegrid.borderLeft(htmlInput), -1 * (this.editablegrid.borderTop(htmlInput) + 1));
+
+	// update style of input field
+	this.updateStyle(htmlInput);
+
+	// select text
+	htmlInput.select();
+};
+
+/**
+ * Number cell editor
+ * @constructor
+ * @class Class to edit a numeric cell with an HTML text input 
+ */
+
+function NumberCellEditor(type, config) { 
+	this.type = type;
+	this.init(config);
+}
+
+NumberCellEditor.prototype = new TextCellEditor(-1, 32);
+
+//editorValue is called in getEditor to initialize field
+NumberCellEditor.prototype.editorValue = function(value) {
+	return (value === null || isNaN(value)) ? "" : (value + '').replace('.', this.column.decimal_point);
+};
+
+//getEditorValue is called before passing to isValid and applyEditing
+NumberCellEditor.prototype.getEditorValue = function(editorInput) {
+	return editorInput.value.replace(',', '.');
+};
+
+//formatValue is called in applyEditing
+NumberCellEditor.prototype.formatValue = function(value)
+{
+	return this.type == 'integer' ? parseInt(value) : parseFloat(value);
+};
+
+/**
+ * Select cell editor
+ * @constructor
+ * @class Class to edit a cell with an HTML select input 
+ */
+
+function SelectCellEditor(config) { 
+	this.minWidth = 75; 
+	this.minHeight = 22; 
+	this.adaptHeight = true; 
+	this.adaptWidth = true;
+	this.init(config); 
+}
+
+SelectCellEditor.prototype = new CellEditor();
+SelectCellEditor.prototype.isValueSelected = function(htmlInput, optionValue, value) { return (!optionValue && !value) || (optionValue == value); };
+SelectCellEditor.prototype.getEditor = function(element, value)
+{
+	// create select list
+	var htmlInput = document.createElement("select");
+
+	// auto adapt dimensions to cell, with a min width
+	if (this.adaptWidth) htmlInput.style.width = Math.max(this.minWidth, this.editablegrid.autoWidth(element)) + 'px'; 
+	if (this.adaptHeight) htmlInput.style.height = Math.max(this.minHeight, this.editablegrid.autoHeight(element)) + 'px';
+
+	// get column option values for this row 
+	var optionValues = this.column.getOptionValuesForEdit(element.rowIndex);
+
+	// add these options, selecting the current one
+	var index = 0, valueFound = false;
+	for (var optionIndex = 0; optionIndex < optionValues.length; optionIndex++) {
+		var optionValue = optionValues[optionIndex];
+
+		// if values are grouped
+		if (typeof optionValue.values == 'object') {
+
+			var optgroup = document.createElement('optgroup');
+			optgroup.label = optionValue.label; 
+			htmlInput.appendChild(optgroup); 
+
+			for (var groupOptionIndex = 0; groupOptionIndex < optionValue.values.length; groupOptionIndex++) {
+				var groupOptionValue = optionValue.values[groupOptionIndex];
+				var option = document.createElement('option');
+				option.text = groupOptionValue.label;
+				option.value = groupOptionValue.value ? groupOptionValue.value : ""; // this otherwise changes a null into a "null" !
+				optgroup.appendChild(option); 
+				if (this.isValueSelected(htmlInput, groupOptionValue.value, value)) { option.selected = true; valueFound = true; } else option.selected = false;  
+				index++;
+			}
+		}
+		else {
+
+			var option = document.createElement('option');
+			option.text = optionValue.label;
+			option.value = optionValue.value ? optionValue.value : ""; // this otherwise changes a null into a "null" !
+			// add does not work as expected in IE7 (cf. second arg)
+			try { htmlInput.add(option, null); } catch (e) { htmlInput.add(option); } 
+			if (this.isValueSelected(htmlInput, optionValue.value, value)) { option.selected = true; valueFound = true; } else option.selected = false;  
+			index++;
+		}
+	}
+
+	// if the current value is not in the list add it to the front
+	if (!valueFound) {
+		var option = document.createElement('option');
+		option.text = value ? value : "";
+		option.value = value ? value : "";
+		// add does not work as expected in IE7 (cf. second arg)
+		try { htmlInput.add(option, htmlInput.options[0]); } catch (e) { htmlInput.add(option); } 
+		htmlInput.selectedIndex = 0;
+	}
+
+	// when a new value is selected we apply it
+	htmlInput.onchange = function(event) { this.onblur = null; this.celleditor.applyEditing(this.element, this.value); };
+
+	return htmlInput; 
+};
+
+/**
+ * Datepicker cell editor
+ * 
+ * Text field editor with date picker capabilities.
+ * Uses the jQuery UI's datepicker.
+ * This editor is used automatically for date columns if we detect that the jQuery UI's datepicker is present. 
+ * 
+ * @constructor Accepts an option object containing the following properties: 
+ * - fieldSize: integer (default=auto-adapt)
+ * - maxLength: integer (default=255)
+ * 
+ * @class Class to edit a cell with a datepicker linked to the HTML text input
+ */
+
+function DateCellEditor(config) 
+{
+	// erase defaults with given options
+	this.init(config); 
+};
+
+//inherits TextCellEditor functionalities
+DateCellEditor.prototype = new TextCellEditor();
+
+//redefine displayEditor to setup datepicker
+DateCellEditor.prototype.displayEditor = function(element, htmlInput) 
+{
+	// call base method
+	TextCellEditor.prototype.displayEditor.call(this, element, htmlInput);
+
+	jQuery(htmlInput).datepicker({ 
+		dateFormat: (this.editablegrid.dateFormat == "EU" ? "dd/mm/yy" : "mm/dd/yy"),
+		changeMonth: true,
+		changeYear: true,
+		yearRange: "c-100:c+10",
+		beforeShow: function() {
+			// the field cannot be blurred until the datepicker has gone away
+			// otherwise we get the "missing instance data" exception
+			this.onblur_backup = this.onblur;
+			this.onblur = null;
+		},
+		onClose: function(dateText) {
+			// apply date if any, otherwise call original onblur event
+			if (dateText != '') this.celleditor.applyEditing(htmlInput.element, dateText);
+			else if (this.onblur_backup != null) this.onblur_backup();
+
+		}
+	}).datepicker('show');
+};

+ 292 - 0
src/static/js/editablegrid_renderers.js

@@ -0,0 +1,292 @@
+/**
+ * Abstract cell renderer
+ * @constructor
+ * @class Base class for all cell renderers
+ * @param {Object} config
+ */
+
+function CellRenderer(config) { this.init(config); }
+
+CellRenderer.prototype.init = function(config) 
+{
+	// override default properties with the ones given
+	for (var p in config) this[p] = config[p];
+};
+
+CellRenderer.prototype._render = function(rowIndex, columnIndex, element, value) 
+{
+	// remember all the things we need
+	element.rowIndex = rowIndex; 
+	element.columnIndex = columnIndex;
+
+	// remove existing content	
+	while (element.hasChildNodes()) element.removeChild(element.firstChild);
+
+	// clear isEditing (in case a currently editeed is being re-rendered by some external call)
+	element.isEditing = false;
+
+	// always apply the number class to numerical cells and column headers
+	if (this.column.isNumerical()) EditableGrid.prototype.addClassName(element, "number");
+
+	// always apply the boolean class to boolean column headers
+	if (this.column.datatype == 'boolean') EditableGrid.prototype.addClassName(element, "boolean");
+
+	// apply a css class corresponding to the column name
+	EditableGrid.prototype.addClassName(element, "editablegrid-" + this.column.name);
+
+	// add a data-title attribute used for responsiveness
+	element.setAttribute('data-title', this.column.label);
+
+	// call the specialized render method
+	return this.render(element, typeof value == 'string' && this.column.datatype != "html" ? (value === null ? null : htmlspecialchars(value, 'ENT_NOQUOTES').replace(/  /g, ' &nbsp;')) : value);
+};
+
+CellRenderer.prototype.render = function(element, value, escapehtml) 
+{
+	var _value = escapehtml ? (typeof value == 'string' && this.column.datatype != "html" ? (value === null ? null : htmlspecialchars(value, 'ENT_NOQUOTES').replace(/  /g, ' &nbsp;')) : value) : value;
+	element.innerHTML = _value ? _value : "";
+};
+
+CellRenderer.prototype.getDisplayValue = function(rowIndex, value) 
+{
+	return value;
+};
+
+/**
+ * Enum cell renderer
+ * @constructor
+ * @class Class to render a cell with enum values
+ */
+
+function EnumCellRenderer(config) { this.init(config); }
+EnumCellRenderer.prototype = new CellRenderer();
+EnumCellRenderer.prototype.getLabel = function(rowIndex, value)
+{
+	var label = "";
+	if (typeof value != 'undefined') {
+		value = value ? value : '';
+		var optionValues = this.column.getOptionValuesForRender(rowIndex);
+		if (optionValues && value in optionValues) label = optionValues[value];
+		if (label == "") {
+			var isNAN = typeof value == 'number' && isNaN(value);
+			label = isNAN ? "" : value;
+		}
+	}
+	return label;
+};
+
+EnumCellRenderer.prototype.render = function(element, value)
+{
+	var label = this.getLabel(element.rowIndex, value);
+	element.innerHTML = label ? (this.column.datatype != "html" ? htmlspecialchars(label, 'ENT_NOQUOTES').replace(/\s\s/g, '&nbsp; ') : label) : ''; 
+};
+
+EnumCellRenderer.prototype.getDisplayValue = function(rowIndex, value) 
+{
+	// if the column has enumerated values, sort and filter on the value label
+	return value === null ? null : this.getLabel(rowIndex, value);
+};
+
+/**
+ * Number cell renderer
+ * @constructor
+ * @class Class to render a cell with numerical values
+ */
+
+function NumberCellRenderer(config) { this.init(config); }
+NumberCellRenderer.prototype = new CellRenderer();
+NumberCellRenderer.prototype.render = function(element, value)
+{
+	var column = this.column || {}; // in case somebody calls new NumberCellRenderer().render(..)
+
+	var isNAN = value === null || (typeof value == 'number' && isNaN(value));
+	var displayValue = isNAN ? (column.nansymbol || "") : value;
+	if (typeof displayValue == 'number') {
+
+		if (column.precision !== null) {
+			// displayValue = displayValue.toFixed(column.precision);
+			displayValue = number_format(displayValue, column.precision, column.decimal_point, column.thousands_separator);
+		}
+
+		if (column.unit !== null) {
+			if (column.unit_before_number) displayValue = column.unit + ' ' + displayValue;
+			else displayValue = displayValue + ' ' + column.unit;
+		}
+	}
+
+	element.innerHTML = displayValue;
+	if (isNAN) EditableGrid.prototype.addClassName(element, "nan");
+	else EditableGrid.prototype.removeClassName(element, "nan");
+};
+
+/**
+ * Checkbox cell renderer
+ * @constructor
+ * @class Class to render a cell with an HTML checkbox
+ */
+
+function CheckboxCellRenderer(config) { this.init(config); }
+CheckboxCellRenderer.prototype = new CellRenderer();
+
+CheckboxCellRenderer.prototype._render = function(rowIndex, columnIndex, element, value) 
+{
+	// if a checkbox already exists keep it, otherwise clear current content
+	if (element.firstChild && (typeof element.firstChild.getAttribute != "function" || element.firstChild.getAttribute("type") != "checkbox"))
+		while (element.hasChildNodes()) element.removeChild(element.firstChild);
+
+	// remember all the things we need
+	element.rowIndex = rowIndex; 
+	element.columnIndex = columnIndex;
+
+	// apply a css class corresponding to the column name
+	EditableGrid.prototype.addClassName(element, "editablegrid-" + this.column.name);
+
+	// add a data-title attribute used for responsiveness
+	element.setAttribute('data-title', this.column.label);
+
+	// call the specialized render method
+	return this.render(element, value);
+};
+
+CheckboxCellRenderer.prototype.render = function(element, value)
+{
+	// convert value to boolean just in case
+	value = (value && value != 0 && value != "false") ? true : false;
+
+	// if check box already created, just update its state
+	if (element.firstChild) { element.firstChild.checked = value; return; }
+
+	// create and initialize checkbox
+	var htmlInput = document.createElement("input"); 
+	htmlInput.setAttribute("type", "checkbox");
+
+	// give access to the cell editor and element from the editor field
+	htmlInput.element = element;
+	htmlInput.cellrenderer = this;
+
+	// this renderer is a little special because it allows direct edition
+	var cellEditor = new CellEditor();
+	cellEditor.editablegrid = this.editablegrid;
+	cellEditor.column = this.column;
+	htmlInput.onclick = function(event) {
+		element.rowIndex = this.cellrenderer.editablegrid.getRowIndex(element.parentNode); // in case it has changed due to sorting or remove
+		element.isEditing = true;
+		cellEditor.applyEditing(element, htmlInput.checked ? true : false); 
+	};
+
+	element.appendChild(htmlInput);
+	htmlInput.checked = value;
+	htmlInput.disabled = (!this.column.editable || !this.editablegrid.isEditable(element.rowIndex, element.columnIndex));
+
+	EditableGrid.prototype.addClassName(element, "boolean");
+};
+
+/**
+ * Email cell renderer
+ * @constructor
+ * @class Class to render a cell with emails
+ */
+
+function EmailCellRenderer(config) { this.init(config); }
+EmailCellRenderer.prototype = new CellRenderer();
+EmailCellRenderer.prototype.render = function(element, value)
+{
+	element.innerHTML = value ? "<a href='mailto:" + value + "'>" + value + "</a>" : "";
+};
+
+/**
+ * Website cell renderer
+ * @constructor
+ * @class Class to render a cell with websites
+ */
+
+function WebsiteCellRenderer(config) { this.init(config); }
+WebsiteCellRenderer.prototype = new CellRenderer();
+WebsiteCellRenderer.prototype.render = function(element, value)
+{
+	element.innerHTML = value ? "<a href='" + (value.indexOf("//") == -1 ? "http://" + value : value) + "'>" + value + "</a>" : "";
+};
+
+/**
+ * Date cell renderer
+ * @constructor
+ * @class Class to render a cell containing a date
+ */
+
+function DateCellRenderer(config) { this.init(config); }
+DateCellRenderer.prototype = new CellRenderer;
+
+DateCellRenderer.prototype.render = function(cell, value) 
+{
+	var date = this.editablegrid.checkDate(value);
+	if (typeof date == "object") cell.innerHTML = date.formattedDate;
+	else cell.innerHTML = value;
+	cell.style.whiteSpace = 'nowrap';
+};
+
+/**
+ * Sort header renderer
+ * @constructor
+ * @class Class to add sorting functionalities to headers
+ */
+
+function SortHeaderRenderer(columnName, cellRenderer) { this.columnName = columnName; this.cellRenderer = cellRenderer; };
+SortHeaderRenderer.prototype = new CellRenderer();
+SortHeaderRenderer.prototype.render = function(cell, value) 
+{
+	if (!value) { if (this.cellRenderer) this.cellRenderer.render(cell, value); }
+	else {
+
+		// create a link that will sort (alternatively ascending/descending)
+		var link = document.createElement("a");
+		cell.appendChild(link);
+		link.columnName = this.columnName;
+		link.style.cursor = "pointer";
+		link.innerHTML = value;
+		link.editablegrid = this.editablegrid;
+		link.renderer = this;
+		link.onclick = function() {
+			with (this.editablegrid) {
+
+				var cols = tHead.rows[0].cells;
+				var clearPrevious = -1;
+				var backOnFirstPage = false;
+
+				if (sortedColumnName != this.columnName) {
+					clearPrevious = sortedColumnName;
+					sortedColumnName = this.columnName;
+					sortDescending = false;
+					backOnFirstPage = true;
+				}
+				else {
+					if (!sortDescending) sortDescending = true;
+					else { 					
+						clearPrevious = sortedColumnName;
+						sortedColumnName = -1; 
+						sortDescending = false; 
+						backOnFirstPage = true;
+					}
+				} 
+
+				// render header for previous sort column (not needed anymore since the grid is now fully refreshed after a sort - cf. possible pagination)
+				// var j = getColumnIndex(clearPrevious);
+				// if (j >= 0) columns[j].headerRenderer._render(-1, j, cols[j], columns[j].label);
+
+				sort(sortedColumnName, sortDescending, backOnFirstPage);
+
+				// render header for new sort column (not needed anymore since the grid is now fully refreshed after a sort - cf. possible pagination)
+				// var j = getColumnIndex(sortedColumnName);
+				// if (j >= 0) columns[j].headerRenderer._render(-1, j, cols[j], columns[j].label);
+			}
+		};
+
+		// add an arrow to indicate if sort is ascending or descending
+		if (this.editablegrid.sortedColumnName == this.columnName) {
+			cell.appendChild(document.createTextNode("\u00a0"));
+			cell.appendChild(this.editablegrid.sortDescending ? this.editablegrid.sortDownImage: this.editablegrid.sortUpImage);
+		}
+
+		// call user renderer if any
+		if (this.cellRenderer) this.cellRenderer.render(cell, value);
+	}
+};

+ 774 - 0
src/static/js/editablegrid_utils.js

@@ -0,0 +1,774 @@
+EditableGrid.prototype._convertOptions = function(optionValues)
+{
+	// option values should be an *ordered* array of value/label pairs, but to stay compatible with existing enum providers 
+	if (optionValues !== null && (!(optionValues instanceof Array)) && typeof optionValues == 'object') {
+		var _converted = []; 
+		for (var value in optionValues) {
+			if (typeof optionValues[value] == 'object') _converted.push({ label : value, values: this._convertOptions(optionValues[value])}); // group
+			else _converted.push({ value : value, label: optionValues[value]});
+		}
+		optionValues = _converted;
+	}
+
+	return optionValues; 
+};
+
+EditableGrid.prototype.setCookie = function(c_name, value, exdays)
+{
+	var exdate = new Date();
+	exdate.setDate(exdate.getDate() + exdays);
+	var c_value = escape(value) + ((exdays == null) ? "" : "; expires=" + exdate.toUTCString());
+	document.cookie = c_name + "=" + c_value;
+};
+
+EditableGrid.prototype.getCookie = function(c_name)
+{
+	var _cookies = document.cookie.split(";");
+	for (var i = 0; i < _cookies.length; i++) {
+		var x = _cookies[i].substr(0, _cookies[i].indexOf("="));
+		var y = _cookies[i].substr(_cookies[i].indexOf("=") + 1);
+		x = x.replace(/^\s+|\s+$/g, "");
+		if (x == c_name) return unescape(y);
+	}
+
+	return null;
+};
+
+EditableGrid.prototype.has_local_storage = function() 
+{
+	try { return 'localStorage' in window && window['localStorage'] !== null; } catch(e) { return false; }
+};
+
+EditableGrid.prototype._localset = function(key, value) 
+{
+	if (this.has_local_storage()) localStorage.setItem(key, value);
+	else this.setCookie(key, value, null);
+};
+
+EditableGrid.prototype._localunset = function(key) 
+{
+	if (this.has_local_storage()) localStorage.removeItem(key);
+	else this.setCookie(key, null, null);
+};
+
+EditableGrid.prototype._localget = function(key) 
+{
+	if (this.has_local_storage()) return localStorage.getItem(key);
+	return this.getCookie(key);
+};
+
+EditableGrid.prototype._localisset = function(key) 
+{
+	if (this.has_local_storage()) return localStorage.getItem(key) !== null && localStorage.getItem(key) != 'undefined';
+	return this.getCookie(key) !== null;
+};
+
+EditableGrid.prototype.localset = function(key, value) 
+{
+	if (this.enableStore) return this._localset(this.name + '_' + key, value);
+};
+
+EditableGrid.prototype.localunset = function(key) 
+{
+	if (this.enableStore) return this._localunset(this.name + '_' + key, value);
+};
+
+EditableGrid.prototype.localget = function(key) 
+{
+	return this.enableStore ? this._localget(this.name + '_' + key) : null;
+};
+
+EditableGrid.prototype.localisset = function(key) 
+{
+	return this.enableStore ? this._localget(this.name + '_' + key) !== null : false;
+};
+
+EditableGrid.prototype.unsort = function(a,b) 
+{
+	// at index 2 we have the originalIndex
+	aa = isNaN(a[2]) ? 0 : parseFloat(a[2]);
+	bb = isNaN(b[2]) ? 0 : parseFloat(b[2]);
+	return aa-bb;
+};
+
+/**
+ * returns a sort function which further sorts according to the original index
+ * this ensures the sort will always be stable
+ * used to sort a tree where only the first level is actually sorted
+ */
+EditableGrid.prototype.sort_stable = function(sort_function, descending) 
+{
+	return function (a, b) {
+		var sort = descending ? sort_function(b, a) : sort_function(a, b);
+		if (sort != 0) return sort;
+		return EditableGrid.prototype.unsort(a, b);
+	};
+};
+
+EditableGrid.prototype.sort_numeric = function(a,b) 
+{
+	aa = isNaN(parseFloat(a[0])) ? 0 : parseFloat(a[0]);
+	bb = isNaN(parseFloat(b[0])) ? 0 : parseFloat(b[0]);
+	return aa-bb;
+};
+
+EditableGrid.prototype.sort_boolean = function(a,b) 
+{
+	aa = !a[0] || a[0] == "false" ? 0 : 1;
+	bb = !b[0] || b[0] == "false" ? 0 : 1;
+	return aa-bb;
+};
+
+EditableGrid.prototype.sort_alpha = function(a,b) 
+{
+	if (a[0].toLowerCase()==b[0].toLowerCase()) return 0;
+	return a[0].toLowerCase().localeCompare(b[0].toLowerCase());
+};
+
+EditableGrid.prototype.sort_date = function(a,b) 
+{
+	date = EditableGrid.prototype.checkDate(a[0]);
+	aa = typeof date == "object" ? date.sortDate : 0;
+	date = EditableGrid.prototype.checkDate(b[0]);
+	bb = typeof date == "object" ? date.sortDate : 0;
+	return aa-bb;
+};
+
+/**
+ * Returns computed style property for element
+ * @private
+ */
+EditableGrid.prototype.getStyle = function(element, stylePropCamelStyle, stylePropCSSStyle)
+{
+	stylePropCSSStyle = stylePropCSSStyle || stylePropCamelStyle;
+	if (element.currentStyle) return element.currentStyle[stylePropCamelStyle];
+	else if (window.getComputedStyle) return document.defaultView.getComputedStyle(element,null).getPropertyValue(stylePropCSSStyle);
+	return element.style[stylePropCamelStyle];
+};
+
+/**
+ * Returns true if the element has a static positioning
+ * @private
+ */
+EditableGrid.prototype.isStatic = function (element) 
+{
+	var position = this.getStyle(element, 'position');
+	return (!position || position == "static");
+};
+
+EditableGrid.prototype.verticalAlign = function (element) 
+{
+	return this.getStyle(element, "verticalAlign", "vertical-align");
+};
+
+EditableGrid.prototype.paddingLeft = function (element) 
+{
+	var padding = parseInt(this.getStyle(element, "paddingLeft", "padding-left"));
+	return isNaN(padding) ? 0 : Math.max(0, padding);
+};
+
+EditableGrid.prototype.paddingRight = function (element) 
+{
+	var padding = parseInt(this.getStyle(element, "paddingRight", "padding-right"));
+	return isNaN(padding) ? 0 : Math.max(0, padding);
+};
+
+EditableGrid.prototype.paddingTop = function (element) 
+{
+	var padding = parseInt(this.getStyle(element, "paddingTop", "padding-top"));
+	return isNaN(padding) ? 0 : Math.max(0, padding);
+};
+
+EditableGrid.prototype.paddingBottom = function (element) 
+{
+	var padding = parseInt(this.getStyle(element, "paddingBottom", "padding-bottom"));
+	return isNaN(padding) ? 0 : Math.max(0, padding);
+};
+
+EditableGrid.prototype.borderLeft = function (element) 
+{
+	var border_l = parseInt(this.getStyle(element, "borderRightWidth", "border-right-width"));
+	var border_r = parseInt(this.getStyle(element, "borderLeftWidth", "border-left-width"));
+	border_l = isNaN(border_l) ? 0 : border_l;
+	border_r = isNaN(border_r) ? 0 : border_r;
+	return Math.max(border_l, border_r);
+};
+
+EditableGrid.prototype.borderRight = function (element) 
+{
+	return this.borderLeft(element);
+};
+
+EditableGrid.prototype.borderTop = function (element) 
+{
+	var border_t = parseInt(this.getStyle(element, "borderTopWidth", "border-top-width"));
+	var border_b = parseInt(this.getStyle(element, "borderBottomWidth", "border-bottom-width"));
+	border_t = isNaN(border_t) ? 0 : border_t;
+	border_b = isNaN(border_b) ? 0 : border_b;
+	return Math.max(border_t, border_b);
+};
+
+EditableGrid.prototype.borderBottom = function (element) 
+{
+	return this.borderTop(element);
+};
+
+/**
+ * Returns auto width for editor
+ * @private
+ */
+EditableGrid.prototype.autoWidth = function (element) 
+{
+	return element.offsetWidth - this.paddingLeft(element) - this.paddingRight(element) - this.borderLeft(element) - this.borderRight(element);
+};
+
+/**
+ * Returns auto height for editor
+ * @private
+ */
+EditableGrid.prototype.autoHeight = function (element) 
+{
+	return element.offsetHeight - this.paddingTop(element) - this.paddingBottom(element) - this.borderTop(element) - this.borderBottom(element);
+};
+
+/**
+ * Detects the directory when the js sources can be found
+ * @private
+ */
+EditableGrid.prototype.detectDir = function() 
+{
+	var base = location.href;
+	var e = document.getElementsByTagName('base');
+	for (var i=0; i<e.length; i++) if(e[i].href) base = e[i].href;
+
+	var e = document.getElementsByTagName('script');
+	for (var i=0; i<e.length; i++) {
+		if (e[i].src && /(^|\/)editablegrid[^\/]*\.js([?#].*)?$/i.test(e[i].src)) {
+			var src = new URI(e[i].src);
+			var srcAbs = src.toAbsolute(base);
+			srcAbs.path = srcAbs.path.replace(/[^\/]+$/, ''); // remove filename
+			srcAbs.path = srcAbs.path.replace(/\/$/, ''); // remove trailing slash
+			delete srcAbs.query;
+			delete srcAbs.fragment;
+			return srcAbs.toString();
+		}
+	}
+
+	return false;
+};
+
+/**
+ * Detect is 2 values are exactly the same (type and value). Numeric NaN are considered the same.
+ * @param v1
+ * @param v2
+ * @return boolean
+ */
+EditableGrid.prototype.isSame = function(v1, v2) 
+{ 
+	if (v1 === v2) return true;
+	if (typeof v1 == 'number' && isNaN(v1) && typeof v2 == 'number' && isNaN(v2)) return true;
+	if (v1 === '' && v2 === null) return true;
+	if (v2 === '' && v1 === null) return true;
+	return false;
+};
+
+/**
+ * class name manipulation
+ * @private
+ */
+EditableGrid.prototype.strip = function(str) { return str.replace(/^\s+/, '').replace(/\s+$/, ''); };
+EditableGrid.prototype.hasClassName = function(element, className) { return (element.className.length > 0 && (element.className == className || new RegExp("(^|\\s)" + className + "(\\s|$)").test(element.className))); };
+EditableGrid.prototype.addClassName = function(element, className) { if (!this.hasClassName(element, className)) element.className += (element.className ? ' ' : '') + className; };
+EditableGrid.prototype.removeClassName = function(element, className) { element.className = this.strip(element.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ')); };
+
+/**
+ * Useful string methods 
+ * @private
+ */
+String.prototype.trim = function() { return (this.replace(/^[\s\xA0]+/, "").replace(/[\s\xA0]+$/, "")); };
+String.prototype.contains = function(str) { return (this.match(str)==str); };
+String.prototype.startsWith = function(str) { return (this.match("^"+str)==str); };
+String.prototype.endsWith = function(str) { return (this.match(str+"$")==str); };
+
+//Accepted formats: (for EU just switch month and day)
+
+//mm-dd-yyyy
+//mm/dd/yyyy
+//mm.dd.yyyy
+//mm dd yyyy
+//mmm dd yyyy
+//mmddyyyy
+
+//m-d-yyyy
+//m/d/yyyy
+//m.d.yyyy,
+//m d yyyy
+//mmm d yyyy
+
+////m-d-yy
+////m/d/yy
+////m.d.yy
+////m d yy,
+////mmm d yy (yy is 20yy) 
+
+/**
+ * Checks validity of a date string 
+ * @private
+ */
+EditableGrid.prototype.checkDate = function(strDate, strDatestyle) 
+{
+	strDatestyle = strDatestyle || this.dateFormat;
+	strDatestyle = strDatestyle || "EU";
+
+	var strDate;
+	var strDateArray;
+	var strDay;
+	var strMonth;
+	var strYear;
+	var intday;
+	var intMonth;
+	var intYear;
+	var booFound = false;
+	var strSeparatorArray = new Array("-"," ","/",".");
+	var intElementNr;
+	var err = 0;
+
+	var strMonthArray = this.shortMonthNames;
+	strMonthArray = strMonthArray || ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
+
+	if (!strDate || strDate.length < 1) return 0;
+
+	for (intElementNr = 0; intElementNr < strSeparatorArray.length; intElementNr++) {
+		if (strDate.indexOf(strSeparatorArray[intElementNr]) != -1) {
+			strDateArray = strDate.split(strSeparatorArray[intElementNr]);
+			if (strDateArray.length != 3) return 1;
+			else {
+				strDay = strDateArray[0];
+				strMonth = strDateArray[1];
+				strYear = strDateArray[2];
+			}
+			booFound = true;
+		}
+	}
+
+	if (booFound == false) {
+		if (strDate.length <= 5) return 1;
+		strDay = strDate.substr(0, 2);
+		strMonth = strDate.substr(2, 2);
+		strYear = strDate.substr(4);
+	}
+
+	// if (strYear.length == 2) strYear = '20' + strYear;
+
+	// US style
+	if (strDatestyle == "US") {
+		strTemp = strDay;
+		strDay = strMonth;
+		strMonth = strTemp;
+	}
+
+	// get and check day
+	intday = parseInt(strDay, 10);
+	if (isNaN(intday)) return 2;
+
+	// get and check month
+	intMonth = parseInt(strMonth, 10);
+	if (isNaN(intMonth)) {
+		for (i = 0;i<12;i++) {
+			if (strMonth.toUpperCase() == strMonthArray[i].toUpperCase()) {
+				intMonth = i+1;
+				strMonth = strMonthArray[i];
+				i = 12;
+			}
+		}
+		if (isNaN(intMonth)) return 3;
+	}
+	if (intMonth>12 || intMonth<1) return 5;
+
+	// get and check year
+	intYear = parseInt(strYear, 10);
+	if (isNaN(intYear)) return 4;
+	if (intYear < 70) { intYear = 2000 + intYear; strYear = '' + intYear; } // 70 become 1970, 69 becomes 1969, as with PHP's date_parse_from_format
+	if (intYear < 100) { intYear = 1900 + intYear; strYear = '' + intYear; }
+	if (intYear < 1900 || intYear > 2100) return 11;
+
+	// check day in month
+	if ((intMonth == 1 || intMonth == 3 || intMonth == 5 || intMonth == 7 || intMonth == 8 || intMonth == 10 || intMonth == 12) && (intday > 31 || intday < 1)) return 6;
+	if ((intMonth == 4 || intMonth == 6 || intMonth == 9 || intMonth == 11) && (intday > 30 || intday < 1)) return 7;
+	if (intMonth == 2) {
+		if (intday < 1) return 8;
+		if (LeapYear(intYear) == true) { if (intday > 29) return 9; }
+		else if (intday > 28) return 10;
+	}
+
+	// return formatted date
+	return { 
+		formattedDate: (strDatestyle == "US" ? strMonthArray[intMonth-1] + " " + intday+" " + strYear : intday + " " + strMonthArray[intMonth-1]/*.toLowerCase()*/ + " " + strYear),
+		sortDate: Date.parse(intMonth + "/" + intday + "/" + intYear),
+		dbDate: intYear + "-" + intMonth + "-" + intday 
+	};
+};
+
+function LeapYear(intYear) 
+{
+	if (intYear % 100 == 0) { if (intYear % 400 == 0) return true; }
+	else if ((intYear % 4) == 0) return true;
+	return false;
+}
+
+//See RFC3986
+URI = function(uri) 
+{ 
+	this.scheme = null;
+	this.authority = null;
+	this.path = '';
+	this.query = null;
+	this.fragment = null;
+
+	this.parse = function(uri) {
+		var m = uri.match(/^(([A-Za-z][0-9A-Za-z+.-]*)(:))?((\/\/)([^\/?#]*))?([^?#]*)((\?)([^#]*))?((#)(.*))?/);
+		this.scheme = m[3] ? m[2] : null;
+		this.authority = m[5] ? m[6] : null;
+		this.path = m[7];
+		this.query = m[9] ? m[10] : null;
+		this.fragment = m[12] ? m[13] : null;
+		return this;
+	};
+
+	this.toString = function() {
+		var result = '';
+		if(this.scheme != null) result = result + this.scheme + ':';
+		if(this.authority != null) result = result + '//' + this.authority;
+		if(this.path != null) result = result + this.path;
+		if(this.query != null) result = result + '?' + this.query;
+		if(this.fragment != null) result = result + '#' + this.fragment;
+		return result;
+	};
+
+	this.toAbsolute = function(base) {
+		var base = new URI(base);
+		var r = this;
+		var t = new URI;
+
+		if(base.scheme == null) return false;
+
+		if(r.scheme != null && r.scheme.toLowerCase() == base.scheme.toLowerCase()) {
+			r.scheme = null;
+		}
+
+		if(r.scheme != null) {
+			t.scheme = r.scheme;
+			t.authority = r.authority;
+			t.path = removeDotSegments(r.path);
+			t.query = r.query;
+		} else {
+			if(r.authority != null) {
+				t.authority = r.authority;
+				t.path = removeDotSegments(r.path);
+				t.query = r.query;
+			} else {
+				if(r.path == '') {
+					t.path = base.path;
+					if(r.query != null) {
+						t.query = r.query;
+					} else {
+						t.query = base.query;
+					}
+				} else {
+					if(r.path.substr(0,1) == '/') {
+						t.path = removeDotSegments(r.path);
+					} else {
+						if(base.authority != null && base.path == '') {
+							t.path = '/'+r.path;
+						} else {
+							t.path = base.path.replace(/[^\/]+$/,'')+r.path;
+						}
+						t.path = removeDotSegments(t.path);
+					}
+					t.query = r.query;
+				}
+				t.authority = base.authority;
+			}
+			t.scheme = base.scheme;
+		}
+		t.fragment = r.fragment;
+
+		return t;
+	};
+
+	function removeDotSegments(path) {
+		var out = '';
+		while(path) {
+			if(path.substr(0,3)=='../' || path.substr(0,2)=='./') {
+				path = path.replace(/^\.+/,'').substr(1);
+			} else if(path.substr(0,3)=='/./' || path=='/.') {
+				path = '/'+path.substr(3);
+			} else if(path.substr(0,4)=='/../' || path=='/..') {
+				path = '/'+path.substr(4);
+				out = out.replace(/\/?[^\/]*$/, '');
+			} else if(path=='.' || path=='..') {
+				path = '';
+			} else {
+				var rm = path.match(/^\/?[^\/]*/)[0];
+				path = path.substr(rm.length);
+				out = out + rm;
+			}
+		}
+		return out;
+	}
+
+	if(uri) {
+		this.parse(uri);
+	}
+};
+
+function get_html_translation_table (table, quote_style) {
+	// http://kevin.vanzonneveld.net
+	// +   original by: Philip Peterson
+	// +    revised by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
+	// +   bugfixed by: noname
+	// +   bugfixed by: Alex
+	// +   bugfixed by: Marco
+	// +   bugfixed by: madipta
+	// +   improved by: KELAN
+	// +   improved by: Brett Zamir (http://brett-zamir.me)
+	// +   bugfixed by: Brett Zamir (http://brett-zamir.me)
+	// +      input by: Frank Forte
+	// +   bugfixed by: T.Wild
+	// +      input by: Ratheous
+	// %          note: It has been decided that we're not going to add global
+	// %          note: dependencies to php.js, meaning the constants are not
+	// %          note: real constants, but strings instead. Integers are also supported if someone
+	// %          note: chooses to create the constants themselves.
+	// *     example 1: get_html_translation_table('HTML_SPECIALCHARS');
+	// *     returns 1: {'"': '&quot;', '&': '&amp;', '<': '&lt;', '>': '&gt;'}
+
+	var entities = {}, hash_map = {}, decimal = 0, symbol = '';
+	var constMappingTable = {}, constMappingQuoteStyle = {};
+	var useTable = {}, useQuoteStyle = {};
+
+	// Translate arguments
+	constMappingTable[0]      = 'HTML_SPECIALCHARS';
+	constMappingTable[1]      = 'HTML_ENTITIES';
+	constMappingQuoteStyle[0] = 'ENT_NOQUOTES';
+	constMappingQuoteStyle[2] = 'ENT_COMPAT';
+	constMappingQuoteStyle[3] = 'ENT_QUOTES';
+
+	useTable       = !isNaN(table) ? constMappingTable[table] : table ? table.toUpperCase() : 'HTML_SPECIALCHARS';
+	useQuoteStyle = !isNaN(quote_style) ? constMappingQuoteStyle[quote_style] : quote_style ? quote_style.toUpperCase() : 'ENT_COMPAT';
+
+	if (useTable !== 'HTML_SPECIALCHARS' && useTable !== 'HTML_ENTITIES') {
+		throw new Error("Table: "+useTable+' not supported');
+		// return false;
+	}
+
+	if (useTable === 'HTML_ENTITIES') {
+		entities['160'] = '&nbsp;';
+		entities['161'] = '&iexcl;';
+		entities['162'] = '&cent;';
+		entities['163'] = '&pound;';
+		entities['164'] = '&curren;';
+		entities['165'] = '&yen;';
+		entities['166'] = '&brvbar;';
+		entities['167'] = '&sect;';
+		entities['168'] = '&uml;';
+		entities['169'] = '&copy;';
+		entities['170'] = '&ordf;';
+		entities['171'] = '&laquo;';
+		entities['172'] = '&not;';
+		entities['173'] = '&shy;';
+		entities['174'] = '&reg;';
+		entities['175'] = '&macr;';
+		entities['176'] = '&deg;';
+		entities['177'] = '&plusmn;';
+		entities['178'] = '&sup2;';
+		entities['179'] = '&sup3;';
+		entities['180'] = '&acute;';
+		entities['181'] = '&micro;';
+		entities['182'] = '&para;';
+		entities['183'] = '&middot;';
+		entities['184'] = '&cedil;';
+		entities['185'] = '&sup1;';
+		entities['186'] = '&ordm;';
+		entities['187'] = '&raquo;';
+		entities['188'] = '&frac14;';
+		entities['189'] = '&frac12;';
+		entities['190'] = '&frac34;';
+		entities['191'] = '&iquest;';
+		entities['192'] = '&Agrave;';
+		entities['193'] = '&Aacute;';
+		entities['194'] = '&Acirc;';
+		entities['195'] = '&Atilde;';
+		entities['196'] = '&Auml;';
+		entities['197'] = '&Aring;';
+		entities['198'] = '&AElig;';
+		entities['199'] = '&Ccedil;';
+		entities['200'] = '&Egrave;';
+		entities['201'] = '&Eacute;';
+		entities['202'] = '&Ecirc;';
+		entities['203'] = '&Euml;';
+		entities['204'] = '&Igrave;';
+		entities['205'] = '&Iacute;';
+		entities['206'] = '&Icirc;';
+		entities['207'] = '&Iuml;';
+		entities['208'] = '&ETH;';
+		entities['209'] = '&Ntilde;';
+		entities['210'] = '&Ograve;';
+		entities['211'] = '&Oacute;';
+		entities['212'] = '&Ocirc;';
+		entities['213'] = '&Otilde;';
+		entities['214'] = '&Ouml;';
+		entities['215'] = '&times;';
+		entities['216'] = '&Oslash;';
+		entities['217'] = '&Ugrave;';
+		entities['218'] = '&Uacute;';
+		entities['219'] = '&Ucirc;';
+		entities['220'] = '&Uuml;';
+		entities['221'] = '&Yacute;';
+		entities['222'] = '&THORN;';
+		entities['223'] = '&szlig;';
+		entities['224'] = '&agrave;';
+		entities['225'] = '&aacute;';
+		entities['226'] = '&acirc;';
+		entities['227'] = '&atilde;';
+		entities['228'] = '&auml;';
+		entities['229'] = '&aring;';
+		entities['230'] = '&aelig;';
+		entities['231'] = '&ccedil;';
+		entities['232'] = '&egrave;';
+		entities['233'] = '&eacute;';
+		entities['234'] = '&ecirc;';
+		entities['235'] = '&euml;';
+		entities['236'] = '&igrave;';
+		entities['237'] = '&iacute;';
+		entities['238'] = '&icirc;';
+		entities['239'] = '&iuml;';
+		entities['240'] = '&eth;';
+		entities['241'] = '&ntilde;';
+		entities['242'] = '&ograve;';
+		entities['243'] = '&oacute;';
+		entities['244'] = '&ocirc;';
+		entities['245'] = '&otilde;';
+		entities['246'] = '&ouml;';
+		entities['247'] = '&divide;';
+		entities['248'] = '&oslash;';
+		entities['249'] = '&ugrave;';
+		entities['250'] = '&uacute;';
+		entities['251'] = '&ucirc;';
+		entities['252'] = '&uuml;';
+		entities['253'] = '&yacute;';
+		entities['254'] = '&thorn;';
+		entities['255'] = '&yuml;';
+	}
+
+	if (useQuoteStyle !== 'ENT_NOQUOTES') {
+		entities['34'] = '&quot;';
+	}
+	if (useQuoteStyle === 'ENT_QUOTES') {
+		entities['39'] = '&#39;';
+	}
+	entities['60'] = '&lt;';
+	entities['62'] = '&gt;';
+
+
+	// ascii decimals to real symbols
+	for (decimal in entities) {
+		symbol = String.fromCharCode(decimal);
+		hash_map[symbol] = entities[decimal];
+	}
+
+	return hash_map;
+}
+
+function htmlentities(string, quote_style) 
+{
+	var hash_map = {}, symbol = '', tmp_str = '';
+	tmp_str = string.toString();
+	if (false === (hash_map = this.get_html_translation_table('HTML_ENTITIES', quote_style))) return false;
+	tmp_str = tmp_str.split('&').join('&amp;'); // replace & first, otherwise & in htlm codes will be replaced too!
+	hash_map["'"] = '&#039;';
+	for (symbol in hash_map) tmp_str = tmp_str.split(symbol).join(hash_map[symbol]);
+	return tmp_str;
+}
+
+function htmlspecialchars(string, quote_style) 
+{
+	var hash_map = {}, symbol = '', tmp_str = '';
+	tmp_str = string.toString();
+	if (false === (hash_map = this.get_html_translation_table('HTML_SPECIALCHARS', quote_style))) return false;
+	tmp_str = tmp_str.split('&').join('&amp;'); // replace & first, otherwise & in htlm codes will be replaced too!
+	for (symbol in hash_map) tmp_str = tmp_str.split(symbol).join(hash_map[symbol]);
+	return tmp_str;
+}
+
+function number_format (number, decimals, dec_point, thousands_sep) {
+	// http://kevin.vanzonneveld.net
+	// +   original by: Jonas Raoni Soares Silva (http://www.jsfromhell.com)
+	// +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
+	// +     bugfix by: Michael White (http://getsprink.com)
+	// +     bugfix by: Benjamin Lupton
+	// +     bugfix by: Allan Jensen (http://www.winternet.no)
+	// +    revised by: Jonas Raoni Soares Silva (http://www.jsfromhell.com)
+	// +     bugfix by: Howard Yeend
+	// +    revised by: Luke Smith (http://lucassmith.name)
+	// +     bugfix by: Diogo Resende
+	// +     bugfix by: Rival
+	// +      input by: Kheang Hok Chin (http://www.distantia.ca/)
+	// +   improved by: davook
+	// +   improved by: Brett Zamir (http://brett-zamir.me)
+	// +      input by: Jay Klehr
+	// +   improved by: Brett Zamir (http://brett-zamir.me)
+	// +      input by: Amir Habibi (http://www.residence-mixte.com/)
+	// +     bugfix by: Brett Zamir (http://brett-zamir.me)
+	// +   improved by: Theriault
+	// +      input by: Amirouche
+	// +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
+	// *     example 1: number_format(1234.56);
+	// *     returns 1: '1,235'
+	// *     example 2: number_format(1234.56, 2, ',', ' ');
+	// *     returns 2: '1 234,56'
+	// *     example 3: number_format(1234.5678, 2, '.', '');
+	// *     returns 3: '1234.57'
+	// *     example 4: number_format(67, 2, ',', '.');
+	// *     returns 4: '67,00'
+	// *     example 5: number_format(1000);
+	// *     returns 5: '1,000'
+	// *     example 6: number_format(67.311, 2);
+	// *     returns 6: '67.31'
+	// *     example 7: number_format(1000.55, 1);
+	// *     returns 7: '1,000.6'
+	// *     example 8: number_format(67000, 5, ',', '.');
+	// *     returns 8: '67.000,00000'
+	// *     example 9: number_format(0.9, 0);
+	// *     returns 9: '1'
+	// *    example 10: number_format('1.20', 2);
+	// *    returns 10: '1.20'
+	// *    example 11: number_format('1.20', 4);
+	// *    returns 11: '1.2000'
+	// *    example 12: number_format('1.2000', 3);
+	// *    returns 12: '1.200'
+	// *    example 13: number_format('1 000,50', 2, '.', ' ');
+	// *    returns 13: '100 050.00'
+	// Strip all characters but numerical ones.
+	number = (number + '').replace(/[^0-9+\-Ee.]/g, '');
+	var n = !isFinite(+number) ? 0 : +number,
+			prec = !isFinite(+decimals) ? 0 : /*Math.abs(*/decimals/*)*/,
+					sep = (typeof thousands_sep === 'undefined') ? ',' : thousands_sep,
+							dec = (typeof dec_point === 'undefined') ? '.' : dec_point,
+									s = '',
+									toFixedFix = function (n, prec) {
+								var k = Math.pow(10, prec);
+								return '' + Math.round(n * k) / k;
+							};
+							// Fix for IE parseFloat(0.55).toFixed(0) = 0;
+							s = (prec < 0 ? ('' + n) : (prec ? toFixedFix(n, prec) : '' + Math.round(n))).split('.');
+							if (s[0].length > 3) {
+								s[0] = s[0].replace(/\B(?=(?:\d{3})+(?!\d))/g, sep);
+							}
+							if ((s[1] || '').length < prec) {
+								s[1] = s[1] || '';
+								s[1] += new Array(prec - s[1].length + 1).join('0');
+							}
+							return s.join(dec);
+}
+

+ 74 - 0
src/static/js/editablegrid_validators.js

@@ -0,0 +1,74 @@
+/**
+ * Abstract cell validator
+ * @constructor
+ * @class Base class for all cell validators
+ */
+
+function CellValidator(config) 
+{
+	// default properties
+    var props = { isValid: null };
+
+    // override default properties with the ones given
+    for (var p in props) if (typeof config != 'undefined' && typeof config[p] != 'undefined') this[p] = config[p];
+}
+
+CellValidator.prototype.isValid = function(value) 
+{
+	return true;
+};
+
+/**
+ * Number cell validator
+ * @constructor
+ * @class Class to validate a numeric cell
+ */
+
+function NumberCellValidator(type) { this.type = type; }
+NumberCellValidator.prototype = new CellValidator;
+NumberCellValidator.prototype.isValid = function(value) 
+{
+	// check that it is a valid number
+	if (isNaN(value)) return false;
+	
+	// for integers check that it's not a float
+	if (this.type == "integer" && value != "" && parseInt(value) != parseFloat(value)) return false;
+	
+	// the integer or double is valid
+	return true;
+};
+
+/**
+ * Email cell validator
+ * @constructor
+ * @class Class to validate a cell containing an email
+ */
+
+function EmailCellValidator() {}
+EmailCellValidator.prototype = new CellValidator;
+EmailCellValidator.prototype.isValid = function(value) { return value == "" || /^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/.test(value); };
+
+/**
+ * Website cell validator
+ * @constructor
+ * @class Class to validate a cell containing a website
+ */
+
+function WebsiteCellValidator() {}
+WebsiteCellValidator.prototype = new CellValidator;
+WebsiteCellValidator.prototype.isValid = function(value) { return value == "" || (value.indexOf(".") > 0 && value.indexOf(".") < (value.length - 2)); };
+
+/**
+ * Date cell validator
+ * @constructor
+ * @augments CellValidator
+ * @class Class to validate a cell containing a date
+ */
+
+function DateCellValidator(grid) { this.grid = grid; }
+DateCellValidator.prototype = new CellValidator;
+
+DateCellValidator.prototype.isValid = function(value) 
+{
+	return value == "" || typeof this.grid.checkDate(value) == "object";
+};

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 0
src/static/js/jquery-1.11.1.min.js


+ 17 - 0
src/templates/delete.html

@@ -0,0 +1,17 @@
+<html>
+<head>
+    <title>DP Tape DB - digilst - GUI - DELETE</title>
+    <link rel="stylesheet" type="text/css" href="{{url_for('static', filename='css/bootstrap.css')}}">
+</head>
+<body>
+    <div align="center">
+        <h1>DP Tape DB - digilst</h1>
+        <h2>Delete row</h2>
+        <form action="{{url_for('write_delete')}}" method="POST">
+            <input type="text" name="mongoid" placeholder="MongoDB ID"><br>
+            <input type="submit" value="Delete">
+        </form>
+        <a href="{{url_for('index')}}"><h4>Back</h4></a>
+    </div>
+</body>
+</html>

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 187 - 0
src/templates/index.html


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 170 - 0
src/templates/index_old.html


+ 29 - 0
src/templates/insert.html

@@ -0,0 +1,29 @@
+<html>
+<head>
+    <title>DP Tape DB - digilst - GUI - INSERT</title>
+    <link rel="stylesheet" type="text/css" href="{{url_for('static', filename='css/bootstrap.css')}}">
+</head>
+<body>
+    <div align="center">
+        <h1>DP Tape DB - digilst</h1>
+        <h2>insert row</h2>
+        <form action="{{url_for('write_insert')}}" method="POST">
+            <input type="text" name="date" id="date" placeholder="Date"><br>
+            <input type="text" name="date2" id="date2" placeholder="Date2"><br>
+            <input type="text" name="titel" id="titel" placeholder="Titel"><br>
+            <input type="text" name="languages" id="languages" placeholder="Language"><br>
+            <input type="text" name="country" id="country" placeholder="Country"><br>
+            <input type="text" name="city" id="city" placeholder="City"><br>
+            <input type="text" name="place" id="place" placeholder="Place"><br>
+            <input type="text" name="category" id="category" placeholder="Category"><br>
+            <input type="text" name="duration" id="duration" placeholder="Duration"><br>
+            <input type="text" name="location" id="location" placeholder="Location"><br>
+            <input type="text" name="format" id="format" placeholder="Format"><br>
+            <input type="text" name="note" id="note" placeholder="Note"><br>
+            <input type="text" name="status" id="status" placeholder="Status"><br><br>
+            <input type="submit" value="Insert">
+        </form>
+        <a href="{{url_for('index')}}"><h4>Back</h4></a>
+    </div>
+</body>
+</html>

+ 1 - 0
src/templates/ok.txt

@@ -0,0 +1 @@
+ok

+ 33 - 0
src/templates/update.html

@@ -0,0 +1,33 @@
+<html>
+<head>
+    <title>DP Tape DB - digilst - GUI - UPDATE</title>
+    <link rel="stylesheet" type="text/css" href="{{url_for('static', filename='css/bootstrap.css')}}">
+</head>
+<body>
+    <div align="center">
+        <h1>DP Tape DB - digilst</h1>
+        <h2>Update field</h2>
+        <form action="{{url_for('write_update')}}" method="POST">
+            <select name="field">
+                <option>Date</option>
+                <option>Date2</option>
+                <option>Titel</option>
+                <option>Languages</option>
+                <option>Country</option>
+                <option>City</option>
+                <option>Place</option>
+                <option>Category</option>
+                <option>Duration</option>
+                <option>Location</option>
+                <option>Format</option>
+                <option>Note</option>
+                <option>Status</option>
+            </select><br>
+            <input type="text" name="value" placeholder="Neuer Wert f&uuml;r ausgew&auml;hltes feld"><br>
+            <input type="text" name="mongoid" placeholder="MongoDB ID"><br>
+            <input type="submit" value="Update">
+        </form>
+        <a href="{{url_for('index')}}"><h4>Back</h4></a>
+    </div>
+</body>
+</html>

BIN
venv/.DS_Store


+ 1 - 0
venv/.Python

@@ -0,0 +1 @@
+/System/Library/Frameworks/Python.framework/Versions/2.7/Python

+ 80 - 0
venv/bin/activate

@@ -0,0 +1,80 @@
+# This file must be used with "source bin/activate" *from bash*
+# you cannot run it directly
+
+deactivate () {
+    unset pydoc
+
+    # reset old environment variables
+    if [ -n "$_OLD_VIRTUAL_PATH" ] ; then
+        PATH="$_OLD_VIRTUAL_PATH"
+        export PATH
+        unset _OLD_VIRTUAL_PATH
+    fi
+    if [ -n "$_OLD_VIRTUAL_PYTHONHOME" ] ; then
+        PYTHONHOME="$_OLD_VIRTUAL_PYTHONHOME"
+        export PYTHONHOME
+        unset _OLD_VIRTUAL_PYTHONHOME
+    fi
+
+    # This should detect bash and zsh, which have a hash command that must
+    # be called to get it to forget past commands.  Without forgetting
+    # past commands the $PATH changes we made may not be respected
+    if [ -n "$BASH" -o -n "$ZSH_VERSION" ] ; then
+        hash -r 2>/dev/null
+    fi
+
+    if [ -n "$_OLD_VIRTUAL_PS1" ] ; then
+        PS1="$_OLD_VIRTUAL_PS1"
+        export PS1
+        unset _OLD_VIRTUAL_PS1
+    fi
+
+    unset VIRTUAL_ENV
+    if [ ! "$1" = "nondestructive" ] ; then
+    # Self destruct!
+        unset -f deactivate
+    fi
+}
+
+# unset irrelevant variables
+deactivate nondestructive
+
+VIRTUAL_ENV="/Users/nanak/dev/nirmalaVidyaDatabase/dpTapeDBgui/venv"
+export VIRTUAL_ENV
+
+_OLD_VIRTUAL_PATH="$PATH"
+PATH="$VIRTUAL_ENV/bin:$PATH"
+export PATH
+
+# unset PYTHONHOME if set
+# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
+# could use `if (set -u; : $PYTHONHOME) ;` in bash
+if [ -n "$PYTHONHOME" ] ; then
+    _OLD_VIRTUAL_PYTHONHOME="$PYTHONHOME"
+    unset PYTHONHOME
+fi
+
+if [ -z "$VIRTUAL_ENV_DISABLE_PROMPT" ] ; then
+    _OLD_VIRTUAL_PS1="$PS1"
+    if [ "x" != x ] ; then
+        PS1="$PS1"
+    else
+    if [ "`basename \"$VIRTUAL_ENV\"`" = "__" ] ; then
+        # special case for Aspen magic directories
+        # see http://www.zetadev.com/software/aspen/
+        PS1="[`basename \`dirname \"$VIRTUAL_ENV\"\``] $PS1"
+    else
+        PS1="(`basename \"$VIRTUAL_ENV\"`)$PS1"
+    fi
+    fi
+    export PS1
+fi
+
+alias pydoc="python -m pydoc"
+
+# This should detect bash and zsh, which have a hash command that must
+# be called to get it to forget past commands.  Without forgetting
+# past commands the $PATH changes we made may not be respected
+if [ -n "$BASH" -o -n "$ZSH_VERSION" ] ; then
+    hash -r 2>/dev/null
+fi

+ 42 - 0
venv/bin/activate.csh

@@ -0,0 +1,42 @@
+# This file must be used with "source bin/activate.csh" *from csh*.
+# You cannot run it directly.
+# Created by Davide Di Blasi <davidedb@gmail.com>.
+
+alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; test "\!:*" != "nondestructive" && unalias deactivate && unalias pydoc'
+
+# Unset irrelevant variables.
+deactivate nondestructive
+
+setenv VIRTUAL_ENV "/Users/nanak/dev/nirmalaVidyaDatabase/dpTapeDBgui/venv"
+
+set _OLD_VIRTUAL_PATH="$PATH"
+setenv PATH "$VIRTUAL_ENV/bin:$PATH"
+
+
+
+if ("" != "") then
+    set env_name = ""
+else
+    if (`basename "$VIRTUAL_ENV"` == "__") then
+        # special case for Aspen magic directories
+        # see http://www.zetadev.com/software/aspen/
+        set env_name = `basename \`dirname "$VIRTUAL_ENV"\``
+    else
+        set env_name = `basename "$VIRTUAL_ENV"`
+    endif
+endif
+
+# Could be in a non-interactive environment,
+# in which case, $prompt is undefined and we wouldn't
+# care about the prompt anyway.
+if ( $?prompt ) then
+    set _OLD_VIRTUAL_PROMPT="$prompt"
+    set prompt = "[$env_name] $prompt"
+endif
+
+unset env_name
+
+alias pydoc python -m pydoc
+
+rehash
+

+ 74 - 0
venv/bin/activate.fish

@@ -0,0 +1,74 @@
+# This file must be used with "source bin/activate.fish" *from fish* (http://fishshell.com)
+# you cannot run it directly
+
+function deactivate  -d "Exit virtualenv and return to normal shell environment"
+    # reset old environment variables
+    if test -n "$_OLD_VIRTUAL_PATH" 
+        set -gx PATH $_OLD_VIRTUAL_PATH
+        set -e _OLD_VIRTUAL_PATH
+    end
+    if test -n "$_OLD_VIRTUAL_PYTHONHOME"
+        set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
+        set -e _OLD_VIRTUAL_PYTHONHOME
+    end
+    
+    if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
+        # set an empty local fish_function_path, so fish_prompt doesn't automatically reload
+        set -l fish_function_path
+        # erase the virtualenv's fish_prompt function, and restore the original
+        functions -e fish_prompt
+        functions -c _old_fish_prompt fish_prompt
+        functions -e _old_fish_prompt
+        set -e _OLD_FISH_PROMPT_OVERRIDE
+    end
+    
+    set -e VIRTUAL_ENV
+    if test "$argv[1]" != "nondestructive"
+        # Self destruct!
+        functions -e deactivate
+    end
+end
+
+# unset irrelevant variables
+deactivate nondestructive
+
+set -gx VIRTUAL_ENV "/Users/nanak/dev/nirmalaVidyaDatabase/dpTapeDBgui/venv"
+
+set -gx _OLD_VIRTUAL_PATH $PATH
+set -gx PATH "$VIRTUAL_ENV/bin" $PATH
+
+# unset PYTHONHOME if set
+if set -q PYTHONHOME
+    set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
+    set -e PYTHONHOME
+end
+
+if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
+    # fish uses a function instead of an env var to generate the prompt.
+    
+    # copy the current fish_prompt function as the function _old_fish_prompt
+    functions -c fish_prompt _old_fish_prompt
+    
+    # with the original prompt function copied, we can override with our own.
+    function fish_prompt
+        # Prompt override?
+        if test -n ""
+            printf "%s%s" "" (set_color normal)
+            _old_fish_prompt
+            return
+        end
+        # ...Otherwise, prepend env
+        set -l _checkbase (basename "$VIRTUAL_ENV")
+        if test $_checkbase = "__"
+            # special case for Aspen magic directories
+            # see http://www.zetadev.com/software/aspen/
+            printf "%s[%s]%s " (set_color -b blue white) (basename (dirname "$VIRTUAL_ENV")) (set_color normal) 
+            _old_fish_prompt
+        else
+            printf "%s(%s)%s" (set_color -b blue white) (basename "$VIRTUAL_ENV") (set_color normal)
+            _old_fish_prompt
+        end
+    end 
+    
+    set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
+end

+ 34 - 0
venv/bin/activate_this.py

@@ -0,0 +1,34 @@
+"""By using execfile(this_file, dict(__file__=this_file)) you will
+activate this virtualenv environment.
+
+This can be used when you must use an existing Python interpreter, not
+the virtualenv bin/python
+"""
+
+try:
+    __file__
+except NameError:
+    raise AssertionError(
+        "You must run this like execfile('path/to/activate_this.py', dict(__file__='path/to/activate_this.py'))")
+import sys
+import os
+
+old_os_path = os.environ['PATH']
+os.environ['PATH'] = os.path.dirname(os.path.abspath(__file__)) + os.pathsep + old_os_path
+base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+if sys.platform == 'win32':
+    site_packages = os.path.join(base, 'Lib', 'site-packages')
+else:
+    site_packages = os.path.join(base, 'lib', 'python%s' % sys.version[:3], 'site-packages')
+prev_sys_path = list(sys.path)
+import site
+site.addsitedir(site_packages)
+sys.real_prefix = sys.prefix
+sys.prefix = base
+# Move the added items to the front of the path:
+new_sys_path = []
+for item in list(sys.path):
+    if item not in prev_sys_path:
+        new_sys_path.append(item)
+        sys.path.remove(item)
+sys.path[:0] = new_sys_path

+ 11 - 0
venv/bin/easy_install

@@ -0,0 +1,11 @@
+#!/Users/nanak/dev/nirmalaVidyaDatabase/dpTapeDBgui/venv/bin/python
+
+# -*- coding: utf-8 -*-
+import re
+import sys
+
+from setuptools.command.easy_install import main
+
+if __name__ == '__main__':
+    sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
+    sys.exit(main())

+ 11 - 0
venv/bin/easy_install-2.7

@@ -0,0 +1,11 @@
+#!/Users/nanak/dev/nirmalaVidyaDatabase/dpTapeDBgui/venv/bin/python
+
+# -*- coding: utf-8 -*-
+import re
+import sys
+
+from setuptools.command.easy_install import main
+
+if __name__ == '__main__':
+    sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
+    sys.exit(main())

+ 11 - 0
venv/bin/pip

@@ -0,0 +1,11 @@
+#!/Users/nanak/dev/nirmalaVidyaDatabase/dpTapeDBgui/venv/bin/python
+
+# -*- coding: utf-8 -*-
+import re
+import sys
+
+from pip import main
+
+if __name__ == '__main__':
+    sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
+    sys.exit(main())

+ 11 - 0
venv/bin/pip2

@@ -0,0 +1,11 @@
+#!/Users/nanak/dev/nirmalaVidyaDatabase/dpTapeDBgui/venv/bin/python
+
+# -*- coding: utf-8 -*-
+import re
+import sys
+
+from pip import main
+
+if __name__ == '__main__':
+    sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
+    sys.exit(main())

+ 11 - 0
venv/bin/pip2.7

@@ -0,0 +1,11 @@
+#!/Users/nanak/dev/nirmalaVidyaDatabase/dpTapeDBgui/venv/bin/python
+
+# -*- coding: utf-8 -*-
+import re
+import sys
+
+from pip import main
+
+if __name__ == '__main__':
+    sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
+    sys.exit(main())

BIN
venv/bin/python


+ 1 - 0
venv/bin/python2

@@ -0,0 +1 @@
+python

+ 1 - 0
venv/bin/python2.7

@@ -0,0 +1 @@
+python

+ 1 - 0
venv/include/python2.7

@@ -0,0 +1 @@
+/System/Library/Frameworks/Python.framework/Versions/2.7/include/python2.7

+ 1 - 0
venv/lib/python2.7/UserDict.py

@@ -0,0 +1 @@
+/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/UserDict.py

BIN
venv/lib/python2.7/UserDict.pyc


+ 1 - 0
venv/lib/python2.7/_abcoll.py

@@ -0,0 +1 @@
+/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/_abcoll.py

BIN
venv/lib/python2.7/_abcoll.pyc


+ 1 - 0
venv/lib/python2.7/_weakrefset.py

@@ -0,0 +1 @@
+/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/_weakrefset.py

BIN
venv/lib/python2.7/_weakrefset.pyc


+ 1 - 0
venv/lib/python2.7/abc.py

@@ -0,0 +1 @@
+/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/abc.py

BIN
venv/lib/python2.7/abc.pyc


+ 1 - 0
venv/lib/python2.7/codecs.py

@@ -0,0 +1 @@
+/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/codecs.py

BIN
venv/lib/python2.7/codecs.pyc


+ 1 - 0
venv/lib/python2.7/config

@@ -0,0 +1 @@
+/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/config

+ 1 - 0
venv/lib/python2.7/copy_reg.py

@@ -0,0 +1 @@
+/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/copy_reg.py

BIN
venv/lib/python2.7/copy_reg.pyc


+ 101 - 0
venv/lib/python2.7/distutils/__init__.py

@@ -0,0 +1,101 @@
+import os
+import sys
+import warnings 
+import imp
+import opcode # opcode is not a virtualenv module, so we can use it to find the stdlib
+              # Important! To work on pypy, this must be a module that resides in the
+              # lib-python/modified-x.y.z directory
+
+dirname = os.path.dirname
+
+distutils_path = os.path.join(os.path.dirname(opcode.__file__), 'distutils')
+if os.path.normpath(distutils_path) == os.path.dirname(os.path.normpath(__file__)):
+    warnings.warn(
+        "The virtualenv distutils package at %s appears to be in the same location as the system distutils?")
+else:
+    __path__.insert(0, distutils_path)
+    real_distutils = imp.load_module("_virtualenv_distutils", None, distutils_path, ('', '', imp.PKG_DIRECTORY))
+    # Copy the relevant attributes
+    try:
+        __revision__ = real_distutils.__revision__
+    except AttributeError:
+        pass
+    __version__ = real_distutils.__version__
+
+from distutils import dist, sysconfig
+
+try:
+    basestring
+except NameError:
+    basestring = str
+
+## patch build_ext (distutils doesn't know how to get the libs directory
+## path on windows - it hardcodes the paths around the patched sys.prefix)
+
+if sys.platform == 'win32':
+    from distutils.command.build_ext import build_ext as old_build_ext
+    class build_ext(old_build_ext):
+        def finalize_options (self):
+            if self.library_dirs is None:
+                self.library_dirs = []
+            elif isinstance(self.library_dirs, basestring):
+                self.library_dirs = self.library_dirs.split(os.pathsep)
+            
+            self.library_dirs.insert(0, os.path.join(sys.real_prefix, "Libs"))
+            old_build_ext.finalize_options(self)
+            
+    from distutils.command import build_ext as build_ext_module 
+    build_ext_module.build_ext = build_ext
+
+## distutils.dist patches:
+
+old_find_config_files = dist.Distribution.find_config_files
+def find_config_files(self):
+    found = old_find_config_files(self)
+    system_distutils = os.path.join(distutils_path, 'distutils.cfg')
+    #if os.path.exists(system_distutils):
+    #    found.insert(0, system_distutils)
+        # What to call the per-user config file
+    if os.name == 'posix':
+        user_filename = ".pydistutils.cfg"
+    else:
+        user_filename = "pydistutils.cfg"
+    user_filename = os.path.join(sys.prefix, user_filename)
+    if os.path.isfile(user_filename):
+        for item in list(found):
+            if item.endswith('pydistutils.cfg'):
+                found.remove(item)
+        found.append(user_filename)
+    return found
+dist.Distribution.find_config_files = find_config_files
+
+## distutils.sysconfig patches:
+
+old_get_python_inc = sysconfig.get_python_inc
+def sysconfig_get_python_inc(plat_specific=0, prefix=None):
+    if prefix is None:
+        prefix = sys.real_prefix
+    return old_get_python_inc(plat_specific, prefix)
+sysconfig_get_python_inc.__doc__ = old_get_python_inc.__doc__
+sysconfig.get_python_inc = sysconfig_get_python_inc
+
+old_get_python_lib = sysconfig.get_python_lib
+def sysconfig_get_python_lib(plat_specific=0, standard_lib=0, prefix=None):
+    if standard_lib and prefix is None:
+        prefix = sys.real_prefix
+    return old_get_python_lib(plat_specific, standard_lib, prefix)
+sysconfig_get_python_lib.__doc__ = old_get_python_lib.__doc__
+sysconfig.get_python_lib = sysconfig_get_python_lib
+
+old_get_config_vars = sysconfig.get_config_vars
+def sysconfig_get_config_vars(*args):
+    real_vars = old_get_config_vars(*args)
+    if sys.platform == 'win32':
+        lib_dir = os.path.join(sys.real_prefix, "libs")
+        if isinstance(real_vars, dict) and 'LIBDIR' not in real_vars:
+            real_vars['LIBDIR'] = lib_dir # asked for all
+        elif isinstance(real_vars, list) and 'LIBDIR' in args:
+            real_vars = real_vars + [lib_dir] # asked for list
+    return real_vars
+sysconfig_get_config_vars.__doc__ = old_get_config_vars.__doc__
+sysconfig.get_config_vars = sysconfig_get_config_vars

BIN
venv/lib/python2.7/distutils/__init__.pyc


+ 6 - 0
venv/lib/python2.7/distutils/distutils.cfg

@@ -0,0 +1,6 @@
+# This is a config file local to this virtualenv installation
+# You may include options that will be used by all distutils commands,
+# and by easy_install.  For instance:
+#
+#   [easy_install]
+#   find_links = http://mylocalsite

+ 1 - 0
venv/lib/python2.7/encodings

@@ -0,0 +1 @@
+/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/encodings

+ 1 - 0
venv/lib/python2.7/fnmatch.py

@@ -0,0 +1 @@
+/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/fnmatch.py

BIN
venv/lib/python2.7/fnmatch.pyc


+ 1 - 0
venv/lib/python2.7/genericpath.py

@@ -0,0 +1 @@
+/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/genericpath.py

BIN
venv/lib/python2.7/genericpath.pyc


+ 1 - 0
venv/lib/python2.7/lib-dynload

@@ -0,0 +1 @@
+/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-dynload

+ 1 - 0
venv/lib/python2.7/linecache.py

@@ -0,0 +1 @@
+/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/linecache.py

BIN
venv/lib/python2.7/linecache.pyc


+ 1 - 0
venv/lib/python2.7/locale.py

@@ -0,0 +1 @@
+/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/locale.py

BIN
venv/lib/python2.7/locale.pyc


+ 0 - 0
venv/lib/python2.7/no-global-site-packages.txt


+ 1 - 0
venv/lib/python2.7/ntpath.py

@@ -0,0 +1 @@
+/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/ntpath.py

+ 1 - 0
venv/lib/python2.7/orig-prefix.txt

@@ -0,0 +1 @@
+/System/Library/Frameworks/Python.framework/Versions/2.7

+ 1 - 0
venv/lib/python2.7/os.py

@@ -0,0 +1 @@
+/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/os.py

BIN
venv/lib/python2.7/os.pyc


+ 1 - 0
venv/lib/python2.7/posixpath.py

@@ -0,0 +1 @@
+/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/posixpath.py

BIN
venv/lib/python2.7/posixpath.pyc


+ 1 - 0
venv/lib/python2.7/re.py

@@ -0,0 +1 @@
+/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/re.py

BIN
venv/lib/python2.7/re.pyc


+ 58 - 0
venv/lib/python2.7/site-packages/Flask-0.10.1-py2.7.egg-info/PKG-INFO

@@ -0,0 +1,58 @@
+Metadata-Version: 1.1
+Name: Flask
+Version: 0.10.1
+Summary: A microframework based on Werkzeug, Jinja2 and good intentions
+Home-page: http://github.com/mitsuhiko/flask/
+Author: Armin Ronacher
+Author-email: armin.ronacher@active-4.com
+License: BSD
+Description: 
+        Flask
+        -----
+        
+        Flask is a microframework for Python based on Werkzeug, Jinja 2 and good
+        intentions. And before you ask: It's BSD licensed!
+        
+        Flask is Fun
+        ````````````
+        
+        .. code:: python
+        
+            from flask import Flask
+            app = Flask(__name__)
+        
+            @app.route("/")
+            def hello():
+                return "Hello World!"
+        
+            if __name__ == "__main__":
+                app.run()
+        
+        And Easy to Setup
+        `````````````````
+        
+        .. code:: bash
+        
+            $ pip install Flask
+            $ python hello.py
+             * Running on http://localhost:5000/
+        
+        Links
+        `````
+        
+        * `website <http://flask.pocoo.org/>`_
+        * `documentation <http://flask.pocoo.org/docs/>`_
+        * `development version
+          <http://github.com/mitsuhiko/flask/zipball/master#egg=Flask-dev>`_
+        
+        
+Platform: any
+Classifier: Development Status :: 4 - Beta
+Classifier: Environment :: Web Environment
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
+Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
+Classifier: Topic :: Software Development :: Libraries :: Python Modules

+ 238 - 0
venv/lib/python2.7/site-packages/Flask-0.10.1-py2.7.egg-info/SOURCES.txt

@@ -0,0 +1,238 @@
+AUTHORS
+CHANGES
+LICENSE
+MANIFEST.in
+Makefile
+README
+run-tests.py
+setup.cfg
+setup.py
+Flask.egg-info/PKG-INFO
+Flask.egg-info/SOURCES.txt
+Flask.egg-info/dependency_links.txt
+Flask.egg-info/not-zip-safe
+Flask.egg-info/requires.txt
+Flask.egg-info/top_level.txt
+artwork/.DS_Store
+artwork/LICENSE
+artwork/logo-full.svg
+docs/.gitignore
+docs/Makefile
+docs/advanced_foreword.rst
+docs/api.rst
+docs/appcontext.rst
+docs/becomingbig.rst
+docs/blueprints.rst
+docs/changelog.rst
+docs/conf.py
+docs/config.rst
+docs/contents.rst.inc
+docs/design.rst
+docs/errorhandling.rst
+docs/extensiondev.rst
+docs/extensions.rst
+docs/flaskdocext.py
+docs/flaskext.py
+docs/flaskstyle.sty
+docs/foreword.rst
+docs/htmlfaq.rst
+docs/index.rst
+docs/installation.rst
+docs/latexindex.rst
+docs/license.rst
+docs/logo.pdf
+docs/make.bat
+docs/python3.rst
+docs/quickstart.rst
+docs/reqcontext.rst
+docs/security.rst
+docs/shell.rst
+docs/signals.rst
+docs/styleguide.rst
+docs/templating.rst
+docs/testing.rst
+docs/unicode.rst
+docs/upgrading.rst
+docs/views.rst
+docs/_static/debugger.png
+docs/_static/flask.png
+docs/_static/flaskr.png
+docs/_static/logo-full.png
+docs/_static/no.png
+docs/_static/touch-icon.png
+docs/_static/yes.png
+docs/_templates/sidebarintro.html
+docs/_templates/sidebarlogo.html
+docs/_themes/.git
+docs/_themes/.gitignore
+docs/_themes/LICENSE
+docs/_themes/README
+docs/_themes/flask_theme_support.py
+docs/_themes/flask/layout.html
+docs/_themes/flask/relations.html
+docs/_themes/flask/theme.conf
+docs/_themes/flask/static/flasky.css_t
+docs/_themes/flask/static/small_flask.css
+docs/_themes/flask_small/layout.html
+docs/_themes/flask_small/theme.conf
+docs/_themes/flask_small/static/flasky.css_t
+docs/deploying/cgi.rst
+docs/deploying/fastcgi.rst
+docs/deploying/index.rst
+docs/deploying/mod_wsgi.rst
+docs/deploying/uwsgi.rst
+docs/deploying/wsgi-standalone.rst
+docs/patterns/apierrors.rst
+docs/patterns/appdispatch.rst
+docs/patterns/appfactories.rst
+docs/patterns/caching.rst
+docs/patterns/celery.rst
+docs/patterns/deferredcallbacks.rst
+docs/patterns/distribute.rst
+docs/patterns/errorpages.rst
+docs/patterns/fabric.rst
+docs/patterns/favicon.rst
+docs/patterns/fileuploads.rst
+docs/patterns/flashing.rst
+docs/patterns/index.rst
+docs/patterns/jquery.rst
+docs/patterns/lazyloading.rst
+docs/patterns/methodoverrides.rst
+docs/patterns/mongokit.rst
+docs/patterns/packages.rst
+docs/patterns/requestchecksum.rst
+docs/patterns/sqlalchemy.rst
+docs/patterns/sqlite3.rst
+docs/patterns/streaming.rst
+docs/patterns/templateinheritance.rst
+docs/patterns/urlprocessors.rst
+docs/patterns/viewdecorators.rst
+docs/patterns/wtforms.rst
+docs/tutorial/css.rst
+docs/tutorial/dbcon.rst
+docs/tutorial/dbinit.rst
+docs/tutorial/folders.rst
+docs/tutorial/index.rst
+docs/tutorial/introduction.rst
+docs/tutorial/schema.rst
+docs/tutorial/setup.rst
+docs/tutorial/templates.rst
+docs/tutorial/testing.rst
+docs/tutorial/views.rst
+examples/.DS_Store
+examples/blueprintexample/blueprintexample.py
+examples/blueprintexample/blueprintexample_test.py
+examples/blueprintexample/simple_page/__init__.py
+examples/blueprintexample/simple_page/simple_page.py
+examples/blueprintexample/simple_page/templates/pages/hello.html
+examples/blueprintexample/simple_page/templates/pages/index.html
+examples/blueprintexample/simple_page/templates/pages/layout.html
+examples/blueprintexample/simple_page/templates/pages/world.html
+examples/flaskr/README
+examples/flaskr/flaskr.py
+examples/flaskr/flaskr_tests.py
+examples/flaskr/schema.sql
+examples/flaskr/static/style.css
+examples/flaskr/templates/layout.html
+examples/flaskr/templates/login.html
+examples/flaskr/templates/show_entries.html
+examples/jqueryexample/jqueryexample.py
+examples/jqueryexample/templates/index.html
+examples/jqueryexample/templates/layout.html
+examples/minitwit/README
+examples/minitwit/minitwit.py
+examples/minitwit/minitwit_tests.py
+examples/minitwit/schema.sql
+examples/minitwit/static/style.css
+examples/minitwit/templates/layout.html
+examples/minitwit/templates/login.html
+examples/minitwit/templates/register.html
+examples/minitwit/templates/timeline.html
+examples/persona/.DS_Store
+examples/persona/persona.py
+examples/persona/static/.DS_Store
+examples/persona/static/persona.js
+examples/persona/static/spinner.png
+examples/persona/static/style.css
+examples/persona/templates/index.html
+examples/persona/templates/layout.html
+flask/__init__.py
+flask/_compat.py
+flask/app.py
+flask/blueprints.py
+flask/config.py
+flask/ctx.py
+flask/debughelpers.py
+flask/exthook.py
+flask/globals.py
+flask/helpers.py
+flask/json.py
+flask/logging.py
+flask/module.py
+flask/sessions.py
+flask/signals.py
+flask/templating.py
+flask/testing.py
+flask/views.py
+flask/wrappers.py
+flask/ext/__init__.py
+flask/testsuite/__init__.py
+flask/testsuite/appctx.py
+flask/testsuite/basic.py
+flask/testsuite/blueprints.py
+flask/testsuite/config.py
+flask/testsuite/deprecations.py
+flask/testsuite/examples.py
+flask/testsuite/ext.py
+flask/testsuite/helpers.py
+flask/testsuite/regression.py
+flask/testsuite/reqctx.py
+flask/testsuite/signals.py
+flask/testsuite/subclassing.py
+flask/testsuite/templating.py
+flask/testsuite/testing.py
+flask/testsuite/views.py
+flask/testsuite/static/index.html
+flask/testsuite/templates/_macro.html
+flask/testsuite/templates/context_template.html
+flask/testsuite/templates/escaping_template.html
+flask/testsuite/templates/mail.txt
+flask/testsuite/templates/simple_template.html
+flask/testsuite/templates/template_filter.html
+flask/testsuite/templates/template_test.html
+flask/testsuite/templates/nested/nested.txt
+flask/testsuite/test_apps/config_module_app.py
+flask/testsuite/test_apps/flask_newext_simple.py
+flask/testsuite/test_apps/importerror.py
+flask/testsuite/test_apps/main_app.py
+flask/testsuite/test_apps/blueprintapp/__init__.py
+flask/testsuite/test_apps/blueprintapp/apps/__init__.py
+flask/testsuite/test_apps/blueprintapp/apps/admin/__init__.py
+flask/testsuite/test_apps/blueprintapp/apps/admin/static/test.txt
+flask/testsuite/test_apps/blueprintapp/apps/admin/static/css/test.css
+flask/testsuite/test_apps/blueprintapp/apps/admin/templates/admin/index.html
+flask/testsuite/test_apps/blueprintapp/apps/frontend/__init__.py
+flask/testsuite/test_apps/blueprintapp/apps/frontend/templates/frontend/index.html
+flask/testsuite/test_apps/config_package_app/__init__.py
+flask/testsuite/test_apps/flask_broken/__init__.py
+flask/testsuite/test_apps/flask_broken/b.py
+flask/testsuite/test_apps/flask_newext_package/__init__.py
+flask/testsuite/test_apps/flask_newext_package/submodule.py
+flask/testsuite/test_apps/flaskext/__init__.py
+flask/testsuite/test_apps/flaskext/oldext_simple.py
+flask/testsuite/test_apps/flaskext/oldext_package/__init__.py
+flask/testsuite/test_apps/flaskext/oldext_package/submodule.py
+flask/testsuite/test_apps/lib/python2.5/site-packages/SiteEgg.egg
+flask/testsuite/test_apps/lib/python2.5/site-packages/site_app.py
+flask/testsuite/test_apps/lib/python2.5/site-packages/site_package/__init__.py
+flask/testsuite/test_apps/moduleapp/__init__.py
+flask/testsuite/test_apps/moduleapp/apps/__init__.py
+flask/testsuite/test_apps/moduleapp/apps/admin/__init__.py
+flask/testsuite/test_apps/moduleapp/apps/admin/static/test.txt
+flask/testsuite/test_apps/moduleapp/apps/admin/static/css/test.css
+flask/testsuite/test_apps/moduleapp/apps/admin/templates/index.html
+flask/testsuite/test_apps/moduleapp/apps/frontend/__init__.py
+flask/testsuite/test_apps/moduleapp/apps/frontend/templates/index.html
+flask/testsuite/test_apps/path/installed_package/__init__.py
+flask/testsuite/test_apps/subdomaintestmodule/__init__.py
+flask/testsuite/test_apps/subdomaintestmodule/static/hello.txt

+ 1 - 0
venv/lib/python2.7/site-packages/Flask-0.10.1-py2.7.egg-info/dependency_links.txt

@@ -0,0 +1 @@
+

+ 148 - 0
venv/lib/python2.7/site-packages/Flask-0.10.1-py2.7.egg-info/installed-files.txt

@@ -0,0 +1,148 @@
+../flask/__init__.py
+../flask/_compat.py
+../flask/app.py
+../flask/blueprints.py
+../flask/config.py
+../flask/ctx.py
+../flask/debughelpers.py
+../flask/exthook.py
+../flask/globals.py
+../flask/helpers.py
+../flask/json.py
+../flask/logging.py
+../flask/module.py
+../flask/sessions.py
+../flask/signals.py
+../flask/templating.py
+../flask/testing.py
+../flask/views.py
+../flask/wrappers.py
+../flask/ext/__init__.py
+../flask/testsuite/__init__.py
+../flask/testsuite/appctx.py
+../flask/testsuite/basic.py
+../flask/testsuite/blueprints.py
+../flask/testsuite/config.py
+../flask/testsuite/deprecations.py
+../flask/testsuite/examples.py
+../flask/testsuite/ext.py
+../flask/testsuite/helpers.py
+../flask/testsuite/regression.py
+../flask/testsuite/reqctx.py
+../flask/testsuite/signals.py
+../flask/testsuite/subclassing.py
+../flask/testsuite/templating.py
+../flask/testsuite/testing.py
+../flask/testsuite/views.py
+../flask/testsuite/static/index.html
+../flask/testsuite/templates/_macro.html
+../flask/testsuite/templates/context_template.html
+../flask/testsuite/templates/escaping_template.html
+../flask/testsuite/templates/mail.txt
+../flask/testsuite/templates/simple_template.html
+../flask/testsuite/templates/template_filter.html
+../flask/testsuite/templates/template_test.html
+../flask/testsuite/templates/nested/nested.txt
+../flask/testsuite/test_apps/config_module_app.py
+../flask/testsuite/test_apps/flask_newext_simple.py
+../flask/testsuite/test_apps/importerror.py
+../flask/testsuite/test_apps/main_app.py
+../flask/testsuite/test_apps/blueprintapp/__init__.py
+../flask/testsuite/test_apps/blueprintapp/apps/__init__.py
+../flask/testsuite/test_apps/blueprintapp/apps/admin/__init__.py
+../flask/testsuite/test_apps/blueprintapp/apps/admin/static/test.txt
+../flask/testsuite/test_apps/blueprintapp/apps/admin/static/css/test.css
+../flask/testsuite/test_apps/blueprintapp/apps/admin/templates/admin/index.html
+../flask/testsuite/test_apps/blueprintapp/apps/frontend/__init__.py
+../flask/testsuite/test_apps/blueprintapp/apps/frontend/templates/frontend/index.html
+../flask/testsuite/test_apps/config_package_app/__init__.py
+../flask/testsuite/test_apps/flask_broken/__init__.py
+../flask/testsuite/test_apps/flask_broken/b.py
+../flask/testsuite/test_apps/flask_newext_package/__init__.py
+../flask/testsuite/test_apps/flask_newext_package/submodule.py
+../flask/testsuite/test_apps/flaskext/__init__.py
+../flask/testsuite/test_apps/flaskext/oldext_simple.py
+../flask/testsuite/test_apps/flaskext/oldext_package/__init__.py
+../flask/testsuite/test_apps/flaskext/oldext_package/submodule.py
+../flask/testsuite/test_apps/lib/python2.5/site-packages/SiteEgg.egg
+../flask/testsuite/test_apps/lib/python2.5/site-packages/site_app.py
+../flask/testsuite/test_apps/lib/python2.5/site-packages/site_package/__init__.py
+../flask/testsuite/test_apps/moduleapp/__init__.py
+../flask/testsuite/test_apps/moduleapp/apps/__init__.py
+../flask/testsuite/test_apps/moduleapp/apps/admin/__init__.py
+../flask/testsuite/test_apps/moduleapp/apps/admin/static/test.txt
+../flask/testsuite/test_apps/moduleapp/apps/admin/static/css/test.css
+../flask/testsuite/test_apps/moduleapp/apps/admin/templates/index.html
+../flask/testsuite/test_apps/moduleapp/apps/frontend/__init__.py
+../flask/testsuite/test_apps/moduleapp/apps/frontend/templates/index.html
+../flask/testsuite/test_apps/path/installed_package/__init__.py
+../flask/testsuite/test_apps/subdomaintestmodule/__init__.py
+../flask/testsuite/test_apps/subdomaintestmodule/static/hello.txt
+../flask/__init__.pyc
+../flask/_compat.pyc
+../flask/app.pyc
+../flask/blueprints.pyc
+../flask/config.pyc
+../flask/ctx.pyc
+../flask/debughelpers.pyc
+../flask/exthook.pyc
+../flask/globals.pyc
+../flask/helpers.pyc
+../flask/json.pyc
+../flask/logging.pyc
+../flask/module.pyc
+../flask/sessions.pyc
+../flask/signals.pyc
+../flask/templating.pyc
+../flask/testing.pyc
+../flask/views.pyc
+../flask/wrappers.pyc
+../flask/ext/__init__.pyc
+../flask/testsuite/__init__.pyc
+../flask/testsuite/appctx.pyc
+../flask/testsuite/basic.pyc
+../flask/testsuite/blueprints.pyc
+../flask/testsuite/config.pyc
+../flask/testsuite/deprecations.pyc
+../flask/testsuite/examples.pyc
+../flask/testsuite/ext.pyc
+../flask/testsuite/helpers.pyc
+../flask/testsuite/regression.pyc
+../flask/testsuite/reqctx.pyc
+../flask/testsuite/signals.pyc
+../flask/testsuite/subclassing.pyc
+../flask/testsuite/templating.pyc
+../flask/testsuite/testing.pyc
+../flask/testsuite/views.pyc
+../flask/testsuite/test_apps/config_module_app.pyc
+../flask/testsuite/test_apps/flask_newext_simple.pyc
+../flask/testsuite/test_apps/importerror.pyc
+../flask/testsuite/test_apps/main_app.pyc
+../flask/testsuite/test_apps/blueprintapp/__init__.pyc
+../flask/testsuite/test_apps/blueprintapp/apps/__init__.pyc
+../flask/testsuite/test_apps/blueprintapp/apps/admin/__init__.pyc
+../flask/testsuite/test_apps/blueprintapp/apps/frontend/__init__.pyc
+../flask/testsuite/test_apps/config_package_app/__init__.pyc
+../flask/testsuite/test_apps/flask_broken/__init__.pyc
+../flask/testsuite/test_apps/flask_broken/b.pyc
+../flask/testsuite/test_apps/flask_newext_package/__init__.pyc
+../flask/testsuite/test_apps/flask_newext_package/submodule.pyc
+../flask/testsuite/test_apps/flaskext/__init__.pyc
+../flask/testsuite/test_apps/flaskext/oldext_simple.pyc
+../flask/testsuite/test_apps/flaskext/oldext_package/__init__.pyc
+../flask/testsuite/test_apps/flaskext/oldext_package/submodule.pyc
+../flask/testsuite/test_apps/lib/python2.5/site-packages/site_app.pyc
+../flask/testsuite/test_apps/lib/python2.5/site-packages/site_package/__init__.pyc
+../flask/testsuite/test_apps/moduleapp/__init__.pyc
+../flask/testsuite/test_apps/moduleapp/apps/__init__.pyc
+../flask/testsuite/test_apps/moduleapp/apps/admin/__init__.pyc
+../flask/testsuite/test_apps/moduleapp/apps/frontend/__init__.pyc
+../flask/testsuite/test_apps/path/installed_package/__init__.pyc
+../flask/testsuite/test_apps/subdomaintestmodule/__init__.pyc
+./
+dependency_links.txt
+not-zip-safe
+PKG-INFO
+requires.txt
+SOURCES.txt
+top_level.txt

+ 1 - 0
venv/lib/python2.7/site-packages/Flask-0.10.1-py2.7.egg-info/not-zip-safe

@@ -0,0 +1 @@
+

+ 3 - 0
venv/lib/python2.7/site-packages/Flask-0.10.1-py2.7.egg-info/requires.txt

@@ -0,0 +1,3 @@
+Werkzeug>=0.7
+Jinja2>=2.4
+itsdangerous>=0.21

+ 1 - 0
venv/lib/python2.7/site-packages/Flask-0.10.1-py2.7.egg-info/top_level.txt

@@ -0,0 +1 @@
+flask

+ 55 - 0
venv/lib/python2.7/site-packages/Jinja2-2.7.3-py2.7.egg-info/PKG-INFO

@@ -0,0 +1,55 @@
+Metadata-Version: 1.1
+Name: Jinja2
+Version: 2.7.3
+Summary: A small but fast and easy to use stand-alone template engine written in pure python.
+Home-page: http://jinja.pocoo.org/
+Author: Armin Ronacher
+Author-email: armin.ronacher@active-4.com
+License: BSD
+Description: 
+        Jinja2
+        ~~~~~~
+        
+        Jinja2 is a template engine written in pure Python.  It provides a
+        `Django`_ inspired non-XML syntax but supports inline expressions and
+        an optional `sandboxed`_ environment.
+        
+        Nutshell
+        --------
+        
+        Here a small example of a Jinja template::
+        
+            {% extends 'base.html' %}
+            {% block title %}Memberlist{% endblock %}
+            {% block content %}
+              <ul>
+              {% for user in users %}
+                <li><a href="{{ user.url }}">{{ user.username }}</a></li>
+              {% endfor %}
+              </ul>
+            {% endblock %}
+        
+        Philosophy
+        ----------
+        
+        Application logic is for the controller but don't try to make the life
+        for the template designer too hard by giving him too few functionality.
+        
+        For more informations visit the new `Jinja2 webpage`_ and `documentation`_.
+        
+        .. _sandboxed: http://en.wikipedia.org/wiki/Sandbox_(computer_security)
+        .. _Django: http://www.djangoproject.com/
+        .. _Jinja2 webpage: http://jinja.pocoo.org/
+        .. _documentation: http://jinja.pocoo.org/2/documentation/
+        
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Environment :: Web Environment
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
+Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Classifier: Topic :: Text Processing :: Markup :: HTML

+ 126 - 0
venv/lib/python2.7/site-packages/Jinja2-2.7.3-py2.7.egg-info/SOURCES.txt

@@ -0,0 +1,126 @@
+AUTHORS
+CHANGES
+LICENSE
+MANIFEST.in
+Makefile
+README.rst
+run-tests.py
+setup.cfg
+setup.py
+Jinja2.egg-info/PKG-INFO
+Jinja2.egg-info/SOURCES.txt
+Jinja2.egg-info/dependency_links.txt
+Jinja2.egg-info/entry_points.txt
+Jinja2.egg-info/not-zip-safe
+Jinja2.egg-info/requires.txt
+Jinja2.egg-info/top_level.txt
+artwork/jinjalogo.svg
+docs/Makefile
+docs/api.rst
+docs/cache_extension.py
+docs/changelog.rst
+docs/conf.py
+docs/contents.rst.inc
+docs/extensions.rst
+docs/faq.rst
+docs/index.rst
+docs/integration.rst
+docs/intro.rst
+docs/jinjaext.py
+docs/jinjastyle.sty
+docs/latexindex.rst
+docs/logo.pdf
+docs/sandbox.rst
+docs/switching.rst
+docs/templates.rst
+docs/tricks.rst
+docs/_static/.ignore
+docs/_static/jinja-small.png
+docs/_templates/sidebarintro.html
+docs/_templates/sidebarlogo.html
+docs/_themes/LICENSE
+docs/_themes/README
+docs/_themes/jinja/layout.html
+docs/_themes/jinja/relations.html
+docs/_themes/jinja/theme.conf
+docs/_themes/jinja/static/jinja.css_t
+examples/bench.py
+examples/profile.py
+examples/basic/cycle.py
+examples/basic/debugger.py
+examples/basic/inheritance.py
+examples/basic/test.py
+examples/basic/test_filter_and_linestatements.py
+examples/basic/test_loop_filter.py
+examples/basic/translate.py
+examples/basic/templates/broken.html
+examples/basic/templates/subbroken.html
+examples/rwbench/djangoext.py
+examples/rwbench/rwbench.py
+examples/rwbench/django/_form.html
+examples/rwbench/django/_input_field.html
+examples/rwbench/django/_textarea.html
+examples/rwbench/django/index.html
+examples/rwbench/django/layout.html
+examples/rwbench/genshi/helpers.html
+examples/rwbench/genshi/index.html
+examples/rwbench/genshi/layout.html
+examples/rwbench/jinja/helpers.html
+examples/rwbench/jinja/index.html
+examples/rwbench/jinja/layout.html
+examples/rwbench/mako/helpers.html
+examples/rwbench/mako/index.html
+examples/rwbench/mako/layout.html
+ext/djangojinja2.py
+ext/inlinegettext.py
+ext/jinja.el
+ext/Vim/jinja.vim
+ext/django2jinja/django2jinja.py
+ext/django2jinja/example.py
+ext/django2jinja/templates/index.html
+ext/django2jinja/templates/layout.html
+ext/django2jinja/templates/subtemplate.html
+jinja2/__init__.py
+jinja2/_compat.py
+jinja2/_stringdefs.py
+jinja2/bccache.py
+jinja2/compiler.py
+jinja2/constants.py
+jinja2/debug.py
+jinja2/defaults.py
+jinja2/environment.py
+jinja2/exceptions.py
+jinja2/ext.py
+jinja2/filters.py
+jinja2/lexer.py
+jinja2/loaders.py
+jinja2/meta.py
+jinja2/nodes.py
+jinja2/optimizer.py
+jinja2/parser.py
+jinja2/runtime.py
+jinja2/sandbox.py
+jinja2/tests.py
+jinja2/utils.py
+jinja2/visitor.py
+jinja2/testsuite/__init__.py
+jinja2/testsuite/api.py
+jinja2/testsuite/bytecode_cache.py
+jinja2/testsuite/core_tags.py
+jinja2/testsuite/debug.py
+jinja2/testsuite/doctests.py
+jinja2/testsuite/ext.py
+jinja2/testsuite/filters.py
+jinja2/testsuite/imports.py
+jinja2/testsuite/inheritance.py
+jinja2/testsuite/lexnparse.py
+jinja2/testsuite/loader.py
+jinja2/testsuite/regression.py
+jinja2/testsuite/security.py
+jinja2/testsuite/tests.py
+jinja2/testsuite/utils.py
+jinja2/testsuite/res/__init__.py
+jinja2/testsuite/res/templates/broken.html
+jinja2/testsuite/res/templates/syntaxerror.html
+jinja2/testsuite/res/templates/test.html
+jinja2/testsuite/res/templates/foo/test.html

+ 1 - 0
venv/lib/python2.7/site-packages/Jinja2-2.7.3-py2.7.egg-info/dependency_links.txt

@@ -0,0 +1 @@
+

+ 4 - 0
venv/lib/python2.7/site-packages/Jinja2-2.7.3-py2.7.egg-info/entry_points.txt

@@ -0,0 +1,4 @@
+
+    [babel.extractors]
+    jinja2 = jinja2.ext:babel_extract[i18n]
+    

+ 92 - 0
venv/lib/python2.7/site-packages/Jinja2-2.7.3-py2.7.egg-info/installed-files.txt

@@ -0,0 +1,92 @@
+../jinja2/__init__.py
+../jinja2/_compat.py
+../jinja2/_stringdefs.py
+../jinja2/bccache.py
+../jinja2/compiler.py
+../jinja2/constants.py
+../jinja2/debug.py
+../jinja2/defaults.py
+../jinja2/environment.py
+../jinja2/exceptions.py
+../jinja2/ext.py
+../jinja2/filters.py
+../jinja2/lexer.py
+../jinja2/loaders.py
+../jinja2/meta.py
+../jinja2/nodes.py
+../jinja2/optimizer.py
+../jinja2/parser.py
+../jinja2/runtime.py
+../jinja2/sandbox.py
+../jinja2/tests.py
+../jinja2/utils.py
+../jinja2/visitor.py
+../jinja2/testsuite/__init__.py
+../jinja2/testsuite/api.py
+../jinja2/testsuite/bytecode_cache.py
+../jinja2/testsuite/core_tags.py
+../jinja2/testsuite/debug.py
+../jinja2/testsuite/doctests.py
+../jinja2/testsuite/ext.py
+../jinja2/testsuite/filters.py
+../jinja2/testsuite/imports.py
+../jinja2/testsuite/inheritance.py
+../jinja2/testsuite/lexnparse.py
+../jinja2/testsuite/loader.py
+../jinja2/testsuite/regression.py
+../jinja2/testsuite/security.py
+../jinja2/testsuite/tests.py
+../jinja2/testsuite/utils.py
+../jinja2/testsuite/res/__init__.py
+../jinja2/testsuite/res/templates/broken.html
+../jinja2/testsuite/res/templates/syntaxerror.html
+../jinja2/testsuite/res/templates/test.html
+../jinja2/testsuite/res/templates/foo/test.html
+../jinja2/__init__.pyc
+../jinja2/_compat.pyc
+../jinja2/_stringdefs.pyc
+../jinja2/bccache.pyc
+../jinja2/compiler.pyc
+../jinja2/constants.pyc
+../jinja2/debug.pyc
+../jinja2/defaults.pyc
+../jinja2/environment.pyc
+../jinja2/exceptions.pyc
+../jinja2/ext.pyc
+../jinja2/filters.pyc
+../jinja2/lexer.pyc
+../jinja2/loaders.pyc
+../jinja2/meta.pyc
+../jinja2/nodes.pyc
+../jinja2/optimizer.pyc
+../jinja2/parser.pyc
+../jinja2/runtime.pyc
+../jinja2/sandbox.pyc
+../jinja2/tests.pyc
+../jinja2/utils.pyc
+../jinja2/visitor.pyc
+../jinja2/testsuite/__init__.pyc
+../jinja2/testsuite/api.pyc
+../jinja2/testsuite/bytecode_cache.pyc
+../jinja2/testsuite/core_tags.pyc
+../jinja2/testsuite/debug.pyc
+../jinja2/testsuite/doctests.pyc
+../jinja2/testsuite/ext.pyc
+../jinja2/testsuite/filters.pyc
+../jinja2/testsuite/imports.pyc
+../jinja2/testsuite/inheritance.pyc
+../jinja2/testsuite/lexnparse.pyc
+../jinja2/testsuite/loader.pyc
+../jinja2/testsuite/regression.pyc
+../jinja2/testsuite/security.pyc
+../jinja2/testsuite/tests.pyc
+../jinja2/testsuite/utils.pyc
+../jinja2/testsuite/res/__init__.pyc
+./
+dependency_links.txt
+entry_points.txt
+not-zip-safe
+PKG-INFO
+requires.txt
+SOURCES.txt
+top_level.txt

+ 1 - 0
venv/lib/python2.7/site-packages/Jinja2-2.7.3-py2.7.egg-info/not-zip-safe

@@ -0,0 +1 @@
+

+ 4 - 0
venv/lib/python2.7/site-packages/Jinja2-2.7.3-py2.7.egg-info/requires.txt

@@ -0,0 +1,4 @@
+markupsafe
+
+[i18n]
+Babel>=0.8

+ 1 - 0
venv/lib/python2.7/site-packages/Jinja2-2.7.3-py2.7.egg-info/top_level.txt

@@ -0,0 +1 @@
+jinja2

+ 119 - 0
venv/lib/python2.7/site-packages/MarkupSafe-0.23-py2.7.egg-info/PKG-INFO

@@ -0,0 +1,119 @@
+Metadata-Version: 1.1
+Name: MarkupSafe
+Version: 0.23
+Summary: Implements a XML/HTML/XHTML Markup safe string for Python
+Home-page: http://github.com/mitsuhiko/markupsafe
+Author: Armin Ronacher
+Author-email: armin.ronacher@active-4.com
+License: BSD
+Description: MarkupSafe
+        ==========
+        
+        Implements a unicode subclass that supports HTML strings:
+        
+        >>> from markupsafe import Markup, escape
+        >>> escape("<script>alert(document.cookie);</script>")
+        Markup(u'&lt;script&gt;alert(document.cookie);&lt;/script&gt;')
+        >>> tmpl = Markup("<em>%s</em>")
+        >>> tmpl % "Peter > Lustig"
+        Markup(u'<em>Peter &gt; Lustig</em>')
+        
+        If you want to make an object unicode that is not yet unicode
+        but don't want to lose the taint information, you can use the
+        `soft_unicode` function.  (On Python 3 you can also use `soft_str` which
+        is a different name for the same function).
+        
+        >>> from markupsafe import soft_unicode
+        >>> soft_unicode(42)
+        u'42'
+        >>> soft_unicode(Markup('foo'))
+        Markup(u'foo')
+        
+        HTML Representations
+        --------------------
+        
+        Objects can customize their HTML markup equivalent by overriding
+        the `__html__` function:
+        
+        >>> class Foo(object):
+        ...  def __html__(self):
+        ...   return '<strong>Nice</strong>'
+        ...
+        >>> escape(Foo())
+        Markup(u'<strong>Nice</strong>')
+        >>> Markup(Foo())
+        Markup(u'<strong>Nice</strong>')
+        
+        Silent Escapes
+        --------------
+        
+        Since MarkupSafe 0.10 there is now also a separate escape function
+        called `escape_silent` that returns an empty string for `None` for
+        consistency with other systems that return empty strings for `None`
+        when escaping (for instance Pylons' webhelpers).
+        
+        If you also want to use this for the escape method of the Markup
+        object, you can create your own subclass that does that::
+        
+            from markupsafe import Markup, escape_silent as escape
+        
+            class SilentMarkup(Markup):
+                __slots__ = ()
+        
+                @classmethod
+                def escape(cls, s):
+                    return cls(escape(s))
+        
+        New-Style String Formatting
+        ---------------------------
+        
+        Starting with MarkupSafe 0.21 new style string formats from Python 2.6 and
+        3.x are now fully supported.  Previously the escape behavior of those
+        functions was spotty at best.  The new implementations operates under the
+        following algorithm:
+        
+        1.  if an object has an ``__html_format__`` method it is called as
+            replacement for ``__format__`` with the format specifier.  It either
+            has to return a string or markup object.
+        2.  if an object has an ``__html__`` method it is called.
+        3.  otherwise the default format system of Python kicks in and the result
+            is HTML escaped.
+        
+        Here is how you can implement your own formatting::
+        
+            class User(object):
+        
+                def __init__(self, id, username):
+                    self.id = id
+                    self.username = username
+        
+                def __html_format__(self, format_spec):
+                    if format_spec == 'link':
+                        return Markup('<a href="/user/{0}">{1}</a>').format(
+                            self.id,
+                            self.__html__(),
+                        )
+                    elif format_spec:
+                        raise ValueError('Invalid format spec')
+                    return self.__html__()
+        
+                def __html__(self):
+                    return Markup('<span class=user>{0}</span>').format(self.username)
+        
+        And to format that user:
+        
+        >>> user = User(1, 'foo')
+        >>> Markup('<p>User: {0:link}').format(user)
+        Markup(u'<p>User: <a href="/user/1"><span class=user>foo</span></a>')
+        
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Environment :: Web Environment
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
+Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Classifier: Topic :: Text Processing :: Markup :: HTML

+ 17 - 0
venv/lib/python2.7/site-packages/MarkupSafe-0.23-py2.7.egg-info/SOURCES.txt

@@ -0,0 +1,17 @@
+AUTHORS
+LICENSE
+MANIFEST.in
+README.rst
+setup.cfg
+setup.py
+MarkupSafe.egg-info/PKG-INFO
+MarkupSafe.egg-info/SOURCES.txt
+MarkupSafe.egg-info/dependency_links.txt
+MarkupSafe.egg-info/not-zip-safe
+MarkupSafe.egg-info/top_level.txt
+markupsafe/__init__.py
+markupsafe/_compat.py
+markupsafe/_constants.py
+markupsafe/_native.py
+markupsafe/_speedups.c
+markupsafe/tests.py

+ 1 - 0
venv/lib/python2.7/site-packages/MarkupSafe-0.23-py2.7.egg-info/dependency_links.txt

@@ -0,0 +1 @@
+

+ 18 - 0
venv/lib/python2.7/site-packages/MarkupSafe-0.23-py2.7.egg-info/installed-files.txt

@@ -0,0 +1,18 @@
+../markupsafe/__init__.py
+../markupsafe/_compat.py
+../markupsafe/_constants.py
+../markupsafe/_native.py
+../markupsafe/tests.py
+../markupsafe/_speedups.c
+../markupsafe/__init__.pyc
+../markupsafe/_compat.pyc
+../markupsafe/_constants.pyc
+../markupsafe/_native.pyc
+../markupsafe/tests.pyc
+../markupsafe/_speedups.so
+./
+dependency_links.txt
+not-zip-safe
+PKG-INFO
+SOURCES.txt
+top_level.txt

+ 1 - 0
venv/lib/python2.7/site-packages/MarkupSafe-0.23-py2.7.egg-info/not-zip-safe

@@ -0,0 +1 @@
+

+ 1 - 0
venv/lib/python2.7/site-packages/MarkupSafe-0.23-py2.7.egg-info/top_level.txt

@@ -0,0 +1 @@
+markupsafe

+ 54 - 0
venv/lib/python2.7/site-packages/Werkzeug-0.10.4.dist-info/DESCRIPTION.rst

@@ -0,0 +1,54 @@
+Werkzeug
+========
+
+Werkzeug started as simple collection of various utilities for WSGI
+applications and has become one of the most advanced WSGI utility
+modules.  It includes a powerful debugger, full featured request and
+response objects, HTTP utilities to handle entity tags, cache control
+headers, HTTP dates, cookie handling, file uploads, a powerful URL
+routing system and a bunch of community contributed addon modules.
+
+Werkzeug is unicode aware and doesn't enforce a specific template
+engine, database adapter or anything else.  It doesn't even enforce
+a specific way of handling requests and leaves all that up to the
+developer. It's most useful for end user applications which should work
+on as many server environments as possible (such as blogs, wikis,
+bulletin boards, etc.).
+
+Details and example applications are available on the
+`Werkzeug website <http://werkzeug.pocoo.org/>`_.
+
+
+Features
+--------
+
+-   unicode awareness
+
+-   request and response objects
+
+-   various utility functions for dealing with HTTP headers such as
+    `Accept` and `Cache-Control` headers.
+
+-   thread local objects with proper cleanup at request end
+
+-   an interactive debugger
+
+-   A simple WSGI server with support for threading and forking
+    with an automatic reloader.
+
+-   a flexible URL routing system with REST support.
+
+-   fully WSGI compatible
+
+
+Development Version
+-------------------
+
+The Werkzeug development version can be installed by cloning the git
+repository from `github`_::
+
+    git clone git@github.com:mitsuhiko/werkzeug.git
+
+.. _github: http://github.com/mitsuhiko/werkzeug
+
+

+ 73 - 0
venv/lib/python2.7/site-packages/Werkzeug-0.10.4.dist-info/METADATA

@@ -0,0 +1,73 @@
+Metadata-Version: 2.0
+Name: Werkzeug
+Version: 0.10.4
+Summary: The Swiss Army knife of Python web development
+Home-page: http://werkzeug.pocoo.org/
+Author: Armin Ronacher
+Author-email: armin.ronacher@active-4.com
+License: BSD
+Platform: any
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Environment :: Web Environment
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
+Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+
+Werkzeug
+========
+
+Werkzeug started as simple collection of various utilities for WSGI
+applications and has become one of the most advanced WSGI utility
+modules.  It includes a powerful debugger, full featured request and
+response objects, HTTP utilities to handle entity tags, cache control
+headers, HTTP dates, cookie handling, file uploads, a powerful URL
+routing system and a bunch of community contributed addon modules.
+
+Werkzeug is unicode aware and doesn't enforce a specific template
+engine, database adapter or anything else.  It doesn't even enforce
+a specific way of handling requests and leaves all that up to the
+developer. It's most useful for end user applications which should work
+on as many server environments as possible (such as blogs, wikis,
+bulletin boards, etc.).
+
+Details and example applications are available on the
+`Werkzeug website <http://werkzeug.pocoo.org/>`_.
+
+
+Features
+--------
+
+-   unicode awareness
+
+-   request and response objects
+
+-   various utility functions for dealing with HTTP headers such as
+    `Accept` and `Cache-Control` headers.
+
+-   thread local objects with proper cleanup at request end
+
+-   an interactive debugger
+
+-   A simple WSGI server with support for threading and forking
+    with an automatic reloader.
+
+-   a flexible URL routing system with REST support.
+
+-   fully WSGI compatible
+
+
+Development Version
+-------------------
+
+The Werkzeug development version can be installed by cloning the git
+repository from `github`_::
+
+    git clone git@github.com:mitsuhiko/werkzeug.git
+
+.. _github: http://github.com/mitsuhiko/werkzeug
+
+

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.