Version 1.0: Working with TimeStamper API and GTDServlet.

This commit is contained in:
Jonathan Bernard 2013-09-22 16:38:36 -05:00
parent f00ec0e7a9
commit 1ea1d7d75f
6 changed files with 683 additions and 69 deletions

14
Makefile Normal file
View File

@ -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'

View File

@ -1,66 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<title>What I am Doing</title>
<link rel="stylesheet" href="css/display.css" type="text/css">
<link
href='http://fonts.googleapis.com/css?family=Average+Sans|Quicksand:300,400,700|Oleo+Script+Swash+Caps|Ubuntu|Rationale|Exo:200,400|Cantarell|Jura|Play|Seaweed+Script|Rosario|Bubbler+One|Spinnaker|Advent+Pro'
rel='stylesheet' type='text/css'>
<meta name="viewport" content="width=device-width, user-scalable=no">
</head>
<body>
<section id=current-task>
<h3>Current Activity</h3>
<span class=task>QD Lobby Map: Meeting with JCA.</span>
<div class=task-notes>
<p>Discussed current display procedure, plans for new display.
Thoughts for new display:</p>
<ul>
<li><p>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.</p></li>
<li><p>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.</p>
<p>In other words, does the display show the current week/month or does it show
the last 7/30 days?</p>
<p>The decision was to expirement and see which was a more impressive display.</p></li>
<li><p>I need to investigate the latest version of KML in case there are new
features available that would be useful.</p></li>
</ul>
<p>Other notes:</p>
<ul>
<li>QD is running Google Earth 7.1 (latest Beta)</li>
<li>Chris will send me the Order Tour source code.</li>
<li>We planned to have my old workstation setup and configured with GoToMyPC to
allow access to the QD development environment.</li>
</ul>
</div>
</section>
<section id=priorities>
<h3>Next Actions (unsorted)</h3>
<div class=next-action>
<span class=action>Respond to ELance job proposal.</span>
<span class=date>Mon, 08/05</span>
</div>
<div class=next-action>
<span class=action>Create matching project folder structure in
the <em>done</em> folder as the project folder and move action items
appropriately.</span>
<span class=project><a href=/project/gtd-cli">GTD CLI</a></span>
</div>
<div class=next-action>
<span class=action>Implement category drill down.</span>
<span class=project><a href="/project/time-analyzer">Time analyzer software</a></span>
<span class=details>Double-clicking on a category should show a
graph of sub-items in that category.</span>
</div>
</section>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -110,10 +110,10 @@ body {
font-family: Advent Pro, Rosario, Jura, Average Sans, Cantarell;
margin: 0.5rem 1rem; }
section {
body > 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 } }
}

73
src/www/index.html Normal file
View File

@ -0,0 +1,73 @@
<!DOCTYPE HTML>
<html>
<head>
<title>What I am Doing</title>
<link rel="stylesheet" href="css/personal-display.css" type="text/css">
<link href='//fonts.googleapis.com/css?family=Play|Advent+Pro' rel='stylesheet' type='text/css'>
<!-- PROD Libraries
-->
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.10.0/jquery.min.js" defer></script>
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.4.4/underscore-min.js" defer></script>
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/backbone.js/1.0.0/backbone-min.js" defer></script>
<!-- DEV Libraries
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.10.0/jquery.js" defer></script>
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.4.4/underscore.js" defer></script>
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/backbone.js/1.0.0/backbone.js" defer></script>
-->
<script src="js/personal-display.js" type="text/javascript" defer></script>
<meta name="viewport" content="width=device-width, user-scalable=no">
</head>
<body>
<section id=current-task>
<h3>Current Activity</h3>
<span class=task>Loading...</span>
<div class=task-notes></div>
</section>
<section id=priorities>
<h3>Next Actions (unsorted)</h3>
</section>
<section id=config-dialog>
<form>
<h3>Configuration</h3>
<span class=validate-tips></span>
<section class=timestamper-config>
<span class=config-section-header>TimeStamper</span>
<div><label>Server Name: </label>
<input type=text class=host></div>
<div><label>Username: </label>
<input type=text class=username></div>
<div><label>Password: </label>
<input type=password class=password></div>
<div><label>Timeline: </label>
<select class=timeline>
<option value="none">None</option>
</select></div>
</section>
<section class=gtd-config>
<span class=config-section-header>Getting Things Done</span>
<div><label>Server Name: </label>
<input type=text class=host></div>
<div><label>Username: </label>
<input type=text class=username></div>
<div><label>Password: </label>
<input type=password class=password></div>
<ul> <li><label>Context: </label>
<select class=category>
<option class=default-option value="none">Add a category...</option>
</select></li> </ul>
</section>
<div class=button-panel>
<div class=global-config>
<label>Refresh (sec): </label>
<input type=text class=refresh></div>
<div class=save-button><a href="#">Save and Close</a></div>
</div>
<div class=wait-overlay><img src="img/loading-spinner.gif"><br><span></span></div>
</form>
</section>
</body>
</html>

View File

@ -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(
"<option class=default-option value='none'>" +
"Add a category...</option>");
_.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 = $(
"<li class><span class=remove-button>remove</span>" +
"<span class=category-name></span></li>");
$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);