From 1ea1d7d75f53f7b8c8226a65db322fd1e1d1bc04 Mon Sep 17 00:00:00 2001 From: Jonathan Bernard Date: Sun, 22 Sep 2013 16:38:36 -0500 Subject: [PATCH] Version 1.0: Working with TimeStamper API and GTDServlet. --- Makefile | 14 + mockup/display.html | 66 --- resources/img/loading-spinner.gif | Bin 0 -> 12961 bytes .../www/css/personal-display.scss | 100 +++- src/www/index.html | 73 +++ src/www/js/personal-display.js | 499 ++++++++++++++++++ 6 files changed, 683 insertions(+), 69 deletions(-) create mode 100644 Makefile delete mode 100644 mockup/display.html create mode 100644 resources/img/loading-spinner.gif rename mockup/css/display.scss => src/www/css/personal-display.scss (62%) create mode 100644 src/www/index.html create mode 100644 src/www/js/personal-display.js diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..eabca30 --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +build : + mkdir -p build/css + cp src/www/*.* build + cp -r src/www/js build + cp -r resources/* build/. + sass src/www/css/personal-display.scss build/css/personal-display.css + tar czf personal-display.tar.gz build + +clean : + rm -r build + +local-deploy: build + cp -r build ~/temp/server + ssh jdb-server 'rm -r ~/public_html/personal-display; mv temp/build ~/public_html/personal-display' diff --git a/mockup/display.html b/mockup/display.html deleted file mode 100644 index e8e257a..0000000 --- a/mockup/display.html +++ /dev/null @@ -1,66 +0,0 @@ - - - - What I am Doing - - - - - -
-

Current Activity

- QD Lobby Map: Meeting with JCA. -
-

Discussed current display procedure, plans for new display. -Thoughts for new display:

- -
    -
  • Predefined window of time (TBD) for the orders to be shown on the map. All -orders displayed will be chosen based on when they were placed.

  • -
  • Discussed whether we wanted to have a periodic refresh, meaning we would -blank the display at the beginning of the week/month and let it fill back in -as orders come in, or keep a rolling refresh, meaning we would drop orders -from the display when they are more than a week/month old.

    - -

    In other words, does the display show the current week/month or does it show -the last 7/30 days?

    - -

    The decision was to expirement and see which was a more impressive display.

  • -
  • I need to investigate the latest version of KML in case there are new -features available that would be useful.

  • -
- - -

Other notes:

- -
    -
  • QD is running Google Earth 7.1 (latest Beta)
  • -
  • Chris will send me the Order Tour source code.
  • -
  • We planned to have my old workstation setup and configured with GoToMyPC to -allow access to the QD development environment.
  • -
-
-
-
-

Next Actions (unsorted)

-
- Respond to ELance job proposal. - Mon, 08/05 -
-
- Create matching project folder structure in - the done folder as the project folder and move action items - appropriately. - GTD CLI -
-
- Implement category drill down. - Time analyzer software - Double-clicking on a category should show a - graph of sub-items in that category. -
-
- - diff --git a/resources/img/loading-spinner.gif b/resources/img/loading-spinner.gif new file mode 100644 index 0000000000000000000000000000000000000000..1f1577cb9c35b1bbed087d2317bc8f0fc381fbaa GIT binary patch literal 12961 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 section { padding: 0.2rem; - & > h3 { + h3 { border-bottom: solid 2px $accent1; color: $accent2; font-family: Play, Jura, Exo, Rationale, Quicksand, Average Sans, sans-serif; @@ -131,13 +131,19 @@ section { .next-action { margin-bottom: 0.5rem; + span { display: none; } + + .action { display: inline-block; } + .date, .project { background: $bgColor2; border-radius: 5px; color: $mutedFgColor; + display: inline-block; font-size: 66%; + margin: 0 0.5rem; max-width: 33%; - padding: 0 0.5em; + padding: 0 0.5rem; white-space: nowrap; } .details { @@ -147,12 +153,100 @@ section { .project:before { content: 'Project: '; + display: inline-block; font-family: Play; font-variant: small-caps; } .date:before { content: 'Due: '; + display: inline-block; font-family: Play; font-variant: small-caps; } } } + +#config-dialog { + display: none; + + background: rgba(0, 0, 0, 0.5); + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + + form { + border: solid thin $accent1; + background: $bgColor; + border-radius: 10px; + margin-left: 10%; + margin-right: 10%; + margin-top: 1em; + padding: 0 0.5em; + position: relative; + width: 80%; + max-width: 32em; + + .validate-tips { display: block; } + + .wait-overlay { + display: none; + + background: rgba(0, 0, 0, 0.8); + padding: 1em 0.5em; + position: absolute; + text-align: center; + + top: 0; + bottom:0; + left: 0; + right: 0; } + + .button-panel { + padding: 0.5em; + text-align: right; + width: 100%; + + .global-config { float: left; } + + .save-button { + border: $accent2 solid thin; + border-radius: 5px; + display: inline-block; + padding: 0.1em 0.3em; } } } + + .config-section-header { color: $accent2; } + + label { + display: inline-block; + width: 6rem; } + + input, select { width: 8rem; } + + .timestamper-config, .gtd-config { + vertical-align: top; + display: inline-block; + width: 15em; } + + .category-name { + display: inline-block; + margin-left: 0.2rem; + width: 8rem; } + + .remove-button { + color: $mutedFgColor; + cursor: pointer; + display: inline-block; + margin-left: 2rem; + width: 4rem; } + + ul { + list-style: none; + margin: 0; + padding: 0; + + li { + display-style: block; + margin: 0; + padding: 0 } } +} diff --git a/src/www/index.html b/src/www/index.html new file mode 100644 index 0000000..455647c --- /dev/null +++ b/src/www/index.html @@ -0,0 +1,73 @@ + + + + What I am Doing + + + + + + + + + + + + + + + +
+

Current Activity

+ Loading... +
+
+
+

Next Actions (unsorted)

+
+
+
+

Configuration

+ +
+ TimeStamper +
+
+
+
+
+
+
+
+
+
+ Getting Things Done +
+
+
+
+
+
+
  • +
+
+
+
+ +
+ +
+

+
+
+ + diff --git a/src/www/js/personal-display.js b/src/www/js/personal-display.js new file mode 100644 index 0000000..b930dd1 --- /dev/null +++ b/src/www/js/personal-display.js @@ -0,0 +1,499 @@ +(function() { + var root = this; + + var PD = root.PersonalDisplay = {}; + + PD.hasHTML5LocalStorage = function() { + try { + return 'localStorage' in window && window['localStorage'] !== null; } + catch (e) { return false; } } + + /// ## Models + + PD.TimelineMarkModel = Backbone.Model.extend({ + initialize: function() { _.bindAll(this, "equals"); }, + equals: function(that) { return this.id == that.id; } + }); + + PD.GTDEntryModel = Backbone.Model.extend({ + initialize: function() { _.bindAll(this, "equals"); }, + equals: function(that) { return this.id == that.id; } + }); + + // ## Views + + PD.CurrentActivityView = Backbone.View.extend({ + el: $("#current-task"), + + initialize: function() { + _.bindAll(this, "render"); + + this.model.on('change', this.render, this); }, + + render: function() { + this.$el.find(".task").text(this.model.get("mark")); + this.$el.find(".task-notes").text(this.model.get("notes")); + return this; + } + }); + + PD.GTDNextActionView = Backbone.View.extend({ + className: "next-action", + tagName: "div", + ignoredProperties: ["action", "details", "id"], + + initialize: function() { + _.bindAll(this, "render"); + $("#priorities").append(this.el); + + this.model.on('change', this.render, this); }, + + render: function() { + var elements = []; + + // Look for the "action" property first + var actionEl = $(document.createElement("span")) + .addClass("action") + actionEl.text(this.model.get("action").toString()); + elements.push(actionEl); + + // Add span elements for each of the other attributes. + _.each(this.model.attributes, function(val, key) { + if (!_.contains(this.ignoredProperties, key)) { + var el = $(document.createElement("span")) + .addClass(key.toString()); + el.text(val); + elements.push(el); } }, this); + + // Finally, look for the "details" property + if (this.model.get("details")) { + var detailEl = $(document.createElement("span")) + .addClass("details"); + detailEl.text(this.model.get("details").toString()); + elements.push(detailEl); } + + // Clear the old data and add our new elements in order + this.$el.empty(); + _.each(elements, function(el) { this.$el.append(el); }, this); + + return this; + } + }); + + PD.GTDNextActionCollection = Backbone.Collection.extend({ + model: PD.GTDEntryModel, + + initialize: function() { + _.bindAll(this, "addNextActionView", "removeNextActionView"); + this.views = {}; + this.on('add', this.addNextActionView); + this.on('remove', this.removeNextActionView); }, + + addNextActionView: function(entryModel) { + var view = new PD.GTDNextActionView({model: entryModel}); + view.render(); + this.views[entryModel.get("id")] = view; }, + + removeNextActionView: function(entryModel) { + var view = this.views[entryModel.get("id")]; + view.$el.remove(); } + }); + + PD.ConfigDialog = Backbone.View.extend({ + el: $("#config-dialog"), + + events: { + "blur .timestamper-config .password" : "tsLogin", + "blur .gtd-config .password" : "gtdLogin", + + "change .gtd-config .category" : "addCategory", + "click .remove-button" : "removeCategory", + + "click .save-button" : "saveAndClose" }, + + initialize: function() { + _.bindAll(this, "show", "hide", "tsLogin", "gtdLogin", + "loadTsData", "loadGtdData", "addCategory", "makeCategoryItem", + "removeCategory", "saveAndClose"); }, + + show: function() { + var $tsSection = this.$el.find(".timestamper-config"); + var $gtdSection = this.$el.find(".gtd-config"); + + // Load TimeStamper configuration values. + if (PD.tsCfg) { + $tsSection.find(".username").val(PD.tsCfg.username); + $tsSection.find(".password").val(PD.tsCfg.password); + $tsSection.find(".host").val(PD.tsCfg.host); + + if (PD.tsAuth) { + this.loadTsData(PD.tsCfg.host, PD.tsCfg.username); } + + this.$el.find('.timeline').val(PD.tsCfg.timelineId); } + + // Or suggest a default server. + else { $tsSection.find(".host").val("timestamper.jdb-labs.com"); } + + // Load GTD configuration values. + if (PD.gtdCfg) { + $gtdSection.find(".username").val(PD.gtdCfg.username); + $gtdSection.find(".password").val(PD.gtdCfg.password); + $gtdSection.find(".host").val(PD.gtdCfg.host); + + if (PD.gtdAuth) { this.loadGtdData(PD.gtdCfg.host); } + + // Create the items for the selected categories + $(".category-name").parent().remove(); + _.forEach(PD.gtdCfg.categories, this.makeCategoryItem); } + + this.$el.find('.refresh').val( + PD.refreshPeriod ? PD.refreshPeriod / 1000 : 15); + + this.$el.fadeIn(); }, + + hide: function() { this.$el.fadeOut(); }, + + tsLogin: function() { + var username = this.$el.find(".timestamper-config .username").val(); + var password = this.$el.find(".timestamper-config .password").val(); + var host = this.$el.find(".timestamper-config .host").val(); + + if (!PD.tsCfg) { PD.tsCfg = {}; } + + // Hide the configuration dialog. + this.$el.find(".wait-overlay span").text("Connecting to " + host); + this.$el.find(".wait-overlay").fadeIn(); + + // Try to log in to the TimeStamper service. + $.ajax({ + url: "https://" + host + "/ts_api/login", + xhrFields: { withCredentials: true }, + processData: false, + type: 'POST', + async: false, + + data: JSON.stringify( + {"username": username, "password": password}), + + error: function(jqXHR, textStatus, error) { + if (jqXHR.status == 401) { $(".validate-tips") + .text("Invalid username/password combination for " + + "the TimeStamper service."); } + else { $(".validate-tips").text("There was an error " + + "trying to log into the TimeStamper service: " + + error); } + PD.tsAuth = false; }, + + success: function(data, textStatus, jqXHR) { + PD.tsAuth = true; + + $(".validate-tips").text(""); + + // Load the user's timelines. + PD.configDialog.loadTsData(host, username); + } + }); + + // Success or failure we hide the wait overlay. + this.$el.find(".wait-overlay").fadeOut(); + + }, + + gtdLogin: function() { + var username = this.$el.find(".gtd-config .username").val(); + var password = this.$el.find(".gtd-config .password").val(); + var host = this.$el.find(".gtd-config .host").val(); + + if (!PD.gtdCfg) { PD.gtdCfg = {}; } + + // Hide the configuration dialog. + this.$el.find(".wait-overlay span").text("Connecting to " + host); + this.$el.find(".wait-overlay").fadeIn(); + + // Try to log in to the GTD service. + $.ajax({ + url: "http://" + host + "/gtd/login", + xhrFields: { withCredentials: true }, + processData: false, + type: 'POST', + async: false, + + data: JSON.stringify( + {"username": username, "password": password}), + + error: function(jqXHR, textStatus, error) { + if (jqXHR.status == 401) { $(".validate-tips") + .text("Invalid username/password combination for " + + "the Getting Things Done service."); } + else { $(".validate-tips").text("There was an error " + + "trying to log into the Getting Things Done service: " + + error); } + PD.gtdAuth = false; }, + + success: function(data, textStatus, jqXHR) { + PD.gtdAuth = true; + + $(".validate-tips").text(""); + + PD.configDialog.loadGtdData(host); } + }); + + this.$el.find(".wait-overlay").fadeOut(); + }, + + loadTsData: function(host, username) { + // (Re)load the user's timelines. + PD.tsCfg.timelines = JSON.parse($.ajax({ + url: 'https://' + host + '/ts_api/timelines/' + username, + xhrFields: { withCredentials: true }, + async: false}).responseText); + + // Populate the available timelines list. + var $timelineSelectEl = this.$el.find(".timestamper-config .timeline"); + $timelineSelectEl.empty(); + _.forEach(PD.tsCfg.timelines, function(timeline) { + var $optionEl = $(document.createElement("option")); + $optionEl.attr("value", timeline.id); + $optionEl.text(timeline.description); + $timelineSelectEl.append($optionEl); }); }, + + loadGtdData: function(host) { + // Load the user's contexts + PD.gtdCfg.contexts = JSON.parse($.ajax({ + url: 'http://' + host + '/gtd/contexts', + xhrFields: { withCredentials: true }, + async: false }).responseText); + + // Load the user's projects + PD.gtdCfg.projects = JSON.parse($.ajax({ + url: 'http://' + host + '/gtd/projects', + xhrFields: { withCredentials: true }, + async: false }).responseText); + + // Populate the available contexts and projects drop-down. + var $categorySelectEl = $(".gtd-config .category") + $categorySelectEl.empty(); + $categorySelectEl.append( + ""); + _.forEach(PD.gtdCfg.contexts.concat(PD.gtdCfg.projects), + function(category) { + var $optionEl = $(document.createElement("option")); + $optionEl.attr("value", category.id); + $optionEl.text(category.id); + $categorySelectEl.append($optionEl); }); + $categorySelectEl[0].selectedIndex = 0; }, + + makeCategoryItem: function(catName) { + var $liEl = $( + "
  • remove" + + "
  • "); + $liEl.find('.category-name').text(catName); + this.$el.find(".gtd-config ul").append($liEl); }, + + addCategory: function(source) { + var selectEl = source.target; + var $selectEl = $(selectEl); + if (selectEl.selectedIndex == 0) { return; } + this.makeCategoryItem($selectEl.val()); + selectEl.selectedIndex = 0; }, + + removeCategory: function(source) { + $(source.target).parent().remove(); }, + + saveAndClose: function() { + if (!PD.tsCfg) { PD.tsCfg = {}; } + if (!PD.gtdCfg) { PD.gtdCfg = {}; } + + // Save TimeStamper configuration. + var $tsEl = this.$el.find(".timestamper-config"); + PD.tsCfg.host = $tsEl.find(".host").val(); + PD.tsCfg.username = $tsEl.find(".username").val(); + PD.tsCfg.password = $tsEl.find(".password").val(); + PD.tsCfg.timelineId = $tsEl.find(".timeline").val(); + + // Save Getting Things Done configuration. + var $gtdEl = this.$el.find(".gtd-config"); + PD.gtdCfg.host = $gtdEl.find(".host").val(); + PD.gtdCfg.username = $gtdEl.find(".username").val(); + PD.gtdCfg.password = $gtdEl.find(".password").val(); + PD.gtdCfg.categories = _.map( + this.$el.find(".category-name"), + function(span) { return $(span).text(); }); + + // Save global data + PD.refreshPeriod = parseInt(this.$el.find(".refresh").val()) * 1000; + + if (PD.hasHTML5LocalStorage()) { + localStorage.setItem("tsCfg", JSON.stringify(PD.tsCfg)); + localStorage.setItem("gtdCfg", JSON.stringify(PD.gtdCfg)); + localStorage.setItem("refreshPeriod", + JSON.stringify(PD.refreshPeriod)); } + + this.hide(); + } + }); + + PD.Main = Backbone.View.extend({ + el: $("body"), + + initialize: function() { + + _.bindAll(this, "refresh"); + + // Create our config dialog view. + PD.configDialog = new PD.ConfigDialog(); + + // Create our initial models and views. + PD.currentActivityModel = new PD.TimelineMarkModel({}); + PD.currentActivityView = new PD.CurrentActivityView( + {model: PD.currentActivityModel}) + + // Test for localStorage support + if (!PD.hasHTML5LocalStorage()) { + alert("Your browser does not support HTML5 localStorage." + + "Without this I cannot store your preferences."); + PD.configDialog.show(); } + else { + PD.tsCfg = JSON.parse(localStorage.getItem('tsCfg')); + PD.gtdCfg = JSON.parse(localStorage.getItem('gtdCfg')); + PD.refreshPeriod = JSON.parse( + localStorage.getItem('refreshPeriod')); } + + PD.gtdNextActionCollection = new PD.GTDNextActionCollection(); + + // Perform the initial refresh. + this.refresh(); + + // Schedule future refreshes. + setInterval(this.refresh, PD.refreshPeriod ? PD.refreshPeriod : 15000); + }, + + refresh: function() { + // If the dialog is still open we skip this sync to give the user + // a chance to finish configuration. + if ($("#config-dialog").is(":visible")) { return; } + + // Otherwise, if we do not have configuration information, open the + // dialog so the user can enter it. + if (!(PD.tsCfg && PD.gtdCfg)) { PD.configDialog.show(); return; } + + // Check that we are authenticated to the services we need. Try to + // authenticate if we are not. + if (!PD.tsAuth) { + $.ajax({ + url: "https://" + PD.tsCfg.host + "/ts_api/login", + xhrFields: { withCredentials: true }, + processData: false, + type: "POST", + async: false, + + data: JSON.stringify( + { "username": PD.tsCfg.username, + "password": PD.tsCfg.password }), + + error: function(jqXHR, textStatus, error) { + // TODO: Handle error. + PD.tsAuth=false; + alert("Unable to authenticate to the TimeStamper " + + "service: " + error); + PD.configDialog.show(); }, + + success: function(data, textStatus, jqXHR) { + PD.tsAuth = true; }}); } + + if (!PD.gtdAuth) { + $.ajax({ + url: "http://" + PD.gtdCfg.host + "/gtd/login", + xhrFields: { withCredentials: true }, + processData: false, + type: "POST", + async: false, + + data: JSON.stringify( + { "username": PD.gtdCfg.username, + "password": PD.gtdCfg.password }), + + error: function(jqXHR, textStatus, error) { + // TODO: Handle error. + PD.gtdAuth=false; + alert("Unable to authenticate to the GTD service: " + + error); + PD.configDialog.show(); }, + + success: function(data, textStatus, jqXHR) { + PD.gtdAuth = true; }}); } + + + // Check that we have successfully authenticated to both services. + // If we are not, we will skip this refresh. + if (!(PD.tsAuth && PD.gtdAuth)) { return; } + + // Get the latest timestamp from the TimeStamper service. + $.ajax({ + url: "https://" + PD.tsCfg.host + "/ts_api/entries/" + + PD.tsCfg.username + "/" + PD.tsCfg.timelineId, + xhrFields: { withCredentials: true }, + data: {"order": "asc" }, + dataType: 'json', + type: 'GET', + async: true, + + error: function(jqXHR, textStatus, errorText) { + if (jqXHR.status == 401) { PD.tsAuth = false; } + else { + alert("Unable to retrieve current timestamp: " + errorText); + PD.configDialog.show(); } + }, + + success: function(data, textStatus, jqXHR) { + PD.currentActivityModel.set(data[0]); } + }); + + // Get the list of GTD entries for each of our categories. + var categories = _.reduce( + PD.gtdCfg.categories, + function(acc, cat) { return acc ? acc + "," + cat : cat; }, ""); + + $.ajax({ + url: "http://" + PD.gtdCfg.host + "/gtd/next-actions/" + + categories, + xhrFields: { withCredentials: true }, + dataType: 'json', + type: 'GET', + async: true, + + error: function(jqXHR, textStatus, errorText) { + if (jqXHR.status == 401) { PD.gtdAtuh = false; } + else if (jqXHR.status == 500) { return; } + else { + alert("Unable to retrieve next actions: " + errorText); + PD.configDialog.show(); } + }, + + success: function(data, textStatus, jqXHR) { + var collection = PD.gtdNextActionCollection; + + // Add all the retrieved items to the collection. + _.forEach(data, function(actionAttr) { + + // Try to find this entry in out collection. + var model = collection.get(actionAttr.id); + // Update it if found + if (model) { model.set(actionAttr); } + // Insert a new model if not found. + else { collection.add( + new PD.GTDEntryModel(actionAttr)); }}); + + // Look through our collection for entries that are no + // longer in our retrieved data and remove them. + collection.forEach(function(model) { + if (!_.any(data, model.equals)) { + collection.remove(model); }}); + } + }); + } + }); + + PD.main = new PD.Main(); +}).call(this);