From 186049b3ed33f025eeb87eb34c19a28e1d5ba70a Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 2 Oct 2012 01:05:51 +0200 Subject: restructure code, switch from GTK to Web based UI - move barcode generation scripts into generation/ - move code to src/ - remove database analysis from invoice/graph - put database creation sql files into sql/ - remove glade builder file - add new templates/ directory, which contains files used by the Web-UI --- .gitignore | 5 +- Makefile | 22 +- barcodelist.rb | 58 - create_db.sql | 59 - db.vala | 299 - device.vala | 280 - generation/barcodelist.rb | 58 + generation/ktt.pdf | Bin 0 -> 199241 bytes generation/passlist.rb | 71 + insert-prices.sql | 183 - invoice/generate-invoice.py | 6 +- invoice/graph/cairoplot.py | 2336 ------- invoice/graph/graphs.py | 223 - invoice/graph/series.py | 1140 ---- ktt.pdf | Bin 199241 -> 0 bytes main.vala | 125 - passlist.rb | 71 - sql/tables.sql | 8 + sql/trigger.sql | 27 + sql/views.sql | 22 + src/db.vala | 740 ++ src/device.vala | 280 + src/graph-data.vala | 185 + src/main.vala | 113 + src/price.vapi | 9 + src/session.vala | 141 + src/template.vala | 103 + src/web.vala | 592 ++ templates/base.html | 25 + templates/css/base.css | 65 + templates/css/bootstrap.css | 5774 ++++++++++++++++ templates/errors/403.html | 1 + templates/errors/todo.html | 1 + templates/img/glyphicons-halflings-white.png | Bin 0 -> 8777 bytes templates/img/glyphicons-halflings.png | Bin 0 -> 12799 bytes templates/index.html | 1 + templates/js/bootstrap.js | 2027 ++++++ templates/js/jquery.flot.js | 2599 +++++++ templates/js/jquery.flot.navigate.js | 321 + templates/js/jquery.flot.pie.js | 750 ++ templates/js/jquery.flot.selection.js | 344 + templates/js/jquery.js | 9440 ++++++++++++++++++++++++++ templates/js/jquery.mousewheel.js | 84 + templates/js/sorttable.js | 40 + templates/logout.html | 1 + templates/menu.html | 20 + templates/menu_login.html | 5 + templates/menu_logout.html | 12 + templates/products/entry.html | 28 + templates/products/index.html | 19 + templates/products/new.html | 22 + templates/products/restock.html | 15 + templates/stats/index.html | 19 + templates/stats/profit_per_day.html | 80 + templates/stats/profit_per_product.html | 42 + templates/stats/profit_per_weekday.html | 30 + templates/stats/stock.html | 83 + templates/users/entry.html | 17 + templates/users/invoice.html | 39 + ui.vala | 196 - user-interface.ui | 774 --- web.vala | 75 - 62 files changed, 24272 insertions(+), 5833 deletions(-) delete mode 100644 barcodelist.rb delete mode 100644 create_db.sql delete mode 100644 db.vala delete mode 100644 device.vala create mode 100644 generation/barcodelist.rb create mode 100644 generation/ktt.pdf create mode 100644 generation/passlist.rb delete mode 100644 insert-prices.sql delete mode 100755 invoice/graph/cairoplot.py delete mode 100755 invoice/graph/graphs.py delete mode 100755 invoice/graph/series.py delete mode 100644 ktt.pdf delete mode 100644 main.vala delete mode 100644 passlist.rb create mode 100644 sql/tables.sql create mode 100644 sql/trigger.sql create mode 100644 sql/views.sql create mode 100644 src/db.vala create mode 100644 src/device.vala create mode 100644 src/graph-data.vala create mode 100644 src/main.vala create mode 100644 src/price.vapi create mode 100644 src/session.vala create mode 100644 src/template.vala create mode 100644 src/web.vala create mode 100644 templates/base.html create mode 100644 templates/css/base.css create mode 100644 templates/css/bootstrap.css create mode 100644 templates/errors/403.html create mode 100644 templates/errors/todo.html create mode 100644 templates/img/glyphicons-halflings-white.png create mode 100644 templates/img/glyphicons-halflings.png create mode 100644 templates/index.html create mode 100644 templates/js/bootstrap.js create mode 100644 templates/js/jquery.flot.js create mode 100644 templates/js/jquery.flot.navigate.js create mode 100644 templates/js/jquery.flot.pie.js create mode 100644 templates/js/jquery.flot.selection.js create mode 100644 templates/js/jquery.js create mode 100644 templates/js/jquery.mousewheel.js create mode 100644 templates/js/sorttable.js create mode 100644 templates/logout.html create mode 100644 templates/menu.html create mode 100644 templates/menu_login.html create mode 100644 templates/menu_logout.html create mode 100644 templates/products/entry.html create mode 100644 templates/products/index.html create mode 100644 templates/products/new.html create mode 100644 templates/products/restock.html create mode 100644 templates/stats/index.html create mode 100644 templates/stats/profit_per_day.html create mode 100644 templates/stats/profit_per_product.html create mode 100644 templates/stats/profit_per_weekday.html create mode 100644 templates/stats/stock.html create mode 100644 templates/users/entry.html create mode 100644 templates/users/invoice.html delete mode 100644 ui.vala delete mode 100644 user-interface.ui delete mode 100644 web.vala diff --git a/.gitignore b/.gitignore index c9f4e47..688ccdb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ barcode-scanner *.swp -*.pyc -invoice/graph/*.svg +shop.db +invoice/*.pyc +invoice/__pycache__ diff --git a/Makefile b/Makefile index 2c7d106..81b7680 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,20 @@ -PREFIX=/usr/local +SRC=src/main.vala src/device.vala src/db.vala src/web.vala src/graph-data.vala src/template.vala src/session.vala src/price.vapi +DEPS=--pkg posix --pkg linux --pkg libsoup-2.4 --pkg sqlite3 --pkg gee-1.0 --pkg gmodule-2.0 --pkg gio-2.0 +FLAGS=-X -w -# web.vala (currently disabled) -barcode-scanner: main.vala device.vala db.vala ui.vala - valac-0.16 --output $@ --pkg posix --pkg linux --pkg libsoup-2.4 --pkg sqlite3 --pkg gtk+-3.0 --pkg gee-1.0 --pkg gmodule-2.0 --pkg gio-2.0 $^ +barcode-scanner: $(SRC) + valac-0.16 --output $@ $(FLAGS) $(DEPS) $^ -shop.db: create_db.sql - sqlite3 shop.db < create_db.sql +shop.db: sql/tables.sql sql/views.sql sql/trigger.sql + @for file in $^ ; do \ + echo "sqlite3 shop.db < $$file"; \ + sqlite3 shop.db < $$file; \ + done -install: barcode-scanner - install -m755 barcode-scanner $(DESTDIR)$(PREFIX)/bin/barcode-scanner +run: barcode-scanner + ./barcode-scanner /dev/ttyS0 clean: - rm -f barcode-scanner + @rm -f barcode-scanner src/*.c .PHONY: clean install diff --git a/barcodelist.rb b/barcodelist.rb deleted file mode 100644 index 515403f..0000000 --- a/barcodelist.rb +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env ruby -w -# encoding: UTF-8 - -require "csv" - -#csv input "userid,firstname,lastname" - -@template = %q{ - \documentclass[a4paper,landscape]{article} - \usepackage[utf8]{inputenc} - \usepackage{graphicx} - \usepackage{longtable} - \usepackage[top=0.5cm,right=0.5cm,bottom=0.5cm,left=0.5cm,landscape]{geometry} - \renewcommand{\familydefault}{\sfdefault} - \title{Barcodelist} - \author{Kreativitaet trifft Technik} - \date{\today} - \begin{document} - \begin{center} - \begin{longtable}{|c|c|c|} - %s - \end{longtable} - \end{center} - \end{document}} - -@line = %q{ - \hline - %s - \hline - %s - \hline} - -@graphics = %q{ \includegraphics{%s} %s} -@name = %q{ %s %s %s} - -@csv = CSV.read(ARGV[0]) - -#generate barcodes -@csv.each{|r| - system("barcode -n -E -b 'USER %s' -o '%s.eps' -e 39\n" % [r[0], r[0]]) -} - -#generate latex -tmp = "" -graphics = "" -name = "" -1.upto(@csv.length){|i| - le = i % 3 == 0 || i >= @csv.length - sign = le ? "\\\\" : "&" - graphics += @graphics % [@csv[i-1][0], sign] - name += @name % [@csv[i-1][1], @csv[i-1][2], sign] - if le - tmp += @line % [graphics, name] - graphics = "" - name = "" - end -} -File.open("barcode.latex", "w+"){|f| f.write(@template % tmp)} diff --git a/create_db.sql b/create_db.sql deleted file mode 100644 index 2bbb162..0000000 --- a/create_db.sql +++ /dev/null @@ -1,59 +0,0 @@ -CREATE TABLE products (id INTEGER PRIMARY KEY NOT NULL, name TEXT, amount INTEGER NOT NULL DEFAULT 0); -CREATE TABLE purchases (user INTEGER NOT NULL DEFAULT 0, product INTEGER NOT NULL DEFAULT 0, timestamp INTEGER NOT NULL DEFAULT 0); -CREATE TABLE restock (user INTEGER NOT NULL DEFAULT 0, product INTEGER NOT NULL DEFAULT 0, amount INTEGER NOT NULL DEFAULT 0, timestamp INTEGER NOT NULL DEFAULT 0); -CREATE TABLE prices (product INTEGER NOT NULL DEFAULT 0, valid_from INTEGER NOT NULL DEFAULT 0, memberprice INTEGER NOT NULL DEFAULT 0, guestprice INTEGER NOT NULL DEFAULT 0); -CREATE TABLE users (id INTEGER PRIMARY KEY NOT NULL, email TEXT, firstname TEXT NOT NULL, lastname TEXT NOT NULL, gender TEXT, street TEXT, plz INTEGER, city TEXT); -BEGIN TRANSACTION; -INSERT INTO products VALUES(40084015,'Duplo',42); -INSERT INTO products VALUES(40084107,'Ü-Ei',64); -INSERT INTO products VALUES(40111315,'Twix',30); -INSERT INTO products VALUES(40114606,'KitKat',45); -INSERT INTO products VALUES(40358802,'Knoppers',41); -INSERT INTO products VALUES(76222498,'Milka Tender milch',15); -INSERT INTO products VALUES(76222504,'Milka Tender nuss',13); -INSERT INTO products VALUES(4001686128244,'Haribo Staffeten',28); -INSERT INTO products VALUES(4001686150689,'Haribo Konfekt',19); -INSERT INTO products VALUES(4001686216125,'Haribo Salino',23); -INSERT INTO products VALUES(4001686301265,'Haribo Goldbären',24); -INSERT INTO products VALUES(4001686309506,'Haribo Happy Cherries',10); -INSERT INTO products VALUES(4001686310229,'Haribo Weinland',22); -INSERT INTO products VALUES(4001686312025,'Haribo Schnuller',18); -INSERT INTO products VALUES(4001686313046,'Haribo Saure Bohnen',17); -INSERT INTO products VALUES(4001686315101,'Haribo Happy Cola',20); -INSERT INTO products VALUES(4001686367346,'Haribo Tropifrutti',21); -INSERT INTO products VALUES(4001686386613,'Haribo Saftgoldbären',3); -INSERT INTO products VALUES(4001686390085,'Haribo Phantasia',11); -INSERT INTO products VALUES(4001686720028,'Haribo Colorado Mini',0); -INSERT INTO products VALUES(4001686721445,'Haribo Colorado',18); -INSERT INTO products VALUES(4003586000477,'Chipsfrisch Ungarisch',18); -INSERT INTO products VALUES(4003586000491,'Chipsfrisch Oriento',22); -INSERT INTO products VALUES(4006220013185,'Orangen Saft',0); -INSERT INTO products VALUES(4007495314014,'Gouda Käse-Sticks',28); -INSERT INTO products VALUES(4029764001807,'Club Mate',112); -INSERT INTO products VALUES(4047046003325,'Senseo Klassisch',29); -INSERT INTO products VALUES(4047046003356,'Senseo Mild',16); -INSERT INTO products VALUES(4047046003417,'Senseo Entkoffeiniert',16); -INSERT INTO products VALUES(4047046005008,'Senseo Cappuccino Choco',8); -INSERT INTO products VALUES(4104450004086,'Vilsa medium',19); -INSERT INTO products VALUES(4104450004383,'Vilsa Limette',64); -INSERT INTO products VALUES(4104450005878,'Vilsa Classic',1); -INSERT INTO products VALUES(5000159407236,'Mars',16); -INSERT INTO products VALUES(5000159407397,'Snickers',6); -INSERT INTO products VALUES(5000159407410,'Snickers (2x)',48); -INSERT INTO products VALUES(5000159418539,'Balisto Jogurt Beeren Mix',36); -INSERT INTO products VALUES(5000159418546,'Balisto Muesli-Mix',40); -INSERT INTO products VALUES(5000159418577,'Balisto Korn-Mix',40); -INSERT INTO products VALUES(5449000017888,'Coca Cola',56); -INSERT INTO products VALUES(5449000017895,'Coca Cola Light',65); -INSERT INTO products VALUES(5449000017918,'Fanta',67); -INSERT INTO products VALUES(5449000017932,'Sprite',52); -INSERT INTO products VALUES(5449000134264,'Coca Cola Zero',55); -INSERT INTO products VALUES(5900951020704,'m&m´s Peanut Big Pack',0); -INSERT INTO products VALUES(7613032625474,'Lion',50); -INSERT INTO products VALUES(7613032850340,'KitKat Chunky',22); -INSERT INTO products VALUES(7622300243876,'Milka Tender Schwarzwälder Kirsch',0); -INSERT INTO products VALUES(7622300065478,'Milka Tender Tiramisu',0); -INSERT INTO products VALUES(8410036002015,'Freixenet Cava Semi Seco 0.75L',3); -INSERT INTO products VALUES(8690504018568,'Üker Sesamsticks',5); -INSERT INTO products VALUES(8710866001036,'Gouda Käse-Sticks',39); -COMMIT; diff --git a/db.vala b/db.vala deleted file mode 100644 index 10687f4..0000000 --- a/db.vala +++ /dev/null @@ -1,299 +0,0 @@ -/* Copyright 2012, Sebastian Reichel - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -public struct StockEntry { - public string id; - public string name; - public int amount; - public string memberprice; - public string guestprice; -} - -public class Database { - private Sqlite.Database db; - private Sqlite.Statement product_stmt; - private Sqlite.Statement products_stmt; - private Sqlite.Statement purchase_stmt1; - private Sqlite.Statement purchase_stmt2; - private Sqlite.Statement undo_stmt1; - private Sqlite.Statement undo_stmt2; - private Sqlite.Statement undo_stmt3; - private Sqlite.Statement stock_stmt1; - private Sqlite.Statement stock_stmt2; - private Sqlite.Statement price_stmt; - private Sqlite.Statement stock_status_stmt; - int32 user = 0; - bool logged_in = false; - private static string product_query = "SELECT name FROM products WHERE id = ?"; - private static string products_query = "SELECT id, name FROM products"; - private static string purchase_query1 = "INSERT INTO purchases ('user', 'product', 'timestamp') VALUES (?, ?, ?)"; - private static string purchase_query2 = "UPDATE products SET amount = amount - 1 WHERE id = ?"; - private static string undo_query1 = "SELECT product FROM purchases WHERE user = ? ORDER BY timestamp DESC LIMIT 1"; - private static string undo_query2 = "DELETE FROM purchases WHERE user = ? ORDER BY timestamp DESC LIMIT 1"; - private static string undo_query3 = "UPDATE products SET amount = amount + 1 WHERE id = ?"; - private static string stock_query1 = "INSERT INTO restock ('user', 'product', 'amount', 'timestamp') VALUES (?, ?, ?, ?)"; - private static string stock_query2 = "UPDATE products SET amount = amount + ? WHERE id = ?"; - private static string price_query = "SELECT memberprice, guestprice FROM prices WHERE product = ? AND valid_from <= ? ORDER BY valid_from DESC LIMIT 1"; - private static string stock_status_query = "SELECT id, name, amount, memberprice, guestprice FROM products, prices WHERE products.id = prices.product AND products.amount > 0 AND prices.valid_from = (SELECT valid_from FROM prices WHERE product = products.id ORDER BY valid_from DESC LIMIT 1)"; - - public Database(string file) { - int rc; - - rc = Sqlite.Database.open(file, out db); - if(rc != Sqlite.OK) { - error("could not open database!"); - } - - rc = this.db.prepare_v2(purchase_query1, -1, out purchase_stmt1); - if(rc != Sqlite.OK) { - error("could not prepare first purchase statement!"); - } - - rc = this.db.prepare_v2(purchase_query2, -1, out purchase_stmt2); - if(rc != Sqlite.OK) { - error("could not prepare second purchase statement!"); - } - - rc = this.db.prepare_v2(product_query, -1, out product_stmt); - if(rc != Sqlite.OK) { - error("could not prepare article statement!"); - } - - rc = this.db.prepare_v2(products_query, -1, out products_stmt); - if(rc != Sqlite.OK) { - error("could not prepare products statement!"); - } - - rc = this.db.prepare_v2(undo_query1, -1, out undo_stmt1); - if(rc != Sqlite.OK) { - error("could not prepare first undo statement!"); - } - - rc = this.db.prepare_v2(undo_query2, -1, out undo_stmt2); - if(rc != Sqlite.OK) { - error("could not prepare second undo statement!"); - } - - rc = this.db.prepare_v2(undo_query3, -1, out undo_stmt3); - if(rc != Sqlite.OK) { - error("could not prepare third undo statement!"); - } - - rc = this.db.prepare_v2(stock_query1, -1, out stock_stmt1); - if(rc != Sqlite.OK) { - error("could not prepare first stock statement!"); - } - - rc = this.db.prepare_v2(stock_query2, -1, out stock_stmt2); - if(rc != Sqlite.OK) { - error("could not prepare second stock statement!"); - } - - rc = this.db.prepare_v2(price_query, -1, out price_stmt); - if(rc != Sqlite.OK) { - error("could not prepare price statement!"); - } - - rc = this.db.prepare_v2(stock_status_query, -1, out stock_status_stmt); - if(rc != Sqlite.OK) { - error("could not prepare stock status statement!"); - } - } - - public bool login(int32 id) { - this.user = id; - this.logged_in = true; - return true; - } - - public bool logout() { - this.user = 0; - this.logged_in = false; - return true; - } - - public Gee.HashMap get_products() { - var result = new Gee.HashMap(null, null); - this.products_stmt.reset(); - - while(this.products_stmt.step() == Sqlite.ROW) - result[this.products_stmt.column_text(0)] = this.products_stmt.column_text(1); - - return result; - } - - public Gee.List get_stock() { - var result = new Gee.ArrayList(); - this.stock_status_stmt.reset(); - - while(this.stock_status_stmt.step() == Sqlite.ROW) { - StockEntry entry = { - this.stock_status_stmt.column_text(0), - this.stock_status_stmt.column_text(1), - this.stock_status_stmt.column_int(2), - null, - null - }; - - entry.memberprice = "%d.%02d€".printf(this.stock_status_stmt.column_int(3) / 100, this.stock_status_stmt.column_int(3) % 100); - entry.guestprice = "%d.%02d€".printf(this.stock_status_stmt.column_int(4) / 100, this.stock_status_stmt.column_int(4) % 100); - - result.add(entry); - } - - return result; - } - - public bool buy(uint64 article) { - if(is_logged_in()) { - int rc = 0; - int64 timestamp = (new DateTime.now_utc()).to_unix(); - - this.purchase_stmt1.reset(); - this.purchase_stmt1.bind_text(1, "%d".printf(user)); - this.purchase_stmt1.bind_text(2, "%llu".printf(article)); - this.purchase_stmt1.bind_text(3, "%llu".printf(timestamp)); - - rc = this.purchase_stmt1.step(); - if(rc != Sqlite.DONE) - error("[interner Fehler: %d]".printf(rc)); - - this.purchase_stmt2.reset(); - this.purchase_stmt2.bind_text(1, "%llu".printf(article)); - - rc = this.purchase_stmt2.step(); - if(rc != Sqlite.DONE) - error("[interner Fehler: %d]".printf(rc)); - - return true; - } else { - return false; - } - } - - public string get_product_name(uint64 article) { - this.product_stmt.reset(); - this.product_stmt.bind_text(1, "%llu".printf(article)); - - int rc = this.product_stmt.step(); - - switch(rc) { - case Sqlite.ROW: - return this.product_stmt.column_text(0); - case Sqlite.DONE: - return "unbekanntes Produkt: %llu".printf(article); - default: - return "[interner Fehler: %d]".printf(rc); - } - } - - public int get_product_price(uint64 article) { - int64 timestamp = (new DateTime.now_utc()).to_unix(); - bool member = user != 0; - - this.price_stmt.reset(); - this.price_stmt.bind_text(1, "%llu".printf(article)); - this.price_stmt.bind_text(2, "%lld".printf(timestamp)); - - int rc = this.price_stmt.step(); - - switch(rc) { - case Sqlite.ROW: - if(member) - return this.price_stmt.column_int(0); - else - return this.price_stmt.column_int(1); - case Sqlite.DONE: - write_to_log("unbekanntes Produkt: %llu\n", article); - return 0; - default: - write_to_log("[interner Fehler: %d]\n", rc); - return 0; - } - } - - public bool undo() { - if(is_logged_in()) { - uint64 pid = 0; - int rc = 0; - - this.undo_stmt1.reset(); - this.undo_stmt1.bind_text(1, "%d".printf(user)); - - rc = this.undo_stmt1.step(); - switch(rc) { - case Sqlite.ROW: - pid = uint64.parse(this.undo_stmt1.column_text(0)); - break; - case Sqlite.DONE: - write_to_log("undo not possible without purchases"); - return false; - default: - error("[interner Fehler: %d]".printf(rc)); - } - - this.undo_stmt2.reset(); - this.undo_stmt2.bind_text(1, "%d".printf(user)); - - rc = this.undo_stmt2.step(); - if(rc != Sqlite.DONE) - error("[interner Fehler: %d]".printf(rc)); - - this.undo_stmt3.reset(); - this.undo_stmt3.bind_text(1, "%llu".printf(pid)); - - rc = this.undo_stmt3.step(); - if(rc != Sqlite.DONE) - error("[interner Fehler: %d]".printf(rc)); - - return true; - } - - return false; - } - - public bool restock(uint64 product, uint64 amount) { - if(is_logged_in()) { - int rc = 0; - int64 timestamp = (new DateTime.now_utc()).to_unix(); - - this.stock_stmt1.reset(); - this.stock_stmt1.bind_text(1, "%d".printf(user)); - this.stock_stmt1.bind_text(2, "%llu".printf(product)); - this.stock_stmt1.bind_text(3, "%llu".printf(amount)); - this.stock_stmt1.bind_text(4, "%llu".printf(timestamp)); - - rc = this.stock_stmt1.step(); - if(rc != Sqlite.DONE) - error("[interner Fehler: %d]".printf(rc)); - - this.stock_stmt2.reset(); - this.stock_stmt2.bind_text(1, "%llu".printf(amount)); - this.stock_stmt2.bind_text(2, "%llu".printf(product)); - - rc = this.stock_stmt2.step(); - if(rc != Sqlite.DONE) - error("[interner Fehler: %d]".printf(rc)); - - return true; - } - - return false; - } - - public bool is_logged_in() { - return this.logged_in; - } -} diff --git a/device.vala b/device.vala deleted file mode 100644 index f624d70..0000000 --- a/device.vala +++ /dev/null @@ -1,280 +0,0 @@ -/* Copyright 2012, Sebastian Reichel - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -public class Device { - private Posix.termios newtio; - private Posix.termios restoretio; - public int fd=-1; - private IOChannel io_read; - public int byterate; - private File lockfile; - - public signal void received_barcode(string barcode); - - public Device(string device, int rate, int bits, int stopbits) { - Posix.speed_t baudrate = Posix.B9600; - - try { - /* check lock file */ - lockfile = File.new_for_path("/var/lock/LCK.." + device.replace("/dev/", "")); - if(lockfile.query_exists()) { - int pid = -1; - uint8[] data; - if(lockfile.load_contents(null, out data, null)) { - pid = int.parse((string) data); - } else { - error("Can't read lock file!\n"); - } - - if(Posix.kill(pid, 0) == 0) { - error("serial device is locked!\n"); - } - } - - var pid = "%d\n".printf(Posix.getpid()); - lockfile.replace_contents(pid.data, null, false, FileCreateFlags.NONE, null); - - fd = Posix.open(device, Posix.O_RDWR /*| Posix.O_NONBLOCK*/); - - if(fd < 0) { - fd = -1; - lockfile.delete(); - error("Could not open device!\n"); - } - - } catch(Error e) { - error("Could not create lock file: %s!\n", e.message); - } - - - Posix.tcflush(fd, Posix.TCIOFLUSH); - - Posix.tcgetattr(fd, out restoretio); - - /* apply settings */ - switch(rate) { - case 300: - baudrate = Posix.B300; - break; - case 600: - baudrate = Posix.B600; - break; - case 1200: - baudrate = Posix.B1200; - break; - case 2400: - baudrate = Posix.B2400; - break; - case 4800: - baudrate = Posix.B4800; - break; - case 9600: - baudrate = Posix.B9600; - break; - case 19200: - baudrate = Posix.B19200; - break; - case 38400: - baudrate = Posix.B38400; - break; - case 57600: - baudrate = Posix.B57600; - break; - case 115200: - baudrate = Posix.B115200; - break; - case 230400: - baudrate = Posix.B230400; - break; - default: - /* not supported */ - rate = 9600; - break; - } - - Posix.cfsetospeed(ref newtio, baudrate); - Posix.cfsetispeed(ref newtio, baudrate); - - switch(bits) { - case 5: - newtio.c_cflag = (newtio.c_cflag & ~Posix.CSIZE) | Posix.CS5; - break; - case 6: - newtio.c_cflag = (newtio.c_cflag & ~Posix.CSIZE) | Posix.CS6; - break; - case 7: - newtio.c_cflag = (newtio.c_cflag & ~Posix.CSIZE) | Posix.CS7; - break; - case 8: - default: - newtio.c_cflag = (newtio.c_cflag & ~Posix.CSIZE) | Posix.CS8; - break; - } - - newtio.c_cflag |= Posix.CLOCAL | Posix.CREAD; - - newtio.c_cflag &= ~(Posix.PARENB | Posix.PARODD); - - /* TODO: parity */ - - newtio.c_cflag &= ~Linux.Termios.CRTSCTS; - - if(stopbits == 2) - newtio.c_cflag |= Posix.CSTOPB; - else - newtio.c_cflag &= ~Posix.CSTOPB; - - newtio.c_iflag = Posix.IGNBRK; - - newtio.c_lflag = 0; - newtio.c_oflag = 0; - - newtio.c_cc[Posix.VTIME]=1; - newtio.c_cc[Posix.VMIN]=1; - - newtio.c_lflag &= ~(Posix.ECHONL|Posix.NOFLSH); - - int mcs=0; - Posix.ioctl(fd, Linux.Termios.TIOCMGET, out mcs); - mcs |= Linux.Termios.TIOCM_RTS; - Posix.ioctl(fd, Linux.Termios.TIOCMSET, out mcs); - - Posix.tcsetattr(fd, Posix.TCSANOW, newtio); - - this.byterate = rate/bits; - - try { - io_read = new IOChannel.unix_new(fd); - io_read.set_line_term("\r\n", 2); - if(io_read.set_encoding(null) != IOStatus.NORMAL) - error("Failed to set encoding"); - if(!(io_read.add_watch(IOCondition.IN | IOCondition.HUP, device_read) != 0)) { - error("Could not bind IOChannel"); - } - } catch(IOChannelError e) { - error("IOChannel: %s", e.message); - } - } - - ~Device() { - /* restore old tty config */ - Posix.tcsetattr(fd, Posix.TCSANOW, restoretio); - - /* close file */ - Posix.close(fd); - - /* remove lock */ - lockfile.delete(); - } - - private bool device_read(IOChannel gio, IOCondition cond) { - IOStatus ret; - string msg; - size_t len, term_char; - - if((cond & IOCondition.HUP) == IOCondition.HUP) - error("Lost device"); - - try { - ret = gio.read_line(out msg, out len, out term_char); - msg = msg[0:(long)term_char]; - - if(msg.has_prefix("USER ") || msg.has_prefix("STOCK") || msg.has_prefix("AMOUNT ")) { - if(!check_code39_checksum(msg)) - received_barcode("SCANNER RETURNED INCORRECT DATA"); - else {/* remove checksum */ - msg = msg[0:-1]; - received_barcode(msg); - } - } - else - received_barcode(msg); - } - catch(IOChannelError e) { - stderr.printf("IOChannel Error: %s", e.message); - return false; - } - catch(ConvertError e) { - stderr.printf("Convert Error: %s", e.message); - return false; - } - return true; - } - - private ssize_t write(void *buf, size_t count) { - ssize_t size = Posix.write(fd, buf, count); - return size; - } - - private bool check_code39_checksum(string data) { - int result = 0; - - for(int i = 0; i= '0' && data[i] <= '9') - result += data[i] - '0'; - else if(data[i] >= 'A' && data[i] <= 'Z') - result += data[i] - 'A' + 10; - else - switch(data[i]) { - case '-': - result += 36; break; - case '.': - result += 37; break; - case ' ': - result += 38; break; - case '$': - result += 39; break; - case '/': - result += 40; break; - case '+': - result += 41; break; - case '%': - result += 42; break; - default: - /* invalid character */ - return false; - } - - result %= 43; - } - - if(result < 10) - result = result + '0'; - else if(result < 36) - result = result - 10 + 'A'; - else - switch(result) { - case 36: result = '-'; break; - case 37: result = '.'; break; - case 38: result = ' '; break; - case 39: result = '$'; break; - case 40: result = '/'; break; - case 41: result = '+'; break; - case 42: result = '%'; break; - } - - return (data[data.length-1] == result); - } - - /** - * @param duration duration of the blink in 0.1 seconds - */ - public void blink(uint duration) { - uint size = byterate/10 * duration; - var msg = new uint8[size]; - Posix.memset(msg, 0xFF, msg.length); - this.write(msg, msg.length); -} -} diff --git a/generation/barcodelist.rb b/generation/barcodelist.rb new file mode 100644 index 0000000..515403f --- /dev/null +++ b/generation/barcodelist.rb @@ -0,0 +1,58 @@ +#!/usr/bin/env ruby -w +# encoding: UTF-8 + +require "csv" + +#csv input "userid,firstname,lastname" + +@template = %q{ + \documentclass[a4paper,landscape]{article} + \usepackage[utf8]{inputenc} + \usepackage{graphicx} + \usepackage{longtable} + \usepackage[top=0.5cm,right=0.5cm,bottom=0.5cm,left=0.5cm,landscape]{geometry} + \renewcommand{\familydefault}{\sfdefault} + \title{Barcodelist} + \author{Kreativitaet trifft Technik} + \date{\today} + \begin{document} + \begin{center} + \begin{longtable}{|c|c|c|} + %s + \end{longtable} + \end{center} + \end{document}} + +@line = %q{ + \hline + %s + \hline + %s + \hline} + +@graphics = %q{ \includegraphics{%s} %s} +@name = %q{ %s %s %s} + +@csv = CSV.read(ARGV[0]) + +#generate barcodes +@csv.each{|r| + system("barcode -n -E -b 'USER %s' -o '%s.eps' -e 39\n" % [r[0], r[0]]) +} + +#generate latex +tmp = "" +graphics = "" +name = "" +1.upto(@csv.length){|i| + le = i % 3 == 0 || i >= @csv.length + sign = le ? "\\\\" : "&" + graphics += @graphics % [@csv[i-1][0], sign] + name += @name % [@csv[i-1][1], @csv[i-1][2], sign] + if le + tmp += @line % [graphics, name] + graphics = "" + name = "" + end +} +File.open("barcode.latex", "w+"){|f| f.write(@template % tmp)} diff --git a/generation/ktt.pdf b/generation/ktt.pdf new file mode 100644 index 0000000..60222d1 Binary files /dev/null and b/generation/ktt.pdf differ diff --git a/generation/passlist.rb b/generation/passlist.rb new file mode 100644 index 0000000..cb02bd1 --- /dev/null +++ b/generation/passlist.rb @@ -0,0 +1,71 @@ +#!/usr/bin/env ruby -w +# encoding: UTF-8 + +require "csv" + +#csv input "userid,firstname,lastname" + +@template = %q{ + \documentclass[a4paper,landscape]{article} + \usepackage[utf8]{inputenc} + \usepackage{graphicx} + \usepackage{longtable} + \usepackage{array} + \usepackage{rotating} + \usepackage[top=0.5cm,right=0.5cm,bottom=0.5cm,left=0.5cm,landscape]{geometry} + \renewcommand{\familydefault}{\sfdefault} + \renewcommand{\arraystretch}{2} + \renewcommand{\tabcolsep}{0cm} + \title{Barcodelist} + \author{Kreativitaet trifft Technik} + \date{\today} + \begin{document} + \begin{center} + \begin{longtable}{| >{\centering\arraybackslash}p{8.5cm}| >{\centering\arraybackslash}p{8.5cm}| >{\centering\arraybackslash}p{8.5cm}|} + %s + \end{longtable} + \end{center} + \end{document}} + +@line = %q{ + \hline + %s + \hline + %s + \hline} + +@graphics = %q{ \includegraphics{%s} \rule{0cm}{3.5cm} %s} +@name = %q{ %s %s %s} +@ktt = %q{\includegraphics[width=8cm,angle=180]{ktt} %s } + +@csv = CSV.read(ARGV[0]) + +#generate barcodes +@csv.each{|r| + system("barcode -n -E -b 'USER %s' -o '%s.eps' -e 39\n" % [r[0], r[0]]) +} + +#generate latex +tmp = "" +graphics = "" +name = "" +1.upto(@csv.length){|i| + le = i % 3 == 0 || i >= @csv.length + sign = le ? "\\\\" : "&" + graphics += @graphics % [@csv[i-1][0], sign] + name += @name % [@csv[i-1][1], @csv[i-1][2], sign] + if le + tmp += @line % [graphics, name] + graphics = "" + name = "" + 1.upto(3) {|j| + sign = j == 3 ? "\\\\" : "&" + graphics += @ktt % sign + name += " " + sign + } + tmp += @line % [name, graphics] + graphics = "" + name = "" + end +} +File.open("barcode.latex", "w+"){|f| f.write(@template % tmp)} diff --git a/insert-prices.sql b/insert-prices.sql deleted file mode 100644 index e72d0e1..0000000 --- a/insert-prices.sql +++ /dev/null @@ -1,183 +0,0 @@ -BEGIN TRANSACTION; -INSERT INTO prices (product, memberprice, guestprice) VALUES (4029764001807,150,250); -INSERT INTO prices (product, memberprice, guestprice) VALUES (5449000017888,150,240); -INSERT INTO prices (product, memberprice, guestprice) VALUES (5449000017895,150,240); -INSERT INTO prices (product, memberprice, guestprice) VALUES (5449000134264,150,240); -INSERT INTO prices (product, memberprice, guestprice) VALUES (5449000017918,150,240); -INSERT INTO prices (product, memberprice, guestprice) VALUES (5449000017932,150,240); -INSERT INTO prices (product, memberprice, guestprice) VALUES (4104450004383,150,240); -INSERT INTO prices (product, memberprice, guestprice) VALUES (4104450004086,150,240); -INSERT INTO prices (product, memberprice, guestprice) VALUES (4104450005878,150,240); -INSERT INTO prices (product, memberprice, guestprice) VALUES (4001686720028,140,180); -INSERT INTO prices (product, memberprice, guestprice) VALUES (4001686310229,140,180); -INSERT INTO prices (product, memberprice, guestprice) VALUES (4001686390085,140,180); -INSERT INTO prices (product, memberprice, guestprice) VALUES (4001686386613,140,180); -INSERT INTO prices (product, memberprice, guestprice) VALUES (4001686301265,140,180); -INSERT INTO prices (product, memberprice, guestprice) VALUES (4001686313046,140,180); -INSERT INTO prices (product, memberprice, guestprice) VALUES (4001686216125,140,180); -INSERT INTO prices (product, memberprice, guestprice) VALUES (4001686312025,140,180); -INSERT INTO prices (product, memberprice, guestprice) VALUES (4003586000491,100,130); -INSERT INTO prices (product, memberprice, guestprice) VALUES (8690504018568,40,60); -INSERT INTO prices (product, memberprice, guestprice) VALUES (40084107,80,100); -INSERT INTO prices (product, memberprice, guestprice) VALUES (7613032850340,80,100); -INSERT INTO prices (product, memberprice, guestprice) VALUES (40111315,80,100); -INSERT INTO prices (product, memberprice, guestprice) VALUES (5000159407236,80,100); -INSERT INTO prices (product, memberprice, guestprice) VALUES (5000159407397,80,100); -INSERT INTO prices (product, memberprice, guestprice) VALUES (5000159418539,80,100); -INSERT INTO prices (product, memberprice, guestprice) VALUES (40114606,80,100); -INSERT INTO prices (product, memberprice, guestprice) VALUES (7613032625474,80,100); -INSERT INTO prices (product, memberprice, guestprice) VALUES (40358802,40,60); -INSERT INTO prices (product, memberprice, guestprice) VALUES (40084015,30,40); -INSERT INTO prices (product, memberprice, guestprice) VALUES (4047046003325,50,100); -INSERT INTO prices (product, memberprice, guestprice) VALUES (4047046003356,50,100); -INSERT INTO prices (product, memberprice, guestprice) VALUES (4047046003417,50,100); -INSERT INTO prices (product, memberprice, guestprice) VALUES (4047046005008,50,100); -INSERT INTO prices (product, memberprice, guestprice) VALUES (4001686128244,140,180); -INSERT INTO prices (product, memberprice, guestprice) VALUES (4001686150689,140,180); -INSERT INTO prices (product, memberprice, guestprice) VALUES (4001686309506,140,180); -INSERT INTO prices (product, memberprice, guestprice) VALUES (4001686315101,140,180); -INSERT INTO prices (product, memberprice, guestprice) VALUES (4001686367346,140,180); -INSERT INTO prices (product, memberprice, guestprice) VALUES (4001686721445,140,180); -INSERT INTO prices (product, memberprice, guestprice) VALUES (4003586000477,100,130); -INSERT INTO prices (product, memberprice, guestprice) VALUES (76222498,80,100); -INSERT INTO prices (product, memberprice, guestprice) VALUES (76222504,80,100); -INSERT INTO prices (product, memberprice, guestprice) VALUES (5000159407410,140,180); -INSERT INTO prices (product, memberprice, guestprice) VALUES (5000159418546,80,100); -INSERT INTO prices (product, memberprice, guestprice) VALUES (5000159418577,80,100); -INSERT INTO prices (product, memberprice, guestprice) VALUES (4007495314014,100,130); -COMMIT; -BEGIN TRANSACTION; -INSERT INTO "prices" VALUES(4029764001807,1340402400,78,250); -INSERT INTO "prices" VALUES(5449000017888,1340402400,103,250); -INSERT INTO "prices" VALUES(5449000017895,1340402400,86,250); -INSERT INTO "prices" VALUES(5449000134264,1340402400,103,250); -INSERT INTO "prices" VALUES(5449000017918,1340402400,86,250); -INSERT INTO "prices" VALUES(5449000017932,1340402400,86,250); -INSERT INTO "prices" VALUES(4104450004383,1340402400,82,250); -INSERT INTO "prices" VALUES(4047046003325,1340402400,15,100); -INSERT INTO "prices" VALUES(4047046003417,1340402400,15,100); -INSERT INTO "prices" VALUES(4047046005008,1340402400,31,110); -INSERT INTO "prices" VALUES(4001686128244,1340402400,75,180); -INSERT INTO "prices" VALUES(4001686150689,1340402400,75,180); -INSERT INTO "prices" VALUES(4001686216125,1340402400,75,180); -INSERT INTO "prices" VALUES(4001686301265,1340402400,75,180); -INSERT INTO "prices" VALUES(4001686386613,1340402400,75,180); -INSERT INTO "prices" VALUES(4001686309506,1340402400,75,180); -INSERT INTO "prices" VALUES(4001686310229,1340402400,75,180); -INSERT INTO "prices" VALUES(4001686312025,1340402400,75,180); -INSERT INTO "prices" VALUES(4001686313046,1340402400,75,180); -INSERT INTO "prices" VALUES(4001686315101,1340402400,75,180); -INSERT INTO "prices" VALUES(4001686367346,1340402400,75,180); -INSERT INTO "prices" VALUES(4001686390085,1340402400,75,180); -INSERT INTO "prices" VALUES(4001686721445,1340402400,75,180); -INSERT INTO "prices" VALUES(4003586000477,1340402400,60,140); -INSERT INTO "prices" VALUES(4003586000491,1340402400,60,140); -INSERT INTO "prices" VALUES(4007495314014,1340402400,48,120); -INSERT INTO "prices" VALUES(76222498,1340402400,42,100); -INSERT INTO "prices" VALUES(76222504,1340402400,42,100); -INSERT INTO "prices" VALUES(7622300065478,1340402400,42,100); -INSERT INTO "prices" VALUES(7622300243876,1340402400,42,100); -INSERT INTO "prices" VALUES(5000159407410,1340402400,54,140); -INSERT INTO "prices" VALUES(5000159407236,1340402400,44,100); -INSERT INTO "prices" VALUES(40111315,1340402400,44,100); -INSERT INTO "prices" VALUES(7613032625474,1340402400,34,100); -INSERT INTO "prices" VALUES(40114606,1340402400,34,100); -INSERT INTO "prices" VALUES(7613032850340,1340402400,47,100); -INSERT INTO "prices" VALUES(40084107,1340402400,61,100); -INSERT INTO "prices" VALUES(5000159418539,1340402400,44,100); -INSERT INTO "prices" VALUES(5000159418546,1340402400,44,100); -INSERT INTO "prices" VALUES(5000159418577,1340402400,44,100); -INSERT INTO "prices" VALUES(40358802,1340402400,21,60); -INSERT INTO "prices" VALUES(40084015,1340402400,18,50); -INSERT INTO "prices" VALUES(5900951020704,1340402400,48,130); -COMMIT; -BEGIN TRANSACTION; -INSERT INTO "prices" VALUES(4029764001807,1340575200,150,250); -INSERT INTO "prices" VALUES(5449000017888,1340575200,150,240); -INSERT INTO "prices" VALUES(5449000017895,1340575200,150,240); -INSERT INTO "prices" VALUES(5449000134264,1340575200,150,240); -INSERT INTO "prices" VALUES(5449000017918,1340575200,150,240); -INSERT INTO "prices" VALUES(5449000017932,1340575200,150,240); -INSERT INTO "prices" VALUES(4104450004383,1340575200,150,240); -INSERT INTO "prices" VALUES(4001686310229,1340575200,140,180); -INSERT INTO "prices" VALUES(4001686390085,1340575200,140,180); -INSERT INTO "prices" VALUES(4001686386613,1340575200,140,180); -INSERT INTO "prices" VALUES(4001686301265,1340575200,140,180); -INSERT INTO "prices" VALUES(4001686313046,1340575200,140,180); -INSERT INTO "prices" VALUES(4001686216125,1340575200,140,180); -INSERT INTO "prices" VALUES(4001686312025,1340575200,140,180); -INSERT INTO "prices" VALUES(4003586000491,1340575200,100,130); -INSERT INTO "prices" VALUES(40084107,1340575200,80,100); -INSERT INTO "prices" VALUES(7613032850340,1340575200,80,100); -INSERT INTO "prices" VALUES(40111315,1340575200,80,100); -INSERT INTO "prices" VALUES(5000159407236,1340575200,80,100); -INSERT INTO "prices" VALUES(5000159418539,1340575200,80,100); -INSERT INTO "prices" VALUES(40114606,1340575200,80,100); -INSERT INTO "prices" VALUES(7613032625474,1340575200,80,100); -INSERT INTO "prices" VALUES(40358802,1340575200,40,60); -INSERT INTO "prices" VALUES(40084015,1340575200,30,40); -INSERT INTO "prices" VALUES(4047046003325,1340575200,50,100); -INSERT INTO "prices" VALUES(4047046003417,1340575200,50,100); -INSERT INTO "prices" VALUES(4047046005008,1340575200,50,100); -INSERT INTO "prices" VALUES(4001686128244,1340575200,140,180); -INSERT INTO "prices" VALUES(4001686150689,1340575200,140,180); -INSERT INTO "prices" VALUES(4001686309506,1340575200,140,180); -INSERT INTO "prices" VALUES(4001686315101,1340575200,140,180); -INSERT INTO "prices" VALUES(4001686367346,1340575200,140,180); -INSERT INTO "prices" VALUES(4001686721445,1340575200,140,180); -INSERT INTO "prices" VALUES(4003586000477,1340575200,100,130); -INSERT INTO "prices" VALUES(76222498,1340575200,80,100); -INSERT INTO "prices" VALUES(76222504,1340575200,80,100); -INSERT INTO "prices" VALUES(7622300243876,1340575200,80,100); -INSERT INTO "prices" VALUES(7622300065478,1340575200,80,100); -INSERT INTO "prices" VALUES(5000159407410,1340575200,140,180); -INSERT INTO "prices" VALUES(5000159418546,1340575200,80,100); -INSERT INTO "prices" VALUES(5000159418577,1340575200,80,100); -INSERT INTO "prices" VALUES(4007495314014,1340575200,100,130); -INSERT INTO "prices" VALUES(5900951020704,1340575200,130,160); -COMMIT; -BEGIN TRANSACTION; -INSERT INTO "prices" VALUES(4029764001807,1342648800,150,250); -INSERT INTO "prices" VALUES(5449000017888,1342648800,165,250); -INSERT INTO "prices" VALUES(5449000017895,1342648800,136,250); -INSERT INTO "prices" VALUES(5449000134264,1342648800,165,250); -INSERT INTO "prices" VALUES(5449000017918,1342648800,136,250); -INSERT INTO "prices" VALUES(5449000017932,1342648800,136,250); -INSERT INTO "prices" VALUES(4104450004383,1342648800,145,250); -INSERT INTO "prices" VALUES(4047046003325,1342648800,35,100); -INSERT INTO "prices" VALUES(4047046003417,1342648800,35,100); -INSERT INTO "prices" VALUES(4047046005008,1342648800,53,110); -INSERT INTO "prices" VALUES(4001686128244,1342648800,130,180); -INSERT INTO "prices" VALUES(4001686150689,1342648800,130,180); -INSERT INTO "prices" VALUES(4001686216125,1342648800,130,180); -INSERT INTO "prices" VALUES(4001686301265,1342648800,130,180); -INSERT INTO "prices" VALUES(4001686386613,1342648800,130,180); -INSERT INTO "prices" VALUES(4001686309506,1342648800,130,180); -INSERT INTO "prices" VALUES(4001686310229,1342648800,130,180); -INSERT INTO "prices" VALUES(4001686312025,1342648800,130,180); -INSERT INTO "prices" VALUES(4001686313046,1342648800,130,180); -INSERT INTO "prices" VALUES(4001686315101,1342648800,130,180); -INSERT INTO "prices" VALUES(4001686367346,1342648800,130,180); -INSERT INTO "prices" VALUES(4001686390085,1342648800,130,180); -INSERT INTO "prices" VALUES(4001686721445,1342648800,130,180); -INSERT INTO "prices" VALUES(4003586000477,1342648800,110,140); -INSERT INTO "prices" VALUES(4003586000491,1342648800,105,140); -INSERT INTO "prices" VALUES(4007495314014,1342648800,82,120); -INSERT INTO "prices" VALUES(76222498,1342648800,71,100); -INSERT INTO "prices" VALUES(76222504,1342648800,71,100); -INSERT INTO "prices" VALUES(7622300065478,1342648800,71,100); -INSERT INTO "prices" VALUES(7622300243876,1342648800,71,100); -INSERT INTO "prices" VALUES(5000159407410,1342648800,92,140); -INSERT INTO "prices" VALUES(5000159407236,1342648800,75,100); -INSERT INTO "prices" VALUES(40111315,1342648800,75,100); -INSERT INTO "prices" VALUES(7613032625474,1342648800,58,100); -INSERT INTO "prices" VALUES(40114606,1342648800,58,100); -INSERT INTO "prices" VALUES(7613032850340,1342648800,80,100); -INSERT INTO "prices" VALUES(40084107,1342648800,80,100); -INSERT INTO "prices" VALUES(5000159418539,1342648800,75,100); -INSERT INTO "prices" VALUES(5000159418546,1342648800,75,100); -INSERT INTO "prices" VALUES(5000159418577,1342648800,75,100); -INSERT INTO "prices" VALUES(40358802,1342648800,36,60); -INSERT INTO "prices" VALUES(40084015,1342648800,30,50); -INSERT INTO "prices" VALUES(5900951020704,1342648800,83,130); -COMMIT; diff --git a/invoice/generate-invoice.py b/invoice/generate-invoice.py index 6613bfc..56db6ec 100755 --- a/invoice/generate-invoice.py +++ b/invoice/generate-invoice.py @@ -58,7 +58,7 @@ def get_invoice_data(user, start=0, stop=0): if stop > 0: stopcondition = " AND timestamp <= %d" % stop - c.execute("SELECT date(timestamp, 'unixepoch', 'localtime'), time(timestamp, 'unixepoch', 'localtime'), products.name, purchases.product, purchases.timestamp FROM purchases, products WHERE user = ? AND products.id = purchases.product" + startcondition + stopcondition + " ORDER BY timestamp;", (user,)) + c.execute("SELECT date(timestamp, 'unixepoch', 'localtime'), time(timestamp, 'unixepoch', 'localtime'), products.name, sells.product, sells.timestamp FROM sells, products WHERE user = ? AND products.id = sells.product" + startcondition + stopcondition + " ORDER BY timestamp;", (user,)) result = [] for row in c: @@ -194,7 +194,7 @@ def get_invoice_amount(user, start=0, stop=0): if user < 0: return 0 else: - query = "SELECT SUM(memberprice) FROM users, purchases purch, prices \ + query = "SELECT SUM(memberprice) FROM users, sells purch, prices \ WHERE users.id = ? AND users.id = purch.user AND purch.product = prices.product \ AND purch.timestamp >= ? AND purch.timestamp <= ? AND prices.valid_from = \ (SELECT valid_from FROM prices WHERE product = purch.product AND \ @@ -265,7 +265,7 @@ def get_users_with_purchases(start, stop): connection = sqlite3.connect('shop.db') c = connection.cursor() - c.execute("SELECT user FROM purchases WHERE timestamp >= ? AND timestamp <= ? GROUP BY user ORDER BY user;", (start,stop)) + c.execute("SELECT user FROM sells WHERE timestamp >= ? AND timestamp <= ? GROUP BY user ORDER BY user;", (start,stop)) for row in c: result.append(row[0]) diff --git a/invoice/graph/cairoplot.py b/invoice/graph/cairoplot.py deleted file mode 100755 index a15f329..0000000 --- a/invoice/graph/cairoplot.py +++ /dev/null @@ -1,2336 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# CairoPlot.py -# -# Copyright (c) 2008 Rodrigo Moreira Araújo -# -# Author: Rodrigo Moreiro Araujo -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public License -# as published by the Free Software Foundation; either version 2 of -# the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 -# USA - -#Contributor: João S. O. Bueno - -#TODO: review BarPlot Code -#TODO: x_label colision problem on Horizontal Bar Plot -#TODO: y_label's eat too much space on HBP - - -__version__ = 1.2 - -import cairo -import math -import random -from series import Series, Group, Data - -HORZ = 0 -VERT = 1 -NORM = 2 - -COLORS = {"red" : (1.0,0.0,0.0,1.0), "lime" : (0.0,1.0,0.0,1.0), "blue" : (0.0,0.0,1.0,1.0), - "maroon" : (0.5,0.0,0.0,1.0), "green" : (0.0,0.5,0.0,1.0), "navy" : (0.0,0.0,0.5,1.0), - "yellow" : (1.0,1.0,0.0,1.0), "magenta" : (1.0,0.0,1.0,1.0), "cyan" : (0.0,1.0,1.0,1.0), - "orange" : (1.0,0.5,0.0,1.0), "white" : (1.0,1.0,1.0,1.0), "black" : (0.0,0.0,0.0,1.0), - "gray" : (0.5,0.5,0.5,1.0), "light_gray" : (0.9,0.9,0.9,1.0), - "transparent" : (0.0,0.0,0.0,0.0)} - -THEMES = {"black_red" : [(0.0,0.0,0.0,1.0), (1.0,0.0,0.0,1.0)], - "red_green_blue" : [(1.0,0.0,0.0,1.0), (0.0,1.0,0.0,1.0), (0.0,0.0,1.0,1.0)], - "red_orange_yellow" : [(1.0,0.2,0.0,1.0), (1.0,0.7,0.0,1.0), (1.0,1.0,0.0,1.0)], - "yellow_orange_red" : [(1.0,1.0,0.0,1.0), (1.0,0.7,0.0,1.0), (1.0,0.2,0.0,1.0)], - "rainbow" : [(1.0,0.0,0.0,1.0), (1.0,0.5,0.0,1.0), (1.0,1.0,0.0,1.0), (0.0,1.0,0.0,1.0), (0.0,0.0,1.0,1.0), (0.3, 0.0, 0.5,1.0), (0.5, 0.0, 1.0, 1.0)]} - -def colors_from_theme( theme, series_length, mode = 'solid' ): - colors = [] - if theme not in THEMES.keys() : - raise Exception, "Theme not defined" - color_steps = THEMES[theme] - n_colors = len(color_steps) - if series_length <= n_colors: - colors = [color + tuple([mode]) for color in color_steps[0:n_colors]] - else: - iterations = [(series_length - n_colors)/(n_colors - 1) for i in color_steps[:-1]] - over_iterations = (series_length - n_colors) % (n_colors - 1) - for i in range(n_colors - 1): - if over_iterations <= 0: - break - iterations[i] += 1 - over_iterations -= 1 - for index,color in enumerate(color_steps[:-1]): - colors.append(color + tuple([mode])) - if iterations[index] == 0: - continue - next_color = color_steps[index+1] - color_step = ((next_color[0] - color[0])/(iterations[index] + 1), - (next_color[1] - color[1])/(iterations[index] + 1), - (next_color[2] - color[2])/(iterations[index] + 1), - (next_color[3] - color[3])/(iterations[index] + 1)) - for i in range( iterations[index] ): - colors.append((color[0] + color_step[0]*(i+1), - color[1] + color_step[1]*(i+1), - color[2] + color_step[2]*(i+1), - color[3] + color_step[3]*(i+1), - mode)) - colors.append(color_steps[-1] + tuple([mode])) - return colors - - -def other_direction(direction): - "explicit is better than implicit" - if direction == HORZ: - return VERT - else: - return HORZ - -#Class definition - -class Plot(object): - def __init__(self, - surface=None, - data=None, - width=640, - height=480, - background=None, - border = 0, - x_labels = None, - y_labels = None, - series_colors = None): - random.seed(2) - self.create_surface(surface, width, height) - self.dimensions = {} - self.dimensions[HORZ] = width - self.dimensions[VERT] = height - self.context = cairo.Context(self.surface) - self.labels={} - self.labels[HORZ] = x_labels - self.labels[VERT] = y_labels - self.load_series(data, x_labels, y_labels, series_colors) - self.font_size = 10 - self.set_background (background) - self.border = border - self.borders = {} - self.line_color = (0.5, 0.5, 0.5) - self.line_width = 0.5 - self.label_color = (0.0, 0.0, 0.0) - self.grid_color = (0.8, 0.8, 0.8) - - def create_surface(self, surface, width=None, height=None): - self.filename = None - if isinstance(surface, cairo.Surface): - self.surface = surface - return - if not type(surface) in (str, unicode): - raise TypeError("Surface should be either a Cairo surface or a filename, not %s" % surface) - sufix = surface.rsplit(".")[-1].lower() - self.filename = surface - if sufix == "png": - self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) - elif sufix == "ps": - self.surface = cairo.PSSurface(surface, width, height) - elif sufix == "pdf": - self.surface = cairo.PSSurface(surface, width, height) - else: - if sufix != "svg": - self.filename += ".svg" - self.surface = cairo.SVGSurface(self.filename, width, height) - - def commit(self): - try: - self.context.show_page() - if self.filename and self.filename.endswith(".png"): - self.surface.write_to_png(self.filename) - else: - self.surface.finish() - except cairo.Error: - pass - - def load_series (self, data, x_labels=None, y_labels=None, series_colors=None): - self.series_labels = [] - self.series = None - - #The pretty way - #if not isinstance(data, Series): - # # Not an instance of Series - # self.series = Series(data) - #else: - # self.series = data - # - #self.series_labels = self.series.get_names() - - #TODO: Remove on next version - # The ugly way, keeping retrocompatibility... - if callable(data) or type(data) is list and callable(data[0]): # Lambda or List of lambdas - self.series = data - self.series_labels = None - elif isinstance(data, Series): # Instance of Series - self.series = data - self.series_labels = data.get_names() - else: # Anything else - self.series = Series(data) - self.series_labels = self.series.get_names() - - #TODO: allow user passed series_widths - self.series_widths = [1.0 for group in self.series] - - #TODO: Remove on next version - self.process_colors( series_colors ) - - def process_colors( self, series_colors, length = None, mode = 'solid' ): - #series_colors might be None, a theme, a string of colors names or a list of color tuples - if length is None : - length = len( self.series.to_list() ) - - #no colors passed - if not series_colors: - #Randomize colors - self.series_colors = [ [random.random() for i in range(3)] + [1.0, mode] for series in range( length ) ] - else: - #Just theme pattern - if not hasattr( series_colors, "__iter__" ): - theme = series_colors - self.series_colors = colors_from_theme( theme.lower(), length ) - - #Theme pattern and mode - elif not hasattr(series_colors, '__delitem__') and not hasattr( series_colors[0], "__iter__" ): - theme = series_colors[0] - mode = series_colors[1] - self.series_colors = colors_from_theme( theme.lower(), length, mode ) - - #List - else: - self.series_colors = series_colors - for index, color in enumerate( self.series_colors ): - #element is a color name - if not hasattr(color, "__iter__"): - self.series_colors[index] = COLORS[color.lower()] + tuple([mode]) - #element is rgb tuple instead of rgba - elif len( color ) == 3 : - self.series_colors[index] += (1.0,mode) - #element has 4 elements, might be rgba tuple or rgb tuple with mode - elif len( color ) == 4 : - #last element is mode - if not hasattr(color[3], "__iter__"): - self.series_colors[index] += tuple([color[3]]) - self.series_colors[index][3] = 1.0 - #last element is alpha - else: - self.series_colors[index] += tuple([mode]) - - def get_width(self): - return self.surface.get_width() - - def get_height(self): - return self.surface.get_height() - - def set_background(self, background): - if background is None: - self.background = (0.0,0.0,0.0,0.0) - elif type(background) in (cairo.LinearGradient, tuple): - self.background = background - elif not hasattr(background,"__iter__"): - colors = background.split(" ") - if len(colors) == 1 and colors[0] in COLORS: - self.background = COLORS[background] - elif len(colors) > 1: - self.background = cairo.LinearGradient(self.dimensions[HORZ] / 2, 0, self.dimensions[HORZ] / 2, self.dimensions[VERT]) - for index,color in enumerate(colors): - self.background.add_color_stop_rgba(float(index)/(len(colors)-1),*COLORS[color]) - else: - raise TypeError ("Background should be either cairo.LinearGradient or a 3/4-tuple, not %s" % type(background)) - - def render_background(self): - if isinstance(self.background, cairo.LinearGradient): - self.context.set_source(self.background) - else: - self.context.set_source_rgba(*self.background) - self.context.rectangle(0,0, self.dimensions[HORZ], self.dimensions[VERT]) - self.context.fill() - - def render_bounding_box(self): - self.context.set_source_rgba(*self.line_color) - self.context.set_line_width(self.line_width) - self.context.rectangle(self.border, self.border, - self.dimensions[HORZ] - 2 * self.border, - self.dimensions[VERT] - 2 * self.border) - self.context.stroke() - - def render(self): - pass - -class ScatterPlot( Plot ): - def __init__(self, - surface=None, - data=None, - errorx=None, - errory=None, - width=640, - height=480, - background=None, - border=0, - axis = False, - dash = False, - discrete = False, - dots = 0, - grid = False, - series_legend = False, - x_labels = None, - y_labels = None, - x_bounds = None, - y_bounds = None, - z_bounds = None, - x_title = None, - y_title = None, - series_colors = None, - circle_colors = None ): - - self.bounds = {} - self.bounds[HORZ] = x_bounds - self.bounds[VERT] = y_bounds - self.bounds[NORM] = z_bounds - self.titles = {} - self.titles[HORZ] = x_title - self.titles[VERT] = y_title - self.max_value = {} - self.axis = axis - self.discrete = discrete - self.dots = dots - self.grid = grid - self.series_legend = series_legend - self.variable_radius = False - self.x_label_angle = math.pi / 2.5 - self.circle_colors = circle_colors - - Plot.__init__(self, surface, data, width, height, background, border, x_labels, y_labels, series_colors) - - self.dash = None - if dash: - if hasattr(dash, "keys"): - self.dash = [dash[key] for key in self.series_labels] - elif max([hasattr(item,'__delitem__') for item in data]) : - self.dash = dash - else: - self.dash = [dash] - - self.load_errors(errorx, errory) - - def convert_list_to_tuple(self, data): - #Data must be converted from lists of coordinates to a single - # list of tuples - out_data = zip(*data) - if len(data) == 3: - self.variable_radius = True - return out_data - - def load_series(self, data, x_labels = None, y_labels = None, series_colors=None): - #TODO: In cairoplot 2.0 keep only the Series instances - - # Convert Data and Group to Series - if isinstance(data, Data) or isinstance(data, Group): - data = Series(data) - - # Series - if isinstance(data, Series): - for group in data: - for item in group: - if len(item) is 3: - self.variable_radius = True - - #Dictionary with lists - if hasattr(data, "keys") : - if hasattr( data.values()[0][0], "__delitem__" ) : - for key in data.keys() : - data[key] = self.convert_list_to_tuple(data[key]) - elif len(data.values()[0][0]) == 3: - self.variable_radius = True - #List - elif hasattr(data[0], "__delitem__") : - #List of lists - if hasattr(data[0][0], "__delitem__") : - for index,value in enumerate(data) : - data[index] = self.convert_list_to_tuple(value) - #List - elif type(data[0][0]) != type((0,0)): - data = self.convert_list_to_tuple(data) - #Three dimensional data - elif len(data[0][0]) == 3: - self.variable_radius = True - - #List with three dimensional tuples - elif len(data[0]) == 3: - self.variable_radius = True - Plot.load_series(self, data, x_labels, y_labels, series_colors) - self.calc_boundaries() - self.calc_labels() - - def load_errors(self, errorx, errory): - self.errors = None - if errorx == None and errory == None: - return - self.errors = {} - self.errors[HORZ] = None - self.errors[VERT] = None - #asimetric errors - if errorx and hasattr(errorx[0], "__delitem__"): - self.errors[HORZ] = errorx - #simetric errors - elif errorx: - self.errors[HORZ] = [errorx] - #asimetric errors - if errory and hasattr(errory[0], "__delitem__"): - self.errors[VERT] = errory - #simetric errors - elif errory: - self.errors[VERT] = [errory] - - def calc_labels(self): - if not self.labels[HORZ]: - amplitude = self.bounds[HORZ][1] - self.bounds[HORZ][0] - if amplitude % 10: #if horizontal labels need floating points - self.labels[HORZ] = ["%.2lf" % (float(self.bounds[HORZ][0] + (amplitude * i / 10.0))) for i in range(11) ] - else: - self.labels[HORZ] = ["%d" % (int(self.bounds[HORZ][0] + (amplitude * i / 10.0))) for i in range(11) ] - if not self.labels[VERT]: - amplitude = self.bounds[VERT][1] - self.bounds[VERT][0] - if amplitude % 10: #if vertical labels need floating points - self.labels[VERT] = ["%.2lf" % (float(self.bounds[VERT][0] + (amplitude * i / 10.0))) for i in range(11) ] - else: - self.labels[VERT] = ["%d" % (int(self.bounds[VERT][0] + (amplitude * i / 10.0))) for i in range(11) ] - - def calc_extents(self, direction): - self.context.set_font_size(self.font_size * 0.8) - self.max_value[direction] = max(self.context.text_extents(item)[2] for item in self.labels[direction]) - self.borders[other_direction(direction)] = self.max_value[direction] + self.border + 20 - - def calc_boundaries(self): - #HORZ = 0, VERT = 1, NORM = 2 - min_data_value = [0,0,0] - max_data_value = [0,0,0] - - for group in self.series: - if type(group[0].content) in (int, float, long): - group = [Data((index, item.content)) for index,item in enumerate(group)] - - for point in group: - for index, item in enumerate(point.content): - if item > max_data_value[index]: - max_data_value[index] = item - elif item < min_data_value[index]: - min_data_value[index] = item - - if not self.bounds[HORZ]: - self.bounds[HORZ] = (min_data_value[HORZ], max_data_value[HORZ]) - if not self.bounds[VERT]: - self.bounds[VERT] = (min_data_value[VERT], max_data_value[VERT]) - if not self.bounds[NORM]: - self.bounds[NORM] = (min_data_value[NORM], max_data_value[NORM]) - - def calc_all_extents(self): - self.calc_extents(HORZ) - self.calc_extents(VERT) - - self.plot_height = self.dimensions[VERT] - 2 * self.borders[VERT] - self.plot_width = self.dimensions[HORZ] - 2* self.borders[HORZ] - - self.plot_top = self.dimensions[VERT] - self.borders[VERT] - - def calc_steps(self): - #Calculates all the x, y, z and color steps - series_amplitude = [self.bounds[index][1] - self.bounds[index][0] for index in range(3)] - - if series_amplitude[HORZ]: - self.horizontal_step = float (self.plot_width) / series_amplitude[HORZ] - else: - self.horizontal_step = 0.00 - - if series_amplitude[VERT]: - self.vertical_step = float (self.plot_height) / series_amplitude[VERT] - else: - self.vertical_step = 0.00 - - if series_amplitude[NORM]: - if self.variable_radius: - self.z_step = float (self.bounds[NORM][1]) / series_amplitude[NORM] - if self.circle_colors: - self.circle_color_step = tuple([float(self.circle_colors[1][i]-self.circle_colors[0][i])/series_amplitude[NORM] for i in range(4)]) - else: - self.z_step = 0.00 - self.circle_color_step = ( 0.0, 0.0, 0.0, 0.0 ) - - def get_circle_color(self, value): - return tuple( [self.circle_colors[0][i] + value*self.circle_color_step[i] for i in range(4)] ) - - def render(self): - self.calc_all_extents() - self.calc_steps() - self.render_background() - self.render_bounding_box() - if self.axis: - self.render_axis() - if self.grid: - self.render_grid() - self.render_labels() - self.render_plot() - if self.errors: - self.render_errors() - if self.series_legend and self.series_labels: - self.render_legend() - - def render_axis(self): - #Draws both the axis lines and their titles - cr = self.context - cr.set_source_rgba(*self.line_color) - cr.move_to(self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT]) - cr.line_to(self.borders[HORZ], self.borders[VERT]) - cr.stroke() - - cr.move_to(self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT]) - cr.line_to(self.dimensions[HORZ] - self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT]) - cr.stroke() - - cr.set_source_rgba(*self.label_color) - self.context.set_font_size( 1.2 * self.font_size ) - if self.titles[HORZ]: - title_width,title_height = cr.text_extents(self.titles[HORZ])[2:4] - cr.move_to( self.dimensions[HORZ]/2 - title_width/2, self.borders[VERT] - title_height/2 ) - cr.show_text( self.titles[HORZ] ) - - if self.titles[VERT]: - title_width,title_height = cr.text_extents(self.titles[VERT])[2:4] - cr.move_to( self.dimensions[HORZ] - self.borders[HORZ] + title_height/2, self.dimensions[VERT]/2 - title_width/2) - cr.save() - cr.rotate( math.pi/2 ) - cr.show_text( self.titles[VERT] ) - cr.restore() - - def render_grid(self): - cr = self.context - horizontal_step = float( self.plot_height ) / ( len( self.labels[VERT] ) - 1 ) - vertical_step = float( self.plot_width ) / ( len( self.labels[HORZ] ) - 1 ) - - x = self.borders[HORZ] + vertical_step - y = self.plot_top - horizontal_step - - for label in self.labels[HORZ][:-1]: - cr.set_source_rgba(*self.grid_color) - cr.move_to(x, self.dimensions[VERT] - self.borders[VERT]) - cr.line_to(x, self.borders[VERT]) - cr.stroke() - x += vertical_step - for label in self.labels[VERT][:-1]: - cr.set_source_rgba(*self.grid_color) - cr.move_to(self.borders[HORZ], y) - cr.line_to(self.dimensions[HORZ] - self.borders[HORZ], y) - cr.stroke() - y -= horizontal_step - - def render_labels(self): - self.context.set_font_size(self.font_size * 0.8) - self.render_horz_labels() - self.render_vert_labels() - - def render_horz_labels(self): - cr = self.context - step = float( self.plot_width ) / ( len( self.labels[HORZ] ) - 1 ) - x = self.borders[HORZ] - y = self.dimensions[VERT] - self.borders[VERT] + 5 - - # store rotation matrix from the initial state - rotation_matrix = cr.get_matrix() - rotation_matrix.rotate(self.x_label_angle) - - cr.set_source_rgba(*self.label_color) - - for item in self.labels[HORZ]: - width = cr.text_extents(item)[2] - cr.move_to(x, y) - cr.save() - cr.set_matrix(rotation_matrix) - cr.show_text(item) - cr.restore() - x += step - - def render_vert_labels(self): - cr = self.context - step = ( self.plot_height ) / ( len( self.labels[VERT] ) - 1 ) - y = self.plot_top - cr.set_source_rgba(*self.label_color) - for item in self.labels[VERT]: - width = cr.text_extents(item)[2] - cr.move_to(self.borders[HORZ] - width - 5,y) - cr.show_text(item) - y -= step - - def render_legend(self): - cr = self.context - cr.set_font_size(self.font_size) - cr.set_line_width(self.line_width) - - widest_word = max(self.series_labels, key = lambda item: self.context.text_extents(item)[2]) - tallest_word = max(self.series_labels, key = lambda item: self.context.text_extents(item)[3]) - max_width = self.context.text_extents(widest_word)[2] - max_height = self.context.text_extents(tallest_word)[3] * 1.1 - - color_box_height = max_height / 2 - color_box_width = color_box_height * 2 - - #Draw a bounding box - bounding_box_width = max_width + color_box_width + 15 - bounding_box_height = (len(self.series_labels)+0.5) * max_height - cr.set_source_rgba(1,1,1) - cr.rectangle(self.dimensions[HORZ] - self.borders[HORZ] - bounding_box_width, self.borders[VERT], - bounding_box_width, bounding_box_height) - cr.fill() - - cr.set_source_rgba(*self.line_color) - cr.set_line_width(self.line_width) - cr.rectangle(self.dimensions[HORZ] - self.borders[HORZ] - bounding_box_width, self.borders[VERT], - bounding_box_width, bounding_box_height) - cr.stroke() - - for idx,key in enumerate(self.series_labels): - #Draw color box - cr.set_source_rgba(*self.series_colors[idx][:4]) - cr.rectangle(self.dimensions[HORZ] - self.borders[HORZ] - max_width - color_box_width - 10, - self.borders[VERT] + color_box_height + (idx*max_height) , - color_box_width, color_box_height) - cr.fill() - - cr.set_source_rgba(0, 0, 0) - cr.rectangle(self.dimensions[HORZ] - self.borders[HORZ] - max_width - color_box_width - 10, - self.borders[VERT] + color_box_height + (idx*max_height), - color_box_width, color_box_height) - cr.stroke() - - #Draw series labels - cr.set_source_rgba(0, 0, 0) - cr.move_to(self.dimensions[HORZ] - self.borders[HORZ] - max_width - 5, self.borders[VERT] + ((idx+1)*max_height)) - cr.show_text(key) - - def render_errors(self): - cr = self.context - cr.rectangle(self.borders[HORZ], self.borders[VERT], self.plot_width, self.plot_height) - cr.clip() - radius = self.dots - x0 = self.borders[HORZ] - self.bounds[HORZ][0]*self.horizontal_step - y0 = self.borders[VERT] - self.bounds[VERT][0]*self.vertical_step - for index, group in enumerate(self.series): - cr.set_source_rgba(*self.series_colors[index][:4]) - for number, data in enumerate(group): - x = x0 + self.horizontal_step * data.content[0] - y = self.dimensions[VERT] - y0 - self.vertical_step * data.content[1] - if self.errors[HORZ]: - cr.move_to(x, y) - x1 = x - self.horizontal_step * self.errors[HORZ][0][number] - cr.line_to(x1, y) - cr.line_to(x1, y - radius) - cr.line_to(x1, y + radius) - cr.stroke() - if self.errors[HORZ] and len(self.errors[HORZ]) == 2: - cr.move_to(x, y) - x1 = x + self.horizontal_step * self.errors[HORZ][1][number] - cr.line_to(x1, y) - cr.line_to(x1, y - radius) - cr.line_to(x1, y + radius) - cr.stroke() - if self.errors[VERT]: - cr.move_to(x, y) - y1 = y + self.vertical_step * self.errors[VERT][0][number] - cr.line_to(x, y1) - cr.line_to(x - radius, y1) - cr.line_to(x + radius, y1) - cr.stroke() - if self.errors[VERT] and len(self.errors[VERT]) == 2: - cr.move_to(x, y) - y1 = y - self.vertical_step * self.errors[VERT][1][number] - cr.line_to(x, y1) - cr.line_to(x - radius, y1) - cr.line_to(x + radius, y1) - cr.stroke() - - - def render_plot(self): - cr = self.context - if self.discrete: - cr.rectangle(self.borders[HORZ], self.borders[VERT], self.plot_width, self.plot_height) - cr.clip() - x0 = self.borders[HORZ] - self.bounds[HORZ][0]*self.horizontal_step - y0 = self.borders[VERT] - self.bounds[VERT][0]*self.vertical_step - radius = self.dots - for number, group in enumerate (self.series): - cr.set_source_rgba(*self.series_colors[number][:4]) - for data in group : - if self.variable_radius: - radius = data.content[2]*self.z_step - if self.circle_colors: - cr.set_source_rgba( *self.get_circle_color( data.content[2]) ) - x = x0 + self.horizontal_step*data.content[0] - y = y0 + self.vertical_step*data.content[1] - cr.arc(x, self.dimensions[VERT] - y, radius, 0, 2*math.pi) - cr.fill() - else: - cr.rectangle(self.borders[HORZ], self.borders[VERT], self.plot_width, self.plot_height) - cr.clip() - x0 = self.borders[HORZ] - self.bounds[HORZ][0]*self.horizontal_step - y0 = self.borders[VERT] - self.bounds[VERT][0]*self.vertical_step - radius = self.dots - for number, group in enumerate (self.series): - last_data = None - cr.set_source_rgba(*self.series_colors[number][:4]) - for data in group : - x = x0 + self.horizontal_step*data.content[0] - y = y0 + self.vertical_step*data.content[1] - if self.dots: - if self.variable_radius: - radius = data.content[2]*self.z_step - cr.arc(x, self.dimensions[VERT] - y, radius, 0, 2*math.pi) - cr.fill() - if last_data : - old_x = x0 + self.horizontal_step*last_data.content[0] - old_y = y0 + self.vertical_step*last_data.content[1] - cr.move_to( old_x, self.dimensions[VERT] - old_y ) - cr.line_to( x, self.dimensions[VERT] - y) - cr.set_line_width(self.series_widths[number]) - - # Display line as dash line - if self.dash and self.dash[number]: - s = self.series_widths[number] - cr.set_dash([s*3, s*3], 0) - - cr.stroke() - cr.set_dash([]) - last_data = data - -class DotLinePlot(ScatterPlot): - def __init__(self, - surface=None, - data=None, - width=640, - height=480, - background=None, - border=0, - axis = False, - dash = False, - dots = 0, - grid = False, - series_legend = False, - x_labels = None, - y_labels = None, - x_bounds = None, - y_bounds = None, - x_title = None, - y_title = None, - series_colors = None): - - ScatterPlot.__init__(self, surface, data, None, None, width, height, background, border, - axis, dash, False, dots, grid, series_legend, x_labels, y_labels, - x_bounds, y_bounds, None, x_title, y_title, series_colors, None ) - - - def load_series(self, data, x_labels = None, y_labels = None, series_colors=None): - Plot.load_series(self, data, x_labels, y_labels, series_colors) - for group in self.series : - for index,data in enumerate(group): - group[index].content = (index, data.content) - - self.calc_boundaries() - self.calc_labels() - -class FunctionPlot(ScatterPlot): - def __init__(self, - surface=None, - data=None, - width=640, - height=480, - background=None, - border=0, - axis = False, - discrete = False, - dots = 0, - grid = False, - series_legend = False, - x_labels = None, - y_labels = None, - x_bounds = None, - y_bounds = None, - x_title = None, - y_title = None, - series_colors = None, - step = 1): - - self.function = data - self.step = step - self.discrete = discrete - - data, x_bounds = self.load_series_from_function( self.function, x_bounds ) - - ScatterPlot.__init__(self, surface, data, None, None, width, height, background, border, - axis, False, discrete, dots, grid, series_legend, x_labels, y_labels, - x_bounds, y_bounds, None, x_title, y_title, series_colors, None ) - - def load_series(self, data, x_labels = None, y_labels = None, series_colors=None): - Plot.load_series(self, data, x_labels, y_labels, series_colors) - - if len(self.series[0][0]) is 1: - for group_id, group in enumerate(self.series) : - for index,data in enumerate(group): - group[index].content = (self.bounds[HORZ][0] + self.step*index, data.content) - - self.calc_boundaries() - self.calc_labels() - - def load_series_from_function( self, function, x_bounds ): - #TODO: Add the possibility for the user to define multiple functions with different discretization parameters - - #This function converts a function, a list of functions or a dictionary - #of functions into its corresponding array of data - series = Series() - - if isinstance(function, Group) or isinstance(function, Data): - function = Series(function) - - # If is instance of Series - if isinstance(function, Series): - # Overwrite any bounds passed by the function - x_bounds = (function.range[0],function.range[-1]) - - #if no bounds are provided - if x_bounds == None: - x_bounds = (0,10) - - - #TODO: Finish the dict translation - if hasattr(function, "keys"): #dictionary: - for key in function.keys(): - group = Group(name=key) - #data[ key ] = [] - i = x_bounds[0] - while i <= x_bounds[1] : - group.add_data(function[ key ](i)) - #data[ key ].append( function[ key ](i) ) - i += self.step - series.add_group(group) - - elif hasattr(function, "__delitem__"): #list of functions - for index,f in enumerate( function ) : - group = Group() - #data.append( [] ) - i = x_bounds[0] - while i <= x_bounds[1] : - group.add_data(f(i)) - #data[ index ].append( f(i) ) - i += self.step - series.add_group(group) - - elif isinstance(function, Series): # instance of Series - series = function - - else: #function - group = Group() - i = x_bounds[0] - while i <= x_bounds[1] : - group.add_data(function(i)) - i += self.step - series.add_group(group) - - - return series, x_bounds - - def calc_labels(self): - if not self.labels[HORZ]: - self.labels[HORZ] = [] - i = self.bounds[HORZ][0] - while i<=self.bounds[HORZ][1]: - self.labels[HORZ].append(str(i)) - i += float(self.bounds[HORZ][1] - self.bounds[HORZ][0])/10 - ScatterPlot.calc_labels(self) - - def render_plot(self): - if not self.discrete: - ScatterPlot.render_plot(self) - else: - last = None - cr = self.context - for number, group in enumerate (self.series): - cr.set_source_rgba(*self.series_colors[number][:4]) - x0 = self.borders[HORZ] - self.bounds[HORZ][0]*self.horizontal_step - y0 = self.borders[VERT] - self.bounds[VERT][0]*self.vertical_step - for data in group: - x = x0 + self.horizontal_step * data.content[0] - y = y0 + self.vertical_step * data.content[1] - cr.move_to(x, self.dimensions[VERT] - y) - cr.line_to(x, self.plot_top) - cr.set_line_width(self.series_widths[number]) - cr.stroke() - if self.dots: - cr.new_path() - cr.arc(x, self.dimensions[VERT] - y, 3, 0, 2.1 * math.pi) - cr.close_path() - cr.fill() - -class BarPlot(Plot): - def __init__(self, - surface = None, - data = None, - width = 640, - height = 480, - background = "white light_gray", - border = 0, - display_values = False, - grid = False, - rounded_corners = False, - stack = False, - three_dimension = False, - x_labels = None, - y_labels = None, - x_bounds = None, - y_bounds = None, - series_colors = None, - main_dir = None): - - self.bounds = {} - self.bounds[HORZ] = x_bounds - self.bounds[VERT] = y_bounds - self.display_values = display_values - self.grid = grid - self.rounded_corners = rounded_corners - self.stack = stack - self.three_dimension = three_dimension - self.x_label_angle = math.pi / 2.5 - self.main_dir = main_dir - self.max_value = {} - self.plot_dimensions = {} - self.steps = {} - self.value_label_color = (0.5,0.5,0.5,1.0) - - Plot.__init__(self, surface, data, width, height, background, border, x_labels, y_labels, series_colors) - - def load_series(self, data, x_labels = None, y_labels = None, series_colors = None): - Plot.load_series(self, data, x_labels, y_labels, series_colors) - self.calc_boundaries() - - def process_colors(self, series_colors): - #Data for a BarPlot might be a List or a List of Lists. - #On the first case, colors must be generated for all bars, - #On the second, colors must be generated for each of the inner lists. - - #TODO: Didn't get it... - #if hasattr(self.data[0], '__getitem__'): - # length = max(len(series) for series in self.data) - #else: - # length = len( self.data ) - - length = max(len(group) for group in self.series) - - Plot.process_colors( self, series_colors, length, 'linear') - - def calc_boundaries(self): - if not self.bounds[self.main_dir]: - if self.stack: - max_data_value = max(sum(group.to_list()) for group in self.series) - else: - max_data_value = max(max(group.to_list()) for group in self.series) - self.bounds[self.main_dir] = (0, max_data_value) - if not self.bounds[other_direction(self.main_dir)]: - self.bounds[other_direction(self.main_dir)] = (0, len(self.series)) - - def calc_extents(self, direction): - self.max_value[direction] = 0 - if self.labels[direction]: - widest_word = max(self.labels[direction], key = lambda item: self.context.text_extents(item)[2]) - self.max_value[direction] = self.context.text_extents(widest_word)[3 - direction] - self.borders[other_direction(direction)] = (2-direction)*self.max_value[direction] + self.border + direction*(5) - else: - self.borders[other_direction(direction)] = self.border - - def calc_horz_extents(self): - self.calc_extents(HORZ) - - def calc_vert_extents(self): - self.calc_extents(VERT) - - def calc_all_extents(self): - self.calc_horz_extents() - self.calc_vert_extents() - other_dir = other_direction(self.main_dir) - self.value_label = 0 - if self.display_values: - if self.stack: - self.value_label = self.context.text_extents(str(max(sum(group.to_list()) for group in self.series)))[2 + self.main_dir] - else: - self.value_label = self.context.text_extents(str(max(max(group.to_list()) for group in self.series)))[2 + self.main_dir] - if self.labels[self.main_dir]: - self.plot_dimensions[self.main_dir] = self.dimensions[self.main_dir] - 2*self.borders[self.main_dir] - self.value_label - else: - self.plot_dimensions[self.main_dir] = self.dimensions[self.main_dir] - self.borders[self.main_dir] - 1.2*self.border - self.value_label - self.plot_dimensions[other_dir] = self.dimensions[other_dir] - self.borders[other_dir] - self.border - self.plot_top = self.dimensions[VERT] - self.borders[VERT] - - def calc_steps(self): - other_dir = other_direction(self.main_dir) - self.series_amplitude = self.bounds[self.main_dir][1] - self.bounds[self.main_dir][0] - if self.series_amplitude: - self.steps[self.main_dir] = float(self.plot_dimensions[self.main_dir])/self.series_amplitude - else: - self.steps[self.main_dir] = 0.00 - series_length = len(self.series) - self.steps[other_dir] = float(self.plot_dimensions[other_dir])/(series_length + 0.1*(series_length + 1)) - self.space = 0.1*self.steps[other_dir] - - def render(self): - self.calc_all_extents() - self.calc_steps() - self.render_background() - self.render_bounding_box() - if self.grid: - self.render_grid() - if self.three_dimension: - self.render_ground() - if self.display_values: - self.render_values() - self.render_labels() - self.render_plot() - if self.series_labels: - self.render_legend() - - def draw_3d_rectangle_front(self, x0, y0, x1, y1, shift): - self.context.rectangle(x0-shift, y0+shift, x1-x0, y1-y0) - - def draw_3d_rectangle_side(self, x0, y0, x1, y1, shift): - self.context.move_to(x1-shift,y0+shift) - self.context.line_to(x1, y0) - self.context.line_to(x1, y1) - self.context.line_to(x1-shift, y1+shift) - self.context.line_to(x1-shift, y0+shift) - self.context.close_path() - - def draw_3d_rectangle_top(self, x0, y0, x1, y1, shift): - self.context.move_to(x0-shift,y0+shift) - self.context.line_to(x0, y0) - self.context.line_to(x1, y0) - self.context.line_to(x1-shift, y0+shift) - self.context.line_to(x0-shift, y0+shift) - self.context.close_path() - - def draw_round_rectangle(self, x0, y0, x1, y1): - self.context.arc(x0+5, y0+5, 5, -math.pi, -math.pi/2) - self.context.line_to(x1-5, y0) - self.context.arc(x1-5, y0+5, 5, -math.pi/2, 0) - self.context.line_to(x1, y1-5) - self.context.arc(x1-5, y1-5, 5, 0, math.pi/2) - self.context.line_to(x0+5, y1) - self.context.arc(x0+5, y1-5, 5, math.pi/2, math.pi) - self.context.line_to(x0, y0+5) - self.context.close_path() - - def render_ground(self): - self.draw_3d_rectangle_front(self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT], - self.dimensions[HORZ] - self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT] + 5, 10) - self.context.fill() - - self.draw_3d_rectangle_side (self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT], - self.dimensions[HORZ] - self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT] + 5, 10) - self.context.fill() - - self.draw_3d_rectangle_top (self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT], - self.dimensions[HORZ] - self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT] + 5, 10) - self.context.fill() - - def render_labels(self): - self.context.set_font_size(self.font_size * 0.8) - if self.labels[HORZ]: - self.render_horz_labels() - if self.labels[VERT]: - self.render_vert_labels() - - def render_legend(self): - cr = self.context - cr.set_font_size(self.font_size) - cr.set_line_width(self.line_width) - - widest_word = max(self.series_labels, key = lambda item: self.context.text_extents(item)[2]) - tallest_word = max(self.series_labels, key = lambda item: self.context.text_extents(item)[3]) - max_width = self.context.text_extents(widest_word)[2] - max_height = self.context.text_extents(tallest_word)[3] * 1.1 + 5 - - color_box_height = max_height / 2 - color_box_width = color_box_height * 2 - - #Draw a bounding box - bounding_box_width = max_width + color_box_width + 15 - bounding_box_height = (len(self.series_labels)+0.5) * max_height - cr.set_source_rgba(1,1,1) - cr.rectangle(self.dimensions[HORZ] - self.border - bounding_box_width, self.border, - bounding_box_width, bounding_box_height) - cr.fill() - - cr.set_source_rgba(*self.line_color) - cr.set_line_width(self.line_width) - cr.rectangle(self.dimensions[HORZ] - self.border - bounding_box_width, self.border, - bounding_box_width, bounding_box_height) - cr.stroke() - - for idx,key in enumerate(self.series_labels): - #Draw color box - cr.set_source_rgba(*self.series_colors[idx][:4]) - cr.rectangle(self.dimensions[HORZ] - self.border - max_width - color_box_width - 10, - self.border + color_box_height + (idx*max_height) , - color_box_width, color_box_height) - cr.fill() - - cr.set_source_rgba(0, 0, 0) - cr.rectangle(self.dimensions[HORZ] - self.border - max_width - color_box_width - 10, - self.border + color_box_height + (idx*max_height), - color_box_width, color_box_height) - cr.stroke() - - #Draw series labels - cr.set_source_rgba(0, 0, 0) - cr.move_to(self.dimensions[HORZ] - self.border - max_width - 5, self.border + ((idx+1)*max_height)) - cr.show_text(key) - - -class HorizontalBarPlot(BarPlot): - def __init__(self, - surface = None, - data = None, - width = 640, - height = 480, - background = "white light_gray", - border = 0, - display_values = False, - grid = False, - rounded_corners = False, - stack = False, - three_dimension = False, - series_labels = None, - x_labels = None, - y_labels = None, - x_bounds = None, - y_bounds = None, - series_colors = None): - - BarPlot.__init__(self, surface, data, width, height, background, border, - display_values, grid, rounded_corners, stack, three_dimension, - x_labels, y_labels, x_bounds, y_bounds, series_colors, HORZ) - self.series_labels = series_labels - - def calc_vert_extents(self): - self.calc_extents(VERT) - if self.labels[HORZ] and not self.labels[VERT]: - self.borders[HORZ] += 10 - - def draw_rectangle_bottom(self, x0, y0, x1, y1): - self.context.arc(x0+5, y1-5, 5, math.pi/2, math.pi) - self.context.line_to(x0, y0+5) - self.context.arc(x0+5, y0+5, 5, -math.pi, -math.pi/2) - self.context.line_to(x1, y0) - self.context.line_to(x1, y1) - self.context.line_to(x0+5, y1) - self.context.close_path() - - def draw_rectangle_top(self, x0, y0, x1, y1): - self.context.arc(x1-5, y0+5, 5, -math.pi/2, 0) - self.context.line_to(x1, y1-5) - self.context.arc(x1-5, y1-5, 5, 0, math.pi/2) - self.context.line_to(x0, y1) - self.context.line_to(x0, y0) - self.context.line_to(x1, y0) - self.context.close_path() - - def draw_rectangle(self, index, length, x0, y0, x1, y1): - if length == 1: - BarPlot.draw_rectangle(self, x0, y0, x1, y1) - elif index == 0: - self.draw_rectangle_bottom(x0, y0, x1, y1) - elif index == length-1: - self.draw_rectangle_top(x0, y0, x1, y1) - else: - self.context.rectangle(x0, y0, x1-x0, y1-y0) - - #TODO: Review BarPlot.render_grid code - def render_grid(self): - self.context.set_source_rgba(0.8, 0.8, 0.8) - if self.labels[HORZ]: - self.context.set_font_size(self.font_size * 0.8) - step = (self.dimensions[HORZ] - 2*self.borders[HORZ] - self.value_label)/(len(self.labels[HORZ])-1) - x = self.borders[HORZ] - next_x = 0 - for item in self.labels[HORZ]: - width = self.context.text_extents(item)[2] - if x - width/2 > next_x and x - width/2 > self.border: - self.context.move_to(x, self.border) - self.context.line_to(x, self.dimensions[VERT] - self.borders[VERT]) - self.context.stroke() - next_x = x + width/2 - x += step - else: - lines = 11 - horizontal_step = float(self.plot_dimensions[HORZ])/(lines-1) - x = self.borders[HORZ] - for y in xrange(0, lines): - self.context.move_to(x, self.border) - self.context.line_to(x, self.dimensions[VERT] - self.borders[VERT]) - self.context.stroke() - x += horizontal_step - - def render_horz_labels(self): - step = (self.dimensions[HORZ] - 2*self.borders[HORZ])/(len(self.labels[HORZ])-1) - x = self.borders[HORZ] - next_x = 0 - - for item in self.labels[HORZ]: - self.context.set_source_rgba(*self.label_color) - width = self.context.text_extents(item)[2] - if x - width/2 > next_x and x - width/2 > self.border: - self.context.move_to(x - width/2, self.dimensions[VERT] - self.borders[VERT] + self.max_value[HORZ] + 3) - self.context.show_text(item) - next_x = x + width/2 - x += step - - def render_vert_labels(self): - series_length = len(self.labels[VERT]) - step = (self.plot_dimensions[VERT] - (series_length + 1)*self.space)/(len(self.labels[VERT])) - y = self.border + step/2 + self.space - - for item in self.labels[VERT]: - self.context.set_source_rgba(*self.label_color) - width, height = self.context.text_extents(item)[2:4] - self.context.move_to(self.borders[HORZ] - width - 5, y + height/2) - self.context.show_text(item) - y += step + self.space - self.labels[VERT].reverse() - - def render_values(self): - self.context.set_source_rgba(*self.value_label_color) - self.context.set_font_size(self.font_size * 0.8) - if self.stack: - for i,group in enumerate(self.series): - value = sum(group.to_list()) - height = self.context.text_extents(str(value))[3] - x = self.borders[HORZ] + value*self.steps[HORZ] + 2 - y = self.borders[VERT] + (i+0.5)*self.steps[VERT] + (i+1)*self.space + height/2 - self.context.move_to(x, y) - self.context.show_text(str(value)) - else: - for i,group in enumerate(self.series): - inner_step = self.steps[VERT]/len(group) - y0 = self.border + i*self.steps[VERT] + (i+1)*self.space - for number,data in enumerate(group): - height = self.context.text_extents(str(data.content))[3] - self.context.move_to(self.borders[HORZ] + data.content*self.steps[HORZ] + 2, y0 + 0.5*inner_step + height/2, ) - self.context.show_text(str(data.content)) - y0 += inner_step - - def render_plot(self): - if self.stack: - for i,group in enumerate(self.series): - x0 = self.borders[HORZ] - y0 = self.borders[VERT] + i*self.steps[VERT] + (i+1)*self.space - for number,data in enumerate(group): - if self.series_colors[number][4] in ('radial','linear') : - linear = cairo.LinearGradient( data.content*self.steps[HORZ]/2, y0, data.content*self.steps[HORZ]/2, y0 + self.steps[VERT] ) - color = self.series_colors[number] - linear.add_color_stop_rgba(0.0, 3.5*color[0]/5.0, 3.5*color[1]/5.0, 3.5*color[2]/5.0,1.0) - linear.add_color_stop_rgba(1.0, *color[:4]) - self.context.set_source(linear) - elif self.series_colors[number][4] == 'solid': - self.context.set_source_rgba(*self.series_colors[number][:4]) - if self.rounded_corners: - self.draw_rectangle(number, len(group), x0, y0, x0+data.content*self.steps[HORZ], y0+self.steps[VERT]) - self.context.fill() - else: - self.context.rectangle(x0, y0, data.content*self.steps[HORZ], self.steps[VERT]) - self.context.fill() - x0 += data.content*self.steps[HORZ] - else: - for i,group in enumerate(self.series): - inner_step = self.steps[VERT]/len(group) - x0 = self.borders[HORZ] - y0 = self.border + i*self.steps[VERT] + (i+1)*self.space - for number,data in enumerate(group): - linear = cairo.LinearGradient(data.content*self.steps[HORZ]/2, y0, data.content*self.steps[HORZ]/2, y0 + inner_step) - color = self.series_colors[number] - linear.add_color_stop_rgba(0.0, 3.5*color[0]/5.0, 3.5*color[1]/5.0, 3.5*color[2]/5.0,1.0) - linear.add_color_stop_rgba(1.0, *color[:4]) - self.context.set_source(linear) - if self.rounded_corners and data.content != 0: - BarPlot.draw_round_rectangle(self,x0, y0, x0 + data.content*self.steps[HORZ], y0 + inner_step) - self.context.fill() - else: - self.context.rectangle(x0, y0, data.content*self.steps[HORZ], inner_step) - self.context.fill() - y0 += inner_step - -class VerticalBarPlot(BarPlot): - def __init__(self, - surface = None, - data = None, - width = 640, - height = 480, - background = "white light_gray", - border = 0, - display_values = False, - grid = False, - rounded_corners = False, - stack = False, - three_dimension = False, - series_labels = None, - x_labels = None, - y_labels = None, - x_bounds = None, - y_bounds = None, - series_colors = None): - - BarPlot.__init__(self, surface, data, width, height, background, border, - display_values, grid, rounded_corners, stack, three_dimension, - x_labels, y_labels, x_bounds, y_bounds, series_colors, VERT) - self.series_labels = series_labels - - def calc_vert_extents(self): - self.calc_extents(VERT) - if self.labels[VERT] and not self.labels[HORZ]: - self.borders[VERT] += 10 - - def draw_rectangle_bottom(self, x0, y0, x1, y1): - self.context.move_to(x1,y1) - self.context.arc(x1-5, y1-5, 5, 0, math.pi/2) - self.context.line_to(x0+5, y1) - self.context.arc(x0+5, y1-5, 5, math.pi/2, math.pi) - self.context.line_to(x0, y0) - self.context.line_to(x1, y0) - self.context.line_to(x1, y1) - self.context.close_path() - - def draw_rectangle_top(self, x0, y0, x1, y1): - self.context.arc(x0+5, y0+5, 5, -math.pi, -math.pi/2) - self.context.line_to(x1-5, y0) - self.context.arc(x1-5, y0+5, 5, -math.pi/2, 0) - self.context.line_to(x1, y1) - self.context.line_to(x0, y1) - self.context.line_to(x0, y0) - self.context.close_path() - - def draw_rectangle(self, index, length, x0, y0, x1, y1): - if length == 1: - BarPlot.draw_rectangle(self, x0, y0, x1, y1) - elif index == 0: - self.draw_rectangle_bottom(x0, y0, x1, y1) - elif index == length-1: - self.draw_rectangle_top(x0, y0, x1, y1) - else: - self.context.rectangle(x0, y0, x1-x0, y1-y0) - - def render_grid(self): - self.context.set_source_rgba(0.8, 0.8, 0.8) - if self.labels[VERT]: - lines = len(self.labels[VERT]) - vertical_step = float(self.plot_dimensions[self.main_dir])/(lines-1) - y = self.borders[VERT] + self.value_label - else: - lines = 11 - vertical_step = float(self.plot_dimensions[self.main_dir])/(lines-1) - y = 1.2*self.border + self.value_label - for x in xrange(0, lines): - self.context.move_to(self.borders[HORZ], y) - self.context.line_to(self.dimensions[HORZ] - self.border, y) - self.context.stroke() - y += vertical_step - - def render_ground(self): - self.draw_3d_rectangle_front(self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT], - self.dimensions[HORZ] - self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT] + 5, 10) - self.context.fill() - - self.draw_3d_rectangle_side (self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT], - self.dimensions[HORZ] - self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT] + 5, 10) - self.context.fill() - - self.draw_3d_rectangle_top (self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT], - self.dimensions[HORZ] - self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT] + 5, 10) - self.context.fill() - - def render_horz_labels(self): - series_length = len(self.labels[HORZ]) - step = float (self.plot_dimensions[HORZ] - (series_length + 1)*self.space)/len(self.labels[HORZ]) - x = self.borders[HORZ] + step/2 + self.space - next_x = 0 - - for item in self.labels[HORZ]: - self.context.set_source_rgba(*self.label_color) - width = self.context.text_extents(item)[2] - if x - width/2 > next_x and x - width/2 > self.borders[HORZ]: - self.context.move_to(x - width/2, self.dimensions[VERT] - self.borders[VERT] + self.max_value[HORZ] + 3) - self.context.show_text(item) - next_x = x + width/2 - x += step + self.space - - def render_vert_labels(self): - self.context.set_source_rgba(*self.label_color) - y = self.borders[VERT] + self.value_label - step = (self.dimensions[VERT] - 2*self.borders[VERT] - self.value_label)/(len(self.labels[VERT]) - 1) - self.labels[VERT].reverse() - for item in self.labels[VERT]: - width, height = self.context.text_extents(item)[2:4] - self.context.move_to(self.borders[HORZ] - width - 5, y + height/2) - self.context.show_text(item) - y += step - self.labels[VERT].reverse() - - def render_values(self): - self.context.set_source_rgba(*self.value_label_color) - self.context.set_font_size(self.font_size * 0.8) - if self.stack: - for i,group in enumerate(self.series): - value = sum(group.to_list()) - width = self.context.text_extents(str(value))[2] - x = self.borders[HORZ] + (i+0.5)*self.steps[HORZ] + (i+1)*self.space - width/2 - y = value*self.steps[VERT] + 2 - self.context.move_to(x, self.plot_top-y) - self.context.show_text(str(value)) - else: - for i,group in enumerate(self.series): - inner_step = self.steps[HORZ]/len(group) - x0 = self.borders[HORZ] + i*self.steps[HORZ] + (i+1)*self.space - for number,data in enumerate(group): - width = self.context.text_extents(str(data.content))[2] - self.context.move_to(x0 + 0.5*inner_step - width/2, self.plot_top - data.content*self.steps[VERT] - 2) - self.context.show_text(str(data.content)) - x0 += inner_step - - def render_plot(self): - if self.stack: - for i,group in enumerate(self.series): - x0 = self.borders[HORZ] + i*self.steps[HORZ] + (i+1)*self.space - y0 = 0 - for number,data in enumerate(group): - if self.series_colors[number][4] in ('linear','radial'): - linear = cairo.LinearGradient( x0, data.content*self.steps[VERT]/2, x0 + self.steps[HORZ], data.content*self.steps[VERT]/2 ) - color = self.series_colors[number] - linear.add_color_stop_rgba(0.0, 3.5*color[0]/5.0, 3.5*color[1]/5.0, 3.5*color[2]/5.0,1.0) - linear.add_color_stop_rgba(1.0, *color[:4]) - self.context.set_source(linear) - elif self.series_colors[number][4] == 'solid': - self.context.set_source_rgba(*self.series_colors[number][:4]) - if self.rounded_corners: - self.draw_rectangle(number, len(group), x0, self.plot_top - y0 - data.content*self.steps[VERT], x0 + self.steps[HORZ], self.plot_top - y0) - self.context.fill() - else: - self.context.rectangle(x0, self.plot_top - y0 - data.content*self.steps[VERT], self.steps[HORZ], data.content*self.steps[VERT]) - self.context.fill() - y0 += data.content*self.steps[VERT] - else: - for i,group in enumerate(self.series): - inner_step = self.steps[HORZ]/len(group) - y0 = self.borders[VERT] - x0 = self.borders[HORZ] + i*self.steps[HORZ] + (i+1)*self.space - for number,data in enumerate(group): - if self.series_colors[number][4] == 'linear': - linear = cairo.LinearGradient( x0, data.content*self.steps[VERT]/2, x0 + inner_step, data.content*self.steps[VERT]/2 ) - color = self.series_colors[number] - linear.add_color_stop_rgba(0.0, 3.5*color[0]/5.0, 3.5*color[1]/5.0, 3.5*color[2]/5.0,1.0) - linear.add_color_stop_rgba(1.0, *color[:4]) - self.context.set_source(linear) - elif self.series_colors[number][4] == 'solid': - self.context.set_source_rgba(*self.series_colors[number][:4]) - if self.rounded_corners and data.content != 0: - BarPlot.draw_round_rectangle(self, x0, self.plot_top - data.content*self.steps[VERT], x0+inner_step, self.plot_top) - self.context.fill() - elif self.three_dimension: - self.draw_3d_rectangle_front(x0, self.plot_top - data.content*self.steps[VERT], x0+inner_step, self.plot_top, 5) - self.context.fill() - self.draw_3d_rectangle_side(x0, self.plot_top - data.content*self.steps[VERT], x0+inner_step, self.plot_top, 5) - self.context.fill() - self.draw_3d_rectangle_top(x0, self.plot_top - data.content*self.steps[VERT], x0+inner_step, self.plot_top, 5) - self.context.fill() - else: - self.context.rectangle(x0, self.plot_top - data.content*self.steps[VERT], inner_step, data.content*self.steps[VERT]) - self.context.fill() - - x0 += inner_step - -class StreamChart(VerticalBarPlot): - def __init__(self, - surface = None, - data = None, - width = 640, - height = 480, - background = "white light_gray", - border = 0, - grid = False, - series_legend = None, - x_labels = None, - x_bounds = None, - y_bounds = None, - series_colors = None): - - VerticalBarPlot.__init__(self, surface, data, width, height, background, border, - False, grid, False, True, False, - None, x_labels, None, x_bounds, y_bounds, series_colors) - - def calc_steps(self): - other_dir = other_direction(self.main_dir) - self.series_amplitude = self.bounds[self.main_dir][1] - self.bounds[self.main_dir][0] - if self.series_amplitude: - self.steps[self.main_dir] = float(self.plot_dimensions[self.main_dir])/self.series_amplitude - else: - self.steps[self.main_dir] = 0.00 - series_length = len(self.data) - self.steps[other_dir] = float(self.plot_dimensions[other_dir])/series_length - - def render_legend(self): - pass - - def ground(self, index): - sum_values = sum(self.data[index]) - return -0.5*sum_values - - def calc_angles(self): - middle = self.plot_top - self.plot_dimensions[VERT]/2.0 - self.angles = [tuple([0.0 for x in range(len(self.data)+1)])] - for x_index in range(1, len(self.data)-1): - t = [] - x0 = self.borders[HORZ] + (0.5 + x_index - 1)*self.steps[HORZ] - x2 = self.borders[HORZ] + (0.5 + x_index + 1)*self.steps[HORZ] - y0 = middle - self.ground(x_index-1)*self.steps[VERT] - y2 = middle - self.ground(x_index+1)*self.steps[VERT] - t.append(math.atan(float(y0-y2)/(x0-x2))) - for data_index in range(len(self.data[x_index])): - x0 = self.borders[HORZ] + (0.5 + x_index - 1)*self.steps[HORZ] - x2 = self.borders[HORZ] + (0.5 + x_index + 1)*self.steps[HORZ] - y0 = middle - self.ground(x_index-1)*self.steps[VERT] - self.data[x_index-1][data_index]*self.steps[VERT] - y2 = middle - self.ground(x_index+1)*self.steps[VERT] - self.data[x_index+1][data_index]*self.steps[VERT] - - for i in range(0,data_index): - y0 -= self.data[x_index-1][i]*self.steps[VERT] - y2 -= self.data[x_index+1][i]*self.steps[VERT] - - if data_index == len(self.data[0])-1 and False: - self.context.set_source_rgba(0.0,0.0,0.0,0.3) - self.context.move_to(x0,y0) - self.context.line_to(x2,y2) - self.context.stroke() - self.context.arc(x0,y0,2,0,2*math.pi) - self.context.fill() - t.append(math.atan(float(y0-y2)/(x0-x2))) - self.angles.append(tuple(t)) - self.angles.append(tuple([0.0 for x in range(len(self.data)+1)])) - - def render_plot(self): - self.calc_angles() - middle = self.plot_top - self.plot_dimensions[VERT]/2.0 - p = 0.4*self.steps[HORZ] - for data_index in range(len(self.data[0])-1,-1,-1): - self.context.set_source_rgba(*self.series_colors[data_index][:4]) - - #draw the upper line - for x_index in range(len(self.data)-1) : - x1 = self.borders[HORZ] + (0.5 + x_index)*self.steps[HORZ] - y1 = middle - self.ground(x_index)*self.steps[VERT] - self.data[x_index][data_index]*self.steps[VERT] - x2 = self.borders[HORZ] + (0.5 + x_index + 1)*self.steps[HORZ] - y2 = middle - self.ground(x_index + 1)*self.steps[VERT] - self.data[x_index + 1][data_index]*self.steps[VERT] - - for i in range(0,data_index): - y1 -= self.data[x_index][i]*self.steps[VERT] - y2 -= self.data[x_index+1][i]*self.steps[VERT] - - if x_index == 0: - self.context.move_to(x1,y1) - - ang1 = self.angles[x_index][data_index+1] - ang2 = self.angles[x_index+1][data_index+1] + math.pi - self.context.curve_to(x1+p*math.cos(ang1),y1+p*math.sin(ang1), - x2+p*math.cos(ang2),y2+p*math.sin(ang2), - x2,y2) - - for x_index in range(len(self.data)-1,0,-1) : - x1 = self.borders[HORZ] + (0.5 + x_index)*self.steps[HORZ] - y1 = middle - self.ground(x_index)*self.steps[VERT] - x2 = self.borders[HORZ] + (0.5 + x_index - 1)*self.steps[HORZ] - y2 = middle - self.ground(x_index - 1)*self.steps[VERT] - - for i in range(0,data_index): - y1 -= self.data[x_index][i]*self.steps[VERT] - y2 -= self.data[x_index-1][i]*self.steps[VERT] - - if x_index == len(self.data)-1: - self.context.line_to(x1,y1+2) - - #revert angles by pi degrees to take the turn back - ang1 = self.angles[x_index][data_index] + math.pi - ang2 = self.angles[x_index-1][data_index] - self.context.curve_to(x1+p*math.cos(ang1),y1+p*math.sin(ang1), - x2+p*math.cos(ang2),y2+p*math.sin(ang2), - x2,y2+2) - - self.context.close_path() - self.context.fill() - - if False: - self.context.move_to(self.borders[HORZ] + 0.5*self.steps[HORZ], middle) - for x_index in range(len(self.data)-1) : - x1 = self.borders[HORZ] + (0.5 + x_index)*self.steps[HORZ] - y1 = middle - self.ground(x_index)*self.steps[VERT] - self.data[x_index][data_index]*self.steps[VERT] - x2 = self.borders[HORZ] + (0.5 + x_index + 1)*self.steps[HORZ] - y2 = middle - self.ground(x_index + 1)*self.steps[VERT] - self.data[x_index + 1][data_index]*self.steps[VERT] - - for i in range(0,data_index): - y1 -= self.data[x_index][i]*self.steps[VERT] - y2 -= self.data[x_index+1][i]*self.steps[VERT] - - ang1 = self.angles[x_index][data_index+1] - ang2 = self.angles[x_index+1][data_index+1] + math.pi - self.context.set_source_rgba(1.0,0.0,0.0) - self.context.arc(x1+p*math.cos(ang1),y1+p*math.sin(ang1),2,0,2*math.pi) - self.context.fill() - self.context.set_source_rgba(0.0,0.0,0.0) - self.context.arc(x2+p*math.cos(ang2),y2+p*math.sin(ang2),2,0,2*math.pi) - self.context.fill() - '''self.context.set_source_rgba(0.0,0.0,0.0,0.3) - self.context.arc(x2,y2,2,0,2*math.pi) - self.context.fill()''' - self.context.move_to(x1,y1) - self.context.line_to(x1+p*math.cos(ang1),y1+p*math.sin(ang1)) - self.context.stroke() - self.context.move_to(x2,y2) - self.context.line_to(x2+p*math.cos(ang2),y2+p*math.sin(ang2)) - self.context.stroke() - if False: - for x_index in range(len(self.data)-1,0,-1) : - x1 = self.borders[HORZ] + (0.5 + x_index)*self.steps[HORZ] - y1 = middle - self.ground(x_index)*self.steps[VERT] - x2 = self.borders[HORZ] + (0.5 + x_index - 1)*self.steps[HORZ] - y2 = middle - self.ground(x_index - 1)*self.steps[VERT] - - for i in range(0,data_index): - y1 -= self.data[x_index][i]*self.steps[VERT] - y2 -= self.data[x_index-1][i]*self.steps[VERT] - - #revert angles by pi degrees to take the turn back - ang1 = self.angles[x_index][data_index] + math.pi - ang2 = self.angles[x_index-1][data_index] - self.context.set_source_rgba(0.0,1.0,0.0) - self.context.arc(x1+p*math.cos(ang1),y1+p*math.sin(ang1),2,0,2*math.pi) - self.context.fill() - self.context.set_source_rgba(0.0,0.0,1.0) - self.context.arc(x2+p*math.cos(ang2),y2+p*math.sin(ang2),2,0,2*math.pi) - self.context.fill() - '''self.context.set_source_rgba(0.0,0.0,0.0,0.3) - self.context.arc(x2,y2,2,0,2*math.pi) - self.context.fill()''' - self.context.move_to(x1,y1) - self.context.line_to(x1+p*math.cos(ang1),y1+p*math.sin(ang1)) - self.context.stroke() - self.context.move_to(x2,y2) - self.context.line_to(x2+p*math.cos(ang2),y2+p*math.sin(ang2)) - self.context.stroke() - #break - - #self.context.arc(self.dimensions[HORZ]/2, self.dimensions[VERT]/2,50,0,3*math.pi/2) - #self.context.fill() - - -class PiePlot(Plot): - #TODO: Check the old cairoplot, graphs aren't matching - def __init__ (self, - surface = None, - data = None, - width = 640, - height = 480, - background = "white light_gray", - gradient = False, - shadow = False, - colors = None): - - Plot.__init__( self, surface, data, width, height, background, series_colors = colors ) - self.center = (self.dimensions[HORZ]/2, self.dimensions[VERT]/2) - self.total = sum( self.series.to_list() ) - self.radius = min(self.dimensions[HORZ]/3,self.dimensions[VERT]/3) - self.gradient = gradient - self.shadow = shadow - - def sort_function(x,y): - return x.content - y.content - - def load_series(self, data, x_labels=None, y_labels=None, series_colors=None): - Plot.load_series(self, data, x_labels, y_labels, series_colors) - # Already done inside series - #self.data = sorted(self.data) - - def draw_piece(self, angle, next_angle): - self.context.move_to(self.center[0],self.center[1]) - self.context.line_to(self.center[0] + self.radius*math.cos(angle), self.center[1] + self.radius*math.sin(angle)) - self.context.arc(self.center[0], self.center[1], self.radius, angle, next_angle) - self.context.line_to(self.center[0], self.center[1]) - self.context.close_path() - - def render(self): - self.render_background() - self.render_bounding_box() - if self.shadow: - self.render_shadow() - self.render_plot() - self.render_series_labels() - - def render_shadow(self): - horizontal_shift = 3 - vertical_shift = 3 - self.context.set_source_rgba(0, 0, 0, 0.5) - self.context.arc(self.center[0] + horizontal_shift, self.center[1] + vertical_shift, self.radius, 0, 2*math.pi) - self.context.fill() - - def render_series_labels(self): - angle = 0 - next_angle = 0 - x0,y0 = self.center - cr = self.context - for number,key in enumerate(self.series_labels): - # self.data[number] should be just a number - data = sum(self.series[number].to_list()) - - next_angle = angle + 2.0*math.pi*data/self.total - cr.set_source_rgba(*self.series_colors[number][:4]) - w = cr.text_extents(key)[2] - if (angle + next_angle)/2 < math.pi/2 or (angle + next_angle)/2 > 3*math.pi/2: - cr.move_to(x0 + (self.radius+10)*math.cos((angle+next_angle)/2), y0 + (self.radius+10)*math.sin((angle+next_angle)/2) ) - else: - cr.move_to(x0 + (self.radius+10)*math.cos((angle+next_angle)/2) - w, y0 + (self.radius+10)*math.sin((angle+next_angle)/2) ) - cr.show_text(key) - angle = next_angle - - def render_plot(self): - angle = 0 - next_angle = 0 - x0,y0 = self.center - cr = self.context - for number,group in enumerate(self.series): - # Group should be just a number - data = sum(group.to_list()) - next_angle = angle + 2.0*math.pi*data/self.total - if self.gradient or self.series_colors[number][4] in ('linear','radial'): - gradient_color = cairo.RadialGradient(self.center[0], self.center[1], 0, self.center[0], self.center[1], self.radius) - gradient_color.add_color_stop_rgba(0.3, *self.series_colors[number][:4]) - gradient_color.add_color_stop_rgba(1, self.series_colors[number][0]*0.7, - self.series_colors[number][1]*0.7, - self.series_colors[number][2]*0.7, - self.series_colors[number][3]) - cr.set_source(gradient_color) - else: - cr.set_source_rgba(*self.series_colors[number][:4]) - - self.draw_piece(angle, next_angle) - cr.fill() - - cr.set_source_rgba(1.0, 1.0, 1.0) - self.draw_piece(angle, next_angle) - cr.stroke() - - angle = next_angle - -class DonutPlot(PiePlot): - def __init__ (self, - surface = None, - data = None, - width = 640, - height = 480, - background = "white light_gray", - gradient = False, - shadow = False, - colors = None, - inner_radius=-1): - - Plot.__init__( self, surface, data, width, height, background, series_colors = colors ) - - self.center = ( self.dimensions[HORZ]/2, self.dimensions[VERT]/2 ) - self.total = sum( self.series.to_list() ) - self.radius = min( self.dimensions[HORZ]/3,self.dimensions[VERT]/3 ) - self.inner_radius = inner_radius*self.radius - - if inner_radius == -1: - self.inner_radius = self.radius/3 - - self.gradient = gradient - self.shadow = shadow - - def draw_piece(self, angle, next_angle): - self.context.move_to(self.center[0] + (self.inner_radius)*math.cos(angle), self.center[1] + (self.inner_radius)*math.sin(angle)) - self.context.line_to(self.center[0] + self.radius*math.cos(angle), self.center[1] + self.radius*math.sin(angle)) - self.context.arc(self.center[0], self.center[1], self.radius, angle, next_angle) - self.context.line_to(self.center[0] + (self.inner_radius)*math.cos(next_angle), self.center[1] + (self.inner_radius)*math.sin(next_angle)) - self.context.arc_negative(self.center[0], self.center[1], self.inner_radius, next_angle, angle) - self.context.close_path() - - def render_shadow(self): - horizontal_shift = 3 - vertical_shift = 3 - self.context.set_source_rgba(0, 0, 0, 0.5) - self.context.arc(self.center[0] + horizontal_shift, self.center[1] + vertical_shift, self.inner_radius, 0, 2*math.pi) - self.context.arc_negative(self.center[0] + horizontal_shift, self.center[1] + vertical_shift, self.radius, 0, -2*math.pi) - self.context.fill() - -class GanttChart (Plot) : - def __init__(self, - surface = None, - data = None, - width = 640, - height = 480, - x_labels = None, - y_labels = None, - colors = None): - self.bounds = {} - self.max_value = {} - Plot.__init__(self, surface, data, width, height, x_labels = x_labels, y_labels = y_labels, series_colors = colors) - - def load_series(self, data, x_labels=None, y_labels=None, series_colors=None): - Plot.load_series(self, data, x_labels, y_labels, series_colors) - self.calc_boundaries() - - def calc_boundaries(self): - self.bounds[HORZ] = (0,len(self.series)) - end_pos = max(self.series.to_list()) - - #for group in self.series: - # if hasattr(item, "__delitem__"): - # for sub_item in item: - # end_pos = max(sub_item) - # else: - # end_pos = max(item) - self.bounds[VERT] = (0,end_pos) - - def calc_extents(self, direction): - self.max_value[direction] = 0 - if self.labels[direction]: - self.max_value[direction] = max(self.context.text_extents(item)[2] for item in self.labels[direction]) - else: - self.max_value[direction] = self.context.text_extents( str(self.bounds[direction][1] + 1) )[2] - - def calc_horz_extents(self): - self.calc_extents(HORZ) - self.borders[HORZ] = 100 + self.max_value[HORZ] - - def calc_vert_extents(self): - self.calc_extents(VERT) - self.borders[VERT] = self.dimensions[VERT]/(self.bounds[HORZ][1] + 1) - - def calc_steps(self): - self.horizontal_step = (self.dimensions[HORZ] - self.borders[HORZ])/(len(self.labels[VERT])) - self.vertical_step = self.borders[VERT] - - def render(self): - self.calc_horz_extents() - self.calc_vert_extents() - self.calc_steps() - self.render_background() - - self.render_labels() - self.render_grid() - self.render_plot() - - def render_background(self): - cr = self.context - cr.set_source_rgba(255,255,255) - cr.rectangle(0,0,self.dimensions[HORZ], self.dimensions[VERT]) - cr.fill() - for number,group in enumerate(self.series): - linear = cairo.LinearGradient(self.dimensions[HORZ]/2, self.borders[VERT] + number*self.vertical_step, - self.dimensions[HORZ]/2, self.borders[VERT] + (number+1)*self.vertical_step) - linear.add_color_stop_rgba(0,1.0,1.0,1.0,1.0) - linear.add_color_stop_rgba(1.0,0.9,0.9,0.9,1.0) - cr.set_source(linear) - cr.rectangle(0,self.borders[VERT] + number*self.vertical_step,self.dimensions[HORZ],self.vertical_step) - cr.fill() - - def render_grid(self): - cr = self.context - cr.set_source_rgba(0.7, 0.7, 0.7) - cr.set_dash((1,0,0,0,0,0,1)) - cr.set_line_width(0.5) - for number,label in enumerate(self.labels[VERT]): - h = cr.text_extents(label)[3] - cr.move_to(self.borders[HORZ] + number*self.horizontal_step, self.vertical_step/2 + h) - cr.line_to(self.borders[HORZ] + number*self.horizontal_step, self.dimensions[VERT]) - cr.stroke() - - def render_labels(self): - self.context.set_font_size(0.02 * self.dimensions[HORZ]) - - self.render_horz_labels() - self.render_vert_labels() - - def render_horz_labels(self): - cr = self.context - labels = self.labels[HORZ] - if not labels: - labels = [str(i) for i in range(1, self.bounds[HORZ][1] + 1) ] - for number,label in enumerate(labels): - if label != None: - cr.set_source_rgba(0.5, 0.5, 0.5) - w,h = cr.text_extents(label)[2], cr.text_extents(label)[3] - cr.move_to(40,self.borders[VERT] + number*self.vertical_step + self.vertical_step/2 + h/2) - cr.show_text(label) - - def render_vert_labels(self): - cr = self.context - labels = self.labels[VERT] - if not labels: - labels = [str(i) for i in range(1, self.bounds[VERT][1] + 1) ] - for number,label in enumerate(labels): - w,h = cr.text_extents(label)[2], cr.text_extents(label)[3] - cr.move_to(self.borders[HORZ] + number*self.horizontal_step - w/2, self.vertical_step/2) - cr.show_text(label) - - def render_rectangle(self, x0, y0, x1, y1, color): - self.draw_shadow(x0, y0, x1, y1) - self.draw_rectangle(x0, y0, x1, y1, color) - - def draw_rectangular_shadow(self, gradient, x0, y0, w, h): - self.context.set_source(gradient) - self.context.rectangle(x0,y0,w,h) - self.context.fill() - - def draw_circular_shadow(self, x, y, radius, ang_start, ang_end, mult, shadow): - gradient = cairo.RadialGradient(x, y, 0, x, y, 2*radius) - gradient.add_color_stop_rgba(0, 0, 0, 0, shadow) - gradient.add_color_stop_rgba(1, 0, 0, 0, 0) - self.context.set_source(gradient) - self.context.move_to(x,y) - self.context.line_to(x + mult[0]*radius,y + mult[1]*radius) - self.context.arc(x, y, 8, ang_start, ang_end) - self.context.line_to(x,y) - self.context.close_path() - self.context.fill() - - def draw_rectangle(self, x0, y0, x1, y1, color): - cr = self.context - middle = (x0+x1)/2 - linear = cairo.LinearGradient(middle,y0,middle,y1) - linear.add_color_stop_rgba(0,3.5*color[0]/5.0, 3.5*color[1]/5.0, 3.5*color[2]/5.0,1.0) - linear.add_color_stop_rgba(1,*color[:4]) - cr.set_source(linear) - - cr.arc(x0+5, y0+5, 5, 0, 2*math.pi) - cr.arc(x1-5, y0+5, 5, 0, 2*math.pi) - cr.arc(x0+5, y1-5, 5, 0, 2*math.pi) - cr.arc(x1-5, y1-5, 5, 0, 2*math.pi) - cr.rectangle(x0+5,y0,x1-x0-10,y1-y0) - cr.rectangle(x0,y0+5,x1-x0,y1-y0-10) - cr.fill() - - def draw_shadow(self, x0, y0, x1, y1): - shadow = 0.4 - h_mid = (x0+x1)/2 - v_mid = (y0+y1)/2 - h_linear_1 = cairo.LinearGradient(h_mid,y0-4,h_mid,y0+4) - h_linear_2 = cairo.LinearGradient(h_mid,y1-4,h_mid,y1+4) - v_linear_1 = cairo.LinearGradient(x0-4,v_mid,x0+4,v_mid) - v_linear_2 = cairo.LinearGradient(x1-4,v_mid,x1+4,v_mid) - - h_linear_1.add_color_stop_rgba( 0, 0, 0, 0, 0) - h_linear_1.add_color_stop_rgba( 1, 0, 0, 0, shadow) - h_linear_2.add_color_stop_rgba( 0, 0, 0, 0, shadow) - h_linear_2.add_color_stop_rgba( 1, 0, 0, 0, 0) - v_linear_1.add_color_stop_rgba( 0, 0, 0, 0, 0) - v_linear_1.add_color_stop_rgba( 1, 0, 0, 0, shadow) - v_linear_2.add_color_stop_rgba( 0, 0, 0, 0, shadow) - v_linear_2.add_color_stop_rgba( 1, 0, 0, 0, 0) - - self.draw_rectangular_shadow(h_linear_1,x0+4,y0-4,x1-x0-8,8) - self.draw_rectangular_shadow(h_linear_2,x0+4,y1-4,x1-x0-8,8) - self.draw_rectangular_shadow(v_linear_1,x0-4,y0+4,8,y1-y0-8) - self.draw_rectangular_shadow(v_linear_2,x1-4,y0+4,8,y1-y0-8) - - self.draw_circular_shadow(x0+4, y0+4, 4, math.pi, 3*math.pi/2, (-1,0), shadow) - self.draw_circular_shadow(x1-4, y0+4, 4, 3*math.pi/2, 2*math.pi, (0,-1), shadow) - self.draw_circular_shadow(x0+4, y1-4, 4, math.pi/2, math.pi, (0,1), shadow) - self.draw_circular_shadow(x1-4, y1-4, 4, 0, math.pi/2, (1,0), shadow) - - def render_plot(self): - for index,group in enumerate(self.series): - for data in group: - self.render_rectangle(self.borders[HORZ] + data.content[0]*self.horizontal_step, - self.borders[VERT] + index*self.vertical_step + self.vertical_step/4.0, - self.borders[HORZ] + data.content[1]*self.horizontal_step, - self.borders[VERT] + index*self.vertical_step + 3.0*self.vertical_step/4.0, - self.series_colors[index]) - -# Function definition - -def scatter_plot(name, - data = None, - errorx = None, - errory = None, - width = 640, - height = 480, - background = "white light_gray", - border = 0, - axis = False, - dash = False, - discrete = False, - dots = False, - grid = False, - series_legend = False, - x_labels = None, - y_labels = None, - x_bounds = None, - y_bounds = None, - z_bounds = None, - x_title = None, - y_title = None, - series_colors = None, - circle_colors = None): - - ''' - - Function to plot scatter data. - - - Parameters - - data - The values to be ploted might be passed in a two basic: - list of points: [(0,0), (0,1), (0,2)] or [(0,0,1), (0,1,4), (0,2,1)] - lists of coordinates: [ [0,0,0] , [0,1,2] ] or [ [0,0,0] , [0,1,2] , [1,4,1] ] - Notice that these kinds of that can be grouped in order to form more complex data - using lists of lists or dictionaries; - series_colors - Define color values for each of the series - circle_colors - Define a lower and an upper bound for the circle colors for variable radius - (3 dimensions) series - ''' - - plot = ScatterPlot( name, data, errorx, errory, width, height, background, border, - axis, dash, discrete, dots, grid, series_legend, x_labels, y_labels, - x_bounds, y_bounds, z_bounds, x_title, y_title, series_colors, circle_colors ) - plot.render() - plot.commit() - -def dot_line_plot(name, - data, - width, - height, - background = "white light_gray", - border = 0, - axis = False, - dash = False, - dots = False, - grid = False, - series_legend = False, - x_labels = None, - y_labels = None, - x_bounds = None, - y_bounds = None, - x_title = None, - y_title = None, - series_colors = None): - ''' - - Function to plot graphics using dots and lines. - - dot_line_plot (name, data, width, height, background = "white light_gray", border = 0, axis = False, grid = False, x_labels = None, y_labels = None, x_bounds = None, y_bounds = None) - - - Parameters - - name - Name of the desired output file, no need to input the .svg as it will be added at runtim; - data - The list, list of lists or dictionary holding the data to be plotted; - width, height - Dimensions of the output image; - background - A 3 element tuple representing the rgb color expected for the background or a new cairo linear gradient. - If left None, a gray to white gradient will be generated; - border - Distance in pixels of a square border into which the graphics will be drawn; - axis - Whether or not the axis are to be drawn; - dash - Boolean or a list or a dictionary of booleans indicating whether or not the associated series should be drawn in dashed mode; - dots - Whether or not dots should be drawn on each point; - grid - Whether or not the gris is to be drawn; - series_legend - Whether or not the legend is to be drawn; - x_labels, y_labels - lists of strings containing the horizontal and vertical labels for the axis; - x_bounds, y_bounds - tuples containing the lower and upper value bounds for the data to be plotted; - x_title - Whether or not to plot a title over the x axis. - y_title - Whether or not to plot a title over the y axis. - - - Examples of use - - data = [0, 1, 3, 8, 9, 0, 10, 10, 2, 1] - CairoPlot.dot_line_plot('teste', data, 400, 300) - - data = { "john" : [10, 10, 10, 10, 30], "mary" : [0, 0, 3, 5, 15], "philip" : [13, 32, 11, 25, 2] } - x_labels = ["jan/2008", "feb/2008", "mar/2008", "apr/2008", "may/2008" ] - CairoPlot.dot_line_plot( 'test', data, 400, 300, axis = True, grid = True, - series_legend = True, x_labels = x_labels ) - ''' - plot = DotLinePlot( name, data, width, height, background, border, - axis, dash, dots, grid, series_legend, x_labels, y_labels, - x_bounds, y_bounds, x_title, y_title, series_colors ) - plot.render() - plot.commit() - -def function_plot(name, - data, - width, - height, - background = "white light_gray", - border = 0, - axis = True, - dots = False, - discrete = False, - grid = False, - series_legend = False, - x_labels = None, - y_labels = None, - x_bounds = None, - y_bounds = None, - x_title = None, - y_title = None, - series_colors = None, - step = 1): - - ''' - - Function to plot functions. - - function_plot(name, data, width, height, background = "white light_gray", border = 0, axis = True, grid = False, dots = False, x_labels = None, y_labels = None, x_bounds = None, y_bounds = None, step = 1, discrete = False) - - - Parameters - - name - Name of the desired output file, no need to input the .svg as it will be added at runtim; - data - The list, list of lists or dictionary holding the data to be plotted; - width, height - Dimensions of the output image; - background - A 3 element tuple representing the rgb color expected for the background or a new cairo linear gradient. - If left None, a gray to white gradient will be generated; - border - Distance in pixels of a square border into which the graphics will be drawn; - axis - Whether or not the axis are to be drawn; - grid - Whether or not the gris is to be drawn; - dots - Whether or not dots should be shown at each point; - x_labels, y_labels - lists of strings containing the horizontal and vertical labels for the axis; - x_bounds, y_bounds - tuples containing the lower and upper value bounds for the data to be plotted; - step - the horizontal distance from one point to the other. The smaller, the smoother the curve will be; - discrete - whether or not the function should be plotted in discrete format. - - - Example of use - - data = lambda x : x**2 - CairoPlot.function_plot('function4', data, 400, 300, grid = True, x_bounds=(-10,10), step = 0.1) - ''' - - plot = FunctionPlot( name, data, width, height, background, border, - axis, discrete, dots, grid, series_legend, x_labels, y_labels, - x_bounds, y_bounds, x_title, y_title, series_colors, step ) - plot.render() - plot.commit() - -def pie_plot( name, data, width, height, background = "white light_gray", gradient = False, shadow = False, colors = None ): - - ''' - - Function to plot pie graphics. - - pie_plot(name, data, width, height, background = "white light_gray", gradient = False, colors = None) - - - Parameters - - name - Name of the desired output file, no need to input the .svg as it will be added at runtim; - data - The list, list of lists or dictionary holding the data to be plotted; - width, height - Dimensions of the output image; - background - A 3 element tuple representing the rgb color expected for the background or a new cairo linear gradient. - If left None, a gray to white gradient will be generated; - gradient - Whether or not the pie color will be painted with a gradient; - shadow - Whether or not there will be a shadow behind the pie; - colors - List of slices colors. - - - Example of use - - teste_data = {"john" : 123, "mary" : 489, "philip" : 890 , "suzy" : 235} - CairoPlot.pie_plot("pie_teste", teste_data, 500, 500) - ''' - - plot = PiePlot( name, data, width, height, background, gradient, shadow, colors ) - plot.render() - plot.commit() - -def donut_plot(name, data, width, height, background = "white light_gray", gradient = False, shadow = False, colors = None, inner_radius = -1): - - ''' - - Function to plot donut graphics. - - donut_plot(name, data, width, height, background = "white light_gray", gradient = False, inner_radius = -1) - - - Parameters - - name - Name of the desired output file, no need to input the .svg as it will be added at runtim; - data - The list, list of lists or dictionary holding the data to be plotted; - width, height - Dimensions of the output image; - background - A 3 element tuple representing the rgb color expected for the background or a new cairo linear gradient. - If left None, a gray to white gradient will be generated; - shadow - Whether or not there will be a shadow behind the donut; - gradient - Whether or not the donut color will be painted with a gradient; - colors - List of slices colors; - inner_radius - The radius of the donut's inner circle. - - - Example of use - - teste_data = {"john" : 123, "mary" : 489, "philip" : 890 , "suzy" : 235} - CairoPlot.donut_plot("donut_teste", teste_data, 500, 500) - ''' - - plot = DonutPlot(name, data, width, height, background, gradient, shadow, colors, inner_radius) - plot.render() - plot.commit() - -def gantt_chart(name, pieces, width, height, x_labels, y_labels, colors): - - ''' - - Function to generate Gantt Charts. - - gantt_chart(name, pieces, width, height, x_labels, y_labels, colors): - - - Parameters - - name - Name of the desired output file, no need to input the .svg as it will be added at runtim; - pieces - A list defining the spaces to be drawn. The user must pass, for each line, the index of its start and the index of its end. If a line must have two or more spaces, they must be passed inside a list; - width, height - Dimensions of the output image; - x_labels - A list of names for each of the vertical lines; - y_labels - A list of names for each of the horizontal spaces; - colors - List containing the colors expected for each of the horizontal spaces - - - Example of use - - pieces = [ (0.5,5.5) , [(0,4),(6,8)] , (5.5,7) , (7,8)] - x_labels = [ 'teste01', 'teste02', 'teste03', 'teste04'] - y_labels = [ '0001', '0002', '0003', '0004', '0005', '0006', '0007', '0008', '0009', '0010' ] - colors = [ (1.0, 0.0, 0.0), (1.0, 0.7, 0.0), (1.0, 1.0, 0.0), (0.0, 1.0, 0.0) ] - CairoPlot.gantt_chart('gantt_teste', pieces, 600, 300, x_labels, y_labels, colors) - ''' - - plot = GanttChart(name, pieces, width, height, x_labels, y_labels, colors) - plot.render() - plot.commit() - -def vertical_bar_plot(name, - data, - width, - height, - background = "white light_gray", - border = 0, - display_values = False, - grid = False, - rounded_corners = False, - stack = False, - three_dimension = False, - series_labels = None, - x_labels = None, - y_labels = None, - x_bounds = None, - y_bounds = None, - colors = None): - #TODO: Fix docstring for vertical_bar_plot - ''' - - Function to generate vertical Bar Plot Charts. - - bar_plot(name, data, width, height, background, border, grid, rounded_corners, three_dimension, - x_labels, y_labels, x_bounds, y_bounds, colors): - - - Parameters - - name - Name of the desired output file, no need to input the .svg as it will be added at runtime; - data - The list, list of lists or dictionary holding the data to be plotted; - width, height - Dimensions of the output image; - background - A 3 element tuple representing the rgb color expected for the background or a new cairo linear gradient. - If left None, a gray to white gradient will be generated; - border - Distance in pixels of a square border into which the graphics will be drawn; - grid - Whether or not the gris is to be drawn; - rounded_corners - Whether or not the bars should have rounded corners; - three_dimension - Whether or not the bars should be drawn in pseudo 3D; - x_labels, y_labels - lists of strings containing the horizontal and vertical labels for the axis; - x_bounds, y_bounds - tuples containing the lower and upper value bounds for the data to be plotted; - colors - List containing the colors expected for each of the bars. - - - Example of use - - data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - CairoPlot.vertical_bar_plot ('bar2', data, 400, 300, border = 20, grid = True, rounded_corners = False) - ''' - - plot = VerticalBarPlot(name, data, width, height, background, border, - display_values, grid, rounded_corners, stack, three_dimension, - series_labels, x_labels, y_labels, x_bounds, y_bounds, colors) - plot.render() - plot.commit() - -def horizontal_bar_plot(name, - data, - width, - height, - background = "white light_gray", - border = 0, - display_values = False, - grid = False, - rounded_corners = False, - stack = False, - three_dimension = False, - series_labels = None, - x_labels = None, - y_labels = None, - x_bounds = None, - y_bounds = None, - colors = None): - - #TODO: Fix docstring for horizontal_bar_plot - ''' - - Function to generate Horizontal Bar Plot Charts. - - bar_plot(name, data, width, height, background, border, grid, rounded_corners, three_dimension, - x_labels, y_labels, x_bounds, y_bounds, colors): - - - Parameters - - name - Name of the desired output file, no need to input the .svg as it will be added at runtime; - data - The list, list of lists or dictionary holding the data to be plotted; - width, height - Dimensions of the output image; - background - A 3 element tuple representing the rgb color expected for the background or a new cairo linear gradient. - If left None, a gray to white gradient will be generated; - border - Distance in pixels of a square border into which the graphics will be drawn; - grid - Whether or not the gris is to be drawn; - rounded_corners - Whether or not the bars should have rounded corners; - three_dimension - Whether or not the bars should be drawn in pseudo 3D; - x_labels, y_labels - lists of strings containing the horizontal and vertical labels for the axis; - x_bounds, y_bounds - tuples containing the lower and upper value bounds for the data to be plotted; - colors - List containing the colors expected for each of the bars. - - - Example of use - - data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - CairoPlot.bar_plot ('bar2', data, 400, 300, border = 20, grid = True, rounded_corners = False) - ''' - - plot = HorizontalBarPlot(name, data, width, height, background, border, - display_values, grid, rounded_corners, stack, three_dimension, - series_labels, x_labels, y_labels, x_bounds, y_bounds, colors) - plot.render() - plot.commit() - -def stream_chart(name, - data, - width, - height, - background = "white light_gray", - border = 0, - grid = False, - series_legend = None, - x_labels = None, - x_bounds = None, - y_bounds = None, - colors = None): - - #TODO: Fix docstring for horizontal_bar_plot - plot = StreamChart(name, data, width, height, background, border, - grid, series_legend, x_labels, x_bounds, y_bounds, colors) - plot.render() - plot.commit() - - -if __name__ == "__main__": - import tests - import seriestests diff --git a/invoice/graph/graphs.py b/invoice/graph/graphs.py deleted file mode 100755 index b59b7e1..0000000 --- a/invoice/graph/graphs.py +++ /dev/null @@ -1,223 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -import cairoplot, datetime, sqlite3, time - -def TortendiagramUser(): - data = {} - now = int(time.time()) - - query = "SELECT users.id, SUM(memberprice) FROM users, purchases purch, prices \ - WHERE users.id = purch.user AND purch.product = prices.product AND \ - purch.timestamp > ? AND purch.timestamp < ? AND prices.valid_from = \ - (SELECT valid_from FROM prices WHERE product = purch.product AND \ - valid_from < purch.timestamp ORDER BY valid_from DESC LIMIT 1) \ - GROUP BY users.id" - - connection = sqlite3.connect('shop.db') - c = connection.cursor() - c.execute(query, (0, now)) - for row in c: - data["%d (%d.%d Euro)" %(row[0], row[1] / 100, row[1] % 100)] = row[1] - c.close() - - cairoplot.pie_plot("tortendiagram", data, 640, 480) - -def BalkendiagramUserRanking(): - data = {} - names = [] - now = int(time.time()) - - query = "SELECT firstname, lastname, SUM(memberprice) FROM users, purchases purch, prices \ - WHERE users.id = purch.user AND purch.product = prices.product AND \ - purch.timestamp > ? AND purch.timestamp < ? AND prices.valid_from = \ - (SELECT valid_from FROM prices WHERE product = purch.product AND \ - valid_from < purch.timestamp ORDER BY valid_from DESC LIMIT 1) \ - GROUP BY users.id" - - connection = sqlite3.connect('shop.db') - c = connection.cursor() - c.execute(query, (0, now)) - for row in c: - data["%s %s (%d.%d Euro)" % (row[0], row[1], row[2] / 100, row[2] % 100)] = row[2] - c.close() - - count=0 - sorted_data = [] - for key, value in sorted(data.iteritems(), key=lambda (k,v): (v,k), reverse=True): - sorted_data.append(value) - names.append(key) - count+=1 - if count >= 10: - break - - cairoplot.horizontal_bar_plot("ranking", sorted_data, 640, 480, y_labels = names, rounded_corners = True, grid = True) - -def TortendiagramProduct(): - data = {} - - connection = sqlite3.connect('shop.db') - c = connection.cursor() - c.execute("SELECT products.name, SUM(1) FROM products, purchases " + - "WHERE products.id = purchases.product GROUP BY products.id") - for row in c: - data[row[0]] = row[1] - c.close() - - cairoplot.pie_plot("tortendiagram2", data, 640, 480) - -def Lagerbestand(category): - data = {} - - day = 24 * 60 * 60 - interval = 21 - now = int(time.time()) - - dates = [] - dt = datetime.datetime.fromtimestamp(now) - dates.append("%04d-%02d-%02d" % (dt.year, dt.month, dt.day)) - - colors = [ - "black", - "red", - "green", - "blue", - "orange", - (117/255.0, 255/255.0, 20/255.0), - (216/255.0, 20/255.0, 255/255.0), - (204/255.0, 153/255.0, 0/255.0), - (0/255.0, 204/255.0, 255/255.0), - (153/255.0, 77/255.0, 0/255.0), - (128/255.0, 0/255.0, 128/255.0), - (204/255.0, 0/255.0, 0/255.0), - (0/255.0, 0/255.0, 102/255.0), - "yellow", - ] - - connection = sqlite3.connect('shop.db') - c = connection.cursor() - query = "" - name = "" - numbers = [] - - if category == "getraenke": - query = "name LIKE '%Mate%' OR name LIKE '%Apfelsaft%' OR name LIKE '%Fritz%' OR name LIKE '%Coca Cola%' OR name LIKE '%Vilsa%' OR name = 'Fanta' OR name = 'Sprite'" - elif category == "haribo": - query = "name LIKE '%Haribo%'" - elif category == "haribo_total": - query = "name LIKE '%Haribo%'" - name = "Haribo" - interval = 4*7 - elif category == "riegel": - query = "name LIKE '%KitKat%' OR name = 'Lion' OR name LIKE '%Snickers%' OR name = 'Mars' OR name = 'Twix' OR name = 'Duplo'" - elif category == "other": - query = "name LIKE '%Gouda%' OR name LIKE '%Chipsfrisch%' OR name LIKE '%Sesamsticks%'" - elif category == "schoko": - query = "name = 'Ü-Ei' OR name LIKE '%Tender%' OR name = 'Knoppers' OR name LIKE '%m&m%'" - elif category == "balisto": - query = "name LIKE '%Balisto%'" - else: - return - - c.execute("SELECT name, amount FROM products WHERE (%s) AND amount > 0" % query); - for row in c: - data[row[0]] = [int(row[1])] - - current = now - currentid = 1 - while current > (now - interval * day): - for k, v in data.iteritems(): - data[k].append(v[-1]) - - dt = datetime.datetime.fromtimestamp(current - day) - dates.append("%04d-%02d-%02d" % (dt.year, dt.month, dt.day)) - - c.execute("SELECT name, SUM(restock.amount) FROM products, restock WHERE products.id = restock.product AND timestamp > ? AND timestamp < ? GROUP BY name", (current - day, current)); - for row in c: - if row[0] in data: - data[row[0]][currentid] -= row[1] - c.execute("SELECT name, SUM(1) FROM products, purchases WHERE products.id = purchases.product AND timestamp > ? AND timestamp < ? GROUP BY name", (current - day, current)); - for row in c: - if row[0] in data: - data[row[0]][currentid] += row[1] - - current -= day - currentid += 1 - - for k, v in data.iteritems(): - data[k].reverse() - dates.reverse() - - c.close() - - result = {} - if name == "": - result = data - else: - result[name] = [0]*interval; - for product in data: - for i in range(0,interval): - result[name][i] += data[product][i] - - cairoplot.dot_line_plot("lagerbestand_%s" % category, result, 640, 480, series_colors = colors, x_labels = dates, y_title = "Anzahl", axis=True, grid=True, series_legend = True) - -def TotalPurchasesPerDay(): - day = 24 * 60 * 60 - now = int(time.time()) - - colors = [ - "black", - ] - - dates = [] - dt = datetime.datetime.fromtimestamp(now) - dates.append("%04d-%02d-%02d" % (dt.year, dt.month, dt.day)) - - connection = sqlite3.connect('shop.db') - c = connection.cursor() - query = "SELECT SUM(memberprice) FROM purchases purch, prices WHERE purch.product = prices.product AND purch.user > 0 AND purch.timestamp > ? AND purch.timestamp < ? AND prices.valid_from = (SELECT valid_from FROM prices WHERE product = purch.product AND valid_from < purch.timestamp ORDER BY valid_from DESC LIMIT 1)" - - current = now - data = [] - while current > (now - 42 * day): - c.execute(query, (current-day, current)) - - dt = datetime.datetime.fromtimestamp(current - day) - dates.append("%04d-%02d-%02d" % (dt.year, dt.month, dt.day)) - - for row in c: - amount = row[0] or 0 - data.append(int(amount)/100.0) - - current -= day - - data.reverse() - dates.reverse() - - c.execute(query, (0, now)) - total = c.fetchone()[0] - - dt = datetime.datetime.fromtimestamp(now) - start = dt.replace(hour = 8, minute = 0, second = 0, day = 16) - if start > dt: - start = start.replace(month = start.month - 1) - c.execute(query, (start.strftime("%s"), now)) - month = c.fetchone()[0] - - c.close() - - print "Total sales: %.2f€" % (total / 100.0) - print "Total sales this month: %.2f€" % (month / 100.0) - print "Average per day (last 42 days): %.2f€" % (sum(data)/len(data)) - - cairoplot.dot_line_plot("total_sales_per_day", data, 640, 480, series_colors = colors, x_labels = dates, y_title = "Euro", axis=True, grid=True) - -TortendiagramUser() -BalkendiagramUserRanking() - -TortendiagramProduct() - -data = [ "getraenke", "haribo", "haribo_total", "riegel", "other", "schoko", "balisto" ] -for x in data: - Lagerbestand(x) - -TotalPurchasesPerDay() diff --git a/invoice/graph/series.py b/invoice/graph/series.py deleted file mode 100755 index 157ab3d..0000000 --- a/invoice/graph/series.py +++ /dev/null @@ -1,1140 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Serie.py -# -# Copyright (c) 2008 Magnun Leno da Silva -# -# Author: Magnun Leno da Silva -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public License -# as published by the Free Software Foundation; either version 2 of -# the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 -# USA - -# Contributor: Rodrigo Moreiro Araujo - -#import cairoplot -import doctest - -NUMTYPES = (int, float, long) -LISTTYPES = (list, tuple) -STRTYPES = (str, unicode) -FILLING_TYPES = ['linear', 'solid', 'gradient'] -DEFAULT_COLOR_FILLING = 'solid' -#TODO: Define default color list -DEFAULT_COLOR_LIST = None - -class Data(object): - ''' - Class that models the main data structure. - It can hold: - - a number type (int, float or long) - - a tuple, witch represents a point and can have 2 or 3 items (x,y,z) - - if a list is passed it will be converted to a tuple. - - obs: In case a tuple is passed it will convert to tuple - ''' - def __init__(self, data=None, name=None, parent=None): - ''' - Starts main atributes from the Data class - @name - Name for each point; - @content - The real data, can be an int, float, long or tuple, which - represents a point (x,y) or (x,y,z); - @parent - A pointer that give the data access to it's parent. - - Usage: - >>> d = Data(name='empty'); print d - empty: () - >>> d = Data((1,1),'point a'); print d - point a: (1, 1) - >>> d = Data((1,2,3),'point b'); print d - point b: (1, 2, 3) - >>> d = Data([2,3],'point c'); print d - point c: (2, 3) - >>> d = Data(12, 'simple value'); print d - simple value: 12 - ''' - # Initial values - self.__content = None - self.__name = None - - # Setting passed values - self.parent = parent - self.name = name - self.content = data - - # Name property - @apply - def name(): - doc = ''' - Name is a read/write property that controls the input of name. - - If passed an invalid value it cleans the name with None - - Usage: - >>> d = Data(13); d.name = 'name_test'; print d - name_test: 13 - >>> d.name = 11; print d - 13 - >>> d.name = 'other_name'; print d - other_name: 13 - >>> d.name = None; print d - 13 - >>> d.name = 'last_name'; print d - last_name: 13 - >>> d.name = ''; print d - 13 - ''' - def fget(self): - ''' - returns the name as a string - ''' - return self.__name - - def fset(self, name): - ''' - Sets the name of the Data - ''' - if type(name) in STRTYPES and len(name) > 0: - self.__name = name - else: - self.__name = None - - - - return property(**locals()) - - # Content property - @apply - def content(): - doc = ''' - Content is a read/write property that validate the data passed - and return it. - - Usage: - >>> d = Data(); d.content = 13; d.content - 13 - >>> d = Data(); d.content = (1,2); d.content - (1, 2) - >>> d = Data(); d.content = (1,2,3); d.content - (1, 2, 3) - >>> d = Data(); d.content = [1,2,3]; d.content - (1, 2, 3) - >>> d = Data(); d.content = [1.5,.2,3.3]; d.content - (1.5, 0.20000000000000001, 3.2999999999999998) - ''' - def fget(self): - ''' - Return the content of Data - ''' - return self.__content - - def fset(self, data): - ''' - Ensures that data is a valid tuple/list or a number (int, float - or long) - ''' - # Type: None - if data is None: - self.__content = None - return - - # Type: Int or Float - elif type(data) in NUMTYPES: - self.__content = data - - # Type: List or Tuple - elif type(data) in LISTTYPES: - # Ensures the correct size - if len(data) not in (2, 3): - raise TypeError, "Data (as list/tuple) must have 2 or 3 items" - return - - # Ensures that all items in list/tuple is a number - isnum = lambda x : type(x) not in NUMTYPES - - if max(map(isnum, data)): - # An item in data isn't an int or a float - raise TypeError, "All content of data must be a number (int or float)" - - # Convert the tuple to list - if type(data) is list: - data = tuple(data) - - # Append a copy and sets the type - self.__content = data[:] - - # Unknown type! - else: - self.__content = None - raise TypeError, "Data must be an int, float or a tuple with two or three items" - return - - return property(**locals()) - - - def clear(self): - ''' - Clear the all Data (content, name and parent) - ''' - self.content = None - self.name = None - self.parent = None - - def copy(self): - ''' - Returns a copy of the Data structure - ''' - # The copy - new_data = Data() - if self.content is not None: - # If content is a point - if type(self.content) is tuple: - new_data.__content = self.content[:] - - # If content is a number - else: - new_data.__content = self.content - - # If it has a name - if self.name is not None: - new_data.__name = self.name - - return new_data - - def __str__(self): - ''' - Return a string representation of the Data structure - ''' - if self.name is None: - if self.content is None: - return '' - return str(self.content) - else: - if self.content is None: - return self.name+": ()" - return self.name+": "+str(self.content) - - def __len__(self): - ''' - Return the length of the Data. - - If it's a number return 1; - - If it's a list return it's length; - - If its None return 0. - ''' - if self.content is None: - return 0 - elif type(self.content) in NUMTYPES: - return 1 - return len(self.content) - - - - -class Group(object): - ''' - Class that models a group of data. Every value (int, float, long, tuple - or list) passed is converted to a list of Data. - It can receive: - - A single number (int, float, long); - - A list of numbers; - - A tuple of numbers; - - An instance of Data; - - A list of Data; - - Obs: If a tuple with 2 or 3 items is passed it is converted to a point. - If a tuple with only 1 item is passed it's converted to a number; - If a tuple with more than 2 items is passed it's converted to a - list of numbers - ''' - def __init__(self, group=None, name=None, parent=None): - ''' - Starts main atributes in Group instance. - @data_list - a list of data which forms the group; - @range - a range that represent the x axis of possible functions; - @name - name of the data group; - @parent - the Serie parent of this group. - - Usage: - >>> g = Group(13, 'simple number'); print g - simple number ['13'] - >>> g = Group((1,2), 'simple point'); print g - simple point ['(1, 2)'] - >>> g = Group([1,2,3,4], 'list of numbers'); print g - list of numbers ['1', '2', '3', '4'] - >>> g = Group((1,2,3,4),'int in tuple'); print g - int in tuple ['1', '2', '3', '4'] - >>> g = Group([(1,2),(2,3),(3,4)], 'list of points'); print g - list of points ['(1, 2)', '(2, 3)', '(3, 4)'] - >>> g = Group([[1,2,3],[1,2,3]], '2D coordinate lists'); print g - 2D coordinated lists ['(1, 1)', '(2, 2)', '(3, 3)'] - >>> g = Group([[1,2],[1,2],[1,2]], '3D coordinate lists'); print g - 3D coordinated lists ['(1, 1, 1)', '(2, 2, 2)'] - ''' - # Initial values - self.__data_list = [] - self.__range = [] - self.__name = None - - - self.parent = parent - self.name = name - self.data_list = group - - # Name property - @apply - def name(): - doc = ''' - Name is a read/write property that controls the input of name. - - If passed an invalid value it cleans the name with None - - Usage: - >>> g = Group(13); g.name = 'name_test'; print g - name_test ['13'] - >>> g.name = 11; print g - ['13'] - >>> g.name = 'other_name'; print g - other_name ['13'] - >>> g.name = None; print g - ['13'] - >>> g.name = 'last_name'; print g - last_name ['13'] - >>> g.name = ''; print g - ['13'] - ''' - def fget(self): - ''' - Returns the name as a string - ''' - return self.__name - - def fset(self, name): - ''' - Sets the name of the Group - ''' - if type(name) in STRTYPES and len(name) > 0: - self.__name = name - else: - self.__name = None - - return property(**locals()) - - # data_list property - @apply - def data_list(): - doc = ''' - The data_list is a read/write property that can be a list of - numbers, a list of points or a list of 2 or 3 coordinate lists. This - property uses mainly the self.add_data method. - - Usage: - >>> g = Group(); g.data_list = 13; print g - ['13'] - >>> g.data_list = (1,2); print g - ['(1, 2)'] - >>> g.data_list = Data((1,2),'point a'); print g - ['point a: (1, 2)'] - >>> g.data_list = [1,2,3]; print g - ['1', '2', '3'] - >>> g.data_list = (1,2,3,4); print g - ['1', '2', '3', '4'] - >>> g.data_list = [(1,2),(2,3),(3,4)]; print g - ['(1, 2)', '(2, 3)', '(3, 4)'] - >>> g.data_list = [[1,2],[1,2]]; print g - ['(1, 1)', '(2, 2)'] - >>> g.data_list = [[1,2],[1,2],[1,2]]; print g - ['(1, 1, 1)', '(2, 2, 2)'] - >>> g.range = (10); g.data_list = lambda x:x**2; print g - ['(0.0, 0.0)', '(1.0, 1.0)', '(2.0, 4.0)', '(3.0, 9.0)', '(4.0, 16.0)', '(5.0, 25.0)', '(6.0, 36.0)', '(7.0, 49.0)', '(8.0, 64.0)', '(9.0, 81.0)'] - ''' - def fget(self): - ''' - Returns the value of data_list - ''' - return self.__data_list - - def fset(self, group): - ''' - Ensures that group is valid. - ''' - # None - if group is None: - self.__data_list = [] - - # Int/float/long or Instance of Data - elif type(group) in NUMTYPES or isinstance(group, Data): - # Clean data_list - self.__data_list = [] - self.add_data(group) - - # One point - elif type(group) is tuple and len(group) in (2,3): - self.__data_list = [] - self.add_data(group) - - # list of items - elif type(group) in LISTTYPES and type(group[0]) is not list: - # Clean data_list - self.__data_list = [] - for item in group: - # try to append and catch an exception - self.add_data(item) - - # function lambda - elif callable(group): - # Explicit is better than implicit - function = group - # Has range - if len(self.range) is not 0: - # Clean data_list - self.__data_list = [] - # Generate values for the lambda function - for x in self.range: - #self.add_data((x,round(group(x),2))) - self.add_data((x,function(x))) - - # Only have range in parent - elif self.parent is not None and len(self.parent.range) is not 0: - # Copy parent range - self.__range = self.parent.range[:] - # Clean data_list - self.__data_list = [] - # Generate values for the lambda function - for x in self.range: - #self.add_data((x,round(group(x),2))) - self.add_data((x,function(x))) - - # Don't have range anywhere - else: - # x_data don't exist - raise Exception, "Data argument is valid but to use function type please set x_range first" - - # Coordinate Lists - elif type(group) in LISTTYPES and type(group[0]) is list: - # Clean data_list - self.__data_list = [] - data = [] - if len(group) == 3: - data = zip(group[0], group[1], group[2]) - elif len(group) == 2: - data = zip(group[0], group[1]) - else: - raise TypeError, "Only one list of coordinates was received." - - for item in data: - self.add_data(item) - - else: - raise TypeError, "Group type not supported" - - return property(**locals()) - - @apply - def range(): - doc = ''' - The range is a read/write property that generates a range of values - for the x axis of the functions. When passed a tuple it almost works - like the built-in range funtion: - - 1 item, represent the end of the range started from 0; - - 2 items, represents the start and the end, respectively; - - 3 items, the last one represents the step; - - When passed a list the range function understands as a valid range. - - Usage: - >>> g = Group(); g.range = 10; print g.range - [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] - >>> g = Group(); g.range = (5); print g.range - [0.0, 1.0, 2.0, 3.0, 4.0] - >>> g = Group(); g.range = (1,7); print g.range - [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] - >>> g = Group(); g.range = (0,10,2); print g.range - [0.0, 2.0, 4.0, 6.0, 8.0] - >>> - >>> g = Group(); g.range = [0]; print g.range - [0.0] - >>> g = Group(); g.range = [0,10,20]; print g.range - [0.0, 10.0, 20.0] - ''' - def fget(self): - ''' - Returns the range - ''' - return self.__range - - def fset(self, x_range): - ''' - Controls the input of a valid type and generate the range - ''' - # if passed a simple number convert to tuple - if type(x_range) in NUMTYPES: - x_range = (x_range,) - - # A list, just convert to float - if type(x_range) is list and len(x_range) > 0: - # Convert all to float - x_range = map(float, x_range) - # Prevents repeated values and convert back to list - self.__range = list(set(x_range[:])) - # Sort the list to ascending order - self.__range.sort() - - # A tuple, must check the lengths and generate the values - elif type(x_range) is tuple and len(x_range) in (1,2,3): - # Convert all to float - x_range = map(float, x_range) - - # Inital values - start = 0.0 - step = 1.0 - end = 0.0 - - # Only the end and it can't be less or iqual to 0 - if len(x_range) is 1 and x_range > 0: - end = x_range[0] - - # The start and the end but the start must be less then the end - elif len(x_range) is 2 and x_range[0] < x_range[1]: - start = x_range[0] - end = x_range[1] - - # All 3, but the start must be less then the end - elif x_range[0] <= x_range[1]: - start = x_range[0] - end = x_range[1] - step = x_range[2] - - # Starts the range - self.__range = [] - # Generate the range - # Can't use the range function because it doesn't support float values - while start < end: - self.__range.append(start) - start += step - - # Incorrect type - else: - raise Exception, "x_range must be a list with one or more items or a tuple with 2 or 3 items" - - return property(**locals()) - - def add_data(self, data, name=None): - ''' - Append a new data to the data_list. - - If data is an instance of Data, append it - - If it's an int, float, tuple or list create an instance of Data and append it - - Usage: - >>> g = Group() - >>> g.add_data(12); print g - ['12'] - >>> g.add_data(7,'other'); print g - ['12', 'other: 7'] - >>> - >>> g = Group() - >>> g.add_data((1,1),'a'); print g - ['a: (1, 1)'] - >>> g.add_data((2,2),'b'); print g - ['a: (1, 1)', 'b: (2, 2)'] - >>> - >>> g.add_data(Data((1,2),'c')); print g - ['a: (1, 1)', 'b: (2, 2)', 'c: (1, 2)'] - ''' - if not isinstance(data, Data): - # Try to convert - data = Data(data,name,self) - - if data.content is not None: - self.__data_list.append(data.copy()) - self.__data_list[-1].parent = self - - - def to_list(self): - ''' - Returns the group as a list of numbers (int, float or long) or a - list of tuples (points 2D or 3D). - - Usage: - >>> g = Group([1,2,3,4],'g1'); g.to_list() - [1, 2, 3, 4] - >>> g = Group([(1,2),(2,3),(3,4)],'g2'); g.to_list() - [(1, 2), (2, 3), (3, 4)] - >>> g = Group([(1,2,3),(3,4,5)],'g2'); g.to_list() - [(1, 2, 3), (3, 4, 5)] - ''' - return [data.content for data in self] - - def copy(self): - ''' - Returns a copy of this group - ''' - new_group = Group() - new_group.__name = self.__name - if self.__range is not None: - new_group.__range = self.__range[:] - for data in self: - new_group.add_data(data.copy()) - return new_group - - def get_names(self): - ''' - Return a list with the names of all data in this group - ''' - names = [] - for data in self: - if data.name is None: - names.append('Data '+str(data.index()+1)) - else: - names.append(data.name) - return names - - - def __str__ (self): - ''' - Returns a string representing the Group - ''' - ret = "" - if self.name is not None: - ret += self.name + " " - if len(self) > 0: - list_str = [str(item) for item in self] - ret += str(list_str) - else: - ret += "[]" - return ret - - def __getitem__(self, key): - ''' - Makes a Group iterable, based in the data_list property - ''' - return self.data_list[key] - - def __len__(self): - ''' - Returns the length of the Group, based in the data_list property - ''' - return len(self.data_list) - - -class Colors(object): - ''' - Class that models the colors its labels (names) and its properties, RGB - and filling type. - - It can receive: - - A list where each item is a list with 3 or 4 items. The - first 3 items represent the RGB values and the last argument - defines the filling type. The list will be converted to a dict - and each color will receve a name based in its position in the - list. - - A dictionary where each key will be the color name and its item - can be a list with 3 or 4 items. The first 3 items represent - the RGB colors and the last argument defines the filling type. - ''' - def __init__(self, color_list=None): - ''' - Start the color_list property - @ color_list - the list or dict contaning the colors properties. - ''' - self.__color_list = None - - self.color_list = color_list - - @apply - def color_list(): - doc = ''' - >>> c = Colors([[1,1,1],[2,2,2,'linear'],[3,3,3,'gradient']]) - >>> print c.color_list - {'Color 2': [2, 2, 2, 'linear'], 'Color 3': [3, 3, 3, 'gradient'], 'Color 1': [1, 1, 1, 'solid']} - >>> c.color_list = [[1,1,1],(2,2,2,'solid'),(3,3,3,'linear')] - >>> print c.color_list - {'Color 2': [2, 2, 2, 'solid'], 'Color 3': [3, 3, 3, 'linear'], 'Color 1': [1, 1, 1, 'solid']} - >>> c.color_list = {'a':[1,1,1],'b':(2,2,2,'solid'),'c':(3,3,3,'linear'), 'd':(4,4,4)} - >>> print c.color_list - {'a': [1, 1, 1, 'solid'], 'c': [3, 3, 3, 'linear'], 'b': [2, 2, 2, 'solid'], 'd': [4, 4, 4, 'solid']} - ''' - def fget(self): - ''' - Return the color list - ''' - return self.__color_list - - def fset(self, color_list): - ''' - Format the color list to a dictionary - ''' - if color_list is None: - self.__color_list = None - return - - if type(color_list) in LISTTYPES and type(color_list[0]) in LISTTYPES: - old_color_list = color_list[:] - color_list = {} - for index, color in enumerate(old_color_list): - if len(color) is 3 and max(map(type, color)) in NUMTYPES: - color_list['Color '+str(index+1)] = list(color)+[DEFAULT_COLOR_FILLING] - elif len(color) is 4 and max(map(type, color[:-1])) in NUMTYPES and color[-1] in FILLING_TYPES: - color_list['Color '+str(index+1)] = list(color) - else: - raise TypeError, "Unsuported color format" - elif type(color_list) is not dict: - raise TypeError, "Unsuported color format" - - for name, color in color_list.items(): - if len(color) is 3: - if max(map(type, color)) in NUMTYPES: - color_list[name] = list(color)+[DEFAULT_COLOR_FILLING] - else: - raise TypeError, "Unsuported color format" - elif len(color) is 4: - if max(map(type, color[:-1])) in NUMTYPES and color[-1] in FILLING_TYPES: - color_list[name] = list(color) - else: - raise TypeError, "Unsuported color format" - self.__color_list = color_list.copy() - - return property(**locals()) - - -class Series(object): - ''' - Class that models a Series (group of groups). Every value (int, float, - long, tuple or list) passed is converted to a list of Group or Data. - It can receive: - - a single number or point, will be converted to a Group of one Data; - - a list of numbers, will be converted to a group of numbers; - - a list of tuples, will converted to a single Group of points; - - a list of lists of numbers, each 'sublist' will be converted to a - group of numbers; - - a list of lists of tuples, each 'sublist' will be converted to a - group of points; - - a list of lists of lists, the content of the 'sublist' will be - processed as coordinated lists and the result will be converted to - a group of points; - - a Dictionary where each item can be the same of the list: number, - point, list of numbers, list of points or list of lists (coordinated - lists); - - an instance of Data; - - an instance of group. - ''' - def __init__(self, series=None, name=None, property=[], colors=None): - ''' - Starts main atributes in Group instance. - @series - a list, dict of data of which the series is composed; - @name - name of the series; - @property - a list/dict of properties to be used in the plots of - this Series - - Usage: - >>> print Series([1,2,3,4]) - ["Group 1 ['1', '2', '3', '4']"] - >>> print Series([[1,2,3],[4,5,6]]) - ["Group 1 ['1', '2', '3']", "Group 2 ['4', '5', '6']"] - >>> print Series((1,2)) - ["Group 1 ['(1, 2)']"] - >>> print Series([(1,2),(2,3)]) - ["Group 1 ['(1, 2)', '(2, 3)']"] - >>> print Series([[(1,2),(2,3)],[(4,5),(5,6)]]) - ["Group 1 ['(1, 2)', '(2, 3)']", "Group 2 ['(4, 5)', '(5, 6)']"] - >>> print Series([[[1,2,3],[1,2,3],[1,2,3]]]) - ["Group 1 ['(1, 1, 1)', '(2, 2, 2)', '(3, 3, 3)']"] - >>> print Series({'g1':[1,2,3], 'g2':[4,5,6]}) - ["g1 ['1', '2', '3']", "g2 ['4', '5', '6']"] - >>> print Series({'g1':[(1,2),(2,3)], 'g2':[(4,5),(5,6)]}) - ["g1 ['(1, 2)', '(2, 3)']", "g2 ['(4, 5)', '(5, 6)']"] - >>> print Series({'g1':[[1,2],[1,2]], 'g2':[[4,5],[4,5]]}) - ["g1 ['(1, 1)', '(2, 2)']", "g2 ['(4, 4)', '(5, 5)']"] - >>> print Series(Data(1,'d1')) - ["Group 1 ['d1: 1']"] - >>> print Series(Group([(1,2),(2,3)],'g1')) - ["g1 ['(1, 2)', '(2, 3)']"] - ''' - # Intial values - self.__group_list = [] - self.__name = None - self.__range = None - - # TODO: Implement colors with filling - self.__colors = None - - self.name = name - self.group_list = series - self.colors = colors - - # Name property - @apply - def name(): - doc = ''' - Name is a read/write property that controls the input of name. - - If passed an invalid value it cleans the name with None - - Usage: - >>> s = Series(13); s.name = 'name_test'; print s - name_test ["Group 1 ['13']"] - >>> s.name = 11; print s - ["Group 1 ['13']"] - >>> s.name = 'other_name'; print s - other_name ["Group 1 ['13']"] - >>> s.name = None; print s - ["Group 1 ['13']"] - >>> s.name = 'last_name'; print s - last_name ["Group 1 ['13']"] - >>> s.name = ''; print s - ["Group 1 ['13']"] - ''' - def fget(self): - ''' - Returns the name as a string - ''' - return self.__name - - def fset(self, name): - ''' - Sets the name of the Group - ''' - if type(name) in STRTYPES and len(name) > 0: - self.__name = name - else: - self.__name = None - - return property(**locals()) - - - - # Colors property - @apply - def colors(): - doc = ''' - >>> s = Series() - >>> s.colors = [[1,1,1],[2,2,2,'linear'],[3,3,3,'gradient']] - >>> print s.colors - {'Color 2': [2, 2, 2, 'linear'], 'Color 3': [3, 3, 3, 'gradient'], 'Color 1': [1, 1, 1, 'solid']} - >>> s.colors = [[1,1,1],(2,2,2,'solid'),(3,3,3,'linear')] - >>> print s.colors - {'Color 2': [2, 2, 2, 'solid'], 'Color 3': [3, 3, 3, 'linear'], 'Color 1': [1, 1, 1, 'solid']} - >>> s.colors = {'a':[1,1,1],'b':(2,2,2,'solid'),'c':(3,3,3,'linear'), 'd':(4,4,4)} - >>> print s.colors - {'a': [1, 1, 1, 'solid'], 'c': [3, 3, 3, 'linear'], 'b': [2, 2, 2, 'solid'], 'd': [4, 4, 4, 'solid']} - ''' - def fget(self): - ''' - Return the color list - ''' - return self.__colors.color_list - - def fset(self, colors): - ''' - Format the color list to a dictionary - ''' - self.__colors = Colors(colors) - - return property(**locals()) - - @apply - def range(): - doc = ''' - The range is a read/write property that generates a range of values - for the x axis of the functions. When passed a tuple it almost works - like the built-in range funtion: - - 1 item, represent the end of the range started from 0; - - 2 items, represents the start and the end, respectively; - - 3 items, the last one represents the step; - - When passed a list the range function understands as a valid range. - - Usage: - >>> s = Series(); s.range = 10; print s.range - [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0] - >>> s = Series(); s.range = (5); print s.range - [0.0, 1.0, 2.0, 3.0, 4.0, 5.0] - >>> s = Series(); s.range = (1,7); print s.range - [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0] - >>> s = Series(); s.range = (0,10,2); print s.range - [0.0, 2.0, 4.0, 6.0, 8.0, 10.0] - >>> - >>> s = Series(); s.range = [0]; print s.range - [0.0] - >>> s = Series(); s.range = [0,10,20]; print s.range - [0.0, 10.0, 20.0] - ''' - def fget(self): - ''' - Returns the range - ''' - return self.__range - - def fset(self, x_range): - ''' - Controls the input of a valid type and generate the range - ''' - # if passed a simple number convert to tuple - if type(x_range) in NUMTYPES: - x_range = (x_range,) - - # A list, just convert to float - if type(x_range) is list and len(x_range) > 0: - # Convert all to float - x_range = map(float, x_range) - # Prevents repeated values and convert back to list - self.__range = list(set(x_range[:])) - # Sort the list to ascending order - self.__range.sort() - - # A tuple, must check the lengths and generate the values - elif type(x_range) is tuple and len(x_range) in (1,2,3): - # Convert all to float - x_range = map(float, x_range) - - # Inital values - start = 0.0 - step = 1.0 - end = 0.0 - - # Only the end and it can't be less or iqual to 0 - if len(x_range) is 1 and x_range > 0: - end = x_range[0] - - # The start and the end but the start must be lesser then the end - elif len(x_range) is 2 and x_range[0] < x_range[1]: - start = x_range[0] - end = x_range[1] - - # All 3, but the start must be lesser then the end - elif x_range[0] < x_range[1]: - start = x_range[0] - end = x_range[1] - step = x_range[2] - - # Starts the range - self.__range = [] - # Generate the range - # Cnat use the range function becouse it don't suport float values - while start <= end: - self.__range.append(start) - start += step - - # Incorrect type - else: - raise Exception, "x_range must be a list with one or more item or a tuple with 2 or 3 items" - - return property(**locals()) - - @apply - def group_list(): - doc = ''' - The group_list is a read/write property used to pre-process the list - of Groups. - It can be: - - a single number, point or lambda, will be converted to a single - Group of one Data; - - a list of numbers, will be converted to a group of numbers; - - a list of tuples, will converted to a single Group of points; - - a list of lists of numbers, each 'sublist' will be converted to - a group of numbers; - - a list of lists of tuples, each 'sublist' will be converted to a - group of points; - - a list of lists of lists, the content of the 'sublist' will be - processed as coordinated lists and the result will be converted - to a group of points; - - a list of lambdas, each lambda represents a Group; - - a Dictionary where each item can be the same of the list: number, - point, list of numbers, list of points, list of lists - (coordinated lists) or lambdas - - an instance of Data; - - an instance of group. - - Usage: - >>> s = Series() - >>> s.group_list = [1,2,3,4]; print s - ["Group 1 ['1', '2', '3', '4']"] - >>> s.group_list = [[1,2,3],[4,5,6]]; print s - ["Group 1 ['1', '2', '3']", "Group 2 ['4', '5', '6']"] - >>> s.group_list = (1,2); print s - ["Group 1 ['(1, 2)']"] - >>> s.group_list = [(1,2),(2,3)]; print s - ["Group 1 ['(1, 2)', '(2, 3)']"] - >>> s.group_list = [[(1,2),(2,3)],[(4,5),(5,6)]]; print s - ["Group 1 ['(1, 2)', '(2, 3)']", "Group 2 ['(4, 5)', '(5, 6)']"] - >>> s.group_list = [[[1,2,3],[1,2,3],[1,2,3]]]; print s - ["Group 1 ['(1, 1, 1)', '(2, 2, 2)', '(3, 3, 3)']"] - >>> s.group_list = [(0.5,5.5) , [(0,4),(6,8)] , (5.5,7) , (7,9)]; print s - ["Group 1 ['(0.5, 5.5)']", "Group 2 ['(0, 4)', '(6, 8)']", "Group 3 ['(5.5, 7)']", "Group 4 ['(7, 9)']"] - >>> s.group_list = {'g1':[1,2,3], 'g2':[4,5,6]}; print s - ["g1 ['1', '2', '3']", "g2 ['4', '5', '6']"] - >>> s.group_list = {'g1':[(1,2),(2,3)], 'g2':[(4,5),(5,6)]}; print s - ["g1 ['(1, 2)', '(2, 3)']", "g2 ['(4, 5)', '(5, 6)']"] - >>> s.group_list = {'g1':[[1,2],[1,2]], 'g2':[[4,5],[4,5]]}; print s - ["g1 ['(1, 1)', '(2, 2)']", "g2 ['(4, 4)', '(5, 5)']"] - >>> s.range = 10 - >>> s.group_list = lambda x:x*2 - >>> s.group_list = [lambda x:x*2, lambda x:x**2, lambda x:x**3]; print s - ["Group 1 ['(0.0, 0.0)', '(1.0, 2.0)', '(2.0, 4.0)', '(3.0, 6.0)', '(4.0, 8.0)', '(5.0, 10.0)', '(6.0, 12.0)', '(7.0, 14.0)', '(8.0, 16.0)', '(9.0, 18.0)', '(10.0, 20.0)']", "Group 2 ['(0.0, 0.0)', '(1.0, 1.0)', '(2.0, 4.0)', '(3.0, 9.0)', '(4.0, 16.0)', '(5.0, 25.0)', '(6.0, 36.0)', '(7.0, 49.0)', '(8.0, 64.0)', '(9.0, 81.0)', '(10.0, 100.0)']", "Group 3 ['(0.0, 0.0)', '(1.0, 1.0)', '(2.0, 8.0)', '(3.0, 27.0)', '(4.0, 64.0)', '(5.0, 125.0)', '(6.0, 216.0)', '(7.0, 343.0)', '(8.0, 512.0)', '(9.0, 729.0)', '(10.0, 1000.0)']"] - >>> s.group_list = {'linear':lambda x:x*2, 'square':lambda x:x**2, 'cubic':lambda x:x**3}; print s - ["cubic ['(0.0, 0.0)', '(1.0, 1.0)', '(2.0, 8.0)', '(3.0, 27.0)', '(4.0, 64.0)', '(5.0, 125.0)', '(6.0, 216.0)', '(7.0, 343.0)', '(8.0, 512.0)', '(9.0, 729.0)', '(10.0, 1000.0)']", "linear ['(0.0, 0.0)', '(1.0, 2.0)', '(2.0, 4.0)', '(3.0, 6.0)', '(4.0, 8.0)', '(5.0, 10.0)', '(6.0, 12.0)', '(7.0, 14.0)', '(8.0, 16.0)', '(9.0, 18.0)', '(10.0, 20.0)']", "square ['(0.0, 0.0)', '(1.0, 1.0)', '(2.0, 4.0)', '(3.0, 9.0)', '(4.0, 16.0)', '(5.0, 25.0)', '(6.0, 36.0)', '(7.0, 49.0)', '(8.0, 64.0)', '(9.0, 81.0)', '(10.0, 100.0)']"] - >>> s.group_list = Data(1,'d1'); print s - ["Group 1 ['d1: 1']"] - >>> s.group_list = Group([(1,2),(2,3)],'g1'); print s - ["g1 ['(1, 2)', '(2, 3)']"] - ''' - def fget(self): - ''' - Return the group list. - ''' - return self.__group_list - - def fset(self, series): - ''' - Controls the input of a valid group list. - ''' - #TODO: Add support to the following strem of data: [ (0.5,5.5) , [(0,4),(6,8)] , (5.5,7) , (7,9)] - - # Type: None - if series is None: - self.__group_list = [] - - # List or Tuple - elif type(series) in LISTTYPES: - self.__group_list = [] - - is_function = lambda x: callable(x) - # Groups - if list in map(type, series) or max(map(is_function, series)): - for group in series: - self.add_group(group) - - # single group - else: - self.add_group(series) - - #old code - ## List of numbers - #if type(series[0]) in NUMTYPES or type(series[0]) is tuple: - # print series - # self.add_group(series) - # - ## List of anything else - #else: - # for group in series: - # self.add_group(group) - - # Dict representing series of groups - elif type(series) is dict: - self.__group_list = [] - names = series.keys() - names.sort() - for name in names: - self.add_group(Group(series[name],name,self)) - - # A single lambda - elif callable(series): - self.__group_list = [] - self.add_group(series) - - # Int/float, instance of Group or Data - elif type(series) in NUMTYPES or isinstance(series, Group) or isinstance(series, Data): - self.__group_list = [] - self.add_group(series) - - # Default - else: - raise TypeError, "Serie type not supported" - - return property(**locals()) - - def add_group(self, group, name=None): - ''' - Append a new group in group_list - ''' - if not isinstance(group, Group): - #Try to convert - group = Group(group, name, self) - - if len(group.data_list) is not 0: - # Auto naming groups - if group.name is None: - group.name = "Group "+str(len(self.__group_list)+1) - - self.__group_list.append(group) - self.__group_list[-1].parent = self - - def copy(self): - ''' - Returns a copy of the Series - ''' - new_series = Series() - new_series.__name = self.__name - if self.__range is not None: - new_series.__range = self.__range[:] - #Add color property in the copy method - #self.__colors = None - - for group in self: - new_series.add_group(group.copy()) - - return new_series - - def get_names(self): - ''' - Returns a list of the names of all groups in the Serie - ''' - names = [] - for group in self: - if group.name is None: - names.append('Group '+str(group.index()+1)) - else: - names.append(group.name) - - return names - - def to_list(self): - ''' - Returns a list with the content of all groups and data - ''' - big_list = [] - for group in self: - for data in group: - if type(data.content) in NUMTYPES: - big_list.append(data.content) - else: - big_list = big_list + list(data.content) - return big_list - - def __getitem__(self, key): - ''' - Makes the Series iterable, based in the group_list property - ''' - return self.__group_list[key] - - def __str__(self): - ''' - Returns a string that represents the Series - ''' - ret = "" - if self.name is not None: - ret += self.name + " " - if len(self) > 0: - list_str = [str(item) for item in self] - ret += str(list_str) - else: - ret += "[]" - return ret - - def __len__(self): - ''' - Returns the length of the Series, based in the group_lsit property - ''' - return len(self.group_list) - - -if __name__ == '__main__': - doctest.testmod() diff --git a/ktt.pdf b/ktt.pdf deleted file mode 100644 index 60222d1..0000000 Binary files a/ktt.pdf and /dev/null differ diff --git a/main.vala b/main.vala deleted file mode 100644 index 1f2c404..0000000 --- a/main.vala +++ /dev/null @@ -1,125 +0,0 @@ -/* Copyright 2012, Sebastian Reichel - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -public Device dev; -public Database db; -public Gtk.Builder builder; - -public static int main(string[] args) { - Gtk.init (ref args); - - if(args.length < 2) { - stderr.printf("%s \n", args[0]); - return 1; - } - - dev = new Device(args[1], 9600, 8, 1); - db = new Database("shop.db"); - builder = new Gtk.Builder(); - try { - builder.add_from_file("user-interface.ui"); - } catch(Error e) { - stderr.printf("Could not load UI: %s\n", e.message); - return 1; - } - builder.connect_signals(null); - - dev.received_barcode.connect((data) => { - if(interpret(data)) - dev.blink(10); - }); - - init_ui(); - - show_main_window(); - - write_to_log("KtT Shop System has been started"); - - Gtk.main(); - - /* call destructors */ - dev = null; - db = null; - - return 0; -} - -public static bool interpret(string data) { - if(data.has_prefix("USER ")) { - string str_id = data.substring(5); - int32 id = int.parse(str_id); - - /* check if data has valid format */ - if(data != "USER %d".printf(id)) { - write_to_log("ungültige Benutzernummer: %s", data); - return false; - } - - if(db.is_logged_in()) { - write_to_log("Last User forgot to logout!"); - db.logout(); - } - - write_to_log("Login: %d", id); - return db.login(id); - } else if(data == "GUEST") { - if(db.is_logged_in()) { - write_to_log("Last User forgot to logout!"); - db.logout(); - } - - write_to_log("Login: Guest"); - return db.login(0); - } else if(data == "UNDO") { - if(!db.is_logged_in()) { - write_to_log("Can't undo if not logged in!"); - return false; - } else { - write_to_log("Undo last purchase!"); - return db.undo(); - } - } else if(data == "LOGOUT") { - if(db.is_logged_in()) { - write_to_log("Logout!"); - return db.logout(); - } - - return false; - } else if(data == "STOCK") { - if(!db.is_logged_in()) { - write_to_log("You must be logged in to go into the stock mode"); - return false; - } else { - show_restock_dialog(); - return true; - } - } else { - uint64 id = uint64.parse(data); - - /* check if data has valid format */ - if(data != "%llu".printf(id)) { - write_to_log("ungültiges Produkt: %s", data); - return false; - } - - if(db.buy(id)) { - write_to_log("gekaufter Artikel: %s (%d,%02d €)", db.get_product_name(id), db.get_product_price(id)/100, db.get_product_price(id) % 100); - return true; - } else { - write_to_log("Kauf fehlgeschlagen!"); - return false; - } - } -} diff --git a/passlist.rb b/passlist.rb deleted file mode 100644 index cb02bd1..0000000 --- a/passlist.rb +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env ruby -w -# encoding: UTF-8 - -require "csv" - -#csv input "userid,firstname,lastname" - -@template = %q{ - \documentclass[a4paper,landscape]{article} - \usepackage[utf8]{inputenc} - \usepackage{graphicx} - \usepackage{longtable} - \usepackage{array} - \usepackage{rotating} - \usepackage[top=0.5cm,right=0.5cm,bottom=0.5cm,left=0.5cm,landscape]{geometry} - \renewcommand{\familydefault}{\sfdefault} - \renewcommand{\arraystretch}{2} - \renewcommand{\tabcolsep}{0cm} - \title{Barcodelist} - \author{Kreativitaet trifft Technik} - \date{\today} - \begin{document} - \begin{center} - \begin{longtable}{| >{\centering\arraybackslash}p{8.5cm}| >{\centering\arraybackslash}p{8.5cm}| >{\centering\arraybackslash}p{8.5cm}|} - %s - \end{longtable} - \end{center} - \end{document}} - -@line = %q{ - \hline - %s - \hline - %s - \hline} - -@graphics = %q{ \includegraphics{%s} \rule{0cm}{3.5cm} %s} -@name = %q{ %s %s %s} -@ktt = %q{\includegraphics[width=8cm,angle=180]{ktt} %s } - -@csv = CSV.read(ARGV[0]) - -#generate barcodes -@csv.each{|r| - system("barcode -n -E -b 'USER %s' -o '%s.eps' -e 39\n" % [r[0], r[0]]) -} - -#generate latex -tmp = "" -graphics = "" -name = "" -1.upto(@csv.length){|i| - le = i % 3 == 0 || i >= @csv.length - sign = le ? "\\\\" : "&" - graphics += @graphics % [@csv[i-1][0], sign] - name += @name % [@csv[i-1][1], @csv[i-1][2], sign] - if le - tmp += @line % [graphics, name] - graphics = "" - name = "" - 1.upto(3) {|j| - sign = j == 3 ? "\\\\" : "&" - graphics += @ktt % sign - name += " " + sign - } - tmp += @line % [name, graphics] - graphics = "" - name = "" - end -} -File.open("barcode.latex", "w+"){|f| f.write(@template % tmp)} diff --git a/sql/tables.sql b/sql/tables.sql new file mode 100644 index 0000000..c4a43d4 --- /dev/null +++ b/sql/tables.sql @@ -0,0 +1,8 @@ +BEGIN TRANSACTION; +CREATE TABLE IF NOT EXISTS products (id INTEGER PRIMARY KEY NOT NULL, name TEXT, amount INTEGER NOT NULL DEFAULT 0); +CREATE TABLE IF NOT EXISTS sells (user INTEGER NOT NULL REFERENCES users, product INTEGER NOT NULL REFERENCES products, timestamp INTEGER NOT NULL DEFAULT 0); +CREATE TABLE IF NOT EXISTS restock (user INTEGER NOT NULL REFERENCES users, product INTEGER NOT NULL REFERENCES products, amount INTEGER NOT NULL DEFAULT 0, timestamp INTEGER NOT NULL DEFAULT 0, price INTEGER NOT NULL DEFAULT 0); +CREATE TABLE IF NOT EXISTS prices (product INTEGER NOT NULL REFERENCES products, valid_from INTEGER NOT NULL DEFAULT 0, memberprice INTEGER NOT NULL DEFAULT 0, guestprice INTEGER NOT NULL DEFAULT 0); +CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY NOT NULL, email TEXT, firstname TEXT NOT NULL, lastname TEXT NOT NULL, gender TEXT, street TEXT, plz INTEGER, city TEXT); +CREATE TABLE IF NOT EXISTS authentication(user INTEGER PRIMARY KEY NOT NULL REFERENCES users, password TEXT, session CHARACTER(20), superuser BOOLEAN NOT NULL DEFAULT false, disabled BOOLEAN NOT NULL DEFAULT false); +COMMIT; diff --git a/sql/trigger.sql b/sql/trigger.sql new file mode 100644 index 0000000..8a9bede --- /dev/null +++ b/sql/trigger.sql @@ -0,0 +1,27 @@ +BEGIN TRANSACTION; +CREATE TRIGGER IF NOT EXISTS update_product_amount_on_restock_insert AFTER INSERT ON restock BEGIN + UPDATE products SET amount = products.amount + NEW.amount WHERE products.id = NEW.product; +END; + +CREATE TRIGGER IF NOT EXISTS update_product_amount_on_restock_delete AFTER DELETE ON restock BEGIN + UPDATE products SET amount = products.amount - OLD.amount WHERE products.id = OLD.product; +END; + +CREATE TRIGGER IF NOT EXISTS update_product_amount_on_restock_update AFTER UPDATE ON restock BEGIN + UPDATE products SET amount = products.amount - OLD.amount WHERE products.id = OLD.product; + UPDATE products SET amount = products.amount + NEW.amount WHERE products.id = NEW.product; +END; + +CREATE TRIGGER IF NOT EXISTS update_product_amount_on_sells_insert AFTER INSERT ON sells BEGIN + UPDATE products SET amount = products.amount - 1 WHERE products.id = NEW.product; +END; + +CREATE TRIGGER IF NOT EXISTS update_product_amount_on_sells_delete AFTER DELETE ON sells BEGIN + UPDATE products SET amount = products.amount + 1 WHERE products.id = OLD.product; +END; + +CREATE TRIGGER IF NOT EXISTS update_product_amount_on_sells_update AFTER UPDATE ON sells BEGIN + UPDATE products SET amount = products.amount + 1 WHERE products.id = OLD.product; + UPDATE products SET amount = products.amount - 1 WHERE products.id = NEW.product; +END; +COMMIT; diff --git a/sql/views.sql b/sql/views.sql new file mode 100644 index 0000000..6ab5ef2 --- /dev/null +++ b/sql/views.sql @@ -0,0 +1,22 @@ +BEGIN TRANSACTION; +CREATE VIEW IF NOT EXISTS purchaseprices AS SELECT product, SUM(price * amount) / SUM(amount) AS price FROM restock GROUP BY product; +CREATE VIEW IF NOT EXISTS invoice AS + SELECT user, timestamp, id AS productid, name AS productname, + CASE + WHEN user < 0 THEN + (SELECT price + FROM purchaseprices + WHERE purchaseprices.product = id) + else + (SELECT + CASE + WHEN user=0 THEN guestprice + else memberprice + END + FROM prices + WHERE product = id AND valid_from <= timestamp + ORDER BY valid_from DESC LIMIT 1) + END AS price + FROM sells INNER JOIN products ON sells.product = products.id + ORDER BY timestamp; +COMMIT; diff --git a/src/db.vala b/src/db.vala new file mode 100644 index 0000000..43db62b --- /dev/null +++ b/src/db.vala @@ -0,0 +1,740 @@ +/* Copyright 2012, Sebastian Reichel + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +public const int day_in_seconds = 24*60*60; + +public struct StockEntry { + public string id; + public string name; + public int amount; + public string memberprice; + public string guestprice; +} + +public struct PriceEntry { + public int64 valid_from; + public Price memberprice; + public Price guestprice; +} + +public struct RestockEntry { + public int64 timestamp; + public int amount; + public string price; +} + +public struct UserInfo { + public int id; + public string firstname; + public string lastname; + public string email; + public string gender; + public string street; + public int plz; + public string city; +} + +public struct UserAuth { + public int id; + public bool disabled; + public bool superuser; +} + +public struct Product { + public uint64 ean; + public string name; +} + +public struct InvoiceEntry { + public int64 timestamp; + Product product; + Price price; +} + +public struct StatsInfo { + public int count_articles; + public int count_users; + public Price stock_value; + public Price sales_total; + public Price profit_total; + public Price sales_today; + public Price profit_today; + public Price sales_this_month; + public Price profit_this_month; + public Price sales_per_day; + public Price profit_per_day; + public Price sales_per_month; + public Price profit_per_month; +} + +public class Database { + private class Statement { + private Sqlite.Statement stmt; + + public Statement(Sqlite.Database db, string query) { + int rc = db.prepare_v2(query, -1, out stmt); + + if(rc != Sqlite.OK) { + error("could not prepare statement: %s", query); + } + } + + public void reset() { + stmt.reset(); + } + + public int step() { + return stmt.step(); + } + + public int bind_int(int index, int value) { + return stmt.bind_int(index, value); + } + + public int bind_text(int index, string value) { + return stmt.bind_text(index, value); + } + + public int bind_int64(int index, int64 value) { + return stmt.bind_int64(index, value); + } + + public int column_int(int index) { + return stmt.column_int(index); + } + + public string column_text(int index) { + return stmt.column_text(index); + } + + public int64 column_int64(int index) { + return stmt.column_int64(index); + } + } + + private Sqlite.Database db; + private static Gee.HashMap queries = new Gee.HashMap(); + private static Gee.HashMap statements = new Gee.HashMap(); + + int32 user = 0; + bool logged_in = false; + + public Database(string file) { + int rc; + + rc = Sqlite.Database.open(file, out db); + if(rc != Sqlite.OK) { + error("could not open database!"); + } + + /* setup queries */ + queries["product_name"] = "SELECT name FROM products WHERE id = ?"; + queries["product_amount"] = "SELECT amount FROM products WHERE id = ?"; + queries["products"] = "SELECT id, name, amount FROM products ORDER BY name"; + queries["purchase"] = "INSERT INTO sells ('user', 'product', 'timestamp') VALUES (?, ?, ?)"; + queries["last_purchase"] = "SELECT product FROM sells WHERE user = ? ORDER BY timestamp DESC LIMIT 1"; + queries["undo"] = "DELETE FROM sells WHERE user = ? ORDER BY timestamp DESC LIMIT 1"; + queries["product_create"] = "INSERT INTO products ('id', 'name', 'amount') VALUES (?, ?, ?)"; + queries["price_create"] = "INSERT INTO prices ('product', 'valid_from', 'memberprice', 'guestprice') VALUES (?, ?, ?, ?)"; + queries["stock"] = "INSERT INTO restock ('user', 'product', 'amount', 'price', 'timestamp') VALUES (?, ?, ?, ?, ?)"; + queries["price"] = "SELECT memberprice, guestprice FROM prices WHERE product = ? AND valid_from <= ? ORDER BY valid_from DESC LIMIT 1"; + queries["prices"] = "SELECT valid_from, memberprice, guestprice FROM prices WHERE product = ? ORDER BY valid_from ASC;"; + queries["restocks"] = "SELECT timestamp, amount, price FROM restock WHERE product = ? ORDER BY timestamp ASC;"; + queries["profit_complex"] = "SELECT SUM(memberprice - (SELECT price FROM purchaseprices WHERE product = purch.product)) FROM sells purch, prices WHERE purch.product = prices.product AND purch.user > 0 AND purch.timestamp > ? AND purch.timestamp < ? AND prices.valid_from = (SELECT valid_from FROM prices WHERE product = purch.product AND valid_from < purch.timestamp ORDER BY valid_from DESC LIMIT 1);"; + queries["sales_complex"] = "SELECT SUM(memberprice) FROM sells purch, prices WHERE purch.product = prices.product AND purch.user > 0 AND purch.timestamp > ? AND purch.timestamp < ? AND prices.valid_from = (SELECT valid_from FROM prices WHERE product = purch.product AND valid_from < purch.timestamp ORDER BY valid_from DESC LIMIT 1);"; + queries["stock_status"] = "SELECT id, name, amount, memberprice, guestprice FROM products, prices WHERE products.id = prices.product AND prices.valid_from = (SELECT valid_from FROM prices WHERE product = products.id ORDER BY valid_from DESC LIMIT 1) ORDER BY name"; + queries["stock_amount"] = "SELECT timestamp, amount FROM restock WHERE product = ? UNION ALL SELECT timestamp, -1 AS amount FROM sells WHERE product = ? ORDER BY timestamp DESC"; + queries["session_set"] = "UPDATE authentication SET session=? WHERE user = ?"; + queries["session_get"] = "SELECT user FROM authentication WHERE session = ?"; + queries["username"] = "SELECT firstname, lastname FROM users WHERE id = ?"; + queries["password_get"] = "SELECT password FROM authentication WHERE user = ?"; + queries["userinfo"] = "SELECT firstname, lastname, email, gender, street, plz, city FROM users WHERE id = ?"; + queries["userauth"] = "SELECT disabled, superuser FROM authentication WHERE user = ?"; + queries["profit_by_product"] = "SELECT name, SUM(memberprice - (SELECT price FROM purchaseprices WHERE product = purch.product)) AS price FROM sells purch, prices, products WHERE purch.product = products.id AND purch.product = prices.product AND purch.user > 0 AND purch.timestamp > ? AND purch.timestamp < ? AND prices.valid_from = (SELECT valid_from FROM prices WHERE product = purch.product AND valid_from < purch.timestamp ORDER BY valid_from DESC LIMIT 1) GROUP BY name ORDER BY price;"; + queries["invoice"] = "SELECT timestamp, productid, productname, price FROM invoice WHERE user = ? AND timestamp >= ? AND timestamp < ?;"; + queries["purchase_first"] = "SELECT timestamp FROM sells WHERE user = ? ORDER BY timestamp ASC LIMIT 1"; + queries["purchase_last"] = "SELECT timestamp FROM sells WHERE user = ? ORDER BY timestamp DESC LIMIT 1"; + queries["count_articles"] = "SELECT COUNT(*) FROM products"; + queries["count_users"] = "SELECT COUNT(*) FROM users"; + queries["stock_value"] = "SELECT SUM(amount * price) FROM products INNER JOIN purchaseprices ON products.id = purchaseprices.product"; + queries["total_sales"] = "SELECT SUM(price) FROM invoice WHERE user >= 0 AND timestamp >= ?"; + queries["total_profit"] = "SELECT SUM(price - (SELECT price FROM purchaseprices WHERE product = productid)) FROM invoice WHERE user >= 0 AND timestamp >= ?"; + + /* compile queries into statements */ + foreach(var entry in queries.entries) { + statements[entry.key] = new Statement(db, entry.value); + } + } + + public bool login(int32 id) { + this.user = id; + this.logged_in = true; + return true; + } + + public bool logout() { + this.user = 0; + this.logged_in = false; + return true; + } + + public Gee.HashMap get_products() { + var result = new Gee.HashMap(null, null); + statements["products"].reset(); + + while(statements["products"].step() == Sqlite.ROW) + result[statements["products"].column_text(0)] = statements["products"].column_text(1); + + return result; + } + + public stock get_stats_stock() { + var result = new stock(); + var now = time_t(); + + /* init products */ + statements["products"].reset(); + while(statements["products"].step() == Sqlite.ROW) { + var id = uint64.parse(statements["products"].column_text(0)); + var name = statements["products"].column_text(1); + int amount = int.parse(statements["products"].column_text(2)); + var product = new stock.product(id, name); + result.add(product); + product.add(now, amount); + + statements["stock_amount"].reset(); + statements["stock_amount"].bind_text(1, "%llu".printf(id)); + statements["stock_amount"].bind_text(2, "%llu".printf(id)); + + while(statements["stock_amount"].step() == Sqlite.ROW) { + var timestamp = uint64.parse(statements["stock_amount"].column_text(0)); + var diff = statements["stock_amount"].column_int(1); + product.add(timestamp+1, amount); + amount -= diff; + product.add(timestamp, amount); + } + } + + return result; + } + + public profit_per_product get_stats_profit_per_products() { + var result = new profit_per_product(); + + statements["profit_by_product"].reset(); + statements["profit_by_product"].bind_int(1, 0); + statements["profit_by_product"].bind_text(2, "99999999999999"); + + while(statements["profit_by_product"].step() == Sqlite.ROW) { + var name = statements["profit_by_product"].column_text(0); + var profit = statements["profit_by_product"].column_int(1); + result.add(name, profit); + } + + return result; + } + + public profit_per_weekday get_stats_profit_per_weekday() { + var result = new profit_per_weekday(); + + var now = new DateTime.now_utc(); + var today = new DateTime.utc(now.get_year(), now.get_month(), now.get_day_of_month(), 8, 0, 0); + var tomorrow = today.add_days(1); + var weekday = tomorrow.get_day_of_week()-1; + + var to = tomorrow.to_unix(); + var from = to - day_in_seconds; + + var weeks = 8; + + for(int i=0; i get_stock() { + var result = new Gee.ArrayList(); + statements["stock_status"].reset(); + + while(statements["stock_status"].step() == Sqlite.ROW) { + StockEntry entry = { + statements["stock_status"].column_text(0), + statements["stock_status"].column_text(1), + statements["stock_status"].column_int(2), + null, + null + }; + + Price mprice = statements["stock_status"].column_int(3); + Price gprice = statements["stock_status"].column_int(4); + + entry.memberprice = @"$mprice"; + entry.guestprice = @"$gprice"; + + result.add(entry); + } + + return result; + } + + public Gee.List get_prices(uint64 product) { + var result = new Gee.ArrayList(); + statements["prices"].reset(); + statements["prices"].bind_text(1, "%llu".printf(product)); + + while(statements["prices"].step() == Sqlite.ROW) { + PriceEntry entry = { + statements["prices"].column_int64(0), + statements["prices"].column_int(1), + statements["prices"].column_int(2) + }; + + result.add(entry); + } + + return result; + } + + public Gee.List get_restocks(uint64 product) { + var result = new Gee.ArrayList(); + statements["restocks"].reset(); + statements["restocks"].bind_text(1, "%llu".printf(product)); + + while(statements["restocks"].step() == Sqlite.ROW) { + RestockEntry entry = { + statements["restocks"].column_int64(0), + statements["restocks"].column_int(1) + }; + + Price p = statements["restocks"].column_int(2); + entry.price = @"$p"; + + result.add(entry); + } + + return result; + } + + public bool buy(uint64 article) { + if(is_logged_in()) { + int rc = 0; + int64 timestamp = (new DateTime.now_utc()).to_unix(); + + statements["purchase"].reset(); + statements["purchase"].bind_text(1, "%d".printf(user)); + statements["purchase"].bind_text(2, "%llu".printf(article)); + statements["purchase"].bind_text(3, "%llu".printf(timestamp)); + + rc = statements["purchase"].step(); + if(rc != Sqlite.DONE) + error("[interner Fehler: %d]".printf(rc)); + + return true; + } else { + return false; + } + } + + public string get_product_name(uint64 article) { + statements["product_name"].reset(); + statements["product_name"].bind_text(1, "%llu".printf(article)); + + int rc = statements["product_name"].step(); + + switch(rc) { + case Sqlite.ROW: + return statements["product_name"].column_text(0); + case Sqlite.DONE: + return "unbekanntes Produkt: %llu".printf(article); + default: + return "[interner Fehler: %d]".printf(rc); + } + } + + public int get_product_amount(uint64 article) { + statements["product_amount"].reset(); + statements["product_amount"].bind_text(1, "%llu".printf(article)); + + int rc = statements["product_amount"].step(); + + switch(rc) { + case Sqlite.ROW: + return statements["product_amount"].column_int(0); + case Sqlite.DONE: + warning("unbekanntes Produkt: %llu".printf(article)); + return -1; + default: + warning("[interner Fehler: %d]".printf(rc)); + return -1; + } + } + + public int get_product_price(uint64 article) { + int64 timestamp = (new DateTime.now_utc()).to_unix(); + bool member = user != 0; + + statements["price"].reset(); + statements["price"].bind_text(1, "%llu".printf(article)); + statements["price"].bind_text(2, "%lld".printf(timestamp)); + + int rc = statements["price"].step(); + + switch(rc) { + case Sqlite.ROW: + if(member) + return statements["price"].column_int(0); + else + return statements["price"].column_int(1); + case Sqlite.DONE: + write_to_log("unbekanntes Produkt: %llu\n", article); + return 0; + default: + write_to_log("[interner Fehler: %d]\n", rc); + return 0; + } + } + + public bool undo() { + if(is_logged_in()) { + uint64 pid = 0; + int rc = 0; + + statements["last_purchase"].reset(); + statements["last_purchase"].bind_text(1, "%d".printf(user)); + + rc = statements["last_purchase"].step(); + switch(rc) { + case Sqlite.ROW: + pid = uint64.parse(statements["last_purchase"].column_text(0)); + write_to_log("remove purchase of %llu", pid); + break; + case Sqlite.DONE: + write_to_log("undo not possible without purchases"); + return false; + default: + error("[interner Fehler: %d]".printf(rc)); + } + + statements["undo"].reset(); + statements["undo"].bind_text(1, "%d".printf(user)); + + rc = statements["undo"].step(); + if(rc != Sqlite.DONE) + error("[interner Fehler: %d]".printf(rc)); + + return true; + } + + return false; + } + + public bool restock(int user, uint64 product, uint amount, uint price) { + if(user > 0) { + int rc = 0; + int64 timestamp = (new DateTime.now_utc()).to_unix(); + + statements["stock"].reset(); + statements["stock"].bind_int(1, user); + statements["stock"].bind_text(2, @"$product"); + statements["stock"].bind_text(3, @"$amount"); + statements["stock"].bind_text(4, @"$price"); + statements["stock"].bind_int64(5, timestamp); + + rc = statements["stock"].step(); + if(rc != Sqlite.DONE) + error("[interner Fehler: %d]".printf(rc)); + + return true; + } + + return false; + } + + public bool new_product(uint64 id, string name, int memberprice, int guestprice) { + statements["product_create"].reset(); + statements["product_create"].bind_text(1, @"$id"); + statements["product_create"].bind_text(2, name); + statements["product_create"].bind_int(3, 0); + int rc = statements["product_create"].step(); + + if(rc != Sqlite.DONE) { + warning("[interner Fehler: %d]".printf(rc)); + return false; + } + + return new_price(id, 0, memberprice, guestprice); + } + + public bool new_price(uint64 product, int64 timestamp, int memberprice, int guestprice) { + statements["price_create"].reset(); + statements["price_create"].bind_text(1, @"$product"); + statements["price_create"].bind_int64(2, timestamp); + statements["price_create"].bind_int(3, memberprice); + statements["price_create"].bind_int(4, guestprice); + int rc = statements["price_create"].step(); + + if(rc != Sqlite.DONE) { + warning("[interner Fehler: %d]".printf(rc)); + return false; + } + + return true; + } + + public bool is_logged_in() { + return this.logged_in; + } + + public bool check_user_password(int32 user, string password) { + statements["password_get"].reset(); + statements["password_get"].bind_int(1, user); + + if(statements["password_get"].step() == Sqlite.ROW) { + var pwhash_db = statements["password_get"].column_text(0); + var pwhash_user = Checksum.compute_for_string(ChecksumType.SHA256, password); + + stdout.printf("tried login: %s\n", pwhash_user); + + return pwhash_db == pwhash_user; + } else { + return false; + } + } + + public void set_sessionid(int user, string sessionid) { + statements["session_set"].reset(); + statements["session_set"].bind_text(1, sessionid); + statements["session_set"].bind_int(2, user); + + int rc = statements["session_set"].step(); + if(rc != Sqlite.DONE) + error("[interner Fehler: %d]".printf(rc)); + } + + public int get_user_by_sessionid(string sessionid) throws WebSessionError { + statements["session_get"].reset(); + statements["session_get"].bind_text(1, sessionid); + + if(statements["session_get"].step() == Sqlite.ROW) { + return statements["session_get"].column_int(0); + } else { + throw new WebSessionError.SESSION_NOT_FOUND("No such session available in database!"); + } + } + + public UserInfo get_user_info(int user) { + var result = UserInfo(); + statements["userinfo"].reset(); + statements["userinfo"].bind_int(1, user); + + if(statements["userinfo"].step() == Sqlite.ROW) { + result.id = user; + result.firstname = statements["userinfo"].column_text(0); + result.lastname = statements["userinfo"].column_text(1); + result.email = statements["userinfo"].column_text(2); + result.gender = statements["userinfo"].column_text(3); + result.street = statements["userinfo"].column_text(4); + result.plz = statements["userinfo"].column_int(5); + result.city = statements["userinfo"].column_text(6); + } + + return result; + } + + public UserAuth get_user_auth(int user) { + var result = UserAuth(); + result.id = user; + + statements["userauth"].reset(); + statements["userauth"].bind_int(1, user); + if(statements["userauth"].step() == Sqlite.ROW) { + result.disabled = statements["userauth"].column_int(0) == 1; + result.superuser = statements["userauth"].column_int(1) == 1; + } + + return result; + } + + public string get_username(int user) throws WebSessionError { + statements["username"].reset(); + statements["username"].bind_int(1, user); + + if(statements["username"].step() == Sqlite.ROW) { + return statements["username"].column_text(0)+" "+statements["username"].column_text(1); + } else { + throw new WebSessionError.USER_NOT_FOUND("No such user available in database!"); + } + } + + public Gee.List get_invoice(int user, int64 from=0, int64 to=-1) { + var result = new Gee.ArrayList(); + + if(to == -1) { + to = time_t(); + } + + statements["invoice"].reset(); + statements["invoice"].bind_int(1, user); + statements["invoice"].bind_int64(2, from); + statements["invoice"].bind_int64(3, to); + + while(statements["invoice"].step() == Sqlite.ROW) { + InvoiceEntry entry = {}; + entry.timestamp = statements["invoice"].column_int64(0); + entry.product.ean = uint64.parse(statements["invoice"].column_text(1)); + entry.product.name = statements["invoice"].column_text(2); + entry.price = statements["invoice"].column_int(3); + result.add(entry); + } + + return result; + } + + public DateTime get_first_purchase(int user) { + statements["purchase_first"].reset(); + statements["purchase_first"].bind_int(1, user); + + if(statements["purchase_first"].step() == Sqlite.ROW) + return new DateTime.from_unix_utc(statements["purchase_first"].column_int64(0)); + else + return new DateTime.from_unix_utc(0); + } + + public DateTime get_last_purchase(int user) { + statements["purchase_last"].reset(); + statements["purchase_last"].bind_int(1, user); + + if(statements["purchase_last"].step() == Sqlite.ROW) + return new DateTime.from_unix_utc(statements["purchase_last"].column_int64(0)); + else + return new DateTime.from_unix_utc(0); + } + + public StatsInfo get_stats_info() { + var result = StatsInfo(); + + DateTime now = new DateTime.now_local(); + DateTime today = new DateTime.local(now.get_year(), now.get_month(), now.get_hour() < 8 ? now.get_day_of_month()-1 : now.get_day_of_month(), 8, 0, 0); + DateTime month = new DateTime.local(now.get_year(), now.get_day_of_month() < 16 ? now.get_month()-1 : now.get_month(), 16, 8, 0, 0); + + DateTime last4weeks = now.add_days(-28); + DateTime last4months = now.add_months(-4); + + statements["count_articles"].reset(); + if(statements["count_articles"].step() == Sqlite.ROW) + result.count_articles = statements["count_articles"].column_int(0); + + statements["count_users"].reset(); + if(statements["count_users"].step() == Sqlite.ROW) + result.count_users = statements["count_users"].column_int(0); + + statements["stock_value"].reset(); + if(statements["stock_value"].step() == Sqlite.ROW) + result.stock_value = statements["stock_value"].column_int(0); + + statements["total_sales"].reset(); + statements["total_sales"].bind_int64(1, 0); + if(statements["total_sales"].step() == Sqlite.ROW) + result.sales_total = statements["total_sales"].column_int(0); + + statements["total_profit"].reset(); + statements["total_profit"].bind_int64(1, 0); + if(statements["total_profit"].step() == Sqlite.ROW) + result.profit_total = statements["total_profit"].column_int(0); + + statements["total_sales"].reset(); + statements["total_sales"].bind_int64(1, today.to_unix()); + if(statements["total_sales"].step() == Sqlite.ROW) + result.sales_today = statements["total_sales"].column_int(0); + + statements["total_profit"].reset(); + statements["total_profit"].bind_int64(1, today.to_unix()); + if(statements["total_profit"].step() == Sqlite.ROW) + result.profit_today = statements["total_profit"].column_int(0); + + statements["total_sales"].reset(); + statements["total_sales"].bind_int64(1, month.to_unix()); + if(statements["total_sales"].step() == Sqlite.ROW) + result.sales_this_month = statements["total_sales"].column_int(0); + + statements["total_profit"].reset(); + statements["total_profit"].bind_int64(1, month.to_unix()); + if(statements["total_profit"].step() == Sqlite.ROW) + result.profit_this_month = statements["total_profit"].column_int(0); + + statements["total_sales"].reset(); + statements["total_sales"].bind_int64(1, last4weeks.to_unix()); + if(statements["total_sales"].step() == Sqlite.ROW) + result.sales_per_day = statements["total_sales"].column_int(0) / 28; + + statements["total_profit"].reset(); + statements["total_profit"].bind_int64(1, last4weeks.to_unix()); + if(statements["total_profit"].step() == Sqlite.ROW) + result.profit_per_day = statements["total_profit"].column_int(0) / 28; + + statements["total_sales"].reset(); + statements["total_sales"].bind_int64(1, last4months.to_unix()); + if(statements["total_sales"].step() == Sqlite.ROW) + result.sales_per_month = statements["total_sales"].column_int(0) / 4; + + statements["total_profit"].reset(); + statements["total_profit"].bind_int64(1, last4months.to_unix()); + if(statements["total_profit"].step() == Sqlite.ROW) + result.profit_per_month = statements["total_profit"].column_int(0) / 4; + + return result; + } +} diff --git a/src/device.vala b/src/device.vala new file mode 100644 index 0000000..5d7fc09 --- /dev/null +++ b/src/device.vala @@ -0,0 +1,280 @@ +/* Copyright 2012, Sebastian Reichel + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +public class Device { + private Posix.termios newtio; + private Posix.termios restoretio; + public int fd=-1; + private IOChannel io_read; + public int byterate; + private File lockfile; + + public signal void received_barcode(string barcode); + + public Device(string device, int rate, int bits, int stopbits) { + Posix.speed_t baudrate = Posix.B9600; + + try { + /* check lock file */ + lockfile = File.new_for_path("/var/lock/LCK.." + device.replace("/dev/", "")); + if(lockfile.query_exists()) { + int pid = -1; + uint8[] data; + if(lockfile.load_contents(null, out data, null)) { + pid = int.parse((string) data); + } else { + error("Can't read lock file!\n"); + } + + if(Posix.kill(pid, 0) == 0) { + error("serial device is locked!\n"); + } + } + + var pid = "%d\n".printf(Posix.getpid()); + lockfile.replace_contents(pid.data, null, false, FileCreateFlags.NONE, null); + + fd = Posix.open(device, Posix.O_RDWR /*| Posix.O_NONBLOCK*/); + + if(fd < 0) { + fd = -1; + lockfile.delete(); + error("Could not open device!\n"); + } + + } catch(Error e) { + error("Could not create lock file: %s!\n", e.message); + } + + + Posix.tcflush(fd, Posix.TCIOFLUSH); + + Posix.tcgetattr(fd, out restoretio); + + /* apply settings */ + switch(rate) { + case 300: + baudrate = Posix.B300; + break; + case 600: + baudrate = Posix.B600; + break; + case 1200: + baudrate = Posix.B1200; + break; + case 2400: + baudrate = Posix.B2400; + break; + case 4800: + baudrate = Posix.B4800; + break; + case 9600: + baudrate = Posix.B9600; + break; + case 19200: + baudrate = Posix.B19200; + break; + case 38400: + baudrate = Posix.B38400; + break; + case 57600: + baudrate = Posix.B57600; + break; + case 115200: + baudrate = Posix.B115200; + break; + case 230400: + baudrate = Posix.B230400; + break; + default: + /* not supported */ + rate = 9600; + break; + } + + Posix.cfsetospeed(ref newtio, baudrate); + Posix.cfsetispeed(ref newtio, baudrate); + + switch(bits) { + case 5: + newtio.c_cflag = (newtio.c_cflag & ~Posix.CSIZE) | Posix.CS5; + break; + case 6: + newtio.c_cflag = (newtio.c_cflag & ~Posix.CSIZE) | Posix.CS6; + break; + case 7: + newtio.c_cflag = (newtio.c_cflag & ~Posix.CSIZE) | Posix.CS7; + break; + case 8: + default: + newtio.c_cflag = (newtio.c_cflag & ~Posix.CSIZE) | Posix.CS8; + break; + } + + newtio.c_cflag |= Posix.CLOCAL | Posix.CREAD; + + newtio.c_cflag &= ~(Posix.PARENB | Posix.PARODD); + + /* TODO: parity */ + + newtio.c_cflag &= ~Linux.Termios.CRTSCTS; + + if(stopbits == 2) + newtio.c_cflag |= Posix.CSTOPB; + else + newtio.c_cflag &= ~Posix.CSTOPB; + + newtio.c_iflag = Posix.IGNBRK; + + newtio.c_lflag = 0; + newtio.c_oflag = 0; + + newtio.c_cc[Posix.VTIME]=1; + newtio.c_cc[Posix.VMIN]=1; + + newtio.c_lflag &= ~(Posix.ECHONL|Posix.NOFLSH); + + int mcs=0; + Posix.ioctl(fd, Linux.Termios.TIOCMGET, out mcs); + mcs |= Linux.Termios.TIOCM_RTS; + Posix.ioctl(fd, Linux.Termios.TIOCMSET, out mcs); + + Posix.tcsetattr(fd, Posix.TCSANOW, newtio); + + this.byterate = rate/bits; + + try { + io_read = new IOChannel.unix_new(fd); + io_read.set_line_term("\r\n", 2); + if(io_read.set_encoding(null) != IOStatus.NORMAL) + error("Failed to set encoding"); + if(!(io_read.add_watch(IOCondition.IN | IOCondition.HUP, device_read) != 0)) { + error("Could not bind IOChannel"); + } + } catch(IOChannelError e) { + error("IOChannel: %s", e.message); + } + } + + ~Device() { + /* restore old tty config */ + Posix.tcsetattr(fd, Posix.TCSANOW, restoretio); + + /* close file */ + Posix.close(fd); + + /* remove lock */ + lockfile.delete(); + } + + private bool device_read(IOChannel gio, IOCondition cond) { + IOStatus ret; + string msg; + size_t len, term_char; + + if((cond & IOCondition.HUP) == IOCondition.HUP) + error("Lost device"); + + try { + ret = gio.read_line(out msg, out len, out term_char); + msg = msg[0:(long)term_char]; + + if(msg.has_prefix("USER ") || msg.has_prefix("STOCK") || msg.has_prefix("AMOUNT ")) { + if(!check_code39_checksum(msg)) + received_barcode("SCANNER RETURNED INCORRECT DATA"); + else {/* remove checksum */ + msg = msg[0:-1]; + received_barcode(msg); + } + } + else + received_barcode(msg); + } + catch(IOChannelError e) { + stderr.printf("IOChannel Error: %s", e.message); + return false; + } + catch(ConvertError e) { + stderr.printf("Convert Error: %s", e.message); + return false; + } + return true; + } + + private ssize_t write(void *buf, size_t count) { + ssize_t size = Posix.write(fd, buf, count); + return size; + } + + private bool check_code39_checksum(string data) { + int result = 0; + + for(int i = 0; i= '0' && data[i] <= '9') + result += data[i] - '0'; + else if(data[i] >= 'A' && data[i] <= 'Z') + result += data[i] - 'A' + 10; + else + switch(data[i]) { + case '-': + result += 36; break; + case '.': + result += 37; break; + case ' ': + result += 38; break; + case '$': + result += 39; break; + case '/': + result += 40; break; + case '+': + result += 41; break; + case '%': + result += 42; break; + default: + /* invalid character */ + return false; + } + + result %= 43; + } + + if(result < 10) + result = result + '0'; + else if(result < 36) + result = result - 10 + 'A'; + else + switch(result) { + case 36: result = '-'; break; + case 37: result = '.'; break; + case 38: result = ' '; break; + case 39: result = '$'; break; + case 40: result = '/'; break; + case 41: result = '+'; break; + case 42: result = '%'; break; + } + + return (data[data.length-1] == result); + } + + /** + * @param duration duration of the blink in 0.1 seconds + */ + public void blink(uint duration) { + uint size = byterate/10 * duration; + var msg = new uint8[size]; + Posix.memset(msg, 0xFF, msg.length); + this.write(msg, msg.length); + } +} diff --git a/src/graph-data.vala b/src/graph-data.vala new file mode 100644 index 0000000..045c6ad --- /dev/null +++ b/src/graph-data.vala @@ -0,0 +1,185 @@ +/* Copyright 2012, Sebastian Reichel + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +public class stock { + public class product { + public struct subitem { + public uint64 timestamp; + public int amount; + + public string json { + owned get { + return "[%llu, %d]".printf(timestamp*1000, amount); + } + } + } + + public uint64 id; + public string name; + subitem[] data; + + public product(uint64 id, string name) { + this.id = id; + this.name = name; + this.data = null; + } + + public void add(uint64 timestamp, int amount) { + subitem newitem = {timestamp, amount}; + this.data += newitem; + } + + public string json { + owned get { + var data_array = "["; + if(data != null) { + for(int i=0; i < data.length-1; i++) + data_array += data[i].json + ", "; + data_array += data[data.length-1].json; + } + data_array += "]"; + + return "{label: \"%s\", data: %s}".printf(name, data_array); + } + } + } + + Gee.HashMap data; + + public stock() { + data = new Gee.HashMap(); + } + + public void add(product i) { + data[i.id] = i; + } + + public product get_product(uint64 id) { + return data[id]; + } + + public string json { + owned get { + var result = "{"; + foreach(var entry in data.entries) { + uint64? id = entry.key; + string pdata = entry.value.json; + result += @"\"product_$id\": $pdata, "; + } + result = result.substring(0, result.length-2); + result += "}"; + return result; + } + } +} + +public class profit_per_day { + public struct subitem { + public uint64 timestamp; + public Price amount; + + public string json { + owned get { + return @"[$(timestamp*1000), $amount]"; + } + } + } + + private subitem[] profit_data; + private subitem[] sales_data; + + public void add_profit(uint64 timestamp, int amount) { + subitem newitem = {timestamp, amount}; + this.profit_data += newitem; + } + + public void add_sales(uint64 timestamp, int amount) { + subitem newitem = {timestamp, amount}; + this.sales_data += newitem; + } + + public string json { + owned get { + var result = "{\"profit\": {label: \"Profit\", data: ["; + if(profit_data != null) { + for(int i=0; i < profit_data.length-1; i++) + result += profit_data[i].json + ", "; + result += profit_data[profit_data.length-1].json; + } + result += "]},\"sales\": {label: \"Sales\", data:["; + if(sales_data != null) { + for(int i=0; i < sales_data.length-1; i++) + result += sales_data[i].json + ", "; + result += sales_data[sales_data.length-1].json; + } + result += "]}}"; + + return result; + } + } +} + +public class profit_per_weekday { + public Price[] day = new Price[7]; + + public profit_per_weekday() { + for(int i=0; i data; + + public profit_per_product() { + data = new Gee.HashMap(); + } + + public void add(string product, int amount) { + if(data.has_key(product)) + data[product] = data[product] + amount; + else + data[product] = amount; + } + + public string json { + owned get { + var result = "["; + foreach(var e in data.entries) { + result += @"{ label: \"$(e.key)\", data: $((Price) e.value) },"; + } + if(result.length > 1) { + result = result.substring(0, result.length-1); + } + result += "]"; + return result; + } + } +} diff --git a/src/main.vala b/src/main.vala new file mode 100644 index 0000000..74736d2 --- /dev/null +++ b/src/main.vala @@ -0,0 +1,113 @@ +/* Copyright 2012, Sebastian Reichel + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +public Device dev; +public Database db; + +public static int main(string[] args) { + if(args.length < 2) { + stderr.printf("%s \n", args[0]); + return 1; + } + + dev = new Device(args[1], 9600, 8, 1); + db = new Database("shop.db"); + + dev.received_barcode.connect((data) => { + if(interpret(data)) + dev.blink(10); + }); + + write_to_log("KtT Shop System has been started"); + + /* attach webserver to mainloop */ + new WebServer(); + + /* run mainloop */ + new MainLoop().run(); + + /* call destructors */ + dev = null; + db = null; + + return 0; +} + +public void write_to_log(string format, ...) { + var arguments = va_list(); + var message = format.vprintf(arguments); + + stdout.printf(message + "\n"); +} + +public static bool interpret(string data) { + if(data.has_prefix("USER ")) { + string str_id = data.substring(5); + int32 id = int.parse(str_id); + + /* check if data has valid format */ + if(data != "USER %d".printf(id)) { + write_to_log("ungültige Benutzernummer: %s", data); + return false; + } + + if(db.is_logged_in()) { + write_to_log("Last User forgot to logout!"); + db.logout(); + } + + write_to_log("Login: %d", id); + return db.login(id); + } else if(data == "GUEST") { + if(db.is_logged_in()) { + write_to_log("Last User forgot to logout!"); + db.logout(); + } + + write_to_log("Login: Guest"); + return db.login(0); + } else if(data == "UNDO") { + if(!db.is_logged_in()) { + write_to_log("Can't undo if not logged in!"); + return false; + } else { + write_to_log("Undo last purchase!"); + return db.undo(); + } + } else if(data == "LOGOUT") { + if(db.is_logged_in()) { + write_to_log("Logout!"); + return db.logout(); + } + + return false; + } else { + uint64 id = uint64.parse(data); + + /* check if data has valid format */ + if(data != "%llu".printf(id)) { + write_to_log("ungültiges Produkt: %s", data); + return false; + } + + if(db.buy(id)) { + write_to_log("gekaufter Artikel: %s (%d,%02d €)", db.get_product_name(id), db.get_product_price(id)/100, db.get_product_price(id) % 100); + return true; + } else { + write_to_log("Kauf fehlgeschlagen!"); + return false; + } + } +} diff --git a/src/price.vapi b/src/price.vapi new file mode 100644 index 0000000..b60e2a6 --- /dev/null +++ b/src/price.vapi @@ -0,0 +1,9 @@ +[SimpleType] +[GIR (name = "gint")] +[CCode (cname = "gint", cheader_filename = "glib.h", type_id = "G_TYPE_INT", marshaller_type_name = "INT", get_value_function = "g_value_get_int", set_value_function = "g_value_set_int", default_value = "0", type_signature = "i")] +[IntegerType (rank = 6)] +public struct Price : int { + public new string to_string() { + return "%d.%02d".printf(this / 100, this % 100); + } +} diff --git a/src/session.vala b/src/session.vala new file mode 100644 index 0000000..da1a430 --- /dev/null +++ b/src/session.vala @@ -0,0 +1,141 @@ +/* Copyright 2012, Sebastian Reichel + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +public errordomain WebSessionError { + SESSION_NOT_FOUND, + USER_NOT_FOUND +} + +public class WebSession { + public int user { + get; + private set; + default = 0; + } + public string name { + get; + private set; + default = "Guest"; + } + public bool failed { + get; + private set; + default = false; + } + public bool logged_in { + get; + private set; + default = false; + } + public bool superuser { + get; + private set; + default = false; + } + public bool disabled { + get; + private set; + default = false; + } + + private string generate_session_id(int user) { + const string charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890"; + string result = ""; + + Random.set_seed((uint32) time_t() + (uint32) Posix.getpid() + (uint32) user); + + for(int i=0; i<19; i++) { + int character_position = Random.int_range(0,charset.length); + string character = charset[character_position].to_string(); + result += character; + } + + /* TODO: make sure, that session id is unique */ + + return result; + } + + private void setup_auth(int user) { + var auth = db.get_user_auth(user); + this.disabled = auth.disabled; + this.superuser = auth.superuser; + this.logged_in = true; + } + + public void logout() { + if(logged_in) { + db.set_sessionid(user, ""); + logged_in = false; + } + } + + public WebSession(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client) { + var cookies = Soup.cookies_from_request(msg); + + /* Check for existing session */ + foreach(var cookie in cookies) { + if(cookie.name == "session") { + var sessionid = cookie.value; + + try { + user = db.get_user_by_sessionid(sessionid); + name = db.get_username(user); + setup_auth(user); + return; + } catch(WebSessionError e) { + /* invalid session, ignore */ + } + } + } + + /* check for login query */ + if(query == null || !query.contains("user") || !query.contains("password")) + return; + + /* get credentials */ + var userid = int.parse(query["user"]); + var password = query["password"]; + + /* check credentials */ + if(db.check_user_password(userid, password)) { + /* generate session */ + var sessionid = generate_session_id(userid); + + /* set session in database */ + db.set_sessionid(userid, sessionid); + + /* set session in reply cookie */ + cookies = new SList(); + var sessioncookie = new Soup.Cookie("session", sessionid, "", "/", -1); + sessioncookie.domain = null; + cookies.append(sessioncookie); + Soup.cookies_to_response(cookies, msg); + + /* login successful */ + user = userid; + try { + name = db.get_username(user); + } catch(WebSessionError e) { + name = "Unknown User"; + } + + setup_auth(user); + } else { + /* login failed */ + failed=true; + } + } +} + diff --git a/src/template.vala b/src/template.vala new file mode 100644 index 0000000..ef7e43d --- /dev/null +++ b/src/template.vala @@ -0,0 +1,103 @@ +/* Copyright 2012, Sebastian Reichel + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +public errordomain TemplateError { + NOT_FOUND, + NOT_LOADABLE, + NOT_ALLOWED, +} + +public class WebTemplate { + private string template; + public uint8[] data { get { return template.data; } } + + public WebTemplate(string file, WebSession login) throws TemplateError { + var b = File.new_for_path("templates/base.html"); + var m = File.new_for_path("templates/menu.html"); + var f = File.new_for_path("templates/"+file); + File fauth; + + if(login.logged_in) + fauth = File.new_for_path("templates/menu_logout.html"); + else + fauth = File.new_for_path("templates/menu_login.html"); + + uint8[] basis, menu, template, auth; + + if(!b.query_exists()) + throw new TemplateError.NOT_FOUND("templates/base.html not found!"); + + if(!m.query_exists()) + throw new TemplateError.NOT_FOUND("templates/menu.html not found!"); + + if(!fauth.query_exists()) + throw new TemplateError.NOT_FOUND(fauth.get_path()+" not found!"); + + if(!f.query_exists()) + throw new TemplateError.NOT_FOUND("templates/"+file+" not found!"); + + try { + if(!b.load_contents(null, out basis, null)) + throw new TemplateError.NOT_LOADABLE("templates/base.html could not be loaded!"); + if(!m.load_contents(null, out menu, null)) + throw new TemplateError.NOT_LOADABLE("templates/menu.html could not be loaded!"); + if(!fauth.load_contents(null, out auth, null)) + throw new TemplateError.NOT_LOADABLE(fauth.get_path()+" could not be loaded!"); + if(!f.load_contents(null, out template, null)) + throw new TemplateError.NOT_LOADABLE("templates/"+file+" could not be loaded!"); + } catch(Error e) { + throw new TemplateError.NOT_LOADABLE("could not load templates!"); + } + + this.template = ((string) basis).replace("{{{NAVBAR}}}", ((string) menu)); + this.template = this.template.replace("{{{AUTH}}}", ((string) auth)); + this.template = this.template.replace("{{{CONTENT}}}", ((string) template)); + this.template = this.template.replace("{{{USERNAME}}}", login.name); + this.template = this.template.replace("{{{USERID}}}", "%d".printf(login.user)); + } + + public WebTemplate.DATA(string file) throws TemplateError { + var f = File.new_for_path("templates/"+file); + uint8[] template; + + if(!f.query_exists()) + throw new TemplateError.NOT_FOUND("templates/"+file+" not found!"); + + try { + if(!f.load_contents(null, out template, null)) + throw new TemplateError.NOT_LOADABLE("templates/"+file+" could not be loaded!"); + } catch(Error e) { + throw new TemplateError.NOT_LOADABLE("could not load templates!"); + } + + this.template = (string) template; + } + + public void replace(string key, string value) { + template = template.replace("{{{"+key+"}}}", value); + } + + public void menu_set_active(string key) { + try { + var regex_active = new Regex("{{{MENU\\."+key+"}}}"); + var regex_other = new Regex("{{{MENU\\..*}}}"); + + template = regex_active.replace(template, -1, 0, "active"); + template = regex_other.replace(template, -1, 0, ""); + } catch(RegexError e) { + warning ("%s", e.message); + } + } +} diff --git a/src/web.vala b/src/web.vala new file mode 100644 index 0000000..562a1ba --- /dev/null +++ b/src/web.vala @@ -0,0 +1,592 @@ +/* Copyright 2012, Sebastian Reichel + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +public class WebServer { + private Soup.Server srv; + + void handler_default(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client) { + try { + var l = new WebSession(server, msg, path, query, client); + var t = new WebTemplate("index.html", l); + t.replace("TITLE", "KtT Shop System"); + t.menu_set_active("home"); + msg.set_response("text/html", Soup.MemoryUse.COPY, t.data); + } catch(TemplateError e) { + stderr.printf(e.message+"\n"); + handler_404(server, msg, path, query, client); + } + } + + void handler_logout(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client) { + try { + var l = new WebSession(server, msg, path, query, client); + l.logout(); + var t = new WebTemplate("logout.html", l); + t.replace("TITLE", "KtT Shop System"); + t.menu_set_active("home"); + msg.set_response("text/html", Soup.MemoryUse.COPY, t.data); + } catch(TemplateError e) { + stderr.printf(e.message+"\n"); + handler_404(server, msg, path, query, client); + } + } + + void handler_users(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client) { + string[] pathparts = path.split("/"); + + if(pathparts.length <= 2) { + handler_todo(server, msg, path, query, client); + } else { + int id = int.parse(pathparts[2]); + + if(pathparts.length <= 3) { + handler_user_entry(server, msg, path, query, client, id); + } else { + switch(pathparts[3]) { + case "invoice": + uint16 selectedyear = (pathparts.length >= 5 && pathparts[4] != "") ? (uint16) int.parse(pathparts[4]) : (uint16) (new DateTime.now_local()).get_year(); + uint8 selectedmonth = (pathparts.length >= 6 && pathparts[5] != "") ? (uint8) int.parse(pathparts[5]) : (uint8) (new DateTime.now_local()).get_month(); + uint8 selectedday = (pathparts.length >= 7 && pathparts[6] != "") ? (uint8) int.parse(pathparts[6]) : (uint8) (new DateTime.now_local()).get_day_of_month(); + handler_user_invoice(server, msg, path, query, client, id, selectedyear, selectedmonth, selectedday); + break; + case "stats": + handler_todo(server, msg, path, query, client); + break; + default: + handler_404(server, msg, path, query, client); + break; + } + } + } + } + + void handler_user_entry(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client, int id) { + try { + var l = new WebSession(server, msg, path, query, client); + if(id != l.user && !l.superuser) { + handler_403(server, msg, path, query, client); + return; + } + var t = new WebTemplate("users/entry.html", l); + t.replace("TITLE", "KtT Shop System: User Info %llu".printf(id)); + t.menu_set_active("users"); + + var userinfo = db.get_user_info(id); + + t.replace("UID", "%d".printf(userinfo.id)); + t.replace("FIRSTNAME", userinfo.firstname); + t.replace("LASTNAME", userinfo.lastname); + t.replace("EMAIL", userinfo.email); + t.replace("GENDER", userinfo.gender); + t.replace("STREET", userinfo.street); + t.replace("POSTALCODE", "%d".printf(userinfo.plz)); + t.replace("CITY", userinfo.city); + + var userauth = db.get_user_auth(id); + t.replace("DISABLED", userauth.disabled ? "true" : "false"); + t.replace("SUPERUSER", userauth.superuser ? "true" : "false"); + + msg.set_response("text/html", Soup.MemoryUse.COPY, t.data); + } catch(TemplateError e) { + stderr.printf(e.message+"\n"); + handler_404(server, msg, path, query, client); + } + } + + void handler_user_invoice(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client, int id, uint16 selectedyear, uint8 selectedmonth, uint8 selectedday) { + DateTime start, stop; + + DateYear y = (DateYear) selectedyear; + if(!y.valid() || y > 8000) { + selectedyear = (uint16) new DateTime.now_local().get_year(); + y = (DateYear) selectedyear; + } + + DateMonth m = (DateMonth) selectedmonth; + if(selectedmonth != 0 && !m.valid()) { + selectedmonth = (uint8) new DateTime.now_local().get_month(); + m = (DateMonth) selectedmonth; + } + + DateDay d = (DateDay) selectedday; + if(selectedday != 0 && !d.valid()) { + selectedday = (uint8) new DateTime.now_local().get_day_of_month(); + d = (DateDay) selectedday; + } + + try { + var l = new WebSession(server, msg, path, query, client); + if(id != l.user && !l.superuser) { + handler_403(server, msg, path, query, client); + return; + } + var t = new WebTemplate("users/invoice.html", l); + t.replace("TITLE", "KtT Shop System: User Invoice %llu".printf(id)); + t.menu_set_active("users"); + + /* years, in which something has been purchased by the user */ + var first = db.get_first_purchase(id); + var last = db.get_last_purchase(id); + string years = ""; + for(int i=first.get_year(); i <= last.get_year(); i++) { + years += @"
  • $i
  • "; + } + t.replace("YEARS", years); + t.replace("SELECTEDYEAR", @"$selectedyear"); + + /* months, in which something has been purchased by the user */ + string[] monthnames = { "All Months", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }; + string months = @"
  • All Months
  • "; + for(int i=1; i 0 && i < first.get_month()) + months += @"
  • $(monthnames[i])
  • "; + else if(selectedyear < first.get_year()) + months += @"
  • $(monthnames[i])
  • "; + else if(last.get_year() == selectedyear && i > last.get_month()) + months += @"
  • $(monthnames[i])
  • "; + else if(selectedyear > last.get_year()) + months += @"
  • $(monthnames[i])
  • "; + else + months += @"
  • $(monthnames[i])
  • "; + } + t.replace("MONTHS", months); + t.replace("SELECTEDMONTH", @"$(monthnames[selectedmonth])"); + + int dim = m.valid() ? Date.get_days_in_month(m, y) : 0; + string days = @"
  • All Days
  • "; + for(int i=1; i<=dim; i++) { + if(first.get_year() == selectedyear && first.get_month() == selectedmonth && i < first.get_day_of_month()) + days += @"
  • $i
  • "; + else if(selectedyear < first.get_year() || (selectedyear == first.get_year() && selectedmonth < first.get_month())) + days += @"
  • $i
  • "; + else if(last.get_year() == selectedyear && last.get_month() == selectedmonth && i > last.get_day_of_month()) + days += @"
  • $i
  • "; + else if(selectedyear > last.get_year() || (selectedyear == last.get_year() && selectedmonth > last.get_month())) + days += @"
  • $i
  • "; + else + days += @"
  • $i
  • "; + } + t.replace("DAYS", days); + if(selectedday > 0) + t.replace("SELECTEDDAY", @"$selectedday"); + else + t.replace("SELECTEDDAY", "All Days"); + + if(selectedday != 0) { + start = new DateTime.local(selectedyear, selectedmonth, selectedday, 8, 0, 0); + stop = start.add_days(1); + } else if(selectedmonth != 0) { + start = new DateTime.local(selectedyear, selectedmonth, 16, 8, 0, 0); + stop = start.add_months(1); + } else { + start = new DateTime.local(selectedyear, 1, 16, 8, 0, 0); + stop = start.add_years(1); + } + + string table = ""; + Price sum = 0; + foreach(var e in db.get_invoice(id, start.to_unix(), stop.to_unix())) { + var timestamp = new DateTime.from_unix_utc(e.timestamp); + var date = timestamp.format("%d.%m.%Y"); + var time = timestamp.format("%H:%M:%S"); + var product = e.product.name; + var price = e.price; + table += @"$date$time$product$price€"; + sum += e.price; + } + + t.replace("DATA", table); + t.replace("SUM", @"$sum"); + + msg.set_response("text/html", Soup.MemoryUse.COPY, t.data); + } catch(TemplateError e) { + stderr.printf(e.message+"\n"); + handler_404(server, msg, path, query, client); + } + } + + void handler_products(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client) { + string[] pathparts = path.split("/"); + + if(pathparts.length <= 2 || pathparts[2] == "") { + handler_product_list(server, msg, path, query, client); + } else { + uint64 id = uint64.parse(pathparts[2]); + + if(pathparts.length <= 3) { + handler_product_entry(server, msg, path, query, client, id); + } else { + switch(pathparts[3]) { + case "restock": + handler_product_restock(server, msg, path, query, client, id); + break; + default: + handler_product_entry(server, msg, path, query, client, id); + break; + } + } + } + } + + void handler_product_list(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client) { + try { + var l = new WebSession(server, msg, path, query, client); + var t = new WebTemplate("products/index.html", l); + t.replace("TITLE", "KtT Shop System: Product List"); + t.menu_set_active("products"); + + string table = ""; + foreach(var e in db.get_stock()) { + table += "%s%s%d%s€%s€".printf( + e.id, e.id, e.id, e.name, e.amount, e.memberprice, e.guestprice + ); + } + + t.replace("DATA", table); + + if(l.superuser) + t.replace("NEWPRODUCT", "block"); + else + t.replace("NEWPRODUCT", "none"); + + msg.set_response("text/html", Soup.MemoryUse.COPY, t.data); + } catch(TemplateError e) { + stderr.printf(e.message+"\n"); + handler_404(server, msg, path, query, client); + } + } + + void handler_product_entry(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client, uint64 id) { + try { + var l = new WebSession(server, msg, path, query, client); + var t = new WebTemplate("products/entry.html", l); + t.replace("TITLE", "KtT Shop System: Product %llu".printf(id)); + t.menu_set_active("products"); + + /* ean */ + t.replace("EAN", "%llu".printf(id)); + + /* name */ + string name = db.get_product_name(id); + t.replace("NAME", name); + + /* amount */ + t.replace("AMOUNT", "%d".printf(db.get_product_amount(id))); + + if(l.superuser) + t.replace("RESTOCK", "block"); + else + t.replace("RESTOCK", "none"); + + /* prices */ + string prices = ""; + foreach(var e in db.get_prices(id)) { + var time = new DateTime.from_unix_local(e.valid_from); + prices += @"%s$(e.memberprice)€$(e.guestprice)€".printf( + time.format("%Y-%m-%d %H:%M") + ); + } + t.replace("PRICES", prices); + + /* restocks */ + string restocks = ""; + foreach(var e in db.get_restocks(id)) { + var time = new DateTime.from_unix_local(e.timestamp); + restocks += "%s%d%s€".printf( + time.format("%Y-%m-%d %H:%M"), e.amount, e.price + ); + } + t.replace("RESTOCKS", restocks); + + msg.set_response("text/html", Soup.MemoryUse.COPY, t.data); + } catch(TemplateError e) { + stderr.printf(e.message+"\n"); + handler_404(server, msg, path, query, client); + } + } + + void handler_products_new(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client) { + try { + var session = new WebSession(server, msg, path, query, client); + var template = new WebTemplate("products/new.html", session); + template.replace("TITLE", "KtT Shop System: New Product"); + template.menu_set_active("products"); + + if(!session.superuser) { + handler_403(server, msg, path, query, client); + return; + } + + if(query != null && query.contains("name") && query.contains("id") && query.contains("memberprice") && query.contains("guestprice")) { + var name = query["name"]; + var ean = uint64.parse(query["id"]); + Price memberprice = int.parse(query["memberprice"]); + Price guestprice = int.parse(query["guestprice"]); + + if(ean > 0 && memberprice > 0 && guestprice > 0 && db.new_product(ean, name, memberprice, guestprice)) { + template.replace("NAME", name); + template.replace("EAN", @"$ean"); + template.replace("MEMBERPRICE", @"$memberprice€"); + template.replace("GUESTPRICE", @"$guestprice€"); + template.replace("NEW.OK", "block"); + template.replace("NEW.FAIL", "none"); + } else { + template.replace("NAME", "..."); + template.replace("NEW.OK", "none"); + template.replace("NEW.FAIL", "block"); + } + } else { + template.replace("NAME", "..."); + template.replace("NEW.OK", "none"); + template.replace("NEW.FAIL", "block"); + } + + msg.set_response("text/html", Soup.MemoryUse.COPY, template.data); + } catch(TemplateError e) { + stderr.printf(e.message+"\n"); + handler_404(server, msg, path, query, client); + } + } + + void handler_product_restock(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client, uint64 id) { + try { + var session = new WebSession(server, msg, path, query, client); + + if(!session.superuser) { + handler_403(server, msg, path, query, client); + return; + } + + var template = new WebTemplate("products/restock.html", session); + template.replace("TITLE", "KtT Shop System: Restock Product %llu".printf(id)); + template.replace("NAME", db.get_product_name(id)); + template.menu_set_active("products"); + + if(query != null && query.contains("amount") && query.contains("price")) { + int amount = int.parse(query["amount"]); + Price price = int.parse(query["price"]); + + if(amount >= 1 && price >= 1) { + if(db.restock(session.user, id, amount, price)) { + template.replace("AMOUNT", @"$amount"); + template.replace("PRICE", @"$price"); + template.replace("RESTOCK.OK", "block"); + template.replace("RESTOCK.FAIL", "none"); + msg.set_response("text/html", Soup.MemoryUse.COPY, template.data); + return; + } + } + } + + template.replace("RESTOCK.OK", "none"); + template.replace("RESTOCK.FAIL", "block"); + msg.set_response("text/html", Soup.MemoryUse.COPY, template.data); + return; + } catch(TemplateError e) { + stderr.printf(e.message+"\n"); + handler_404(server, msg, path, query, client); + } + } + + void handler_stats(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client) { + try { + var l = new WebSession(server, msg, path, query, client); + var t = new WebTemplate("stats/index.html", l); + t.replace("TITLE", "KtT Shop System: Statistics"); + t.menu_set_active("stats"); + + var stats = db.get_stats_info(); + + t.replace("NUMBER_OF_ARTICLES", @"$(stats.count_articles)"); + t.replace("NUMBER_OF_USERS", @"$(stats.count_users)"); + t.replace("STOCK_VALUE", @"$(stats.stock_value)€"); + t.replace("TOTAL_SALES", @"$(stats.sales_total)€"); + t.replace("TOTAL_PROFIT", @"$(stats.profit_total)€"); + t.replace("SALES_TODAY", @"$(stats.sales_today)€"); + t.replace("PROFIT_TODAY", @"$(stats.profit_today)€"); + t.replace("SALES_THIS_MONTH", @"$(stats.sales_this_month)€"); + t.replace("PROFIT_THIS_MONTH", @"$(stats.profit_this_month)€"); + t.replace("SALES_PER_DAY", @"$(stats.sales_per_day)€"); + t.replace("PROFIT_PER_DAY", @"$(stats.profit_per_day)€"); + t.replace("SALES_PER_MONTH", @"$(stats.sales_per_month)€"); + t.replace("PROFIT_PER_MONTH", @"$(stats.profit_per_month)€"); + + msg.set_response("text/html", Soup.MemoryUse.COPY, t.data); + } catch(TemplateError e) { + stderr.printf(e.message+"\n"); + handler_404(server, msg, path, query, client); + } + } + + void handler_stats_stock(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client) { + try { + var l = new WebSession(server, msg, path, query, client); + var t = new WebTemplate("stats/stock.html", l); + string data = db.get_stats_stock().json; + t.replace("DATA", data); + t.replace("TITLE", "KtT Shop System: Statistics: Stock"); + t.menu_set_active("stats"); + msg.set_response("text/html", Soup.MemoryUse.COPY, t.data); + } catch(TemplateError e) { + stderr.printf(e.message+"\n"); + handler_404(server, msg, path, query, client); + } + } + + void handler_stats_profit_per_day(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client) { + try { + var l = new WebSession(server, msg, path, query, client); + var t = new WebTemplate("stats/profit_per_day.html", l); + string data = db.get_stats_profit_per_day().json; + t.replace("DATA", data); + t.replace("TITLE", "KtT Shop System: Statistics: Profit"); + t.menu_set_active("stats"); + msg.set_response("text/html", Soup.MemoryUse.COPY, t.data); + } catch(TemplateError e) { + stderr.printf(e.message+"\n"); + handler_404(server, msg, path, query, client); + } + } + + void handler_stats_profit_per_weekday(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client) { + try { + var l = new WebSession(server, msg, path, query, client); + var t = new WebTemplate("stats/profit_per_weekday.html", l); + string data = db.get_stats_profit_per_weekday().json; + t.replace("DATA", data); + t.replace("TITLE", "KtT Shop System: Statistics: Profit/Weekday"); + t.menu_set_active("stats"); + msg.set_response("text/html", Soup.MemoryUse.COPY, t.data); + } catch(TemplateError e) { + stderr.printf(e.message+"\n"); + handler_404(server, msg, path, query, client); + } + } + + void handler_stats_profit_per_product(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client) { + try { + var l = new WebSession(server, msg, path, query, client); + var t = new WebTemplate("stats/profit_per_product.html", l); + string data = db.get_stats_profit_per_products().json; + t.replace("DATA", data); + t.replace("TITLE", "KtT Shop System: Statistics: Profit/Product"); + t.menu_set_active("stats"); + msg.set_response("text/html", Soup.MemoryUse.COPY, t.data); + } catch(TemplateError e) { + stderr.printf(e.message+"\n"); + handler_404(server, msg, path, query, client); + } + } + + void handler_js(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client) { + try { + var t = new WebTemplate.DATA(path); + msg.set_response("text/javascript", Soup.MemoryUse.COPY, t.data); + } catch(TemplateError e) { + stderr.printf(e.message+"\n"); + handler_404(server, msg, path, query, client); + } + } + + void handler_css(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client) { + try { + var t = new WebTemplate.DATA(path); + msg.set_response("text/css", Soup.MemoryUse.COPY, t.data); + } catch(TemplateError e) { + stderr.printf(e.message+"\n"); + handler_404(server, msg, path, query, client); + } + } + + void handler_img(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client) { + try { + var f = File.new_for_path("templates/"+path); + uint8[] data = null; + + if(f.query_exists() && f.load_contents(null, out data, null)) { + msg.set_response("image/png", Soup.MemoryUse.COPY, data); + return; + } + } catch(Error e) { + error("there has been some error: %s!\n", e.message); + } + + handler_404(server, msg, path, query, client); + return; + } + + void handler_404(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client) { + string result = "Page not Found\n"; + msg.set_status(404); + msg.set_response("text/plain", Soup.MemoryUse.COPY, result.data); + } + + void handler_403(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client) { + try { + var session = new WebSession(server, msg, path, query, client); + var template = new WebTemplate("errors/403.html", session); + template.replace("TITLE", "Access Denied"); + template.menu_set_active(""); + msg.set_status(403); + msg.set_response("text/html", Soup.MemoryUse.COPY, template.data); + } catch(TemplateError e) { + stderr.printf(e.message+"\n"); + handler_404(server, msg, path, query, client); + } } + + void handler_todo(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client) { + try { + var session = new WebSession(server, msg, path, query, client); + var template = new WebTemplate("errors/todo.html", session); + template.replace("TITLE", "KtT Shop System: ToDo"); + template.menu_set_active(""); + msg.set_response("text/html", Soup.MemoryUse.COPY, template.data); + } catch(TemplateError e) { + stderr.printf(e.message+"\n"); + handler_404(server, msg, path, query, client); + } + } + + public WebServer(int port = 8080) { + srv = new Soup.Server(Soup.SERVER_PORT, port); + + /* index */ + srv.add_handler("/", handler_default); + + /* logout */ + srv.add_handler("/logout", handler_logout); + + /* data (js, css, img) */ + srv.add_handler("/js", handler_js); + srv.add_handler("/css", handler_css); + srv.add_handler("/img", handler_img); + + /* products */ + srv.add_handler("/products", handler_products); + srv.add_handler("/products/new", handler_products_new); + + /* stats */ + srv.add_handler("/stats", handler_stats); + srv.add_handler("/stats/stock", handler_stats_stock); + srv.add_handler("/stats/profit_per_day", handler_stats_profit_per_day); + srv.add_handler("/stats/profit_per_weekday", handler_stats_profit_per_weekday); + srv.add_handler("/stats/profit_per_product", handler_stats_profit_per_product); + + /* users */ + srv.add_handler("/users", handler_users); + + srv.run(); + } +} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..b2ac665 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,25 @@ + + + + + {{{TITLE}}} + + + + + + + + + + + + + +
    + {{{CONTENT}}} +
    + + diff --git a/templates/css/base.css b/templates/css/base.css new file mode 100644 index 0000000..fd4ad8e --- /dev/null +++ b/templates/css/base.css @@ -0,0 +1,65 @@ +/* sane margins for main content */ +.content { + margin-top: 55px; + margin-left: 20px; + margin-right: 20px; +} + +/* class for tables without 100% width */ +.table-nonfluid { + width: auto; +} + +/* navbar padding from sides */ +.navbar-fixed-top .navbar-inner { + padding-right: 5px; + padding-left: 5px; +} + +/* statistics */ +#overviewLegend li > div { + display: inline-block; + margin-right: 4px; +} + +#overviewLegend li { + list-style: none; +} + +#placeholder .legend { + visibility: hidden; +} + +#overviewLegend label { + display: inline; +} + +#overviewLegend input { + margin: 0; +} + +/* dropdown menu with button on the right */ +.btn-group > .cornered-dropdown-toggle { + border-top-right-radius: 0px; + border-bottom-right-radius: 0px; +} + +.btn-group > .dropdown-menu + .btn { + border-left: 0px; +} + +/* disabled entries in dropdown */ +.dropdown-menu .disabled { + color: #999; +} + +.two-columns { + overflow: hidden; + width: 400px; +} + +.two-columns li { + width: 20%; + float: left; + display: inline; +} diff --git a/templates/css/bootstrap.css b/templates/css/bootstrap.css new file mode 100644 index 0000000..9fa6f76 --- /dev/null +++ b/templates/css/bootstrap.css @@ -0,0 +1,5774 @@ +/*! + * Bootstrap v2.1.1 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */ + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +nav, +section { + display: block; +} + +audio, +canvas, +video { + display: inline-block; + *display: inline; + *zoom: 1; +} + +audio:not([controls]) { + display: none; +} + +html { + font-size: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +a:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +a:hover, +a:active { + outline: 0; +} + +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +img { + width: auto\9; + height: auto; + max-width: 100%; + vertical-align: middle; + border: 0; + -ms-interpolation-mode: bicubic; +} + +#map_canvas img { + max-width: none; +} + +button, +input, +select, +textarea { + margin: 0; + font-size: 100%; + vertical-align: middle; +} + +button, +input { + *overflow: visible; + line-height: normal; +} + +button::-moz-focus-inner, +input::-moz-focus-inner { + padding: 0; + border: 0; +} + +button, +input[type="button"], +input[type="reset"], +input[type="submit"] { + cursor: pointer; + -webkit-appearance: button; +} + +input[type="search"] { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + -webkit-appearance: textfield; +} + +input[type="search"]::-webkit-search-decoration, +input[type="search"]::-webkit-search-cancel-button { + -webkit-appearance: none; +} + +textarea { + overflow: auto; + vertical-align: top; +} + +.clearfix { + *zoom: 1; +} + +.clearfix:before, +.clearfix:after { + display: table; + line-height: 0; + content: ""; +} + +.clearfix:after { + clear: both; +} + +.hide-text { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + +.input-block-level { + display: block; + width: 100%; + min-height: 30px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +body { + margin: 0; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 20px; + color: #333333; + background-color: #ffffff; +} + +a { + color: #0088cc; + text-decoration: none; +} + +a:hover { + color: #005580; + text-decoration: underline; +} + +.img-rounded { + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.img-polaroid { + padding: 4px; + background-color: #fff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.img-circle { + -webkit-border-radius: 500px; + -moz-border-radius: 500px; + border-radius: 500px; +} + +.row { + margin-left: -20px; + *zoom: 1; +} + +.row:before, +.row:after { + display: table; + line-height: 0; + content: ""; +} + +.row:after { + clear: both; +} + +[class*="span"] { + float: left; + min-height: 1px; + margin-left: 20px; +} + +.container, +.navbar-static-top .container, +.navbar-fixed-top .container, +.navbar-fixed-bottom .container { + width: 940px; +} + +.span12 { + width: 940px; +} + +.span11 { + width: 860px; +} + +.span10 { + width: 780px; +} + +.span9 { + width: 700px; +} + +.span8 { + width: 620px; +} + +.span7 { + width: 540px; +} + +.span6 { + width: 460px; +} + +.span5 { + width: 380px; +} + +.span4 { + width: 300px; +} + +.span3 { + width: 220px; +} + +.span2 { + width: 140px; +} + +.span1 { + width: 60px; +} + +.offset12 { + margin-left: 980px; +} + +.offset11 { + margin-left: 900px; +} + +.offset10 { + margin-left: 820px; +} + +.offset9 { + margin-left: 740px; +} + +.offset8 { + margin-left: 660px; +} + +.offset7 { + margin-left: 580px; +} + +.offset6 { + margin-left: 500px; +} + +.offset5 { + margin-left: 420px; +} + +.offset4 { + margin-left: 340px; +} + +.offset3 { + margin-left: 260px; +} + +.offset2 { + margin-left: 180px; +} + +.offset1 { + margin-left: 100px; +} + +.row-fluid { + width: 100%; + *zoom: 1; +} + +.row-fluid:before, +.row-fluid:after { + display: table; + line-height: 0; + content: ""; +} + +.row-fluid:after { + clear: both; +} + +.row-fluid [class*="span"] { + display: block; + float: left; + width: 100%; + min-height: 30px; + margin-left: 2.127659574468085%; + *margin-left: 2.074468085106383%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.row-fluid [class*="span"]:first-child { + margin-left: 0; +} + +.row-fluid .span12 { + width: 100%; + *width: 99.94680851063829%; +} + +.row-fluid .span11 { + width: 91.48936170212765%; + *width: 91.43617021276594%; +} + +.row-fluid .span10 { + width: 82.97872340425532%; + *width: 82.92553191489361%; +} + +.row-fluid .span9 { + width: 74.46808510638297%; + *width: 74.41489361702126%; +} + +.row-fluid .span8 { + width: 65.95744680851064%; + *width: 65.90425531914893%; +} + +.row-fluid .span7 { + width: 57.44680851063829%; + *width: 57.39361702127659%; +} + +.row-fluid .span6 { + width: 48.93617021276595%; + *width: 48.88297872340425%; +} + +.row-fluid .span5 { + width: 40.42553191489362%; + *width: 40.37234042553192%; +} + +.row-fluid .span4 { + width: 31.914893617021278%; + *width: 31.861702127659576%; +} + +.row-fluid .span3 { + width: 23.404255319148934%; + *width: 23.351063829787233%; +} + +.row-fluid .span2 { + width: 14.893617021276595%; + *width: 14.840425531914894%; +} + +.row-fluid .span1 { + width: 6.382978723404255%; + *width: 6.329787234042553%; +} + +.row-fluid .offset12 { + margin-left: 104.25531914893617%; + *margin-left: 104.14893617021275%; +} + +.row-fluid .offset12:first-child { + margin-left: 102.12765957446808%; + *margin-left: 102.02127659574467%; +} + +.row-fluid .offset11 { + margin-left: 95.74468085106382%; + *margin-left: 95.6382978723404%; +} + +.row-fluid .offset11:first-child { + margin-left: 93.61702127659574%; + *margin-left: 93.51063829787232%; +} + +.row-fluid .offset10 { + margin-left: 87.23404255319149%; + *margin-left: 87.12765957446807%; +} + +.row-fluid .offset10:first-child { + margin-left: 85.1063829787234%; + *margin-left: 84.99999999999999%; +} + +.row-fluid .offset9 { + margin-left: 78.72340425531914%; + *margin-left: 78.61702127659572%; +} + +.row-fluid .offset9:first-child { + margin-left: 76.59574468085106%; + *margin-left: 76.48936170212764%; +} + +.row-fluid .offset8 { + margin-left: 70.2127659574468%; + *margin-left: 70.10638297872339%; +} + +.row-fluid .offset8:first-child { + margin-left: 68.08510638297872%; + *margin-left: 67.9787234042553%; +} + +.row-fluid .offset7 { + margin-left: 61.70212765957446%; + *margin-left: 61.59574468085106%; +} + +.row-fluid .offset7:first-child { + margin-left: 59.574468085106375%; + *margin-left: 59.46808510638297%; +} + +.row-fluid .offset6 { + margin-left: 53.191489361702125%; + *margin-left: 53.085106382978715%; +} + +.row-fluid .offset6:first-child { + margin-left: 51.063829787234035%; + *margin-left: 50.95744680851063%; +} + +.row-fluid .offset5 { + margin-left: 44.68085106382979%; + *margin-left: 44.57446808510638%; +} + +.row-fluid .offset5:first-child { + margin-left: 42.5531914893617%; + *margin-left: 42.4468085106383%; +} + +.row-fluid .offset4 { + margin-left: 36.170212765957444%; + *margin-left: 36.06382978723405%; +} + +.row-fluid .offset4:first-child { + margin-left: 34.04255319148936%; + *margin-left: 33.93617021276596%; +} + +.row-fluid .offset3 { + margin-left: 27.659574468085104%; + *margin-left: 27.5531914893617%; +} + +.row-fluid .offset3:first-child { + margin-left: 25.53191489361702%; + *margin-left: 25.425531914893618%; +} + +.row-fluid .offset2 { + margin-left: 19.148936170212764%; + *margin-left: 19.04255319148936%; +} + +.row-fluid .offset2:first-child { + margin-left: 17.02127659574468%; + *margin-left: 16.914893617021278%; +} + +.row-fluid .offset1 { + margin-left: 10.638297872340425%; + *margin-left: 10.53191489361702%; +} + +.row-fluid .offset1:first-child { + margin-left: 8.51063829787234%; + *margin-left: 8.404255319148938%; +} + +[class*="span"].hide, +.row-fluid [class*="span"].hide { + display: none; +} + +[class*="span"].pull-right, +.row-fluid [class*="span"].pull-right { + float: right; +} + +.container { + margin-right: auto; + margin-left: auto; + *zoom: 1; +} + +.container:before, +.container:after { + display: table; + line-height: 0; + content: ""; +} + +.container:after { + clear: both; +} + +.container-fluid { + padding-right: 20px; + padding-left: 20px; + *zoom: 1; +} + +.container-fluid:before, +.container-fluid:after { + display: table; + line-height: 0; + content: ""; +} + +.container-fluid:after { + clear: both; +} + +p { + margin: 0 0 10px; +} + +.lead { + margin-bottom: 20px; + font-size: 21px; + font-weight: 200; + line-height: 30px; +} + +small { + font-size: 85%; +} + +strong { + font-weight: bold; +} + +em { + font-style: italic; +} + +cite { + font-style: normal; +} + +.muted { + color: #999999; +} + +.text-warning { + color: #c09853; +} + +.text-error { + color: #b94a48; +} + +.text-info { + color: #3a87ad; +} + +.text-success { + color: #468847; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 10px 0; + font-family: inherit; + font-weight: bold; + line-height: 1; + color: inherit; + text-rendering: optimizelegibility; +} + +h1 small, +h2 small, +h3 small, +h4 small, +h5 small, +h6 small { + font-weight: normal; + line-height: 1; + color: #999999; +} + +h1 { + font-size: 36px; + line-height: 40px; +} + +h2 { + font-size: 30px; + line-height: 40px; +} + +h3 { + font-size: 24px; + line-height: 40px; +} + +h4 { + font-size: 18px; + line-height: 20px; +} + +h5 { + font-size: 14px; + line-height: 20px; +} + +h6 { + font-size: 12px; + line-height: 20px; +} + +h1 small { + font-size: 24px; +} + +h2 small { + font-size: 18px; +} + +h3 small { + font-size: 14px; +} + +h4 small { + font-size: 14px; +} + +.page-header { + padding-bottom: 9px; + margin: 20px 0 30px; + border-bottom: 1px solid #eeeeee; +} + +ul, +ol { + padding: 0; + margin: 0 0 10px 25px; +} + +ul ul, +ul ol, +ol ol, +ol ul { + margin-bottom: 0; +} + +li { + line-height: 20px; +} + +ul.unstyled, +ol.unstyled { + margin-left: 0; + list-style: none; +} + +dl { + margin-bottom: 20px; +} + +dt, +dd { + line-height: 20px; +} + +dt { + font-weight: bold; +} + +dd { + margin-left: 10px; +} + +.dl-horizontal { + *zoom: 1; +} + +.dl-horizontal:before, +.dl-horizontal:after { + display: table; + line-height: 0; + content: ""; +} + +.dl-horizontal:after { + clear: both; +} + +.dl-horizontal dt { + float: left; + width: 160px; + overflow: hidden; + clear: left; + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; +} + +.dl-horizontal dd { + margin-left: 180px; +} + +hr { + margin: 20px 0; + border: 0; + border-top: 1px solid #eeeeee; + border-bottom: 1px solid #ffffff; +} + +abbr[title] { + cursor: help; + border-bottom: 1px dotted #999999; +} + +abbr.initialism { + font-size: 90%; + text-transform: uppercase; +} + +blockquote { + padding: 0 0 0 15px; + margin: 0 0 20px; + border-left: 5px solid #eeeeee; +} + +blockquote p { + margin-bottom: 0; + font-size: 16px; + font-weight: 300; + line-height: 25px; +} + +blockquote small { + display: block; + line-height: 20px; + color: #999999; +} + +blockquote small:before { + content: '\2014 \00A0'; +} + +blockquote.pull-right { + float: right; + padding-right: 15px; + padding-left: 0; + border-right: 5px solid #eeeeee; + border-left: 0; +} + +blockquote.pull-right p, +blockquote.pull-right small { + text-align: right; +} + +blockquote.pull-right small:before { + content: ''; +} + +blockquote.pull-right small:after { + content: '\00A0 \2014'; +} + +q:before, +q:after, +blockquote:before, +blockquote:after { + content: ""; +} + +address { + display: block; + margin-bottom: 20px; + font-style: normal; + line-height: 20px; +} + +code, +pre { + padding: 0 3px 2px; + font-family: Monaco, Menlo, Consolas, "Courier New", monospace; + font-size: 12px; + color: #333333; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +code { + padding: 2px 4px; + color: #d14; + background-color: #f7f7f9; + border: 1px solid #e1e1e8; +} + +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 20px; + word-break: break-all; + word-wrap: break-word; + white-space: pre; + white-space: pre-wrap; + background-color: #f5f5f5; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.15); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +pre.prettyprint { + margin-bottom: 20px; +} + +pre code { + padding: 0; + color: inherit; + background-color: transparent; + border: 0; +} + +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} + +form { + margin: 0 0 20px; +} + +fieldset { + padding: 0; + margin: 0; + border: 0; +} + +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 20px; + font-size: 21px; + line-height: 40px; + color: #333333; + border: 0; + border-bottom: 1px solid #e5e5e5; +} + +legend small { + font-size: 15px; + color: #999999; +} + +label, +input, +button, +select, +textarea { + font-size: 14px; + font-weight: normal; + line-height: 20px; +} + +input, +button, +select, +textarea { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; +} + +label { + display: block; + margin-bottom: 5px; +} + +select, +textarea, +input[type="text"], +input[type="password"], +input[type="datetime"], +input[type="datetime-local"], +input[type="date"], +input[type="month"], +input[type="time"], +input[type="week"], +input[type="number"], +input[type="email"], +input[type="url"], +input[type="search"], +input[type="tel"], +input[type="color"], +.uneditable-input { + display: inline-block; + height: 20px; + padding: 4px 6px; + margin-bottom: 9px; + font-size: 14px; + line-height: 20px; + color: #555555; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +input, +textarea, +.uneditable-input { + width: 206px; +} + +textarea { + height: auto; +} + +textarea, +input[type="text"], +input[type="password"], +input[type="datetime"], +input[type="datetime-local"], +input[type="date"], +input[type="month"], +input[type="time"], +input[type="week"], +input[type="number"], +input[type="email"], +input[type="url"], +input[type="search"], +input[type="tel"], +input[type="color"], +.uneditable-input { + background-color: #ffffff; + border: 1px solid #cccccc; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; + -moz-transition: border linear 0.2s, box-shadow linear 0.2s; + -o-transition: border linear 0.2s, box-shadow linear 0.2s; + transition: border linear 0.2s, box-shadow linear 0.2s; +} + +textarea:focus, +input[type="text"]:focus, +input[type="password"]:focus, +input[type="datetime"]:focus, +input[type="datetime-local"]:focus, +input[type="date"]:focus, +input[type="month"]:focus, +input[type="time"]:focus, +input[type="week"]:focus, +input[type="number"]:focus, +input[type="email"]:focus, +input[type="url"]:focus, +input[type="search"]:focus, +input[type="tel"]:focus, +input[type="color"]:focus, +.uneditable-input:focus { + border-color: rgba(82, 168, 236, 0.8); + outline: 0; + outline: thin dotted \9; + /* IE6-9 */ + + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); +} + +input[type="radio"], +input[type="checkbox"] { + margin: 4px 0 0; + margin-top: 1px \9; + *margin-top: 0; + line-height: normal; + cursor: pointer; +} + +input[type="file"], +input[type="image"], +input[type="submit"], +input[type="reset"], +input[type="button"], +input[type="radio"], +input[type="checkbox"] { + width: auto; +} + +select, +input[type="file"] { + height: 30px; + /* In IE7, the height of the select element cannot be changed by height, only font-size */ + + *margin-top: 4px; + /* For IE7, add top margin to align select with labels */ + + line-height: 30px; +} + +select { + width: 220px; + background-color: #ffffff; + border: 1px solid #cccccc; +} + +select[multiple], +select[size] { + height: auto; +} + +select:focus, +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +.uneditable-input, +.uneditable-textarea { + color: #999999; + cursor: not-allowed; + background-color: #fcfcfc; + border-color: #cccccc; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); +} + +.uneditable-input { + overflow: hidden; + white-space: nowrap; +} + +.uneditable-textarea { + width: auto; + height: auto; +} + +input:-moz-placeholder, +textarea:-moz-placeholder { + color: #999999; +} + +input:-ms-input-placeholder, +textarea:-ms-input-placeholder { + color: #999999; +} + +input::-webkit-input-placeholder, +textarea::-webkit-input-placeholder { + color: #999999; +} + +.radio, +.checkbox { + min-height: 18px; + padding-left: 18px; +} + +.radio input[type="radio"], +.checkbox input[type="checkbox"] { + float: left; + margin-left: -18px; +} + +.controls > .radio:first-child, +.controls > .checkbox:first-child { + padding-top: 5px; +} + +.radio.inline, +.checkbox.inline { + display: inline-block; + padding-top: 5px; + margin-bottom: 0; + vertical-align: middle; +} + +.radio.inline + .radio.inline, +.checkbox.inline + .checkbox.inline { + margin-left: 10px; +} + +.input-mini { + width: 60px; +} + +.input-small { + width: 90px; +} + +.input-medium { + width: 150px; +} + +.input-large { + width: 210px; +} + +.input-xlarge { + width: 270px; +} + +.input-xxlarge { + width: 530px; +} + +input[class*="span"], +select[class*="span"], +textarea[class*="span"], +.uneditable-input[class*="span"], +.row-fluid input[class*="span"], +.row-fluid select[class*="span"], +.row-fluid textarea[class*="span"], +.row-fluid .uneditable-input[class*="span"] { + float: none; + margin-left: 0; +} + +.input-append input[class*="span"], +.input-append .uneditable-input[class*="span"], +.input-prepend input[class*="span"], +.input-prepend .uneditable-input[class*="span"], +.row-fluid input[class*="span"], +.row-fluid select[class*="span"], +.row-fluid textarea[class*="span"], +.row-fluid .uneditable-input[class*="span"], +.row-fluid .input-prepend [class*="span"], +.row-fluid .input-append [class*="span"] { + display: inline-block; +} + +input, +textarea, +.uneditable-input { + margin-left: 0; +} + +.controls-row [class*="span"] + [class*="span"] { + margin-left: 20px; +} + +input.span12, +textarea.span12, +.uneditable-input.span12 { + width: 926px; +} + +input.span11, +textarea.span11, +.uneditable-input.span11 { + width: 846px; +} + +input.span10, +textarea.span10, +.uneditable-input.span10 { + width: 766px; +} + +input.span9, +textarea.span9, +.uneditable-input.span9 { + width: 686px; +} + +input.span8, +textarea.span8, +.uneditable-input.span8 { + width: 606px; +} + +input.span7, +textarea.span7, +.uneditable-input.span7 { + width: 526px; +} + +input.span6, +textarea.span6, +.uneditable-input.span6 { + width: 446px; +} + +input.span5, +textarea.span5, +.uneditable-input.span5 { + width: 366px; +} + +input.span4, +textarea.span4, +.uneditable-input.span4 { + width: 286px; +} + +input.span3, +textarea.span3, +.uneditable-input.span3 { + width: 206px; +} + +input.span2, +textarea.span2, +.uneditable-input.span2 { + width: 126px; +} + +input.span1, +textarea.span1, +.uneditable-input.span1 { + width: 46px; +} + +.controls-row { + *zoom: 1; +} + +.controls-row:before, +.controls-row:after { + display: table; + line-height: 0; + content: ""; +} + +.controls-row:after { + clear: both; +} + +.controls-row [class*="span"] { + float: left; +} + +input[disabled], +select[disabled], +textarea[disabled], +input[readonly], +select[readonly], +textarea[readonly] { + cursor: not-allowed; + background-color: #eeeeee; +} + +input[type="radio"][disabled], +input[type="checkbox"][disabled], +input[type="radio"][readonly], +input[type="checkbox"][readonly] { + background-color: transparent; +} + +.control-group.warning > label, +.control-group.warning .help-block, +.control-group.warning .help-inline { + color: #c09853; +} + +.control-group.warning .checkbox, +.control-group.warning .radio, +.control-group.warning input, +.control-group.warning select, +.control-group.warning textarea { + color: #c09853; +} + +.control-group.warning input, +.control-group.warning select, +.control-group.warning textarea { + border-color: #c09853; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.warning input:focus, +.control-group.warning select:focus, +.control-group.warning textarea:focus { + border-color: #a47e3c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; +} + +.control-group.warning .input-prepend .add-on, +.control-group.warning .input-append .add-on { + color: #c09853; + background-color: #fcf8e3; + border-color: #c09853; +} + +.control-group.error > label, +.control-group.error .help-block, +.control-group.error .help-inline { + color: #b94a48; +} + +.control-group.error .checkbox, +.control-group.error .radio, +.control-group.error input, +.control-group.error select, +.control-group.error textarea { + color: #b94a48; +} + +.control-group.error input, +.control-group.error select, +.control-group.error textarea { + border-color: #b94a48; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.error input:focus, +.control-group.error select:focus, +.control-group.error textarea:focus { + border-color: #953b39; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; +} + +.control-group.error .input-prepend .add-on, +.control-group.error .input-append .add-on { + color: #b94a48; + background-color: #f2dede; + border-color: #b94a48; +} + +.control-group.success > label, +.control-group.success .help-block, +.control-group.success .help-inline { + color: #468847; +} + +.control-group.success .checkbox, +.control-group.success .radio, +.control-group.success input, +.control-group.success select, +.control-group.success textarea { + color: #468847; +} + +.control-group.success input, +.control-group.success select, +.control-group.success textarea { + border-color: #468847; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.success input:focus, +.control-group.success select:focus, +.control-group.success textarea:focus { + border-color: #356635; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; +} + +.control-group.success .input-prepend .add-on, +.control-group.success .input-append .add-on { + color: #468847; + background-color: #dff0d8; + border-color: #468847; +} + +.control-group.info > label, +.control-group.info .help-block, +.control-group.info .help-inline { + color: #3a87ad; +} + +.control-group.info .checkbox, +.control-group.info .radio, +.control-group.info input, +.control-group.info select, +.control-group.info textarea { + color: #3a87ad; +} + +.control-group.info input, +.control-group.info select, +.control-group.info textarea { + border-color: #3a87ad; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.info input:focus, +.control-group.info select:focus, +.control-group.info textarea:focus { + border-color: #2d6987; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3; +} + +.control-group.info .input-prepend .add-on, +.control-group.info .input-append .add-on { + color: #3a87ad; + background-color: #d9edf7; + border-color: #3a87ad; +} + +input:focus:required:invalid, +textarea:focus:required:invalid, +select:focus:required:invalid { + color: #b94a48; + border-color: #ee5f5b; +} + +input:focus:required:invalid:focus, +textarea:focus:required:invalid:focus, +select:focus:required:invalid:focus { + border-color: #e9322d; + -webkit-box-shadow: 0 0 6px #f8b9b7; + -moz-box-shadow: 0 0 6px #f8b9b7; + box-shadow: 0 0 6px #f8b9b7; +} + +.form-actions { + padding: 19px 20px 20px; + margin-top: 20px; + margin-bottom: 20px; + background-color: #f5f5f5; + border-top: 1px solid #e5e5e5; + *zoom: 1; +} + +.form-actions:before, +.form-actions:after { + display: table; + line-height: 0; + content: ""; +} + +.form-actions:after { + clear: both; +} + +.help-block, +.help-inline { + color: #595959; +} + +.help-block { + display: block; + margin-bottom: 10px; +} + +.help-inline { + display: inline-block; + *display: inline; + padding-left: 5px; + vertical-align: middle; + *zoom: 1; +} + +.input-append, +.input-prepend { + margin-bottom: 5px; + font-size: 0; + white-space: nowrap; +} + +.input-append input, +.input-prepend input, +.input-append select, +.input-prepend select, +.input-append .uneditable-input, +.input-prepend .uneditable-input { + position: relative; + margin-bottom: 0; + *margin-left: 0; + font-size: 14px; + vertical-align: top; + -webkit-border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0; +} + +.input-append input:focus, +.input-prepend input:focus, +.input-append select:focus, +.input-prepend select:focus, +.input-append .uneditable-input:focus, +.input-prepend .uneditable-input:focus { + z-index: 2; +} + +.input-append .add-on, +.input-prepend .add-on { + display: inline-block; + width: auto; + height: 20px; + min-width: 16px; + padding: 4px 5px; + font-size: 14px; + font-weight: normal; + line-height: 20px; + text-align: center; + text-shadow: 0 1px 0 #ffffff; + background-color: #eeeeee; + border: 1px solid #ccc; +} + +.input-append .add-on, +.input-prepend .add-on, +.input-append .btn, +.input-prepend .btn { + vertical-align: top; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.input-append .active, +.input-prepend .active { + background-color: #a9dba9; + border-color: #46a546; +} + +.input-prepend .add-on, +.input-prepend .btn { + margin-right: -1px; +} + +.input-prepend .add-on:first-child, +.input-prepend .btn:first-child { + -webkit-border-radius: 3px 0 0 3px; + -moz-border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px; +} + +.input-append input, +.input-append select, +.input-append .uneditable-input { + -webkit-border-radius: 3px 0 0 3px; + -moz-border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px; +} + +.input-append .add-on, +.input-append .btn { + margin-left: -1px; +} + +.input-append .add-on:last-child, +.input-append .btn:last-child { + -webkit-border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0; +} + +.input-prepend.input-append input, +.input-prepend.input-append select, +.input-prepend.input-append .uneditable-input { + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.input-prepend.input-append .add-on:first-child, +.input-prepend.input-append .btn:first-child { + margin-right: -1px; + -webkit-border-radius: 3px 0 0 3px; + -moz-border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px; +} + +.input-prepend.input-append .add-on:last-child, +.input-prepend.input-append .btn:last-child { + margin-left: -1px; + -webkit-border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0; +} + +input.search-query { + padding-right: 14px; + padding-right: 4px \9; + padding-left: 14px; + padding-left: 4px \9; + /* IE7-8 doesn't have border-radius, so don't indent the padding */ + + margin-bottom: 0; + -webkit-border-radius: 15px; + -moz-border-radius: 15px; + border-radius: 15px; +} + +/* Allow for input prepend/append in search forms */ + +.form-search .input-append .search-query, +.form-search .input-prepend .search-query { + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.form-search .input-append .search-query { + -webkit-border-radius: 14px 0 0 14px; + -moz-border-radius: 14px 0 0 14px; + border-radius: 14px 0 0 14px; +} + +.form-search .input-append .btn { + -webkit-border-radius: 0 14px 14px 0; + -moz-border-radius: 0 14px 14px 0; + border-radius: 0 14px 14px 0; +} + +.form-search .input-prepend .search-query { + -webkit-border-radius: 0 14px 14px 0; + -moz-border-radius: 0 14px 14px 0; + border-radius: 0 14px 14px 0; +} + +.form-search .input-prepend .btn { + -webkit-border-radius: 14px 0 0 14px; + -moz-border-radius: 14px 0 0 14px; + border-radius: 14px 0 0 14px; +} + +.form-search input, +.form-inline input, +.form-horizontal input, +.form-search textarea, +.form-inline textarea, +.form-horizontal textarea, +.form-search select, +.form-inline select, +.form-horizontal select, +.form-search .help-inline, +.form-inline .help-inline, +.form-horizontal .help-inline, +.form-search .uneditable-input, +.form-inline .uneditable-input, +.form-horizontal .uneditable-input, +.form-search .input-prepend, +.form-inline .input-prepend, +.form-horizontal .input-prepend, +.form-search .input-append, +.form-inline .input-append, +.form-horizontal .input-append { + display: inline-block; + *display: inline; + margin-bottom: 0; + vertical-align: middle; + *zoom: 1; +} + +.form-search .hide, +.form-inline .hide, +.form-horizontal .hide { + display: none; +} + +.form-search label, +.form-inline label, +.form-search .btn-group, +.form-inline .btn-group { + display: inline-block; +} + +.form-search .input-append, +.form-inline .input-append, +.form-search .input-prepend, +.form-inline .input-prepend { + margin-bottom: 0; +} + +.form-search .radio, +.form-search .checkbox, +.form-inline .radio, +.form-inline .checkbox { + padding-left: 0; + margin-bottom: 0; + vertical-align: middle; +} + +.form-search .radio input[type="radio"], +.form-search .checkbox input[type="checkbox"], +.form-inline .radio input[type="radio"], +.form-inline .checkbox input[type="checkbox"] { + float: left; + margin-right: 3px; + margin-left: 0; +} + +.control-group { + margin-bottom: 10px; +} + +legend + .control-group { + margin-top: 20px; + -webkit-margin-top-collapse: separate; +} + +.form-horizontal .control-group { + margin-bottom: 20px; + *zoom: 1; +} + +.form-horizontal .control-group:before, +.form-horizontal .control-group:after { + display: table; + line-height: 0; + content: ""; +} + +.form-horizontal .control-group:after { + clear: both; +} + +.form-horizontal .control-label { + float: left; + width: 160px; + padding-top: 5px; + text-align: right; +} + +.form-horizontal .controls { + *display: inline-block; + *padding-left: 20px; + margin-left: 180px; + *margin-left: 0; +} + +.form-horizontal .controls:first-child { + *padding-left: 180px; +} + +.form-horizontal .help-block { + margin-bottom: 0; +} + +.form-horizontal input + .help-block, +.form-horizontal select + .help-block, +.form-horizontal textarea + .help-block { + margin-top: 10px; +} + +.form-horizontal .form-actions { + padding-left: 180px; +} + +table { + max-width: 100%; + background-color: transparent; + border-collapse: collapse; + border-spacing: 0; +} + +.table { + width: 100%; + margin-bottom: 20px; +} + +.table th, +.table td { + padding: 8px; + line-height: 20px; + text-align: left; + vertical-align: top; + border-top: 1px solid #dddddd; +} + +.table th { + font-weight: bold; +} + +.table thead th { + vertical-align: bottom; +} + +.table caption + thead tr:first-child th, +.table caption + thead tr:first-child td, +.table colgroup + thead tr:first-child th, +.table colgroup + thead tr:first-child td, +.table thead:first-child tr:first-child th, +.table thead:first-child tr:first-child td { + border-top: 0; +} + +.table tbody + tbody { + border-top: 2px solid #dddddd; +} + +.table-condensed th, +.table-condensed td { + padding: 4px 5px; +} + +.table-bordered { + border: 1px solid #dddddd; + border-collapse: separate; + *border-collapse: collapse; + border-left: 0; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.table-bordered th, +.table-bordered td { + border-left: 1px solid #dddddd; +} + +.table-bordered caption + thead tr:first-child th, +.table-bordered caption + tbody tr:first-child th, +.table-bordered caption + tbody tr:first-child td, +.table-bordered colgroup + thead tr:first-child th, +.table-bordered colgroup + tbody tr:first-child th, +.table-bordered colgroup + tbody tr:first-child td, +.table-bordered thead:first-child tr:first-child th, +.table-bordered tbody:first-child tr:first-child th, +.table-bordered tbody:first-child tr:first-child td { + border-top: 0; +} + +.table-bordered thead:first-child tr:first-child th:first-child, +.table-bordered tbody:first-child tr:first-child td:first-child { + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-topleft: 4px; +} + +.table-bordered thead:first-child tr:first-child th:last-child, +.table-bordered tbody:first-child tr:first-child td:last-child { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -moz-border-radius-topright: 4px; +} + +.table-bordered thead:last-child tr:last-child th:first-child, +.table-bordered tbody:last-child tr:last-child td:first-child, +.table-bordered tfoot:last-child tr:last-child td:first-child { + -webkit-border-radius: 0 0 0 4px; + -moz-border-radius: 0 0 0 4px; + border-radius: 0 0 0 4px; + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -moz-border-radius-bottomleft: 4px; +} + +.table-bordered thead:last-child tr:last-child th:last-child, +.table-bordered tbody:last-child tr:last-child td:last-child, +.table-bordered tfoot:last-child tr:last-child td:last-child { + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -moz-border-radius-bottomright: 4px; +} + +.table-bordered caption + thead tr:first-child th:first-child, +.table-bordered caption + tbody tr:first-child td:first-child, +.table-bordered colgroup + thead tr:first-child th:first-child, +.table-bordered colgroup + tbody tr:first-child td:first-child { + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-topleft: 4px; +} + +.table-bordered caption + thead tr:first-child th:last-child, +.table-bordered caption + tbody tr:first-child td:last-child, +.table-bordered colgroup + thead tr:first-child th:last-child, +.table-bordered colgroup + tbody tr:first-child td:last-child { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -moz-border-radius-topleft: 4px; +} + +.table-striped tbody tr:nth-child(odd) td, +.table-striped tbody tr:nth-child(odd) th { + background-color: #f9f9f9; +} + +.table-hover tbody tr:hover td, +.table-hover tbody tr:hover th { + background-color: #f5f5f5; +} + +table [class*=span], +.row-fluid table [class*=span] { + display: table-cell; + float: none; + margin-left: 0; +} + +.table .span1 { + float: none; + width: 44px; + margin-left: 0; +} + +.table .span2 { + float: none; + width: 124px; + margin-left: 0; +} + +.table .span3 { + float: none; + width: 204px; + margin-left: 0; +} + +.table .span4 { + float: none; + width: 284px; + margin-left: 0; +} + +.table .span5 { + float: none; + width: 364px; + margin-left: 0; +} + +.table .span6 { + float: none; + width: 444px; + margin-left: 0; +} + +.table .span7 { + float: none; + width: 524px; + margin-left: 0; +} + +.table .span8 { + float: none; + width: 604px; + margin-left: 0; +} + +.table .span9 { + float: none; + width: 684px; + margin-left: 0; +} + +.table .span10 { + float: none; + width: 764px; + margin-left: 0; +} + +.table .span11 { + float: none; + width: 844px; + margin-left: 0; +} + +.table .span12 { + float: none; + width: 924px; + margin-left: 0; +} + +.table .span13 { + float: none; + width: 1004px; + margin-left: 0; +} + +.table .span14 { + float: none; + width: 1084px; + margin-left: 0; +} + +.table .span15 { + float: none; + width: 1164px; + margin-left: 0; +} + +.table .span16 { + float: none; + width: 1244px; + margin-left: 0; +} + +.table .span17 { + float: none; + width: 1324px; + margin-left: 0; +} + +.table .span18 { + float: none; + width: 1404px; + margin-left: 0; +} + +.table .span19 { + float: none; + width: 1484px; + margin-left: 0; +} + +.table .span20 { + float: none; + width: 1564px; + margin-left: 0; +} + +.table .span21 { + float: none; + width: 1644px; + margin-left: 0; +} + +.table .span22 { + float: none; + width: 1724px; + margin-left: 0; +} + +.table .span23 { + float: none; + width: 1804px; + margin-left: 0; +} + +.table .span24 { + float: none; + width: 1884px; + margin-left: 0; +} + +.table tbody tr.success td { + background-color: #dff0d8; +} + +.table tbody tr.error td { + background-color: #f2dede; +} + +.table tbody tr.warning td { + background-color: #fcf8e3; +} + +.table tbody tr.info td { + background-color: #d9edf7; +} + +.table-hover tbody tr.success:hover td { + background-color: #d0e9c6; +} + +.table-hover tbody tr.error:hover td { + background-color: #ebcccc; +} + +.table-hover tbody tr.warning:hover td { + background-color: #faf2cc; +} + +.table-hover tbody tr.info:hover td { + background-color: #c4e3f3; +} + +[class^="icon-"], +[class*=" icon-"] { + display: inline-block; + width: 14px; + height: 14px; + margin-top: 1px; + *margin-right: .3em; + line-height: 14px; + vertical-align: text-top; + background-image: url("../img/glyphicons-halflings.png"); + background-position: 14px 14px; + background-repeat: no-repeat; +} + +/* White icons with optional class, or on hover/active states of certain elements */ + +.icon-white, +.nav-tabs > .active > a > [class^="icon-"], +.nav-tabs > .active > a > [class*=" icon-"], +.nav-pills > .active > a > [class^="icon-"], +.nav-pills > .active > a > [class*=" icon-"], +.nav-list > .active > a > [class^="icon-"], +.nav-list > .active > a > [class*=" icon-"], +.navbar-inverse .nav > .active > a > [class^="icon-"], +.navbar-inverse .nav > .active > a > [class*=" icon-"], +.dropdown-menu > li > a:hover > [class^="icon-"], +.dropdown-menu > li > a:hover > [class*=" icon-"], +.dropdown-menu > .active > a > [class^="icon-"], +.dropdown-menu > .active > a > [class*=" icon-"] { + background-image: url("../img/glyphicons-halflings-white.png"); +} + +.icon-glass { + background-position: 0 0; +} + +.icon-music { + background-position: -24px 0; +} + +.icon-search { + background-position: -48px 0; +} + +.icon-envelope { + background-position: -72px 0; +} + +.icon-heart { + background-position: -96px 0; +} + +.icon-star { + background-position: -120px 0; +} + +.icon-star-empty { + background-position: -144px 0; +} + +.icon-user { + background-position: -168px 0; +} + +.icon-film { + background-position: -192px 0; +} + +.icon-th-large { + background-position: -216px 0; +} + +.icon-th { + background-position: -240px 0; +} + +.icon-th-list { + background-position: -264px 0; +} + +.icon-ok { + background-position: -288px 0; +} + +.icon-remove { + background-position: -312px 0; +} + +.icon-zoom-in { + background-position: -336px 0; +} + +.icon-zoom-out { + background-position: -360px 0; +} + +.icon-off { + background-position: -384px 0; +} + +.icon-signal { + background-position: -408px 0; +} + +.icon-cog { + background-position: -432px 0; +} + +.icon-trash { + background-position: -456px 0; +} + +.icon-home { + background-position: 0 -24px; +} + +.icon-file { + background-position: -24px -24px; +} + +.icon-time { + background-position: -48px -24px; +} + +.icon-road { + background-position: -72px -24px; +} + +.icon-download-alt { + background-position: -96px -24px; +} + +.icon-download { + background-position: -120px -24px; +} + +.icon-upload { + background-position: -144px -24px; +} + +.icon-inbox { + background-position: -168px -24px; +} + +.icon-play-circle { + background-position: -192px -24px; +} + +.icon-repeat { + background-position: -216px -24px; +} + +.icon-refresh { + background-position: -240px -24px; +} + +.icon-list-alt { + background-position: -264px -24px; +} + +.icon-lock { + background-position: -287px -24px; +} + +.icon-flag { + background-position: -312px -24px; +} + +.icon-headphones { + background-position: -336px -24px; +} + +.icon-volume-off { + background-position: -360px -24px; +} + +.icon-volume-down { + background-position: -384px -24px; +} + +.icon-volume-up { + background-position: -408px -24px; +} + +.icon-qrcode { + background-position: -432px -24px; +} + +.icon-barcode { + background-position: -456px -24px; +} + +.icon-tag { + background-position: 0 -48px; +} + +.icon-tags { + background-position: -25px -48px; +} + +.icon-book { + background-position: -48px -48px; +} + +.icon-bookmark { + background-position: -72px -48px; +} + +.icon-print { + background-position: -96px -48px; +} + +.icon-camera { + background-position: -120px -48px; +} + +.icon-font { + background-position: -144px -48px; +} + +.icon-bold { + background-position: -167px -48px; +} + +.icon-italic { + background-position: -192px -48px; +} + +.icon-text-height { + background-position: -216px -48px; +} + +.icon-text-width { + background-position: -240px -48px; +} + +.icon-align-left { + background-position: -264px -48px; +} + +.icon-align-center { + background-position: -288px -48px; +} + +.icon-align-right { + background-position: -312px -48px; +} + +.icon-align-justify { + background-position: -336px -48px; +} + +.icon-list { + background-position: -360px -48px; +} + +.icon-indent-left { + background-position: -384px -48px; +} + +.icon-indent-right { + background-position: -408px -48px; +} + +.icon-facetime-video { + background-position: -432px -48px; +} + +.icon-picture { + background-position: -456px -48px; +} + +.icon-pencil { + background-position: 0 -72px; +} + +.icon-map-marker { + background-position: -24px -72px; +} + +.icon-adjust { + background-position: -48px -72px; +} + +.icon-tint { + background-position: -72px -72px; +} + +.icon-edit { + background-position: -96px -72px; +} + +.icon-share { + background-position: -120px -72px; +} + +.icon-check { + background-position: -144px -72px; +} + +.icon-move { + background-position: -168px -72px; +} + +.icon-step-backward { + background-position: -192px -72px; +} + +.icon-fast-backward { + background-position: -216px -72px; +} + +.icon-backward { + background-position: -240px -72px; +} + +.icon-play { + background-position: -264px -72px; +} + +.icon-pause { + background-position: -288px -72px; +} + +.icon-stop { + background-position: -312px -72px; +} + +.icon-forward { + background-position: -336px -72px; +} + +.icon-fast-forward { + background-position: -360px -72px; +} + +.icon-step-forward { + background-position: -384px -72px; +} + +.icon-eject { + background-position: -408px -72px; +} + +.icon-chevron-left { + background-position: -432px -72px; +} + +.icon-chevron-right { + background-position: -456px -72px; +} + +.icon-plus-sign { + background-position: 0 -96px; +} + +.icon-minus-sign { + background-position: -24px -96px; +} + +.icon-remove-sign { + background-position: -48px -96px; +} + +.icon-ok-sign { + background-position: -72px -96px; +} + +.icon-question-sign { + background-position: -96px -96px; +} + +.icon-info-sign { + background-position: -120px -96px; +} + +.icon-screenshot { + background-position: -144px -96px; +} + +.icon-remove-circle { + background-position: -168px -96px; +} + +.icon-ok-circle { + background-position: -192px -96px; +} + +.icon-ban-circle { + background-position: -216px -96px; +} + +.icon-arrow-left { + background-position: -240px -96px; +} + +.icon-arrow-right { + background-position: -264px -96px; +} + +.icon-arrow-up { + background-position: -289px -96px; +} + +.icon-arrow-down { + background-position: -312px -96px; +} + +.icon-share-alt { + background-position: -336px -96px; +} + +.icon-resize-full { + background-position: -360px -96px; +} + +.icon-resize-small { + background-position: -384px -96px; +} + +.icon-plus { + background-position: -408px -96px; +} + +.icon-minus { + background-position: -433px -96px; +} + +.icon-asterisk { + background-position: -456px -96px; +} + +.icon-exclamation-sign { + background-position: 0 -120px; +} + +.icon-gift { + background-position: -24px -120px; +} + +.icon-leaf { + background-position: -48px -120px; +} + +.icon-fire { + background-position: -72px -120px; +} + +.icon-eye-open { + background-position: -96px -120px; +} + +.icon-eye-close { + background-position: -120px -120px; +} + +.icon-warning-sign { + background-position: -144px -120px; +} + +.icon-plane { + background-position: -168px -120px; +} + +.icon-calendar { + background-position: -192px -120px; +} + +.icon-random { + width: 16px; + background-position: -216px -120px; +} + +.icon-comment { + background-position: -240px -120px; +} + +.icon-magnet { + background-position: -264px -120px; +} + +.icon-chevron-up { + background-position: -288px -120px; +} + +.icon-chevron-down { + background-position: -313px -119px; +} + +.icon-retweet { + background-position: -336px -120px; +} + +.icon-shopping-cart { + background-position: -360px -120px; +} + +.icon-folder-close { + background-position: -384px -120px; +} + +.icon-folder-open { + width: 16px; + background-position: -408px -120px; +} + +.icon-resize-vertical { + background-position: -432px -119px; +} + +.icon-resize-horizontal { + background-position: -456px -118px; +} + +.icon-hdd { + background-position: 0 -144px; +} + +.icon-bullhorn { + background-position: -24px -144px; +} + +.icon-bell { + background-position: -48px -144px; +} + +.icon-certificate { + background-position: -72px -144px; +} + +.icon-thumbs-up { + background-position: -96px -144px; +} + +.icon-thumbs-down { + background-position: -120px -144px; +} + +.icon-hand-right { + background-position: -144px -144px; +} + +.icon-hand-left { + background-position: -168px -144px; +} + +.icon-hand-up { + background-position: -192px -144px; +} + +.icon-hand-down { + background-position: -216px -144px; +} + +.icon-circle-arrow-right { + background-position: -240px -144px; +} + +.icon-circle-arrow-left { + background-position: -264px -144px; +} + +.icon-circle-arrow-up { + background-position: -288px -144px; +} + +.icon-circle-arrow-down { + background-position: -312px -144px; +} + +.icon-globe { + background-position: -336px -144px; +} + +.icon-wrench { + background-position: -360px -144px; +} + +.icon-tasks { + background-position: -384px -144px; +} + +.icon-filter { + background-position: -408px -144px; +} + +.icon-briefcase { + background-position: -432px -144px; +} + +.icon-fullscreen { + background-position: -456px -144px; +} + +.dropup, +.dropdown { + position: relative; +} + +.dropdown-toggle { + *margin-bottom: -3px; +} + +.dropdown-toggle:active, +.open .dropdown-toggle { + outline: 0; +} + +.caret { + display: inline-block; + width: 0; + height: 0; + vertical-align: top; + border-top: 4px solid #000000; + border-right: 4px solid transparent; + border-left: 4px solid transparent; + content: ""; +} + +.dropdown .caret { + margin-top: 8px; + margin-left: 2px; +} + +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + list-style: none; + background-color: #ffffff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + *border-right-width: 2px; + *border-bottom-width: 2px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; +} + +.dropdown-menu.pull-right { + right: 0; + left: auto; +} + +.dropdown-menu .divider { + *width: 100%; + height: 1px; + margin: 9px 1px; + *margin: -5px 0 5px; + overflow: hidden; + background-color: #e5e5e5; + border-bottom: 1px solid #ffffff; +} + +.dropdown-menu a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 20px; + color: #333333; + white-space: nowrap; +} + +.dropdown-menu li > a:hover, +.dropdown-menu li > a:focus, +.dropdown-submenu:hover > a { + color: #ffffff; + text-decoration: none; + background-color: #0088cc; + background-color: #0081c2; + background-image: -moz-linear-gradient(top, #0088cc, #0077b3); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3)); + background-image: -webkit-linear-gradient(top, #0088cc, #0077b3); + background-image: -o-linear-gradient(top, #0088cc, #0077b3); + background-image: linear-gradient(to bottom, #0088cc, #0077b3); + background-repeat: repeat-x; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0); +} + +.dropdown-menu .active > a, +.dropdown-menu .active > a:hover { + color: #ffffff; + text-decoration: none; + background-color: #0088cc; + background-color: #0081c2; + background-image: linear-gradient(to bottom, #0088cc, #0077b3); + background-image: -moz-linear-gradient(top, #0088cc, #0077b3); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3)); + background-image: -webkit-linear-gradient(top, #0088cc, #0077b3); + background-image: -o-linear-gradient(top, #0088cc, #0077b3); + background-repeat: repeat-x; + outline: 0; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0); +} + +.dropdown-menu .disabled > a, +.dropdown-menu .disabled > a:hover { + color: #999999; +} + +.dropdown-menu .disabled > a:hover { + text-decoration: none; + cursor: default; + background-color: transparent; +} + +.open { + *z-index: 1000; +} + +.open > .dropdown-menu { + display: block; +} + +.pull-right > .dropdown-menu { + right: 0; + left: auto; +} + +.dropup .caret, +.navbar-fixed-bottom .dropdown .caret { + border-top: 0; + border-bottom: 4px solid #000000; + content: ""; +} + +.dropup .dropdown-menu, +.navbar-fixed-bottom .dropdown .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 1px; +} + +.dropdown-submenu { + position: relative; +} + +.dropdown-submenu > .dropdown-menu { + top: 0; + left: 100%; + margin-top: -6px; + margin-left: -1px; + -webkit-border-radius: 0 6px 6px 6px; + -moz-border-radius: 0 6px 6px 6px; + border-radius: 0 6px 6px 6px; +} + +.dropdown-submenu:hover > .dropdown-menu { + display: block; +} + +.dropdown-submenu > a:after { + display: block; + float: right; + width: 0; + height: 0; + margin-top: 5px; + margin-right: -10px; + border-color: transparent; + border-left-color: #cccccc; + border-style: solid; + border-width: 5px 0 5px 5px; + content: " "; +} + +.dropdown-submenu:hover > a:after { + border-left-color: #ffffff; +} + +.dropdown .dropdown-menu .nav-header { + padding-right: 20px; + padding-left: 20px; +} + +.typeahead { + margin-top: 2px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.well { + min-height: 20px; + padding: 19px; + margin-bottom: 20px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); +} + +.well blockquote { + border-color: #ddd; + border-color: rgba(0, 0, 0, 0.15); +} + +.well-large { + padding: 24px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.well-small { + padding: 9px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.fade { + opacity: 0; + -webkit-transition: opacity 0.15s linear; + -moz-transition: opacity 0.15s linear; + -o-transition: opacity 0.15s linear; + transition: opacity 0.15s linear; +} + +.fade.in { + opacity: 1; +} + +.collapse { + position: relative; + height: 0; + overflow: hidden; + -webkit-transition: height 0.35s ease; + -moz-transition: height 0.35s ease; + -o-transition: height 0.35s ease; + transition: height 0.35s ease; +} + +.collapse.in { + height: auto; +} + +.close { + float: right; + font-size: 20px; + font-weight: bold; + line-height: 20px; + color: #000000; + text-shadow: 0 1px 0 #ffffff; + opacity: 0.2; + filter: alpha(opacity=20); +} + +.close:hover { + color: #000000; + text-decoration: none; + cursor: pointer; + opacity: 0.4; + filter: alpha(opacity=40); +} + +button.close { + padding: 0; + cursor: pointer; + background: transparent; + border: 0; + -webkit-appearance: none; +} + +.btn { + display: inline-block; + *display: inline; + padding: 4px 14px; + margin-bottom: 0; + *margin-left: .3em; + font-size: 14px; + line-height: 20px; + *line-height: 20px; + color: #333333; + text-align: center; + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); + vertical-align: middle; + cursor: pointer; + background-color: #f5f5f5; + *background-color: #e6e6e6; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); + background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); + background-image: linear-gradient(to bottom, #ffffff, #e6e6e6); + background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); + background-repeat: repeat-x; + border: 1px solid #bbbbbb; + *border: 0; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + border-color: #e6e6e6 #e6e6e6 #bfbfbf; + border-bottom-color: #a2a2a2; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe6e6e6', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); + *zoom: 1; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn:hover, +.btn:active, +.btn.active, +.btn.disabled, +.btn[disabled] { + color: #333333; + background-color: #e6e6e6; + *background-color: #d9d9d9; +} + +.btn:active, +.btn.active { + background-color: #cccccc \9; +} + +.btn:first-child { + *margin-left: 0; +} + +.btn:hover { + color: #333333; + text-decoration: none; + background-color: #e6e6e6; + *background-color: #d9d9d9; + /* Buttons in IE7 don't get borders, so darken on hover */ + + background-position: 0 -15px; + -webkit-transition: background-position 0.1s linear; + -moz-transition: background-position 0.1s linear; + -o-transition: background-position 0.1s linear; + transition: background-position 0.1s linear; +} + +.btn:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +.btn.active, +.btn:active { + background-color: #e6e6e6; + background-color: #d9d9d9 \9; + background-image: none; + outline: 0; + -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn.disabled, +.btn[disabled] { + cursor: default; + background-color: #e6e6e6; + background-image: none; + opacity: 0.65; + filter: alpha(opacity=65); + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} + +.btn-large { + padding: 9px 14px; + font-size: 16px; + line-height: normal; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} + +.btn-large [class^="icon-"] { + margin-top: 2px; +} + +.btn-small { + padding: 3px 9px; + font-size: 12px; + line-height: 18px; +} + +.btn-small [class^="icon-"] { + margin-top: 0; +} + +.btn-mini { + padding: 2px 6px; + font-size: 11px; + line-height: 17px; +} + +.btn-block { + display: block; + width: 100%; + padding-right: 0; + padding-left: 0; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.btn-block + .btn-block { + margin-top: 5px; +} + +input[type="submit"].btn-block, +input[type="reset"].btn-block, +input[type="button"].btn-block { + width: 100%; +} + +.btn-primary.active, +.btn-warning.active, +.btn-danger.active, +.btn-success.active, +.btn-info.active, +.btn-inverse.active { + color: rgba(255, 255, 255, 0.75); +} + +.btn { + border-color: #c5c5c5; + border-color: rgba(0, 0, 0, 0.15) rgba(0, 0, 0, 0.15) rgba(0, 0, 0, 0.25); +} + +.btn-primary { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #006dcc; + *background-color: #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(to bottom, #0088cc, #0044cc); + background-image: -moz-linear-gradient(top, #0088cc, #0044cc); + background-repeat: repeat-x; + 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(startColorstr='#ff0088cc', endColorstr='#ff0044cc', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} + +.btn-primary:hover, +.btn-primary:active, +.btn-primary.active, +.btn-primary.disabled, +.btn-primary[disabled] { + color: #ffffff; + background-color: #0044cc; + *background-color: #003bb3; +} + +.btn-primary:active, +.btn-primary.active { + background-color: #003399 \9; +} + +.btn-warning { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #faa732; + *background-color: #f89406; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); + background-image: -webkit-linear-gradient(top, #fbb450, #f89406); + background-image: -o-linear-gradient(top, #fbb450, #f89406); + background-image: linear-gradient(to bottom, #fbb450, #f89406); + background-image: -moz-linear-gradient(top, #fbb450, #f89406); + background-repeat: repeat-x; + border-color: #f89406 #f89406 #ad6704; + 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(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} + +.btn-warning:hover, +.btn-warning:active, +.btn-warning.active, +.btn-warning.disabled, +.btn-warning[disabled] { + color: #ffffff; + background-color: #f89406; + *background-color: #df8505; +} + +.btn-warning:active, +.btn-warning.active { + background-color: #c67605 \9; +} + +.btn-danger { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #da4f49; + *background-color: #bd362f; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f)); + background-image: -webkit-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -o-linear-gradient(top, #ee5f5b, #bd362f); + background-image: linear-gradient(to bottom, #ee5f5b, #bd362f); + background-image: -moz-linear-gradient(top, #ee5f5b, #bd362f); + background-repeat: repeat-x; + border-color: #bd362f #bd362f #802420; + 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(startColorstr='#ffee5f5b', endColorstr='#ffbd362f', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} + +.btn-danger:hover, +.btn-danger:active, +.btn-danger.active, +.btn-danger.disabled, +.btn-danger[disabled] { + color: #ffffff; + background-color: #bd362f; + *background-color: #a9302a; +} + +.btn-danger:active, +.btn-danger.active { + background-color: #942a25 \9; +} + +.btn-success { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #5bb75b; + *background-color: #51a351; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351)); + background-image: -webkit-linear-gradient(top, #62c462, #51a351); + background-image: -o-linear-gradient(top, #62c462, #51a351); + background-image: linear-gradient(to bottom, #62c462, #51a351); + background-image: -moz-linear-gradient(top, #62c462, #51a351); + background-repeat: repeat-x; + border-color: #51a351 #51a351 #387038; + 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(startColorstr='#ff62c462', endColorstr='#ff51a351', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} + +.btn-success:hover, +.btn-success:active, +.btn-success.active, +.btn-success.disabled, +.btn-success[disabled] { + color: #ffffff; + background-color: #51a351; + *background-color: #499249; +} + +.btn-success:active, +.btn-success.active { + background-color: #408140 \9; +} + +.btn-info { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #49afcd; + *background-color: #2f96b4; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4)); + background-image: -webkit-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -o-linear-gradient(top, #5bc0de, #2f96b4); + background-image: linear-gradient(to bottom, #5bc0de, #2f96b4); + background-image: -moz-linear-gradient(top, #5bc0de, #2f96b4); + background-repeat: repeat-x; + border-color: #2f96b4 #2f96b4 #1f6377; + 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(startColorstr='#ff5bc0de', endColorstr='#ff2f96b4', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} + +.btn-info:hover, +.btn-info:active, +.btn-info.active, +.btn-info.disabled, +.btn-info[disabled] { + color: #ffffff; + background-color: #2f96b4; + *background-color: #2a85a0; +} + +.btn-info:active, +.btn-info.active { + background-color: #24748c \9; +} + +.btn-inverse { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #363636; + *background-color: #222222; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#444444), to(#222222)); + background-image: -webkit-linear-gradient(top, #444444, #222222); + background-image: -o-linear-gradient(top, #444444, #222222); + background-image: linear-gradient(to bottom, #444444, #222222); + background-image: -moz-linear-gradient(top, #444444, #222222); + background-repeat: repeat-x; + border-color: #222222 #222222 #000000; + 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(startColorstr='#ff444444', endColorstr='#ff222222', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} + +.btn-inverse:hover, +.btn-inverse:active, +.btn-inverse.active, +.btn-inverse.disabled, +.btn-inverse[disabled] { + color: #ffffff; + background-color: #222222; + *background-color: #151515; +} + +.btn-inverse:active, +.btn-inverse.active { + background-color: #080808 \9; +} + +button.btn, +input[type="submit"].btn { + *padding-top: 3px; + *padding-bottom: 3px; +} + +button.btn::-moz-focus-inner, +input[type="submit"].btn::-moz-focus-inner { + padding: 0; + border: 0; +} + +button.btn.btn-large, +input[type="submit"].btn.btn-large { + *padding-top: 7px; + *padding-bottom: 7px; +} + +button.btn.btn-small, +input[type="submit"].btn.btn-small { + *padding-top: 3px; + *padding-bottom: 3px; +} + +button.btn.btn-mini, +input[type="submit"].btn.btn-mini { + *padding-top: 1px; + *padding-bottom: 1px; +} + +.btn-link, +.btn-link:active, +.btn-link[disabled] { + background-color: transparent; + background-image: none; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} + +.btn-link { + color: #0088cc; + cursor: pointer; + border-color: transparent; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.btn-link:hover { + color: #005580; + text-decoration: underline; + background-color: transparent; +} + +.btn-link[disabled]:hover { + color: #333333; + text-decoration: none; +} + +.btn-group { + position: relative; + *margin-left: .3em; + font-size: 0; + white-space: nowrap; + vertical-align: middle; +} + +.btn-group:first-child { + *margin-left: 0; +} + +.btn-group + .btn-group { + margin-left: 5px; +} + +.btn-toolbar { + margin-top: 10px; + margin-bottom: 10px; + font-size: 0; +} + +.btn-toolbar .btn-group { + display: inline-block; + *display: inline; + /* IE7 inline-block hack */ + + *zoom: 1; +} + +.btn-toolbar .btn + .btn, +.btn-toolbar .btn-group + .btn, +.btn-toolbar .btn + .btn-group { + margin-left: 5px; +} + +.btn-group > .btn { + position: relative; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.btn-group > .btn + .btn { + margin-left: -1px; +} + +.btn-group > .btn, +.btn-group > .dropdown-menu { + font-size: 14px; +} + +.btn-group > .btn-mini { + font-size: 11px; +} + +.btn-group > .btn-small { + font-size: 12px; +} + +.btn-group > .btn-large { + font-size: 16px; +} + +.btn-group > .btn:first-child { + margin-left: 0; + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-bottomleft: 4px; + -moz-border-radius-topleft: 4px; +} + +.btn-group > .btn:last-child, +.btn-group > .dropdown-toggle { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -moz-border-radius-topright: 4px; + -moz-border-radius-bottomright: 4px; +} + +.btn-group > .btn.large:first-child { + margin-left: 0; + -webkit-border-bottom-left-radius: 6px; + border-bottom-left-radius: 6px; + -webkit-border-top-left-radius: 6px; + border-top-left-radius: 6px; + -moz-border-radius-bottomleft: 6px; + -moz-border-radius-topleft: 6px; +} + +.btn-group > .btn.large:last-child, +.btn-group > .large.dropdown-toggle { + -webkit-border-top-right-radius: 6px; + border-top-right-radius: 6px; + -webkit-border-bottom-right-radius: 6px; + border-bottom-right-radius: 6px; + -moz-border-radius-topright: 6px; + -moz-border-radius-bottomright: 6px; +} + +.btn-group > .btn:hover, +.btn-group > .btn:focus, +.btn-group > .btn:active, +.btn-group > .btn.active { + z-index: 2; +} + +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} + +.btn-group > .btn + .dropdown-toggle { + *padding-top: 5px; + padding-right: 8px; + *padding-bottom: 5px; + padding-left: 8px; + -webkit-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn-group > .btn-mini + .dropdown-toggle { + *padding-top: 2px; + padding-right: 5px; + *padding-bottom: 2px; + padding-left: 5px; +} + +.btn-group > .btn-small + .dropdown-toggle { + *padding-top: 5px; + *padding-bottom: 4px; +} + +.btn-group > .btn-large + .dropdown-toggle { + *padding-top: 7px; + padding-right: 12px; + *padding-bottom: 7px; + padding-left: 12px; +} + +.btn-group.open .dropdown-toggle { + background-image: none; + -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn-group.open .btn.dropdown-toggle { + background-color: #e6e6e6; +} + +.btn-group.open .btn-primary.dropdown-toggle { + background-color: #0044cc; +} + +.btn-group.open .btn-warning.dropdown-toggle { + background-color: #f89406; +} + +.btn-group.open .btn-danger.dropdown-toggle { + background-color: #bd362f; +} + +.btn-group.open .btn-success.dropdown-toggle { + background-color: #51a351; +} + +.btn-group.open .btn-info.dropdown-toggle { + background-color: #2f96b4; +} + +.btn-group.open .btn-inverse.dropdown-toggle { + background-color: #222222; +} + +.btn .caret { + margin-top: 8px; + margin-left: 0; +} + +.btn-mini .caret, +.btn-small .caret, +.btn-large .caret { + margin-top: 6px; +} + +.btn-large .caret { + border-top-width: 5px; + border-right-width: 5px; + border-left-width: 5px; +} + +.dropup .btn-large .caret { + border-top: 0; + border-bottom: 5px solid #000000; +} + +.btn-primary .caret, +.btn-warning .caret, +.btn-danger .caret, +.btn-info .caret, +.btn-success .caret, +.btn-inverse .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; +} + +.btn-group-vertical { + display: inline-block; + *display: inline; + /* IE7 inline-block hack */ + + *zoom: 1; +} + +.btn-group-vertical .btn { + display: block; + float: none; + width: 100%; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.btn-group-vertical .btn + .btn { + margin-top: -1px; + margin-left: 0; +} + +.btn-group-vertical .btn:first-child { + -webkit-border-radius: 4px 4px 0 0; + -moz-border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; +} + +.btn-group-vertical .btn:last-child { + -webkit-border-radius: 0 0 4px 4px; + -moz-border-radius: 0 0 4px 4px; + border-radius: 0 0 4px 4px; +} + +.btn-group-vertical .btn-large:first-child { + -webkit-border-radius: 6px 6px 0 0; + -moz-border-radius: 6px 6px 0 0; + border-radius: 6px 6px 0 0; +} + +.btn-group-vertical .btn-large:last-child { + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; +} + +.alert { + padding: 8px 35px 8px 14px; + margin-bottom: 20px; + color: #c09853; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + background-color: #fcf8e3; + border: 1px solid #fbeed5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.alert h4 { + margin: 0; +} + +.alert .close { + position: relative; + top: -2px; + right: -21px; + line-height: 20px; +} + +.alert-success { + color: #468847; + background-color: #dff0d8; + border-color: #d6e9c6; +} + +.alert-danger, +.alert-error { + color: #b94a48; + background-color: #f2dede; + border-color: #eed3d7; +} + +.alert-info { + color: #3a87ad; + background-color: #d9edf7; + border-color: #bce8f1; +} + +.alert-block { + padding-top: 14px; + padding-bottom: 14px; +} + +.alert-block > p, +.alert-block > ul { + margin-bottom: 0; +} + +.alert-block p + p { + margin-top: 5px; +} + +.nav { + margin-bottom: 20px; + margin-left: 0; + list-style: none; +} + +.nav > li > a { + display: block; +} + +.nav > li > a:hover { + text-decoration: none; + background-color: #eeeeee; +} + +.nav > .pull-right { + float: right; +} + +.nav-header { + display: block; + padding: 3px 15px; + font-size: 11px; + font-weight: bold; + line-height: 20px; + color: #999999; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-transform: uppercase; +} + +.nav li + .nav-header { + margin-top: 9px; +} + +.nav-list { + padding-right: 15px; + padding-left: 15px; + margin-bottom: 0; +} + +.nav-list > li > a, +.nav-list .nav-header { + margin-right: -15px; + margin-left: -15px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); +} + +.nav-list > li > a { + padding: 3px 15px; +} + +.nav-list > .active > a, +.nav-list > .active > a:hover { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2); + background-color: #0088cc; +} + +.nav-list [class^="icon-"] { + margin-right: 2px; +} + +.nav-list .divider { + *width: 100%; + height: 1px; + margin: 9px 1px; + *margin: -5px 0 5px; + overflow: hidden; + background-color: #e5e5e5; + border-bottom: 1px solid #ffffff; +} + +.nav-tabs, +.nav-pills { + *zoom: 1; +} + +.nav-tabs:before, +.nav-pills:before, +.nav-tabs:after, +.nav-pills:after { + display: table; + line-height: 0; + content: ""; +} + +.nav-tabs:after, +.nav-pills:after { + clear: both; +} + +.nav-tabs > li, +.nav-pills > li { + float: left; +} + +.nav-tabs > li > a, +.nav-pills > li > a { + padding-right: 12px; + padding-left: 12px; + margin-right: 2px; + line-height: 14px; +} + +.nav-tabs { + border-bottom: 1px solid #ddd; +} + +.nav-tabs > li { + margin-bottom: -1px; +} + +.nav-tabs > li > a { + padding-top: 8px; + padding-bottom: 8px; + line-height: 20px; + border: 1px solid transparent; + -webkit-border-radius: 4px 4px 0 0; + -moz-border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; +} + +.nav-tabs > li > a:hover { + border-color: #eeeeee #eeeeee #dddddd; +} + +.nav-tabs > .active > a, +.nav-tabs > .active > a:hover { + color: #555555; + cursor: default; + background-color: #ffffff; + border: 1px solid #ddd; + border-bottom-color: transparent; +} + +.nav-pills > li > a { + padding-top: 8px; + padding-bottom: 8px; + margin-top: 2px; + margin-bottom: 2px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} + +.nav-pills > .active > a, +.nav-pills > .active > a:hover { + color: #ffffff; + background-color: #0088cc; +} + +.nav-stacked > li { + float: none; +} + +.nav-stacked > li > a { + margin-right: 0; +} + +.nav-tabs.nav-stacked { + border-bottom: 0; +} + +.nav-tabs.nav-stacked > li > a { + border: 1px solid #ddd; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.nav-tabs.nav-stacked > li:first-child > a { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-topright: 4px; + -moz-border-radius-topleft: 4px; +} + +.nav-tabs.nav-stacked > li:last-child > a { + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -moz-border-radius-bottomright: 4px; + -moz-border-radius-bottomleft: 4px; +} + +.nav-tabs.nav-stacked > li > a:hover { + z-index: 2; + border-color: #ddd; +} + +.nav-pills.nav-stacked > li > a { + margin-bottom: 3px; +} + +.nav-pills.nav-stacked > li:last-child > a { + margin-bottom: 1px; +} + +.nav-tabs .dropdown-menu { + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; +} + +.nav-pills .dropdown-menu { + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.nav .dropdown-toggle .caret { + margin-top: 6px; + border-top-color: #0088cc; + border-bottom-color: #0088cc; +} + +.nav .dropdown-toggle:hover .caret { + border-top-color: #005580; + border-bottom-color: #005580; +} + +/* move down carets for tabs */ + +.nav-tabs .dropdown-toggle .caret { + margin-top: 8px; +} + +.nav .active .dropdown-toggle .caret { + border-top-color: #fff; + border-bottom-color: #fff; +} + +.nav-tabs .active .dropdown-toggle .caret { + border-top-color: #555555; + border-bottom-color: #555555; +} + +.nav > .dropdown.active > a:hover { + cursor: pointer; +} + +.nav-tabs .open .dropdown-toggle, +.nav-pills .open .dropdown-toggle, +.nav > li.dropdown.open.active > a:hover { + color: #ffffff; + background-color: #999999; + border-color: #999999; +} + +.nav li.dropdown.open .caret, +.nav li.dropdown.open.active .caret, +.nav li.dropdown.open a:hover .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; + opacity: 1; + filter: alpha(opacity=100); +} + +.tabs-stacked .open > a:hover { + border-color: #999999; +} + +.tabbable { + *zoom: 1; +} + +.tabbable:before, +.tabbable:after { + display: table; + line-height: 0; + content: ""; +} + +.tabbable:after { + clear: both; +} + +.tab-content { + overflow: auto; +} + +.tabs-below > .nav-tabs, +.tabs-right > .nav-tabs, +.tabs-left > .nav-tabs { + border-bottom: 0; +} + +.tab-content > .tab-pane, +.pill-content > .pill-pane { + display: none; +} + +.tab-content > .active, +.pill-content > .active { + display: block; +} + +.tabs-below > .nav-tabs { + border-top: 1px solid #ddd; +} + +.tabs-below > .nav-tabs > li { + margin-top: -1px; + margin-bottom: 0; +} + +.tabs-below > .nav-tabs > li > a { + -webkit-border-radius: 0 0 4px 4px; + -moz-border-radius: 0 0 4px 4px; + border-radius: 0 0 4px 4px; +} + +.tabs-below > .nav-tabs > li > a:hover { + border-top-color: #ddd; + border-bottom-color: transparent; +} + +.tabs-below > .nav-tabs > .active > a, +.tabs-below > .nav-tabs > .active > a:hover { + border-color: transparent #ddd #ddd #ddd; +} + +.tabs-left > .nav-tabs > li, +.tabs-right > .nav-tabs > li { + float: none; +} + +.tabs-left > .nav-tabs > li > a, +.tabs-right > .nav-tabs > li > a { + min-width: 74px; + margin-right: 0; + margin-bottom: 3px; +} + +.tabs-left > .nav-tabs { + float: left; + margin-right: 19px; + border-right: 1px solid #ddd; +} + +.tabs-left > .nav-tabs > li > a { + margin-right: -1px; + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.tabs-left > .nav-tabs > li > a:hover { + border-color: #eeeeee #dddddd #eeeeee #eeeeee; +} + +.tabs-left > .nav-tabs .active > a, +.tabs-left > .nav-tabs .active > a:hover { + border-color: #ddd transparent #ddd #ddd; + *border-right-color: #ffffff; +} + +.tabs-right > .nav-tabs { + float: right; + margin-left: 19px; + border-left: 1px solid #ddd; +} + +.tabs-right > .nav-tabs > li > a { + margin-left: -1px; + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.tabs-right > .nav-tabs > li > a:hover { + border-color: #eeeeee #eeeeee #eeeeee #dddddd; +} + +.tabs-right > .nav-tabs .active > a, +.tabs-right > .nav-tabs .active > a:hover { + border-color: #ddd #ddd #ddd transparent; + *border-left-color: #ffffff; +} + +.nav > .disabled > a { + color: #999999; +} + +.nav > .disabled > a:hover { + text-decoration: none; + cursor: default; + background-color: transparent; +} + +.navbar { + *position: relative; + *z-index: 2; + margin-bottom: 20px; + overflow: visible; + color: #777777; +} + +.navbar-inner { + min-height: 40px; + padding-right: 20px; + padding-left: 20px; + background-color: #fafafa; + background-image: -moz-linear-gradient(top, #ffffff, #f2f2f2); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f2f2f2)); + background-image: -webkit-linear-gradient(top, #ffffff, #f2f2f2); + background-image: -o-linear-gradient(top, #ffffff, #f2f2f2); + background-image: linear-gradient(to bottom, #ffffff, #f2f2f2); + background-repeat: repeat-x; + border: 1px solid #d4d4d4; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff2f2f2', GradientType=0); + *zoom: 1; + -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); + -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); +} + +.navbar-inner:before, +.navbar-inner:after { + display: table; + line-height: 0; + content: ""; +} + +.navbar-inner:after { + clear: both; +} + +.navbar .container { + width: auto; +} + +.nav-collapse.collapse { + height: auto; +} + +.navbar .brand { + display: block; + float: left; + padding: 10px 20px 10px; + margin-left: -20px; + font-size: 20px; + font-weight: 200; + color: #777777; + text-shadow: 0 1px 0 #ffffff; +} + +.navbar .brand:hover { + text-decoration: none; +} + +.navbar-text { + margin-bottom: 0; + line-height: 40px; +} + +.navbar-link { + color: #777777; +} + +.navbar-link:hover { + color: #333333; +} + +.navbar .divider-vertical { + height: 40px; + margin: 0 9px; + border-right: 1px solid #ffffff; + border-left: 1px solid #f2f2f2; +} + +.navbar .btn, +.navbar .btn-group { + margin-top: 5px; +} + +.navbar .btn-group .btn, +.navbar .input-prepend .btn, +.navbar .input-append .btn { + margin-top: 0; +} + +.navbar-form { + margin-bottom: 0; + *zoom: 1; +} + +.navbar-form:before, +.navbar-form:after { + display: table; + line-height: 0; + content: ""; +} + +.navbar-form:after { + clear: both; +} + +.navbar-form input, +.navbar-form select, +.navbar-form .radio, +.navbar-form .checkbox { + margin-top: 5px; +} + +.navbar-form input, +.navbar-form select, +.navbar-form .btn { + display: inline-block; + margin-bottom: 0; +} + +.navbar-form input[type="image"], +.navbar-form input[type="checkbox"], +.navbar-form input[type="radio"] { + margin-top: 3px; +} + +.navbar-form .input-append, +.navbar-form .input-prepend { + margin-top: 6px; + white-space: nowrap; +} + +.navbar-form .input-append input, +.navbar-form .input-prepend input { + margin-top: 0; +} + +.navbar-search { + position: relative; + float: left; + margin-top: 5px; + margin-bottom: 0; +} + +.navbar-search .search-query { + padding: 4px 14px; + margin-bottom: 0; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + font-weight: normal; + line-height: 1; + -webkit-border-radius: 15px; + -moz-border-radius: 15px; + border-radius: 15px; +} + +.navbar-static-top { + position: static; + width: 100%; + margin-bottom: 0; +} + +.navbar-static-top .navbar-inner { + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.navbar-fixed-top, +.navbar-fixed-bottom { + position: fixed; + right: 0; + left: 0; + z-index: 1030; + margin-bottom: 0; +} + +.navbar-fixed-top .navbar-inner, +.navbar-static-top .navbar-inner { + border-width: 0 0 1px; +} + +.navbar-fixed-bottom .navbar-inner { + border-width: 1px 0 0; +} + +.navbar-fixed-top .navbar-inner, +.navbar-fixed-bottom .navbar-inner { + padding-right: 0; + padding-left: 0; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.navbar-static-top .container, +.navbar-fixed-top .container, +.navbar-fixed-bottom .container { + width: 940px; +} + +.navbar-fixed-top { + top: 0; +} + +.navbar-fixed-top .navbar-inner, +.navbar-static-top .navbar-inner { + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.1), 0 1px 10px rgba(0, 0, 0, 0.1); + -moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.1), 0 1px 10px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.1), 0 1px 10px rgba(0, 0, 0, 0.1); +} + +.navbar-fixed-bottom { + bottom: 0; +} + +.navbar-fixed-bottom .navbar-inner { + -webkit-box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.1), 0 -1px 10px rgba(0, 0, 0, 0.1); + -moz-box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.1), 0 -1px 10px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.1), 0 -1px 10px rgba(0, 0, 0, 0.1); +} + +.navbar .nav { + position: relative; + left: 0; + display: block; + float: left; + margin: 0 10px 0 0; +} + +.navbar .nav.pull-right { + float: right; + margin-right: 0; +} + +.navbar .nav > li { + float: left; +} + +.navbar .nav > li > a { + float: none; + padding: 10px 15px 10px; + color: #777777; + text-decoration: none; + text-shadow: 0 1px 0 #ffffff; +} + +.navbar .nav .dropdown-toggle .caret { + margin-top: 8px; +} + +.navbar .nav > li > a:focus, +.navbar .nav > li > a:hover { + color: #333333; + text-decoration: none; + background-color: transparent; +} + +.navbar .nav > .active > a, +.navbar .nav > .active > a:hover, +.navbar .nav > .active > a:focus { + color: #555555; + text-decoration: none; + background-color: #e5e5e5; + -webkit-box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); + -moz-box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); +} + +.navbar .btn-navbar { + display: none; + float: right; + padding: 7px 10px; + margin-right: 5px; + margin-left: 5px; + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #ededed; + *background-color: #e5e5e5; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f2f2f2), to(#e5e5e5)); + background-image: -webkit-linear-gradient(top, #f2f2f2, #e5e5e5); + background-image: -o-linear-gradient(top, #f2f2f2, #e5e5e5); + background-image: linear-gradient(to bottom, #f2f2f2, #e5e5e5); + background-image: -moz-linear-gradient(top, #f2f2f2, #e5e5e5); + background-repeat: repeat-x; + border-color: #e5e5e5 #e5e5e5 #bfbfbf; + 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(startColorstr='#fff2f2f2', endColorstr='#ffe5e5e5', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); +} + +.navbar .btn-navbar:hover, +.navbar .btn-navbar:active, +.navbar .btn-navbar.active, +.navbar .btn-navbar.disabled, +.navbar .btn-navbar[disabled] { + color: #ffffff; + background-color: #e5e5e5; + *background-color: #d9d9d9; +} + +.navbar .btn-navbar:active, +.navbar .btn-navbar.active { + background-color: #cccccc \9; +} + +.navbar .btn-navbar .icon-bar { + display: block; + width: 18px; + height: 2px; + background-color: #f5f5f5; + -webkit-border-radius: 1px; + -moz-border-radius: 1px; + border-radius: 1px; + -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); + -moz-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); +} + +.btn-navbar .icon-bar + .icon-bar { + margin-top: 3px; +} + +.navbar .nav > li > .dropdown-menu:before { + position: absolute; + top: -7px; + left: 9px; + display: inline-block; + border-right: 7px solid transparent; + border-bottom: 7px solid #ccc; + border-left: 7px solid transparent; + border-bottom-color: rgba(0, 0, 0, 0.2); + content: ''; +} + +.navbar .nav > li > .dropdown-menu:after { + position: absolute; + top: -6px; + left: 10px; + display: inline-block; + border-right: 6px solid transparent; + border-bottom: 6px solid #ffffff; + border-left: 6px solid transparent; + content: ''; +} + +.navbar-fixed-bottom .nav > li > .dropdown-menu:before { + top: auto; + bottom: -7px; + border-top: 7px solid #ccc; + border-bottom: 0; + border-top-color: rgba(0, 0, 0, 0.2); +} + +.navbar-fixed-bottom .nav > li > .dropdown-menu:after { + top: auto; + bottom: -6px; + border-top: 6px solid #ffffff; + border-bottom: 0; +} + +.navbar .nav li.dropdown.open > .dropdown-toggle, +.navbar .nav li.dropdown.active > .dropdown-toggle, +.navbar .nav li.dropdown.open.active > .dropdown-toggle { + color: #555555; + background-color: #e5e5e5; +} + +.navbar .nav li.dropdown > .dropdown-toggle .caret { + border-top-color: #777777; + border-bottom-color: #777777; +} + +.navbar .nav li.dropdown.open > .dropdown-toggle .caret, +.navbar .nav li.dropdown.active > .dropdown-toggle .caret, +.navbar .nav li.dropdown.open.active > .dropdown-toggle .caret { + border-top-color: #555555; + border-bottom-color: #555555; +} + +.navbar .pull-right > li > .dropdown-menu, +.navbar .nav > li > .dropdown-menu.pull-right { + right: 0; + left: auto; +} + +.navbar .pull-right > li > .dropdown-menu:before, +.navbar .nav > li > .dropdown-menu.pull-right:before { + right: 12px; + left: auto; +} + +.navbar .pull-right > li > .dropdown-menu:after, +.navbar .nav > li > .dropdown-menu.pull-right:after { + right: 13px; + left: auto; +} + +.navbar .pull-right > li > .dropdown-menu .dropdown-menu, +.navbar .nav > li > .dropdown-menu.pull-right .dropdown-menu { + right: 100%; + left: auto; + margin-right: -1px; + margin-left: 0; + -webkit-border-radius: 6px 0 6px 6px; + -moz-border-radius: 6px 0 6px 6px; + border-radius: 6px 0 6px 6px; +} + +.navbar-inverse { + color: #999999; +} + +.navbar-inverse .navbar-inner { + background-color: #1b1b1b; + background-image: -moz-linear-gradient(top, #222222, #111111); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#222222), to(#111111)); + background-image: -webkit-linear-gradient(top, #222222, #111111); + background-image: -o-linear-gradient(top, #222222, #111111); + background-image: linear-gradient(to bottom, #222222, #111111); + background-repeat: repeat-x; + border-color: #252525; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff111111', GradientType=0); +} + +.navbar-inverse .brand, +.navbar-inverse .nav > li > a { + color: #999999; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); +} + +.navbar-inverse .brand:hover, +.navbar-inverse .nav > li > a:hover { + color: #ffffff; +} + +.navbar-inverse .nav > li > a:focus, +.navbar-inverse .nav > li > a:hover { + color: #ffffff; + background-color: transparent; +} + +.navbar-inverse .nav .active > a, +.navbar-inverse .nav .active > a:hover, +.navbar-inverse .nav .active > a:focus { + color: #ffffff; + background-color: #111111; +} + +.navbar-inverse .navbar-link { + color: #999999; +} + +.navbar-inverse .navbar-link:hover { + color: #ffffff; +} + +.navbar-inverse .divider-vertical { + border-right-color: #222222; + border-left-color: #111111; +} + +.navbar-inverse .nav li.dropdown.open > .dropdown-toggle, +.navbar-inverse .nav li.dropdown.active > .dropdown-toggle, +.navbar-inverse .nav li.dropdown.open.active > .dropdown-toggle { + color: #ffffff; + background-color: #111111; +} + +.navbar-inverse .nav li.dropdown > .dropdown-toggle .caret { + border-top-color: #999999; + border-bottom-color: #999999; +} + +.navbar-inverse .nav li.dropdown.open > .dropdown-toggle .caret, +.navbar-inverse .nav li.dropdown.active > .dropdown-toggle .caret, +.navbar-inverse .nav li.dropdown.open.active > .dropdown-toggle .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; +} + +.navbar-inverse .navbar-search .search-query { + color: #ffffff; + background-color: #515151; + border-color: #111111; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); + -webkit-transition: none; + -moz-transition: none; + -o-transition: none; + transition: none; +} + +.navbar-inverse .navbar-search .search-query:-moz-placeholder { + color: #cccccc; +} + +.navbar-inverse .navbar-search .search-query:-ms-input-placeholder { + color: #cccccc; +} + +.navbar-inverse .navbar-search .search-query::-webkit-input-placeholder { + color: #cccccc; +} + +.navbar-inverse .navbar-search .search-query:focus, +.navbar-inverse .navbar-search .search-query.focused { + padding: 5px 15px; + color: #333333; + text-shadow: 0 1px 0 #ffffff; + background-color: #ffffff; + border: 0; + outline: 0; + -webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); + -moz-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); + box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); +} + +.navbar-inverse .btn-navbar { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #0e0e0e; + *background-color: #040404; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#151515), to(#040404)); + background-image: -webkit-linear-gradient(top, #151515, #040404); + background-image: -o-linear-gradient(top, #151515, #040404); + background-image: linear-gradient(to bottom, #151515, #040404); + background-image: -moz-linear-gradient(top, #151515, #040404); + background-repeat: repeat-x; + border-color: #040404 #040404 #000000; + 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(startColorstr='#ff151515', endColorstr='#ff040404', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} + +.navbar-inverse .btn-navbar:hover, +.navbar-inverse .btn-navbar:active, +.navbar-inverse .btn-navbar.active, +.navbar-inverse .btn-navbar.disabled, +.navbar-inverse .btn-navbar[disabled] { + color: #ffffff; + background-color: #040404; + *background-color: #000000; +} + +.navbar-inverse .btn-navbar:active, +.navbar-inverse .btn-navbar.active { + background-color: #000000 \9; +} + +.breadcrumb { + padding: 8px 15px; + margin: 0 0 20px; + list-style: none; + background-color: #f5f5f5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.breadcrumb li { + display: inline-block; + *display: inline; + text-shadow: 0 1px 0 #ffffff; + *zoom: 1; +} + +.breadcrumb .divider { + padding: 0 5px; + color: #ccc; +} + +.breadcrumb .active { + color: #999999; +} + +.pagination { + height: 40px; + margin: 20px 0; +} + +.pagination ul { + display: inline-block; + *display: inline; + margin-bottom: 0; + margin-left: 0; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + *zoom: 1; + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.pagination ul > li { + display: inline; +} + +.pagination ul > li > a, +.pagination ul > li > span { + float: left; + padding: 0 14px; + line-height: 38px; + text-decoration: none; + background-color: #ffffff; + border: 1px solid #dddddd; + border-left-width: 0; +} + +.pagination ul > li > a:hover, +.pagination ul > .active > a, +.pagination ul > .active > span { + background-color: #f5f5f5; +} + +.pagination ul > .active > a, +.pagination ul > .active > span { + color: #999999; + cursor: default; +} + +.pagination ul > .disabled > span, +.pagination ul > .disabled > a, +.pagination ul > .disabled > a:hover { + color: #999999; + cursor: default; + background-color: transparent; +} + +.pagination ul > li:first-child > a, +.pagination ul > li:first-child > span { + border-left-width: 1px; + -webkit-border-radius: 3px 0 0 3px; + -moz-border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px; +} + +.pagination ul > li:last-child > a, +.pagination ul > li:last-child > span { + -webkit-border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0; +} + +.pagination-centered { + text-align: center; +} + +.pagination-right { + text-align: right; +} + +.pager { + margin: 20px 0; + text-align: center; + list-style: none; + *zoom: 1; +} + +.pager:before, +.pager:after { + display: table; + line-height: 0; + content: ""; +} + +.pager:after { + clear: both; +} + +.pager li { + display: inline; +} + +.pager a, +.pager span { + display: inline-block; + padding: 5px 14px; + background-color: #fff; + border: 1px solid #ddd; + -webkit-border-radius: 15px; + -moz-border-radius: 15px; + border-radius: 15px; +} + +.pager a:hover { + text-decoration: none; + background-color: #f5f5f5; +} + +.pager .next a, +.pager .next span { + float: right; +} + +.pager .previous a { + float: left; +} + +.pager .disabled a, +.pager .disabled a:hover, +.pager .disabled span { + color: #999999; + cursor: default; + background-color: #fff; +} + +.modal-open .modal .dropdown-menu { + z-index: 2050; +} + +.modal-open .modal .dropdown.open { + *z-index: 2050; +} + +.modal-open .modal .popover { + z-index: 2060; +} + +.modal-open .modal .tooltip { + z-index: 2080; +} + +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + background-color: #000000; +} + +.modal-backdrop.fade { + opacity: 0; +} + +.modal-backdrop, +.modal-backdrop.fade.in { + opacity: 0.8; + filter: alpha(opacity=80); +} + +.modal { + position: fixed; + top: 50%; + left: 50%; + z-index: 1050; + width: 560px; + margin: -250px 0 0 -280px; + overflow: auto; + background-color: #ffffff; + border: 1px solid #999; + border: 1px solid rgba(0, 0, 0, 0.3); + *border: 1px solid #999; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -webkit-background-clip: padding-box; + -moz-background-clip: padding-box; + background-clip: padding-box; +} + +.modal.fade { + top: -25%; + -webkit-transition: opacity 0.3s linear, top 0.3s ease-out; + -moz-transition: opacity 0.3s linear, top 0.3s ease-out; + -o-transition: opacity 0.3s linear, top 0.3s ease-out; + transition: opacity 0.3s linear, top 0.3s ease-out; +} + +.modal.fade.in { + top: 50%; +} + +.modal-header { + padding: 9px 15px; + border-bottom: 1px solid #eee; +} + +.modal-header .close { + margin-top: 2px; +} + +.modal-header h3 { + margin: 0; + line-height: 30px; +} + +.modal-body { + max-height: 400px; + padding: 15px; + overflow-y: auto; +} + +.modal-form { + margin-bottom: 0; +} + +.modal-footer { + padding: 14px 15px 15px; + margin-bottom: 0; + text-align: right; + background-color: #f5f5f5; + border-top: 1px solid #ddd; + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; + *zoom: 1; + -webkit-box-shadow: inset 0 1px 0 #ffffff; + -moz-box-shadow: inset 0 1px 0 #ffffff; + box-shadow: inset 0 1px 0 #ffffff; +} + +.modal-footer:before, +.modal-footer:after { + display: table; + line-height: 0; + content: ""; +} + +.modal-footer:after { + clear: both; +} + +.modal-footer .btn + .btn { + margin-bottom: 0; + margin-left: 5px; +} + +.modal-footer .btn-group .btn + .btn { + margin-left: -1px; +} + +.tooltip { + position: absolute; + z-index: 1030; + display: block; + padding: 5px; + font-size: 11px; + opacity: 0; + filter: alpha(opacity=0); + visibility: visible; +} + +.tooltip.in { + opacity: 0.8; + filter: alpha(opacity=80); +} + +.tooltip.top { + margin-top: -3px; +} + +.tooltip.right { + margin-left: 3px; +} + +.tooltip.bottom { + margin-top: 3px; +} + +.tooltip.left { + margin-left: -3px; +} + +.tooltip-inner { + max-width: 200px; + padding: 3px 8px; + color: #ffffff; + text-align: center; + text-decoration: none; + background-color: #000000; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.tooltip.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-top-color: #000000; + border-width: 5px 5px 0; +} + +.tooltip.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-right-color: #000000; + border-width: 5px 5px 5px 0; +} + +.tooltip.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-left-color: #000000; + border-width: 5px 0 5px 5px; +} + +.tooltip.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-bottom-color: #000000; + border-width: 0 5px 5px; +} + +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1010; + display: none; + width: 236px; + padding: 1px; + background-color: #ffffff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; +} + +.popover.top { + margin-bottom: 10px; +} + +.popover.right { + margin-left: 10px; +} + +.popover.bottom { + margin-top: 10px; +} + +.popover.left { + margin-right: 10px; +} + +.popover-title { + padding: 8px 14px; + margin: 0; + font-size: 14px; + font-weight: normal; + line-height: 18px; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + -webkit-border-radius: 5px 5px 0 0; + -moz-border-radius: 5px 5px 0 0; + border-radius: 5px 5px 0 0; +} + +.popover-content { + padding: 9px 14px; +} + +.popover-content p, +.popover-content ul, +.popover-content ol { + margin-bottom: 0; +} + +.popover .arrow, +.popover .arrow:after { + position: absolute; + display: inline-block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.popover .arrow:after { + z-index: -1; + content: ""; +} + +.popover.top .arrow { + bottom: -10px; + left: 50%; + margin-left: -10px; + border-top-color: #ffffff; + border-width: 10px 10px 0; +} + +.popover.top .arrow:after { + bottom: -1px; + left: -11px; + border-top-color: rgba(0, 0, 0, 0.25); + border-width: 11px 11px 0; +} + +.popover.right .arrow { + top: 50%; + left: -10px; + margin-top: -10px; + border-right-color: #ffffff; + border-width: 10px 10px 10px 0; +} + +.popover.right .arrow:after { + bottom: -11px; + left: -1px; + border-right-color: rgba(0, 0, 0, 0.25); + border-width: 11px 11px 11px 0; +} + +.popover.bottom .arrow { + top: -10px; + left: 50%; + margin-left: -10px; + border-bottom-color: #ffffff; + border-width: 0 10px 10px; +} + +.popover.bottom .arrow:after { + top: -1px; + left: -11px; + border-bottom-color: rgba(0, 0, 0, 0.25); + border-width: 0 11px 11px; +} + +.popover.left .arrow { + top: 50%; + right: -10px; + margin-top: -10px; + border-left-color: #ffffff; + border-width: 10px 0 10px 10px; +} + +.popover.left .arrow:after { + right: -1px; + bottom: -11px; + border-left-color: rgba(0, 0, 0, 0.25); + border-width: 11px 0 11px 11px; +} + +.thumbnails { + margin-left: -20px; + list-style: none; + *zoom: 1; +} + +.thumbnails:before, +.thumbnails:after { + display: table; + line-height: 0; + content: ""; +} + +.thumbnails:after { + clear: both; +} + +.row-fluid .thumbnails { + margin-left: 0; +} + +.thumbnails > li { + float: left; + margin-bottom: 20px; + margin-left: 20px; +} + +.thumbnail { + display: block; + padding: 4px; + line-height: 20px; + border: 1px solid #ddd; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); + -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); + -webkit-transition: all 0.2s ease-in-out; + -moz-transition: all 0.2s ease-in-out; + -o-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; +} + +a.thumbnail:hover { + border-color: #0088cc; + -webkit-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); + -moz-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); + box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); +} + +.thumbnail > img { + display: block; + max-width: 100%; + margin-right: auto; + margin-left: auto; +} + +.thumbnail .caption { + padding: 9px; + color: #555555; +} + +.label, +.badge { + font-size: 11.844px; + font-weight: bold; + line-height: 14px; + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + white-space: nowrap; + vertical-align: baseline; + background-color: #999999; +} + +.label { + padding: 1px 4px 2px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.badge { + padding: 1px 9px 2px; + -webkit-border-radius: 9px; + -moz-border-radius: 9px; + border-radius: 9px; +} + +a.label:hover, +a.badge:hover { + color: #ffffff; + text-decoration: none; + cursor: pointer; +} + +.label-important, +.badge-important { + background-color: #b94a48; +} + +.label-important[href], +.badge-important[href] { + background-color: #953b39; +} + +.label-warning, +.badge-warning { + background-color: #f89406; +} + +.label-warning[href], +.badge-warning[href] { + background-color: #c67605; +} + +.label-success, +.badge-success { + background-color: #468847; +} + +.label-success[href], +.badge-success[href] { + background-color: #356635; +} + +.label-info, +.badge-info { + background-color: #3a87ad; +} + +.label-info[href], +.badge-info[href] { + background-color: #2d6987; +} + +.label-inverse, +.badge-inverse { + background-color: #333333; +} + +.label-inverse[href], +.badge-inverse[href] { + background-color: #1a1a1a; +} + +.btn .label, +.btn .badge { + position: relative; + top: -1px; +} + +.btn-mini .label, +.btn-mini .badge { + top: 0; +} + +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-moz-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-ms-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-o-keyframes progress-bar-stripes { + from { + background-position: 0 0; + } + to { + background-position: 40px 0; + } +} + +@keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +.progress { + height: 20px; + margin-bottom: 20px; + overflow: hidden; + background-color: #f7f7f7; + background-image: -moz-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9)); + background-image: -webkit-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -o-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: linear-gradient(to bottom, #f5f5f5, #f9f9f9); + background-repeat: repeat-x; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0); + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.progress .bar { + float: left; + width: 0; + height: 100%; + font-size: 12px; + color: #ffffff; + text-align: center; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #0e90d2; + background-image: -moz-linear-gradient(top, #149bdf, #0480be); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be)); + background-image: -webkit-linear-gradient(top, #149bdf, #0480be); + background-image: -o-linear-gradient(top, #149bdf, #0480be); + background-image: linear-gradient(to bottom, #149bdf, #0480be); + background-repeat: repeat-x; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0); + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-transition: width 0.6s ease; + -moz-transition: width 0.6s ease; + -o-transition: width 0.6s ease; + transition: width 0.6s ease; +} + +.progress .bar + .bar { + -webkit-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -moz-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); +} + +.progress-striped .bar { + background-color: #149bdf; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + -webkit-background-size: 40px 40px; + -moz-background-size: 40px 40px; + -o-background-size: 40px 40px; + background-size: 40px 40px; +} + +.progress.active .bar { + -webkit-animation: progress-bar-stripes 2s linear infinite; + -moz-animation: progress-bar-stripes 2s linear infinite; + -ms-animation: progress-bar-stripes 2s linear infinite; + -o-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; +} + +.progress-danger .bar, +.progress .bar-danger { + background-color: #dd514c; + background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35)); + background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -o-linear-gradient(top, #ee5f5b, #c43c35); + background-image: linear-gradient(to bottom, #ee5f5b, #c43c35); + background-repeat: repeat-x; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffc43c35', GradientType=0); +} + +.progress-danger.progress-striped .bar, +.progress-striped .bar-danger { + background-color: #ee5f5b; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-success .bar, +.progress .bar-success { + background-color: #5eb95e; + background-image: -moz-linear-gradient(top, #62c462, #57a957); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957)); + background-image: -webkit-linear-gradient(top, #62c462, #57a957); + background-image: -o-linear-gradient(top, #62c462, #57a957); + background-image: linear-gradient(to bottom, #62c462, #57a957); + background-repeat: repeat-x; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff57a957', GradientType=0); +} + +.progress-success.progress-striped .bar, +.progress-striped .bar-success { + background-color: #62c462; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-info .bar, +.progress .bar-info { + background-color: #4bb1cf; + background-image: -moz-linear-gradient(top, #5bc0de, #339bb9); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9)); + background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9); + background-image: -o-linear-gradient(top, #5bc0de, #339bb9); + background-image: linear-gradient(to bottom, #5bc0de, #339bb9); + background-repeat: repeat-x; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff339bb9', GradientType=0); +} + +.progress-info.progress-striped .bar, +.progress-striped .bar-info { + background-color: #5bc0de; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-warning .bar, +.progress .bar-warning { + background-color: #faa732; + background-image: -moz-linear-gradient(top, #fbb450, #f89406); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); + background-image: -webkit-linear-gradient(top, #fbb450, #f89406); + background-image: -o-linear-gradient(top, #fbb450, #f89406); + background-image: linear-gradient(to bottom, #fbb450, #f89406); + background-repeat: repeat-x; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0); +} + +.progress-warning.progress-striped .bar, +.progress-striped .bar-warning { + background-color: #fbb450; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.accordion { + margin-bottom: 20px; +} + +.accordion-group { + margin-bottom: 2px; + border: 1px solid #e5e5e5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.accordion-heading { + border-bottom: 0; +} + +.accordion-heading .accordion-toggle { + display: block; + padding: 8px 15px; +} + +.accordion-toggle { + cursor: pointer; +} + +.accordion-inner { + padding: 9px 15px; + border-top: 1px solid #e5e5e5; +} + +.carousel { + position: relative; + margin-bottom: 20px; + line-height: 1; +} + +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; +} + +.carousel .item { + position: relative; + display: none; + -webkit-transition: 0.6s ease-in-out left; + -moz-transition: 0.6s ease-in-out left; + -o-transition: 0.6s ease-in-out left; + transition: 0.6s ease-in-out left; +} + +.carousel .item > img { + display: block; + line-height: 1; +} + +.carousel .active, +.carousel .next, +.carousel .prev { + display: block; +} + +.carousel .active { + left: 0; +} + +.carousel .next, +.carousel .prev { + position: absolute; + top: 0; + width: 100%; +} + +.carousel .next { + left: 100%; +} + +.carousel .prev { + left: -100%; +} + +.carousel .next.left, +.carousel .prev.right { + left: 0; +} + +.carousel .active.left { + left: -100%; +} + +.carousel .active.right { + left: 100%; +} + +.carousel-control { + position: absolute; + top: 40%; + left: 15px; + width: 40px; + height: 40px; + margin-top: -20px; + font-size: 60px; + font-weight: 100; + line-height: 30px; + color: #ffffff; + text-align: center; + background: #222222; + border: 3px solid #ffffff; + -webkit-border-radius: 23px; + -moz-border-radius: 23px; + border-radius: 23px; + opacity: 0.5; + filter: alpha(opacity=50); +} + +.carousel-control.right { + right: 15px; + left: auto; +} + +.carousel-control:hover { + color: #ffffff; + text-decoration: none; + opacity: 0.9; + filter: alpha(opacity=90); +} + +.carousel-caption { + position: absolute; + right: 0; + bottom: 0; + left: 0; + padding: 15px; + background: #333333; + background: rgba(0, 0, 0, 0.75); +} + +.carousel-caption h4, +.carousel-caption p { + line-height: 20px; + color: #ffffff; +} + +.carousel-caption h4 { + margin: 0 0 5px; +} + +.carousel-caption p { + margin-bottom: 0; +} + +.hero-unit { + padding: 60px; + margin-bottom: 30px; + background-color: #eeeeee; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.hero-unit h1 { + margin-bottom: 0; + font-size: 60px; + line-height: 1; + letter-spacing: -1px; + color: inherit; +} + +.hero-unit p { + font-size: 18px; + font-weight: 200; + line-height: 30px; + color: inherit; +} + +.pull-right { + float: right; +} + +.pull-left { + float: left; +} + +.hide { + display: none; +} + +.show { + display: block; +} + +.invisible { + visibility: hidden; +} + +.affix { + position: fixed; +} diff --git a/templates/errors/403.html b/templates/errors/403.html new file mode 100644 index 0000000..94763d7 --- /dev/null +++ b/templates/errors/403.html @@ -0,0 +1 @@ +Access denied! diff --git a/templates/errors/todo.html b/templates/errors/todo.html new file mode 100644 index 0000000..a494f55 --- /dev/null +++ b/templates/errors/todo.html @@ -0,0 +1 @@ +This page is Work in Progress. diff --git a/templates/img/glyphicons-halflings-white.png b/templates/img/glyphicons-halflings-white.png new file mode 100644 index 0000000..3bf6484 Binary files /dev/null and b/templates/img/glyphicons-halflings-white.png differ diff --git a/templates/img/glyphicons-halflings.png b/templates/img/glyphicons-halflings.png new file mode 100644 index 0000000..a996999 Binary files /dev/null and b/templates/img/glyphicons-halflings.png differ diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..c7782bd --- /dev/null +++ b/templates/index.html @@ -0,0 +1 @@ +

    Welcome to the new KtT Shop System.

    diff --git a/templates/js/bootstrap.js b/templates/js/bootstrap.js new file mode 100644 index 0000000..f73fcb8 --- /dev/null +++ b/templates/js/bootstrap.js @@ -0,0 +1,2027 @@ +/* =================================================== + * bootstrap-transition.js v2.1.1 + * http://twitter.github.com/bootstrap/javascript.html#transitions + * =================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + $(function () { + + "use strict"; // jshint ;_; + + + /* CSS TRANSITION SUPPORT (http://www.modernizr.com/) + * ======================================================= */ + + $.support.transition = (function () { + + var transitionEnd = (function () { + + var el = document.createElement('bootstrap') + , transEndEventNames = { + 'WebkitTransition' : 'webkitTransitionEnd' + , 'MozTransition' : 'transitionend' + , 'OTransition' : 'oTransitionEnd otransitionend' + , 'transition' : 'transitionend' + } + , name + + for (name in transEndEventNames){ + if (el.style[name] !== undefined) { + return transEndEventNames[name] + } + } + + }()) + + return transitionEnd && { + end: transitionEnd + } + + })() + + }) + +}(window.jQuery);/* ========================================================== + * bootstrap-alert.js v2.1.1 + * http://twitter.github.com/bootstrap/javascript.html#alerts + * ========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* ALERT CLASS DEFINITION + * ====================== */ + + var dismiss = '[data-dismiss="alert"]' + , Alert = function (el) { + $(el).on('click', dismiss, this.close) + } + + Alert.prototype.close = function (e) { + var $this = $(this) + , selector = $this.attr('data-target') + , $parent + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + $parent = $(selector) + + e && e.preventDefault() + + $parent.length || ($parent = $this.hasClass('alert') ? $this : $this.parent()) + + $parent.trigger(e = $.Event('close')) + + if (e.isDefaultPrevented()) return + + $parent.removeClass('in') + + function removeElement() { + $parent + .trigger('closed') + .remove() + } + + $.support.transition && $parent.hasClass('fade') ? + $parent.on($.support.transition.end, removeElement) : + removeElement() + } + + + /* ALERT PLUGIN DEFINITION + * ======================= */ + + $.fn.alert = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('alert') + if (!data) $this.data('alert', (data = new Alert(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.alert.Constructor = Alert + + + /* ALERT DATA-API + * ============== */ + + $(function () { + $('body').on('click.alert.data-api', dismiss, Alert.prototype.close) + }) + +}(window.jQuery);/* ============================================================ + * bootstrap-button.js v2.1.1 + * http://twitter.github.com/bootstrap/javascript.html#buttons + * ============================================================ + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* BUTTON PUBLIC CLASS DEFINITION + * ============================== */ + + var Button = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, $.fn.button.defaults, options) + } + + Button.prototype.setState = function (state) { + var d = 'disabled' + , $el = this.$element + , data = $el.data() + , val = $el.is('input') ? 'val' : 'html' + + state = state + 'Text' + data.resetText || $el.data('resetText', $el[val]()) + + $el[val](data[state] || this.options[state]) + + // push to event loop to allow forms to submit + setTimeout(function () { + state == 'loadingText' ? + $el.addClass(d).attr(d, d) : + $el.removeClass(d).removeAttr(d) + }, 0) + } + + Button.prototype.toggle = function () { + var $parent = this.$element.closest('[data-toggle="buttons-radio"]') + + $parent && $parent + .find('.active') + .removeClass('active') + + this.$element.toggleClass('active') + } + + + /* BUTTON PLUGIN DEFINITION + * ======================== */ + + $.fn.button = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('button') + , options = typeof option == 'object' && option + if (!data) $this.data('button', (data = new Button(this, options))) + if (option == 'toggle') data.toggle() + else if (option) data.setState(option) + }) + } + + $.fn.button.defaults = { + loadingText: 'loading...' + } + + $.fn.button.Constructor = Button + + + /* BUTTON DATA-API + * =============== */ + + $(function () { + $('body').on('click.button.data-api', '[data-toggle^=button]', function ( e ) { + var $btn = $(e.target) + if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') + $btn.button('toggle') + }) + }) + +}(window.jQuery);/* ========================================================== + * bootstrap-carousel.js v2.1.1 + * http://twitter.github.com/bootstrap/javascript.html#carousel + * ========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* CAROUSEL CLASS DEFINITION + * ========================= */ + + var Carousel = function (element, options) { + this.$element = $(element) + this.options = options + this.options.slide && this.slide(this.options.slide) + this.options.pause == 'hover' && this.$element + .on('mouseenter', $.proxy(this.pause, this)) + .on('mouseleave', $.proxy(this.cycle, this)) + } + + Carousel.prototype = { + + cycle: function (e) { + if (!e) this.paused = false + this.options.interval + && !this.paused + && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) + return this + } + + , to: function (pos) { + var $active = this.$element.find('.item.active') + , children = $active.parent().children() + , activePos = children.index($active) + , that = this + + if (pos > (children.length - 1) || pos < 0) return + + if (this.sliding) { + return this.$element.one('slid', function () { + that.to(pos) + }) + } + + if (activePos == pos) { + return this.pause().cycle() + } + + return this.slide(pos > activePos ? 'next' : 'prev', $(children[pos])) + } + + , pause: function (e) { + if (!e) this.paused = true + if (this.$element.find('.next, .prev').length && $.support.transition.end) { + this.$element.trigger($.support.transition.end) + this.cycle() + } + clearInterval(this.interval) + this.interval = null + return this + } + + , next: function () { + if (this.sliding) return + return this.slide('next') + } + + , prev: function () { + if (this.sliding) return + return this.slide('prev') + } + + , slide: function (type, next) { + var $active = this.$element.find('.item.active') + , $next = next || $active[type]() + , isCycling = this.interval + , direction = type == 'next' ? 'left' : 'right' + , fallback = type == 'next' ? 'first' : 'last' + , that = this + , e = $.Event('slide', { + relatedTarget: $next[0] + }) + + this.sliding = true + + isCycling && this.pause() + + $next = $next.length ? $next : this.$element.find('.item')[fallback]() + + if ($next.hasClass('active')) return + + if ($.support.transition && this.$element.hasClass('slide')) { + this.$element.trigger(e) + if (e.isDefaultPrevented()) return + $next.addClass(type) + $next[0].offsetWidth // force reflow + $active.addClass(direction) + $next.addClass(direction) + this.$element.one($.support.transition.end, function () { + $next.removeClass([type, direction].join(' ')).addClass('active') + $active.removeClass(['active', direction].join(' ')) + that.sliding = false + setTimeout(function () { that.$element.trigger('slid') }, 0) + }) + } else { + this.$element.trigger(e) + if (e.isDefaultPrevented()) return + $active.removeClass('active') + $next.addClass('active') + this.sliding = false + this.$element.trigger('slid') + } + + isCycling && this.cycle() + + return this + } + + } + + + /* CAROUSEL PLUGIN DEFINITION + * ========================== */ + + $.fn.carousel = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('carousel') + , options = $.extend({}, $.fn.carousel.defaults, typeof option == 'object' && option) + , action = typeof option == 'string' ? option : options.slide + if (!data) $this.data('carousel', (data = new Carousel(this, options))) + if (typeof option == 'number') data.to(option) + else if (action) data[action]() + else if (options.interval) data.cycle() + }) + } + + $.fn.carousel.defaults = { + interval: 5000 + , pause: 'hover' + } + + $.fn.carousel.Constructor = Carousel + + + /* CAROUSEL DATA-API + * ================= */ + + $(function () { + $('body').on('click.carousel.data-api', '[data-slide]', function ( e ) { + var $this = $(this), href + , $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 + , options = !$target.data('modal') && $.extend({}, $target.data(), $this.data()) + $target.carousel(options) + e.preventDefault() + }) + }) + +}(window.jQuery);/* ============================================================= + * bootstrap-collapse.js v2.1.1 + * http://twitter.github.com/bootstrap/javascript.html#collapse + * ============================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* COLLAPSE PUBLIC CLASS DEFINITION + * ================================ */ + + var Collapse = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, $.fn.collapse.defaults, options) + + if (this.options.parent) { + this.$parent = $(this.options.parent) + } + + this.options.toggle && this.toggle() + } + + Collapse.prototype = { + + constructor: Collapse + + , dimension: function () { + var hasWidth = this.$element.hasClass('width') + return hasWidth ? 'width' : 'height' + } + + , show: function () { + var dimension + , scroll + , actives + , hasData + + if (this.transitioning) return + + dimension = this.dimension() + scroll = $.camelCase(['scroll', dimension].join('-')) + actives = this.$parent && this.$parent.find('> .accordion-group > .in') + + if (actives && actives.length) { + hasData = actives.data('collapse') + if (hasData && hasData.transitioning) return + actives.collapse('hide') + hasData || actives.data('collapse', null) + } + + this.$element[dimension](0) + this.transition('addClass', $.Event('show'), 'shown') + $.support.transition && this.$element[dimension](this.$element[0][scroll]) + } + + , hide: function () { + var dimension + if (this.transitioning) return + dimension = this.dimension() + this.reset(this.$element[dimension]()) + this.transition('removeClass', $.Event('hide'), 'hidden') + this.$element[dimension](0) + } + + , reset: function (size) { + var dimension = this.dimension() + + this.$element + .removeClass('collapse') + [dimension](size || 'auto') + [0].offsetWidth + + this.$element[size !== null ? 'addClass' : 'removeClass']('collapse') + + return this + } + + , transition: function (method, startEvent, completeEvent) { + var that = this + , complete = function () { + if (startEvent.type == 'show') that.reset() + that.transitioning = 0 + that.$element.trigger(completeEvent) + } + + this.$element.trigger(startEvent) + + if (startEvent.isDefaultPrevented()) return + + this.transitioning = 1 + + this.$element[method]('in') + + $.support.transition && this.$element.hasClass('collapse') ? + this.$element.one($.support.transition.end, complete) : + complete() + } + + , toggle: function () { + this[this.$element.hasClass('in') ? 'hide' : 'show']() + } + + } + + + /* COLLAPSIBLE PLUGIN DEFINITION + * ============================== */ + + $.fn.collapse = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('collapse') + , options = typeof option == 'object' && option + if (!data) $this.data('collapse', (data = new Collapse(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.collapse.defaults = { + toggle: true + } + + $.fn.collapse.Constructor = Collapse + + + /* COLLAPSIBLE DATA-API + * ==================== */ + + $(function () { + $('body').on('click.collapse.data-api', '[data-toggle=collapse]', function (e) { + var $this = $(this), href + , target = $this.attr('data-target') + || e.preventDefault() + || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 + , option = $(target).data('collapse') ? 'toggle' : $this.data() + $this[$(target).hasClass('in') ? 'addClass' : 'removeClass']('collapsed') + $(target).collapse(option) + }) + }) + +}(window.jQuery);/* ============================================================ + * bootstrap-dropdown.js v2.1.1 + * http://twitter.github.com/bootstrap/javascript.html#dropdowns + * ============================================================ + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* DROPDOWN CLASS DEFINITION + * ========================= */ + + var toggle = '[data-toggle=dropdown]' + , Dropdown = function (element) { + var $el = $(element).on('click.dropdown.data-api', this.toggle) + $('html').on('click.dropdown.data-api', function () { + $el.parent().removeClass('open') + }) + } + + Dropdown.prototype = { + + constructor: Dropdown + + , toggle: function (e) { + var $this = $(this) + , $parent + , isActive + + if ($this.is('.disabled, :disabled')) return + + $parent = getParent($this) + + isActive = $parent.hasClass('open') + + clearMenus() + + if (!isActive) { + $parent.toggleClass('open') + $this.focus() + } + + return false + } + + , keydown: function (e) { + var $this + , $items + , $active + , $parent + , isActive + , index + + if (!/(38|40|27)/.test(e.keyCode)) return + + $this = $(this) + + e.preventDefault() + e.stopPropagation() + + if ($this.is('.disabled, :disabled')) return + + $parent = getParent($this) + + isActive = $parent.hasClass('open') + + if (!isActive || (isActive && e.keyCode == 27)) return $this.click() + + $items = $('[role=menu] li:not(.divider) a', $parent) + + if (!$items.length) return + + index = $items.index($items.filter(':focus')) + + if (e.keyCode == 38 && index > 0) index-- // up + if (e.keyCode == 40 && index < $items.length - 1) index++ // down + if (!~index) index = 0 + + $items + .eq(index) + .focus() + } + + } + + function clearMenus() { + getParent($(toggle)) + .removeClass('open') + } + + function getParent($this) { + var selector = $this.attr('data-target') + , $parent + + if (!selector) { + selector = $this.attr('href') + selector = selector && /#/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + $parent = $(selector) + $parent.length || ($parent = $this.parent()) + + return $parent + } + + + /* DROPDOWN PLUGIN DEFINITION + * ========================== */ + + $.fn.dropdown = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('dropdown') + if (!data) $this.data('dropdown', (data = new Dropdown(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.dropdown.Constructor = Dropdown + + + /* APPLY TO STANDARD DROPDOWN ELEMENTS + * =================================== */ + + $(function () { + $('html') + .on('click.dropdown.data-api touchstart.dropdown.data-api', clearMenus) + $('body') + .on('click.dropdown touchstart.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) + .on('click.dropdown.data-api touchstart.dropdown.data-api' , toggle, Dropdown.prototype.toggle) + .on('keydown.dropdown.data-api touchstart.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown) + }) + +}(window.jQuery);/* ========================================================= + * bootstrap-modal.js v2.1.1 + * http://twitter.github.com/bootstrap/javascript.html#modals + * ========================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================= */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* MODAL CLASS DEFINITION + * ====================== */ + + var Modal = function (element, options) { + this.options = options + this.$element = $(element) + .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this)) + this.options.remote && this.$element.find('.modal-body').load(this.options.remote) + } + + Modal.prototype = { + + constructor: Modal + + , toggle: function () { + return this[!this.isShown ? 'show' : 'hide']() + } + + , show: function () { + var that = this + , e = $.Event('show') + + this.$element.trigger(e) + + if (this.isShown || e.isDefaultPrevented()) return + + $('body').addClass('modal-open') + + this.isShown = true + + this.escape() + + this.backdrop(function () { + var transition = $.support.transition && that.$element.hasClass('fade') + + if (!that.$element.parent().length) { + that.$element.appendTo(document.body) //don't move modals dom position + } + + that.$element + .show() + + if (transition) { + that.$element[0].offsetWidth // force reflow + } + + that.$element + .addClass('in') + .attr('aria-hidden', false) + .focus() + + that.enforceFocus() + + transition ? + that.$element.one($.support.transition.end, function () { that.$element.trigger('shown') }) : + that.$element.trigger('shown') + + }) + } + + , hide: function (e) { + e && e.preventDefault() + + var that = this + + e = $.Event('hide') + + this.$element.trigger(e) + + if (!this.isShown || e.isDefaultPrevented()) return + + this.isShown = false + + $('body').removeClass('modal-open') + + this.escape() + + $(document).off('focusin.modal') + + this.$element + .removeClass('in') + .attr('aria-hidden', true) + + $.support.transition && this.$element.hasClass('fade') ? + this.hideWithTransition() : + this.hideModal() + } + + , enforceFocus: function () { + var that = this + $(document).on('focusin.modal', function (e) { + if (that.$element[0] !== e.target && !that.$element.has(e.target).length) { + that.$element.focus() + } + }) + } + + , escape: function () { + var that = this + if (this.isShown && this.options.keyboard) { + this.$element.on('keyup.dismiss.modal', function ( e ) { + e.which == 27 && that.hide() + }) + } else if (!this.isShown) { + this.$element.off('keyup.dismiss.modal') + } + } + + , hideWithTransition: function () { + var that = this + , timeout = setTimeout(function () { + that.$element.off($.support.transition.end) + that.hideModal() + }, 500) + + this.$element.one($.support.transition.end, function () { + clearTimeout(timeout) + that.hideModal() + }) + } + + , hideModal: function (that) { + this.$element + .hide() + .trigger('hidden') + + this.backdrop() + } + + , removeBackdrop: function () { + this.$backdrop.remove() + this.$backdrop = null + } + + , backdrop: function (callback) { + var that = this + , animate = this.$element.hasClass('fade') ? 'fade' : '' + + if (this.isShown && this.options.backdrop) { + var doAnimate = $.support.transition && animate + + this.$backdrop = $('