Skip to content

Instantly share code, notes, and snippets.

@traderd65
Last active November 27, 2021 18:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save traderd65/8bec2bff6d2b949620360d24e6ca5e71 to your computer and use it in GitHub Desktop.
Save traderd65/8bec2bff6d2b949620360d24e6ca5e71 to your computer and use it in GitHub Desktop.
Cloudinary Style Transfer Contest App
Follows the CodePen setup:
13. If not using the “faas” account (recommended):
a. Add style-transfer add-on
b. Enable unsigned uploads
c. Create the upload preset ‘styletransfer” with tag “styletransfer”
“use filename” = false
disallow public_id = true
allowed formats = “jpg, gif, png, jpeg, webp”
type = “upload”
access mode = “public”
format = “jpg”
incoming transformation = “c_fill,g_center,h_800,w_800”
14. Update the cloud name in the webtask to the new cloud name, and references to “faas” in the code:
const cloudinary_cloudname = 'faas';
15. Add the four starter images from the faas account to kick off the CodePen:
https://res.cloudinary.com/faas/image/upload/v1503415275/coffee_cup.jpg
https://res.cloudinary.com/faas/image/upload/v1503415299/sailing_angel.jpg
https://res.cloudinary.com/faas/image/upload/v1503415323/coffee_cup_st.jpg
http://res.cloudinary.com/faas/image/upload/v1503522957/style-transfer-loading.svg
16. Update the three CodePen image URLs to the new cloud account images in the HTML panel to new values as needed:
https://res.cloudinary.com/faas/image/upload/w_200,h_200,c_fill,dpr_2.0/sailing_angel.jpg ...
Follows the Webtask setup:
9. Fork the CodePen: https://codepen.io/cloudinary/pen/LjqJEG
10. Scroll to the bottom of the CodePen JS panel. Update the “webtask_url” variable to the webtask created above.
You’ll likely have to change only the container value if you haven’t renamed the webtask (i.e. style-transfer)
const webtask_url = "https://faas-cloudinary.com/wt-...-0/style-transfer";
11. Update the webtask to point to the new CodePen pen (shows the original URL here for illustration):
const url_codepen = 'https://codepen.io/cloudinary/pen/LjqJEG';
12. Update the webtask to point to the new CodePen CSS:
In the new CodePen, select the “Change View” button in the upper right of the page.
Select “.css” link in the “Direct Code Links” section in the dropdown that appears.
Copy the resulting URL, and update the url_stylecss value with this new URL value:
const url_stylecss = 'https://codepen.io/cloudinary/pen/LjqJEG.css';
After setting up Webtask, CodePen and Cloudinary:
-------------------------------------------------
* Launch the new CodePen pen
* Upload two images, generate a style image and submit it to the contest.
* Click through the view and the vote URLs. Confirm both open correctly.
* Check the webtask storage Confirm your entry shows in the entries list.
* Vote on an entry in the vote page. Check the webtask storage and refresh it. Confirm your vote shows at the bottom.
* Download the data dump. Download the marketing CSV. Are the secrets set correctly?
In case of a need to reset data:
--------------------------------
* Download the data dump and edit as needed.
* Open the webtask. Open the Storage panel.
* Hit the Refresh link at top of the panel.
* Paste the updated data into the text box.
* Save the new data with the Update button.
The content has three moving parts:
-----------------------------------
* CodePen pen (https://codepen.io/cloudinary/pen/LjqJEG)
* webtask.io webtask (https://{custom-domain}/{webtask-container}/style-transfer)
* Cloudinary account
Webtask code:
-------------
https://webtask.it.auth0.com/edit/{container-name}#webtaskName=style-transfer&token={login-token}
Note that the login token provides full access to all webtasks of the account!
Cloudinary account:
-------------------
Cloud name: faas (set cloud admin flag <Unsigned Add-Ons> in console to enable style transfer without signed URLs)
Account Email: cloudinary@mackenzieking.co (grace account with higher limit of allowed style transfer operations)
Application pages:
------------------
* Stylized image generation and submission page
* Submission listing and voting page
The styled image URL generation and submission page is hosted on CodePen. The CodePen pen has the front end HTML/CSS/JS, which displays the source, target, and styled images, and calls the Cloudinary image uploader. From this page, once images have been uploaded and styled, the pen visitor can submit the image to the webtask.io webtask backend, which returns a voting URL.
The webtask.io webtask has the backend ExpressJS code, which generates and displays the submission listing page, displaying all the stylized image entries and their source/target images. If a URL going to the webtask includes a valid image reference token, the referenced image will display first on the page. If the URL includes a valid voting token, voting buttons will display on each of the entries that are not the user’s own entry. The webtask is an ExpressJS app and uses the CodePen CSS.
URL format: https://faas-cloudinary.com/{webtask-container}/style-transfer/vote/{view-token}?vote={vote-token}
Custom domain https://faas-cloudinary.com - registered on namecheap, hosted on cloudflare, points to auth0.com webtask.io servers (refer to Webtask documentation for setting up DNS records and pointing to CloudFlare nameservers for the domain)
webtask-container - the container from the webtask.io profile (created via “wt init”, found with “wt ls --profile {user-profile}”
style-transfer - the webtask taskname, configurable
'use latest';
// this is the official webtask end point
var namor = require('namor');
var request_send = require('request');
// Confs
const cloudinary_cloudname = 'faas';
const webtask_name = 'style-transfer';
const webtask_container = 'wt-60a287cd40c53f6e56bd60ac8922bc3e-0';
const start_date = new Date('2017-09-23T00:00:00').getTime();
const end_date = new Date('2017-09-30T00:00:00').getTime();
// URLs
const url_cloudinarylogo = 'https://cloudinary-res.cloudinary.com/image/asset/dpr_2.0/logo-e0df892053afd966cc0bfe047ba93ca4.png';
const url_codepen = 'https://codepen.io/cloudinary/full/LjqJEG';
const url_stylecss = url_codepen.replace(/full/, 'pen') + '.css';
const url_webtask_base = `https://faas-cloudinary.com/${webtask_container}/${webtask_name}`;
const url_votelink = `${url_webtask_base}/vote`;
/*
*** STORAGE FORMAT ***
{
"entries" : {
"voting-token" {
"ip", // ??? unique user ID or just for logging ???
"date", // submission date
"url", // submitted image url
"email", // email address
"marketing",// accepts marketing
"vote" // vote for best
}
},
// Denormalize the storage to make searching easier.
// Can be removed if storage space becomes an issue. (??? why double fields ???)
"votes" : {
"view-token" : "view-token-vote",
"view-token" : "view-token-vote"
},
"images": {
"url":"view-token",
"url":"view-token"
},
"tallies": {
"view-token": vote_count,
"view-token": vote_count
}
}
*/
import express from 'express';
import { fromExpress } from 'webtask-tools';
import bodyParser from 'body-parser';
const app = express();
app.use(bodyParser.json());
// top level should go to entries page
app.get('/', (req, res) => {
res.redirect(url_webtask_base + '/view/no-token');
});
// Quick start date and end date display
app.get('/date/start', (req, res) => {
res.set('Content-Type', 'text/html'); res.status(200).send(displayDate(start_date));
});
app.get('/date/end', (req, res) => {
res.set('Content-Type', 'text/html'); res.status(200).send(displayDate(end_date));
});
// view entries, even if you can't vote
app.get(['/vote', '/view'], (req, res) => {
res.redirect(url_webtask_base + '/view/no-token');
});
// vote listing page of url format .../vote/votetoken
app.get(['/view/:tokenid', '/vote/:tokenid'], (req, res) => {
var image_list = '';
var top_message = '';
var voting_allowed = false;
var viewtoken = req.params.tokenid;
var votetoken = req.query.vote ? req.query.vote : '';
if (Date.now() < start_date) {
top_message = '<div class="error-message">Voting has not opened yet.</div>';
}
if (Date.now() > end_date) {
top_message = '<div class="error-message">Voting has ended.</div>';
}
var view_order = req.webtaskContext.query.view ? req.webtaskContext.query.view : 'random';
view_order = (['random', 'recent', 'reverse', 'leader'].indexOf(view_order) > -1) ? view_order : 'random';
req.webtaskContext.storage.get(function(error, data) {
if (error) {
image_list = 'Unable to find image list';
} else {
image_list = renderVoteList(data, viewtoken, votetoken, view_order);
}
image_list += renderVoteFlash({});
if (data.entries[viewtoken] && (data.entries[viewtoken]['vote-token'] === votetoken) && (Date.now() > start_date) && (Date.now() < end_date)) {
voting_allowed = true;
}
var voting_script = (voting_allowed) ? renderVotingScript({ viewtoken: viewtoken, votetoken: votetoken }) : '';
// TODO: integrate this into rendering functions below
var voting_header = '<div id="vote-header"><h4><a href="?view=random&vote=' + votetoken + '" id="vote-random" class="oh-yes vote-header ' + ((view_order === 'random') ? 'selected' : '') + '" >Random</a></h4><h4><a href="?view=recent&vote=' + votetoken + '" id="vote-recent" class="oh-yes vote-header ' + ((view_order === 'recent') ? 'selected' : '') + '">Recent</a></h4><h4><a href="?view=reverse&vote=' + votetoken + '" id="vote-reverse" class="oh-yes vote-header ' + ((view_order === 'reverse') ? 'selected' : '') + '">Oldest</a></h4><h4><a href="?view=leader&vote=' + votetoken + '" id="vote-leader" class="oh-yes vote-header ' + ((view_order === 'leader') ? 'selected' : '') + '">Leaderboard</a></h4></div>';
const HTML = renderView({
title: 'Cloudinary Style Transfer Submissions',
top_message: top_message,
body: voting_header + '<div id="image-vote-list">' + image_list + '</div>',
scripts: voting_script
});
res.set('Content-Type', 'text/html');
res.status(200).send(HTML);
});
});
// accept an incoming vote POST
app.post('/vote', (req, res) => {
function cb(ret) {
if (!ret.error) { ret.error = ''; }
res.json(ret);
}
let viewtoken = (req.body.view) ? req.body.view.trim() : "";
var votetoken = (req.body.token) ? req.body.token.trim() : "";
var voteurl = (req.body.vote) ? req.body.vote.trim() : "";
if (viewtoken === "") {
return cb({error: "Token missing, unable to vote"});
}
if (votetoken === "") {
return cb({error: "Token missing, unable to vote."})
}
if (voteurl === "") {
return cb({error: "No vote cast. What's up?"})
}
if (Date.now() > end_date) {
return cb({error: 'Voting has ended.'});
}
if (Date.now() < start_date) {
return cb({error: 'Ooops. Voting has not started.'})
}
req.webtaskContext.storage.get(function (error, data) {
if (error) return cb(error);
data = data || { entries : {}, images : {}, votes: {}, tallies: {} };
// see if valid token
if (!data.entries[viewtoken]) {
return cb({"error" : "Ooops! Can't confirm voting eligibility!"});
}
if (data.entries[viewtoken]['vote-token'] !== votetoken) {
return cb({"error" : "Ooops! Can't confirm voting eligibility!!"});
}
// see if valid vote
if (!data.images[voteurl]) {
return cb({"error" : "Ooops! Can't find vote image!"});
}
// remove previous vote in tallies if exists
if (data.entries[viewtoken].vote) {
data.tallies[data.entries[viewtoken].vote] -= 1;
}
// cast the vote in the entries (this is canonical)
data.entries[viewtoken].vote = data.images[voteurl];
// list of votes to make tallying easier
data.votes[viewtoken] = data.images[voteurl];
// update vote counts, these values are transient, helper values
data.tallies[data.images[voteurl]] = data.tallies[data.images[voteurl]] ? data.tallies[data.images[voteurl]] + 1 : 1;
var attempts = 3;
req.webtaskContext.storage.set(data, function set_cb(error) {
if (error) {
if (error.code === 409 && attempts--) {
data.entries[viewtoken].vote = data.images[voteurl];
data.votes[viewtoken] = data.images[voteurl];
data.tallies[data.images[voteurl]] = data.tallies[data.images[voteurl]] ? data.tallies[data.images[voteurl]] + 1 : 1;
return req.webtaskContext.storage.set(data, set_cb);
}
return cb(error);
}
});
var message = 'Success! Thanks for your vote! You can change your vote by voting again.';
cb({ message: message });
});
});
// admin: get a list of emails and marketing opt-in
app.get('/admin/emails/:email_access', (req, res) => {
if (req.webtaskContext.secrets.ADMIN_EMAIL_DOWNLOAD && (req.params.email_access === req.webtaskContext.secrets.ADMIN_EMAIL_DOWNLOAD)) {
req.webtaskContext.storage.get(function (error, data) {
if (error) return res.json({ error: 'Unable to load storage.'});
var csv_string = "Email,Marketing Okay,Image URL\n";
Object.keys(data.entries).forEach(function(key) {
csv_string += data.entries[key].email + ',';
csv_string += data.entries[key].mkt + ',';
csv_string += '"' + data.entries[key].url + "\"\n";
});
res.set('Content-Type', 'text/csv');
res.status(200).send(csv_string);
});
} else {
res.json({error: 'No access'});
}
});
// Straight dump of the data
app.get('/admin/data/:data_dump', (req, res) => {
if (req.webtaskContext.secrets.ADMIN_DATA_DUMP && (req.params.data_dump === req.webtaskContext.secrets.ADMIN_DATA_DUMP)) {
req.webtaskContext.storage.get(function (error, data) {
if (error) return res.json({ error: 'Unable to load storage'});
res.json(data);
});
} else {
res.json({error: 'No access'});
}
});
// Set (or reset) the tally data for the leaderboard
app.get('/admin/generate_tallies/:tally_token', (req, res) => {
if (req.webtaskContext.secrets.ADMIN_GENERATE_TALLY && (req.params.tally_token === req.webtaskContext.secrets.ADMIN_GENERATE_TALLY)) {
req.webtaskContext.storage.get(function (error, data) {
if (error) return res.json({ error: 'Unable to load storage'});
var tally = {};
Object.keys(data.entries).forEach(function(key) {
if (data.entries[key].vote) {
tally[data.entries[key].vote] = tally[data.entries[key].vote] ? (tally[data.entries[key].vote] + 1) : 1;
}
});
data.tallies = tally;
var attempts = 3;
req.webtaskContext.storage.set(data, function set_cb(error) {
if (error) {
if (error.code === 409 && attempts--) {
// potentially misses tallying a vote that happened during tally counting
data.tallies = tally;
return req.webtaskContext.storage.set(data, set_cb);
}
}
res.json({error: '', message: 'Tallies updated.'});
});
});
} else {
res.json({error: 'No access'});
}
});
// view entries, even if you can't vote
app.get('/leaderboard', (req, res) => {
res.redirect(url_webtask_base + '/leaderboard/no-token');
});
// vote listing page of url format .../vote/votetoken
app.get('/leaderboard/*', (req, res) => {
var image_list = '';
var voting_allowed = false;
req.webtaskContext.storage.get(function(error, data) {
if (error) {
image_list = 'Unable to find image list';
} else {
if (!data.tallies) { data.tallies = {}; }
var tallied = Object.keys(data.tallies).sort(function(a, b) {
return data.tallies[b] - data.tallies[a];
});
var image_list = '';
tallied.every(function(key, index) {
image_list += renderImageViewAndVotes({ listid: index, src: data.entries[key].url, tally: data.tallies[key] });
// display the highest 12
return index < 12;
});
}
const HTML = renderView({
title: 'Cloudinary Style Transfer - Contest Submission Leaderboard',
top_message: '',
body: '<h4 class="oh-yes">Cloudinary Style Transfer - Contest Submission Leaderboard</h4><div id="leaderboard">' + image_list + '</div>',
scripts: ''
});
res.set('Content-Type', 'text/html');
res.status(200).send(HTML);
});
});
// submit an image to the contest
app.post('/submit', (req, res) => {
var url = (req.body.url) ? req.body.url.trim() : "";
var email = (req.body.email) ? req.body.email.trim() : "";
function cb(ret) {
if (!ret.error) { ret.error = ''; }
res.json(ret);
}
if (email === "") {
return cb({error: 'Email address is required.'})
}
if (!validEmail(email)) {
return cb({error: 'Email does not appear valid.'});
}
if (url === '') {
return cb({error: "Cloudinary image url wasn't submitted. What's up?"})
}
// a valid cloudinary image URL is required
if (!validCloudinaryURL(url)) {
return cb({error: 'Cloudinary image url does not appear valid.'});
}
var viewtoken = namor.generate({ words: 3, numbers: 0});
var votingtoken = generateVotingToken();
var entry = {
"date" : new Date().getTime(),
"ip": req.webtaskContext.headers['x-forwarded-for'],
"url" : url,
"email" : email,
"mkt" : req.body.marketing === "1",
"vote-token": votingtoken
}
req.webtaskContext.storage.get(function (error, data) {
if (error) return cb(error);
data = data || { entries: {}, images: {}, votes: {}, tallies: {} };
// confirm hasn't already submitted a vote
if (data.images[url]) {
return cb({"error" : "Ooops! Image already submitted!"});
}
data.entries[viewtoken] = entry;
data.images[url] = viewtoken;
data.tallies[viewtoken] = 0;
var attempts = 3;
req.webtaskContext.storage.set(data, function set_cb(error) {
if (error) {
if (error.code === 409 && attempts--) {
data.entries[viewtoken] = entry;
data.images[url] = viewtoken;
data.tallies[viewtoken] = 0;
return req.webtaskContext.storage.set(data, set_cb);
}
return cb(error);
}
});
var viewlink = `${url_webtask_base}/view/${viewtoken}`;
var votinglink = `${url_webtask_base}/vote/${viewtoken}?vote=${votingtoken}`;
var message = renderSubmitMessage({ viewlink: viewlink, votinglink: votinglink, start_date: displayDate(start_date) });
sendEmailNotification(req, { viewlink: viewlink, votinglink: votinglink, start_date: displayDate(start_date), end_date: displayDate(end_date), submission_email: email });
cb({ vote: votinglink, message: message });
});
});
// Handle 404s somewhat gracefully
app.use(function (req, res, next) {
const HTML = renderView({
title: 'Cloudinary Style Transfer',
top_message: '',
body: "<p>Sorry, can't find that page.</p><a href='" + url_codepen + "' target='_blank'>View the Cloudinary Style Transfer Contest CodePen</a>",
scripts: ''
});
res.status(404).send(HTML);
})
module.exports = fromExpress(app);
/*
*
* Helper and rendering functions
*
*/
// voting success submit message, rendered HTML returned in JSON
function renderSubmitMessage(locals) {
return `<p>Right on! Yay for submitting!</p>
<p><a class="voting-submitted-link" href="${locals.viewlink}" target="_blank">View all the entries submitted, yours first!</a><p>
<p>Starting on ${locals.start_date}, you'll be able to vote on your favorite Neural Artwork Style Transfer image at the following URL (this is your vote, don't share the link!):</p>
<p><a target="_blank" href="${locals.votinglink}">${locals.votinglink}</a></p>
<p>We'll send you an email reminding you when voting opens.</p>
<p>Good luck!</p>
`;
}
// TODO: in an advanced setup, use HTML version, too
function renderEmailMessage(locals) {
return `Thanks for experimenting with the Cloudinary Neural Artwork Style Transfer feature!
You can view all entries here:
${locals.viewlink}
Starting on ${locals.start_date}, you'll be able to vote on your favorite Neural Artwork
Style Transfer image at the following URL (this is your vote, don't share the link!):
${locals.votinglink}
Voting ends on ${locals.end_date}.
Good luck!
`;
}
// Generic page template
function renderView(locals) {
locals.scripts = locals.scripts ? locals.scripts : '';
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport" />
<title>${locals.title}</title>
<link rel="stylesheet prefetch" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css" />
<link rel="stylesheet prefetch" href="https://fonts.googleapis.com/css?family=Roboto:500,400italic,300,700,500italic,400" />
<link rel="stylesheet prefetch" href="https://fonts.googleapis.com/css?family=Bowlby+One+SC" />
<link rel="stylesheet prefetch" href="https://cloudinary.com/stylesheets/g/cloudinary_public.css?1500989656" />
<link rel="stylesheet" href="${url_stylecss}" />
<script type='text/javascript' src='//platform-api.sharethis.com/js/sharethis.js#property=599f09a489ce4100138ae2ba&product=inline-share-buttons' async='async'></script>
</head>
<body>
<h3 class="presents"><a href="http://cloudinary.com/">
<img alt="Cloudinary Logo" class="dark-logo" height="38" src="${url_cloudinarylogo}" /></a><span class="presents-text">Presents</span></h3>
<h1 class="oh-yes">Style Transfer</h1>
<hr class="separator" />
<div id="vote-message" class="vote-message start-none"><hr class="separator" /></div>
${locals.top_message}
${locals.body}
<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js'></script>
${locals.scripts}
</body>
</html>
`;
}
// render the HTML for a voting list
function renderVoteList(data, viewtoken, votetoken, display_order) {
var image_list = '';
var voting_allowed = false;
// randomize array element order, using Durstenfeld shuffle algorithm.
// @see https://en.wikipedia.org/wiki/Fisher-Yates_shuffle#The_modern_algorithm
function shuffleArray(array) {
for (var i = array.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = array[i];
array[i] = array[j];
array[j] = temp;
}
return array;
}
if (data.entries[viewtoken] && (data.entries[viewtoken]['vote-token'] === votetoken) && (Date.now() > start_date) && (Date.now() < end_date)) {
voting_allowed = true;
var own_image = data.entries[viewtoken].url;
var own_vote = data.entries[viewtoken].vote ? data.entries[data.entries[viewtoken].vote].url : '';
// TODO: rethink the leaderboard, too many exceptions made for its rendering here.
if (display_order != 'leader') {
// display the user's own submission first
image_list += renderOwnSubmissionView({ src: own_image });
// display the vote next, hide it for later display if haven't voted yet
let vote_class = (own_vote === '') ? 'start-none' : '';
image_list += renderOwnVoteView({ src: own_vote, canvote: voting_allowed, classes: vote_class});
}
}
// get a list of all the images
var list_of_images = Array();
if (display_order === 'leader') {
voting_allowed = false;
if (!data.tallies) { data.tallies = {}; }
var tallied = Object.keys(data.tallies).sort(function(a, b) {
return data.tallies[b] - data.tallies[a];
});
tallied.every(function(key, index) {
list_of_images.push(data.entries[key].url);
return true;
});
} else {
Object.keys(data.images).every(function(key, index) {
if (key !== own_image) {
list_of_images.push(key);
}
return true;
});
}
// if normal vote order, reorder the list
if (display_order === 'random') {
shuffleArray(list_of_images);
} else if (display_order === 'recent') {
// of note, recent and reverse are flipped here
list_of_images.reverse();
}
list_of_images.every(function(key, index) {
image_list += renderImageView({ listid: index, src: key, canvote: voting_allowed });
return true;
});
return image_list;
}
// General image display view, optionally includes voting markup
function renderImageView(locals) {
let sty = locals.src.match(/.*\/e_style_transfer\,l_(.*)\/(.*)\.(jpe?g|png|gif|webp|svg)$/i);
return `<div class="third-ish candidate"><img id="candidate-${locals.listid}" src="${locals.src}" /><span class="component-images"><img id="candidate-src-${locals.listid}" src="https://res.cloudinary.com/faas/image/upload/w_200,h_200/${sty[1]}" class="component component-left" /><img id="candidate-tgt-${locals.listid}" src="https://res.cloudinary.com/faas/image/upload/w_200,h_200/${sty[2]}" class="component component-right" /></span>` + ((locals.canvote) ? `<div class="third-ish-gogogo"><div class="outer-border"><button class="candidate-vote" data-listid="${locals.listid}" style="text-align: center;">Vote for This Image</button></div></div></div>` : '</div>');
}
// Leaderboard image view with vote tally
function renderImageViewAndVotes(locals) {
return `<div class="third-ish candidate"><img id="candidate-${locals.listid}" src="${locals.src}" /><div class="vote-tally numero-pill">${locals.tally}</div>`;
}
// Image view of one's own submission
function renderOwnSubmissionView(locals) {
let sty = locals.src.match(/.*\/e_style_transfer\,l_(.*)\/(.*)\.(jpe?g|png|gif|webp|svg)$/i);
return `<div class="third-ish candidate"><img class="sourced" src="${locals.src}" /><span class="component-images"><img id="candidate-src-${locals.listid}" src="https://res.cloudinary.com/faas/image/upload/w_200,h_200/${sty[1]}" class="sourced component component-left" /><img id="candidate-tgt-${locals.listid}" src="https://res.cloudinary.com/faas/image/upload/w_200,h_200/${sty[2]}" class="sourced component component-right" /></span><br /><div class="third-ish-gogogo"><div class="vote-details">Your Submission</div></div></div>`;
}
// Image view of one's own vote
function renderOwnVoteView(locals) {
let sty = (locals.src) ? locals.src.match(/.*\/e_style_transfer\,l_(.*)\/(.*)\.(jpe?g|png|gif|webp|svg)$/i) : ['', '', ''];
return `<div id="candidate-voted-container" class="third-ish candidate ${locals.classes}"><img id="candidate-voted" class="sourced" src="${locals.src}" /><span class="component-images"><img id="candidate-voted-source" src="https://res.cloudinary.com/faas/image/upload/w_200,h_200/${sty[1]}" class="sourced component component-left" /><img id="candidate-voted-target" src="https://res.cloudinary.com/faas/image/upload/w_200,h_200/${sty[2]}" class="sourced component component-right" /></span><br /><div class="third-ish-gogogo"><div class="vote-details">Your Current Vote</div></div></div>`;
}
// HTML for the bottom of the screen flash message
function renderVoteFlash(locals) {
return `<div id="vote-flash" class="vote-flash start-none">You Voted!</div>`;
}
// jquery code for voting
// TODO: consider moving this out into a codepen pen for better viewing
function renderVotingScript(locals) {
if (!locals.viewtoken) return ``;
if (!locals.votetoken) return ``;
return `
<script>
$(document).ready(function() {
$(".candidate-vote").on("click", function() {
var vote = $('#candidate-' + $(this).data('listid')).attr('src');
var decom = vote.match(/(.*)\\/e_style_transfer\\,l_(.*)\\/(.*)\\.(jpe?g|png|gif|webp|svg)$/i)
var view = $('#viewing-token').data('value');
var token = $('#voting-token').data('value');
$.ajax({
type: "POST",
url: "${url_votelink}",
data: JSON.stringify({ "view": view, "vote": vote, "token": token }),
dataType: "json",
contentType: 'application/json; charset=utf-8',
success: function(data) {
if (data.error && data.error !== "") {
$('#vote-message').html(data.error).removeClass('start-none');
} else {
$('#candidate-voted').attr('src', vote);
$('#candidate-voted-source').attr('src', decom[1] + '/' + decom[2] );
$('#candidate-voted-target').attr('src', decom[1] + '/' + decom[3] );
$('#candidate-voted-container').removeClass('start-none');
$('#vote-message').html(data.message).removeClass('start-none');
$('#vote-flash').removeClass('start-none').addClass('vote-flash-is-showing').delay(5000).queue(function(next) { $(this).removeClass('vote-flash-is-showing').addClass('start-none'); next(); });
}
},
failure: function(errMsg) {
$('#contest-dialog-error').html(errMsg).removeClass('start-none');
}
});
});
});
</script>
<div id="viewing-token" data-value="${locals.viewtoken}" />
<div id="voting-token" data-value="${locals.votetoken}" />
`;
}
// TODO: In a bigger project, move these into a utils.js module
// https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
function generateVotingToken() {
var d = new Date().getTime();
if (typeof performance !== 'undefined' && typeof performance.now === 'function'){
d += performance.now(); //use high-precision timer if available
}
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
}
function validEmail(email) {
var emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i;
// filter out foo@example.com emails, too
return (email.match(emailRegex) && !email.match(/example\.[a-zA-Z]{2,}/));
}
// TODO: are c_fill,h_XXX,w_XXX allowed here?
// TODO: try \/?([gchewl]_[a-z0-9_]+)\,?
// TODO: consider validating resource URL @see https://support.cloudinary.com/hc/en-us/articles/115000756771-How-to-check-if-an-image-exists-on-my-account-
function validCloudinaryURL(url) {
var urlRegex = /^https:\/\/res\.cloudinary\.com\/faas\/image\/upload\/e_style_transfer\,l_[a-z0-9_\-]+(\/styletransfer)?\/[a-z0-9]+\.(jpe?g|png|gif|webp|svg)$/i;
return url.match(urlRegex);
}
function displayDate(timestamp_to_format) {
var date = new Date(timestamp_to_format);
var monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
return date.getDate() + ' ' + monthNames[date.getMonth()] + ' ' + (date.getYear() + 1900);
}
function sendEmailNotification(req, locals) {
var send_url = '';
var send_api_key = '';
if (req.webtaskContext.secrets.MANDRILL_API_KEY) {
send_url = 'https://mandrillapp.com/api/1.0/messages/send.json';
send_api_key = req.webtaskContext.secrets.MANDRILL_API_KEY;
var body = {
key: send_api_key,
message: {
subject: 'Cloudinary Neural Network Style Transfer Contest',
text: renderEmailMessage(locals),
from_email: 'dan.gilmore@cloudinary.com',
from_name: 'Dan Gilmore',
to: [
{
email: locals.submission_email,
type: 'to'
}
],
}
};
request_send.post({ url: send_url, form: body }, function (err, resp, body) {
// TODO: really unsure what to do in case of error. Do we care?
});
}
}
1. Set up a new webtask.io account and profile if needed
https://webtask.io/docs/101 (install wt-cli, run “wt init --profile PROFILE EMAIL”)
2. Copy the webtask code into a new local file (style-transfer.js)
3. Copy the following config into package.json in the same folder:
{
"dependencies": {
"namor": "1.0.1",
"request": "2.82.0",
}
}
4. Create a new webtask from the command line (needed to use the host name)
“wt create --name style-transfer --host faas-cloudinary.com style-transfer.js --profile PROFILE”
You can confirm the webtask was created with “wt ls” and “wt inspect style-transfer”
5. Open up the webtask editor using the URL found with “wt ls” and “wt edit” commands:
$ wt ls --profile PROFILE
Name: style-transfer
URL: https://wt-...-0.run.webtask.io/style-transfer
$ wt edit --profile PROFILE style-transfer
Attempting to open the following url in your browser:
https://webtask.it.auth0.com/edit/wt-...
6. Configure the following at the top of the webtask code:
// cloudinary cloud name
const cloudinary_cloudname = 'faas';
// webtask name
const webtask_name = 'style-transfer';
// this is profile based
const webtask_container = 'wt-.......................-0';
// the contest start date
const start_date = new Date('2017-08-16T00:00:00').getTime();
// the contest end date
const end_date = new Date('2017-08-30T07:59:59').getTime();
7. Initialize the webtask storage
In the editor (top of webtask code, upper left corner), click the wrench icon.
Select the “storage” menu item from the dropdown. In the left-side panel that opens,
update the default storage from {} to the following and save with the “Update” button.
Make sure to use straight, not curly, double quotes!
{
"entries": {},
"images": {},
"votes": {},
"tallies" : {}
}
The “entries” section is where all the canonical data is stored.
“images”, “votes”, and “tallies” are denormalized data used to simplify lookups.
8. Setup admin download URLs via webtask secrets. Click the Wrench icon again,
select the Secrets menu dropdown, and add values for the three secrets:
ADMIN_DATA_DUMP
ADMIN_GENERATE_TALLY
ADMIN_EMAIL_DOWNLOAD
The values of these secrets are used as the admin URLs for downloading the full JSON from storage, regenerating the vote tallies from the “entries” section, and download the voting CSV for notifying entrants they should vote, and adding opt-in emails to the marketing mailing lists.
The URLs are of the format:
https://faas-cloudinary.com/{webtask-container}/style-transfer/admin/emails/{ADMIN_EMAIL_DOWNLOAD}
https://faas-cloudinary.com/{webtask-container}/style-transfer/admin/data/{ADMIN_DATA_DUMP}
https://faas-cloudinary.com/{webtask-container}/style-transfer/admin/generate_tallies/{ADMIN_GENERATE_TALLY}
9. Setup outbound email to user upon image submission via a webtask secret and the Mandrill API
* https://mandrillapp.com/api/docs/
* https://auth0.com/rules/mandrill
* https://auth0.com/docs/email/providers#configure-mandrill-for-sending-email
MANDRILL_API_KEY
==========
Save the webtask updates with the floppy disc icon. The webtask now needs CodePen pen information to complete.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment