Skip to content
This repository has been archived by the owner on Nov 7, 2020. It is now read-only.

event templates functionality, for sessions and session messages. #398

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions lib/server-models.js
Original file line number Diff line number Diff line change
Expand Up @@ -979,3 +979,21 @@ exports.ServerChatList = client_models.ChatMessageList.extend({
}
});

exports.ServerEventSessionTemplate = client_models.EventSessionTemplate.extend({
idRoot: "event-session-template",
urlRoot: "event-session-template",
});

exports.ServerEventSessionTemplateList = client_models.EventSessionTemplateList.extend({
model: exports.ServerEventSessionTemplate,
});

exports.ServerEventSessionMessageTemplate = client_models.EventSessionMessageTemplate.extend({
idRoot: "event-session-message-template",
urlRoot: "event-session-message-template",
});

exports.ServerEventSessionMessageTemplateList = client_models.EventSessionMessageTemplateList.extend({
model: exports.ServerEventSessionMessageTemplate,
});

18 changes: 17 additions & 1 deletion lib/unhangout-db.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ _.extend(UnhangoutDb.prototype, events.EventEmitter.prototype, {
this.users.comparator = undefined;
this.events = new models.ServerEventList();
this.permalinkSessions = new models.ServerSessionList();
this.eventSessionTemplates = new models.ServerEventSessionTemplateList();
this.eventSessionMessageTemplates = new models.ServerEventSessionMessageTemplateList();

// Users keep a cache of events they admin. Update this cache on any
// relevant changes to events.
Expand Down Expand Up @@ -82,6 +84,8 @@ _.extend(UnhangoutDb.prototype, events.EventEmitter.prototype, {
counter += event.get("sessions").length;
});
logger.info("Loaded " + counter + " sessions from redis.");
logger.info("loaded " + this.eventSessionTemplates.length + " event session templates from redis.");
logger.info("loaded " + this.eventSessionMessageTemplates.length + " event session message templates from redis.");
this.emit("inited");
}
callback && callback(err);
Expand Down Expand Up @@ -154,7 +158,19 @@ _.extend(UnhangoutDb.prototype, events.EventEmitter.prototype, {
newSession.onRestart();

callback();
}]
}],

["event-session-template/*", function(callback, attrs, key) {
var newEventSessionTemplate = new models.ServerEventSessionTemplate(attrs);
that.eventSessionTemplates.add(newEventSessionTemplate, {sort: false});
callback();
}],

["event-session-message-template/*", function(callback, attrs, key) {
var newEventSessionMessageTemplate = new models.ServerEventSessionMessageTemplate(attrs);
that.eventSessionMessageTemplates.add(newEventSessionMessageTemplate, {sort: false});
callback();
}],
];
// This mess is doing three things:
// 1) figuring out all the key names of all the objects of this type in redis
Expand Down
186 changes: 184 additions & 2 deletions lib/unhangout-routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ var _ = require("underscore"),
googleapis = require('googleapis'),
moment = require("moment-timezone"),
mandrill = require("mandrill-api"),
async = require("async");
async = require("async"),
request = require("request"),
util = require("util"),
extractYoutubeId = require("../public/js/extract-youtube-id");

var ensureAuthenticated = utils.ensureAuthenticated;

Expand Down Expand Up @@ -43,6 +46,183 @@ module.exports = {
});
});

app.get("/event-templates/", utils.ensureAdmin, function(req, res) {
var sessionTemplatesKey = options.UNHANGOUT_SESSION_TEMPLATES_SPREADSHEET;
var sessionMessageTemplatesKey = options.UNHANGOUT_SESSION_MESSAGE_TEMPLATES_SPREADSHEET;
res.render('event-templates.ejs', {
user: req.user,
sessionTemplates: db.eventSessionTemplates,
sessionTemplatesKey: sessionTemplatesKey,
sessionMessageTemplates: db.eventSessionMessageTemplates,
sessionMessageTemplatesKey: sessionMessageTemplatesKey,
});
});

app.get("/event-templates/reload/", utils.ensureSuperuser, function(req, res) {
var sessionTemplatesKey = options.UNHANGOUT_SESSION_TEMPLATES_SPREADSHEET;
var sessionMessageTemplatesKey = options.UNHANGOUT_SESSION_MESSAGE_TEMPLATES_SPREADSHEET;
var spreadsheetDataUrl = "https://spreadsheets.google.com/feeds/cells/%s/default/public/basic?alt=json";

var parseSpreadsheet = function(mapping, data) {
var entry,i,cell,col;
var rows = []

for (var i = 0; i < data.feed.entry.length; i++) {
cell = data.feed.entry[i];
col = cell.title.$t.substring(0, 1);
if (col === 'A') {
entry = {};
rows.push(entry);
}
entry[mapping[col]] = cell.content.$t.trim();
}
// Drop the first row which has the headings.
rows = rows.slice(1);
return rows;
};

var fetchTemplatesFunction = function(spreadsheetKey, mapping) {
if (_.isUndefined(spreadsheetKey)) {
// Return an empty array if no key exists.
return function(cb) { cb(null, []); }
}
else {
return function(callback) {
var url = util.format(spreadsheetDataUrl, spreadsheetKey);
var result = function(error, response, json) {
if (!error && response.statusCode == 200) {
rows = parseSpreadsheet(mapping, json);
callback(null, rows);
}
else {
callback(error);
}
}
var requestParams = {
url: url,
json: true,
};
request(requestParams, result);
}
}
};

var fetchSessionTemplates = function() {
var mapping = {
"A": "title",
"B": "limit",
"C": "type",
"D": "url",
"E": "description"
};
return fetchTemplatesFunction(sessionTemplatesKey, mapping);
};

var fetchSessionMessageTemplates = function() {
var mapping = {
"A": "title",
"B": "message",
};
return fetchTemplatesFunction(sessionMessageTemplatesKey, mapping);
};

var validateTemplates = function(data) {
var errors = [];
for (var i=0; i < data.sessionTemplates.length; i++) {
var template = data.sessionTemplates[i];
var leader = util.format("Session template %s: ", i + 1);
var limit = parseInt(template.limit);
if (_.isNaN(limit) || limit < 2 || limit > 10) {
errors.push(leader + "Participant limit must be a number between 2 and 10");
}
switch (template.type) {
// No validation needed for this type.
case "simple":
break;
case "video":
template.ytid = extractYoutubeId.extractYoutubeId(template.url);
if (!template.ytid) {
errors.push(leader + "Unrecognized youtube URL. Use the URL for a single video.");
}
break;
case "webpage":
if (!/^https:\/\//.test(template.url)) {
errors.push(leader + "Only secure pages (those starting with 'https') can be embedded in hangouts.");
}
break;
default:
errors.push(leader + "Session type must be one of: simple, video, webpage");
}
}

for (var i=0; i < data.sessionMessageTemplates.length; i++) {
var template = data.sessionMessageTemplates[i];
var leader = util.format("Session message template %s: ", i + 1);
// This doesn't seem to be triggered, guessing if the
// first cell in a spreadsheet is blank, it's not
// included in the feed. Leaving here for defensive
// programming.
if (_.isEmpty(template.title)) {
errors.push(leader + "Title cannot be empty");
}
if (_.isEmpty(template.message)) {
errors.push(leader + "Message cannot be empty");
}
}
if (_.isEmpty(errors)) {
saveTemplates(data);
}
else {
_.each(errors, function(e) {
req.flash("danger", e);
});
return res.redirect("event-templates");
}
}

var saveTemplates = function(data) {
// Clear out the old templates, the new set is
// saved fresh.
_.each(_.clone(db.eventSessionTemplates.models), function(model) {
logger.debug("Removing '" + model.get("title") + "' from session templates...");
model.destroy();
});
_.each(_.clone(db.eventSessionMessageTemplates.models), function(model) {
logger.debug("Removing '" + model.get("title") + "' from session message templates...");
model.destroy();
});
_.each(data.sessionTemplates, function(template) {
var newModel = new db.eventSessionTemplates.model(template);
logger.debug("Saving '" + newModel.get("title") + "' to session templates...");
newModel.save();
db.eventSessionTemplates.add(newModel);
});
_.each(data.sessionMessageTemplates, function(template) {
var newModel = new db.eventSessionMessageTemplates.model(template);
logger.debug("Saving '" + newModel.get("title") + "' to session message templates...");
newModel.save();
db.eventSessionMessageTemplates.add(newModel);
});
req.flash("success", "Templates reloaded.");
return res.redirect("event-templates");
}

var templatesFetched = function(err, data) {
if (err) {
return res.send(500, util.format('ERROR: %s', err));
}
else {
validateTemplates(data);
}
}

var templateFetchFunctions = {
sessionTemplates: fetchSessionTemplates(),
sessionMessageTemplates: fetchSessionMessageTemplates(),
};
async.parallel(templateFetchFunctions, templatesFetched);
});

app.get("/about/", function(req, res) {
res.render('about.ejs', {user: req.user});
});
Expand Down Expand Up @@ -73,7 +253,9 @@ module.exports = {
title: event.get("title"),
nodeEnv: process.env.NODE_ENV,
numHangoutUrlsAvailable: farming.getNumHangoutsAvailable(),
numHangoutUrlsWarning: options.UNHANGOUT_HANGOUT_URLS_WARNING
numHangoutUrlsWarning: options.UNHANGOUT_HANGOUT_URLS_WARNING,
eventSessionTemplates: db.eventSessionTemplates,
eventSessionMessageTemplates: db.eventSessionMessageTemplates,
};

var template;
Expand Down
2 changes: 2 additions & 0 deletions lib/unhangout-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ var logging = require('./logging'),
fs = require('fs'),
memwatch = require("memwatch"),
slashes = require("connect-slashes");
flash = require("flash");

// This is the primary class that represents the UnhangoutServer.
// I organize the server pieces into a class so we can more easily
Expand Down Expand Up @@ -199,6 +200,7 @@ exports.UnhangoutServer.prototype = {
store: redisSessionStore,
cookie: {maxAge:1000*60*60*24*2}
}));
this.app.use(flash());

if (this.options.mockAuth) {
var mockPassport = require("./passport-mock");
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"deep-copy": "^1.0.0",
"ejs": "1.0.0",
"express": "3.5.1",
"flash": "1.1.0",
"gapitoken": "0.1.3",
"googleapis": "0.7.0",
"mandrill-api": "1.0.41",
Expand All @@ -56,6 +57,7 @@
"passport": "0.2.0",
"passport-google-oauth": "0.1.5",
"redis": "0.10.1",
"request": "2.55.0",
"requirejs-middleware": "git://github.com/yourcelf/requirejs-middleware.git#master",
"sockjs": "0.3.8",
"stylus": "0.42.3",
Expand Down
15 changes: 15 additions & 0 deletions public/css/screen.styl
Original file line number Diff line number Diff line change
Expand Up @@ -3146,6 +3146,21 @@ user-admin-border-size = 2px
}
}

/*-------------------------- EVENT TEMPLATES -------------------------------*/

.event-template-container {
margin-top: 30px;
}

#create-session-from-template-modal {
#sessions_to_create {
width: 6em;
}
#total_connected_users_reference {
padding-top: 7px;
}
}

/* --------------------- IE OVERRIDES -----------------------*/

/* IE overrides */
Expand Down
4 changes: 3 additions & 1 deletion public/js/event-app.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,9 @@ $(document).ready(function() {
model: curEvent, transport: trans
});
this.dialogView = new eventViews.DialogView({
event: curEvent, transport: trans
event: curEvent,
transport: trans,
eventSessionTemplates: new models.EventSessionTemplateList(EVENT_SESSION_TEMPLATES),
});

this.aboutView = new eventViews.AboutEventView({model: curEvent});
Expand Down
Loading