diff --git a/pavement.py b/pavement.py index ac1213d..087ff97 100644 --- a/pavement.py +++ b/pavement.py @@ -126,6 +126,7 @@ def jslint(): """Run linting on javascript """ files = recursive_glob('*.js', 'scripts', 'planner/static', 'test') + files.remove('planner/static/jquery.dform-1.1.0.min.js') return sh('phantomjs scripts/jslintrunner.js ' + ' '.join(files)) diff --git a/planner/__init__.py b/planner/__init__.py index a1f6d6a..2ab3dc3 100644 --- a/planner/__init__.py +++ b/planner/__init__.py @@ -1,12 +1,21 @@ -from flask import Flask, abort, current_app, render_template, Blueprint +from flask import (Flask, abort, current_app, render_template, + Blueprint, request) +import datetime + +import planner from planner.model.connect import TransactionFactory +from planner.model.client import Client, Contact +from planner.model.iteration import Iteration +from planner.model.translate import to_dict, to_model from planner.flags import Flag from planner.config import HeadConfig +from planner.form_data.add_contact import convert_client_dict_form_json +from planner.form_data.add_iteration import convert_iteration_dict_form_json +import json - -ui = Blueprint('views', __name__, template_folder="templates") -api = Blueprint('api', __name__, template_folder="templates") +ui = Blueprint('views', __name__, template_folder='templates') +api = Blueprint('api', __name__, template_folder='templates') feature = Flag(lambda: abort(404), config=lambda: current_app.config) @@ -30,7 +39,10 @@ def index(): @ui.route('/schedule') @feature def schedule(): - return render_template('schedule.html') + with current_app.transaction() as transaction: + iterations = transaction.query(Iteration).all() + # engagements = transaction.query(Engagement).all() + return render_template('schedule.html', iterations=iterations) @ui.route('/add-engagement') @@ -42,7 +54,17 @@ def add_engagement(): @ui.route('/clients') @feature def clients(): - return render_template("clients.html") + with current_app.transaction() as transaction: + clients = transaction.query(Client).all() + return render_template("clients.html", clients=clients) + + +@ui.route('/clients/', methods=["GET"]) +@feature +def client(client_id): + with current_app.transaction() as transaction: + client = transaction.query(Client).get(client_id) + return render_template("contacts.html", client=client) @ui.route('/add-client') @@ -51,10 +73,48 @@ def add_client(): return render_template("add-client.html") -@ui.route('/add-contact') +@ui.route('/add-iteration') +@feature +def add_iteration(): + form_data = convert_iteration_dict_form_json() + with open("planner/static/add_iteration_form.json", 'w') as f: + json.dump(form_data, f) + return render_template("add-iteration.html") + + +@api.route('/iteration/new', methods=["POST"]) +def save_new_iteration(): + form_data = request.form + iteration = to_model(form_data, planner.model.iteration) + date_string = "".join(iteration.startdate.split("/")) + try: + iteration.startdate = datetime.datetime.strptime(date_string, + "%d%m%Y") + except ValueError: + return render_template("add_iteration_validation_error.html") + with current_app.transaction() as transaction: + transaction.add(iteration) + return schedule() + + +@ui.route('/clients//new') @feature -def add_contact(): - return render_template('add-contact.html') +def add_contact(client_id): + contact = to_dict(Contact(clientid=client_id)) + form_data = convert_client_dict_form_json(contact) + filename = "planner/static/add_contact_form_%d.json" % (client_id) + with open(filename, 'w') as f: + json.dump(form_data, f) + return render_template('add-contact.html', clientid=client_id) + + +@api.route('/clients//add', methods=["POST"]) +def save_new_contact(client_id): + form_data = request.form + contact = to_model(form_data, planner.model.client) + with current_app.transaction() as transaction: + transaction.add(contact) + return client(client_id) @api.route('/api/schedule/iteration-for-engagement') diff --git a/planner/form_data/__init__.py b/planner/form_data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/planner/form_data/add_contact.py b/planner/form_data/add_contact.py new file mode 100644 index 0000000..cc1e0fb --- /dev/null +++ b/planner/form_data/add_contact.py @@ -0,0 +1,62 @@ +def convert_client_dict_form_json(client_dict): + print 'function_called with id %s' % (client_dict["clientid"]) + contact_keys = client_dict.keys() + form_data = {"action": "/clients/%d/add" % (client_dict["clientid"]), + "method": "post", "html": []} + keys_to_exclude = ["clientid", "entity", "id"] + for key in contact_keys: + if key not in keys_to_exclude: + key_formatted = format_key_for_form_caption(key) + key_upper_case = key_formatted.title() + form_data["html"].append({"type": "text", + "caption": key_upper_case, + "name": key}) + form_data["html"] = order_html_elements(form_data["html"]) + form_data["html"].append({"type": "submit", + "value": "Add Contact"}) + form_data["html"].append({"type": "hidden", + "value": "Contact", + "name": "entity"}) + form_data["html"].append({"type": "hidden", + "value": client_dict["clientid"], + "name": "clientid"}) + return form_data + + +def format_key_for_form_caption(key): + splits = ["no", "number", "code"] + if any(string in key for string in splits): + split_on = [string for string in splits if string in key][0] + print key.split(split_on) + key = " ".join(key.split(split_on)) + split_on + if key == "streetname": + key = "Street Name" + return key + + +# this is basically hard coding, so very fragile +def order_html_elements(list_of_form_fields): + ordered_list = [] + names = [entry for entry in list_of_form_fields + if "name" in entry["name"] and + "street" not in entry["name"]] + ordered_list.extend(names) + role = get_specific_entry(list_of_form_fields, "role") + ordered_list.extend(role) + email = get_specific_entry(list_of_form_fields, "email") + email[0]["placeholder"] = "name@example.com" + ordered_list.extend(email) + ordered_list.append({"type": "p", "html": "Telephone information"}) + numbers = get_specific_entry(list_of_form_fields, "no") + ordered_list.extend(numbers) + ordered_list.append({"type": "p", "html": "Address information"}) + street_info = get_specific_entry(list_of_form_fields, "street") + ordered_list.extend(street_info) + post_code = get_specific_entry(list_of_form_fields, "code") + ordered_list.extend(post_code) + return ordered_list + + +def get_specific_entry(list_of_form_fields, keyword): + return [entry for entry in list_of_form_fields + if keyword in entry["name"]] diff --git a/planner/form_data/add_iteration.py b/planner/form_data/add_iteration.py new file mode 100644 index 0000000..bed4243 --- /dev/null +++ b/planner/form_data/add_iteration.py @@ -0,0 +1,12 @@ +def convert_iteration_dict_form_json(): + form_data = {"action": "/iteration/new", + "method": "post", + "html": [{"type": "text", + "caption": "Start Date", + "name": "startdate"}, + {"type": "submit", + "value": "Add Iteration"}]} + form_data["html"].append({"type": "hidden", + "value": "Iteration", + "name": "entity"}) + return form_data diff --git a/planner/model/__init__.py b/planner/model/__init__.py index 619d559..70c276e 100644 --- a/planner/model/__init__.py +++ b/planner/model/__init__.py @@ -1,5 +1,3 @@ -from sqlalchemy import Column, Integer, ForeignKey -from sqlalchemy.orm import relationship from sqlalchemy.ext.declarative import declarative_base @@ -8,24 +6,3 @@ class ValidationError(Exception): Base = declarative_base() - - -class EngagementIteration(Base): - __tablename__ = 'EngagementIteration' - engagementid = Column(Integer, ForeignKey('Engagement.id'), - primary_key=True) - iterationid = Column(Integer, ForeignKey('Iteration.id'), primary_key=True) - - engagement = relationship("Engagement") - iteration = relationship("Iteration") - - -class EstimatedEngagementIteration(Base): - __tablename__ = 'EstimatedEngagementIteration' - engagementid = Column(Integer, ForeignKey('Engagement.id'), - primary_key=True) - iterationid = Column(Integer, ForeignKey('Iteration.id'), - primary_key=True) - - engagement = relationship("Engagement") - iteration = relationship("Iteration") diff --git a/planner/model/engagement.py b/planner/model/engagement.py index 7789b92..e974cde 100644 --- a/planner/model/engagement.py +++ b/planner/model/engagement.py @@ -126,3 +126,24 @@ def validate_value(self, key, address): if address not in [0.1, 0.5, 1.0, 2.0]: raise ValidationError(str(address) + ' is invalid') return address + + +class EngagementIteration(Base): + __tablename__ = 'EngagementIteration' + engagementid = Column(Integer, ForeignKey('Engagement.id'), + primary_key=True) + iterationid = Column(Integer, ForeignKey('Iteration.id'), primary_key=True) + + engagement = relationship("Engagement") + iteration = relationship("Iteration") + + +class EstimatedEngagementIteration(Base): + __tablename__ = 'EstimatedEngagementIteration' + engagementid = Column(Integer, ForeignKey('Engagement.id'), + primary_key=True) + iterationid = Column(Integer, ForeignKey('Iteration.id'), + primary_key=True) + + engagement = relationship("Engagement") + iteration = relationship("Iteration") diff --git a/planner/model/team.py b/planner/model/team.py index 9c71c65..80c916c 100644 --- a/planner/model/team.py +++ b/planner/model/team.py @@ -35,7 +35,7 @@ class TeamMember(Base): def validate_title(self, key, address): if address not in ['Mr', 'Miss', 'Dr', 'Mrs']: raise ValidationError( - str(address) + " is not an appropriate title") + str(address) + " is not an appropriate title") return address @validates('gmail') diff --git a/planner/static/add_iteration.js b/planner/static/add_iteration.js new file mode 100644 index 0000000..dd597a3 --- /dev/null +++ b/planner/static/add_iteration.js @@ -0,0 +1,7 @@ +/*global $, document + */ + +$(document).ready(function () { + "use strict"; + $("#add_iteration").dform("/static/add_iteration_form.json"); +}); diff --git a/planner/static/jquery.dform-1.1.0.min.js b/planner/static/jquery.dform-1.1.0.min.js new file mode 100644 index 0000000..4bc8330 --- /dev/null +++ b/planner/static/jquery.dform-1.1.0.min.js @@ -0,0 +1,2 @@ +/*! jquery.dform 2013-06-04 */ +!function(a){var b={},c={},d=a.each,e=function(b){var c=function(d,e,f){"object"==typeof d?a.each(d,function(a,b){c(a,b,f)}):(void 0===f||f===!0)&&(b[d]||(b[d]=[]),b[d].push(e))};return c},f=a.isArray,g=function(b){return a.map(b,function(a,b){return b})},h=function(a,b){var c={};return d(b,function(b,d){a[d]&&(c[d]=a[d])}),c},i=function(b,c){var e={};return d(b,function(b,d){~a.inArray(b,c)||(e[b]=d)}),e},j=function(c,e,f){return a.dform.hasSubscription(c)&&this.each(function(){var g=a(this);d(b[c],function(a,b){b.call(g,e,f)})}),this},k=function(a){var b=a.type,c=this;return this.dform("run","[pre]",a,b),d(a,function(a,d){c.dform("run",a,d,b)}),this.dform("run","[post]",a,b),this};a.extend(a,{keyset:g,withKeys:h,withoutKeys:i,dform:{options:{prefix:"ui-dform-"},defaultType:function(b){return a("<"+b.type+">").dform("attr",b)},types:function(a){return a?c[a]:c},addType:e(c),subscribers:function(a){return a?b[a]:b},subscribe:e(b),hasSubscription:function(a){return b[a]?!0:!1},createElement:function(b){if(!b.type)throw"No element type given! Must always exist.";var e=b.type,f=null,g=a.withoutKeys(b,["type"]);return c[e]?d(c[e],function(a,b){f=b.call(f,g)}):f=a.dform.defaultType(b),a(f)},methods:{run:function(a,b,c){return"string"!=typeof a?k.call(this,a):j.call(this,a,b,c)},append:function(b,c){c&&a.dform.converters&&a.isFunction(a.dform.converters[c])&&(b=a.dform.converters[c](b));var d=a.dform.createElement(b);this.append(d),d.dform("run",b)},attr:function(c,d){var e=a.keyset(b);f(d)&&a.merge(e,d),this.attr(a.withoutKeys(c,e))},ajax:function(b,c,d){var e={error:d,url:b},f=this;"string"!=typeof b&&a.extend(e,b),e.success=function(a){var d=c||b.success;f.dform(a),d&&d.call(f,a)},a.ajax(e)},init:function(b,c){var d=b.type?b:a.extend({type:"form"},b);c&&a.dform.converters&&a.isFunction(a.dform.converters[c])&&(d=a.dform.converters[c](d)),this.is(d.type)?(this.dform("attr",d),this.dform("run",d)):this.dform("append",d)}}}}),a.fn.dform=function(b,c,d){var e=a(this);return a.dform.methods[b]?a.dform.methods[b].apply(e,Array.prototype.slice.call(arguments,1)):"string"==typeof b?a.dform.methods.ajax.call(e,{url:b,dataType:"json"},c,d):a.dform.methods.init.apply(e,arguments),this}}(jQuery),function(a){var b=a.each,c=function(b,c){return function(d){return a(b).dform("attr",d,c)}},d=function(c){var d=this;a.isPlainObject(c)?d.dform("append",c):a.isArray(c)?b(c,function(a,b){d.dform("append",b)}):d.html(c)};a.dform.addType({container:c("
"),text:c(''),password:c(''),submit:c(''),reset:c(''),hidden:c(''),radio:c(''),checkbox:c(''),file:c(''),number:c(''),url:c(''),tel:c(''),email:c(''),checkboxes:c("
",["name"]),radiobuttons:c("
",["name"])}),a.dform.subscribe({"class":function(a){this.addClass(a)},html:d,elements:d,value:function(a){this.val(a)},css:function(a){this.css(a)},options:function(c,d){var e=this;"select"!==d&&"optgroup"!==d||"string"==typeof c?("checkboxes"===d||"radiobuttons"===d)&&b(c,function(b,c){var f="radiobuttons"===d?{type:"radio"}:{type:"checkbox"};"string"==typeof c?f.caption=c:a.extend(f,c),f.value=b,e.dform("append",f)}):b(c,function(b,c){var d={type:"option",value:b};"string"==typeof c&&(d.html=c),"object"==typeof c&&(d=a.extend(d,c)),e.dform("append",d)})},caption:function(b,c){var d={};if("string"==typeof b?d.html=b:a.extend(d,b),"fieldset"==c)d.type="legend",this.dform("append",d);else{d.type="label",this.attr("id")&&(d["for"]=this.attr("id"));var e=a(a.dform.createElement(d));"checkbox"===c||"radio"===c?this.parent().append(a(e)):e.insertBefore(this),e.dform("run",d)}},type:function(b,c){a.dform.options.prefix&&this.addClass(a.dform.options.prefix+c)},url:function(a){this.dform("ajax",a)},"[post]":function(b,c){if("checkboxes"===c||"radiobuttons"===c){var d="checkboxes"===c?"checkbox":"radio";this.children("[type="+d+"]").each(function(){a(this).attr("name",b.name)})}}})}(jQuery),function(a){var b=function(b,c){return a.withKeys(c,a.keyset(a.ui[b].prototype.options))},c=function(a,b){for(var c=b,d=0;d").dform("attr",c).progressbar(b("progressbar",c))},a.isFunction(a.fn.progressbar)),a.dform.addType("slider",function(c){return a("
").dform("attr",c).slider(b("slider",c))},a.isFunction(a.fn.slider)),a.dform.addType("accordion",function(b){return a("
").dform("attr",b)},a.isFunction(a.fn.accordion)),a.dform.addType("tabs",function(b){return a("
").dform("attr",b)},a.isFunction(a.fn.tabs)),a.dform.subscribe("entries",function(b,c){if("accordion"==c){var d=this;a.each(b,function(b,c){var e=a.extend({type:"div"},c);if(a(d).dform("append",e),c.caption){var f=a(d).children("div:last").prev();f.replaceWith('

'+f.html()+"

")}})}},a.isFunction(a.fn.accordion)),a.dform.subscribe("entries",function(b,c){if("tabs"==c){var d=this;this.append("