Date: Tue, 23 Jun 2015 20:53:46 +0200
Subject: [PATCH 08/17] 'Slack' to 'Gitter'
---
.../get-set-for-free-code-camp.json | 8 +++----
seed/field-guides.json | 24 +++++++++----------
2 files changed, 16 insertions(+), 16 deletions(-)
diff --git a/seed/challenges/get-set-for-free-code-camp.json b/seed/challenges/get-set-for-free-code-camp.json
index 379bc26f6e..4408618ae3 100644
--- a/seed/challenges/get-set-for-free-code-camp.json
+++ b/seed/challenges/get-set-for-free-code-camp.json
@@ -234,7 +234,7 @@
"challengeSeed": ["127358841"],
"description": [
"One of the best ways to stay motivated when learning to code is to hang out with other campers.",
- "Slack and Camper News are great ways to communicate with other campers, but there's no substitute for meeting people in-person.",
+ "Gitter and Camper News are great ways to communicate with other campers, but there's no substitute for meeting people in-person.",
"The easiest way to meet other campers in your city is to join your city's Facebook Group. Click here to view our growing list of local groups.",
"Click the link to your city, then, once Facebook loads, click \"Join group\".",
"Our local groups are new, so if you don't see your city on this list, you should follow the directions to create a Facebook group for your city.",
@@ -253,7 +253,7 @@
"nameEs": "Waypoint: Encuentrate con otros Campers en tu Ciudad",
"descriptionEs": [
"Una de las mejores maneras de mantenerte motivado cuando estás aprendiendo a programar es pasar el rato con otros campers.",
- "Slack y Noticias de Campers son una muy buena forma de comunicarte con otros campers, pero no hay ningún substituto para conocerlos en persona.",
+ "Gitter y Noticias de Campers son una muy buena forma de comunicarte con otros campers, pero no hay ningún substituto para conocerlos en persona.",
"La forma más fácil de encontrarte con otros campers en tu ciudad es unirte al grupo de Facebook de tu ciudad o país. Dale click a here para ver la lista de grupos locales.",
"Dale click al link de tu ciudad o país y una vez que Facebook cargue, dale click a \"Join group\".",
"Nuestros grupos locales son pocos, asi que en caso no veas tu ciudad o país en la lista, solamente sigue las instrucciones para crear un grupo de Facebook para ello.",
@@ -275,7 +275,7 @@
"Any time you get stuck or don't know what to do next, follow this simple algorithm (procedure): RSAP (Read, Search, Ask, Post).",
"First, R - Read the documentation or error message. A key skill that good coders have is the ability to interpret and then follow instructions.",
"Next, S - Search Google. Good Google queries take a lot of practice. When you search Google, you usually want to include the language or framework you're using. You also want to limit the results to a recent period.",
- "Then, if you still haven't found an answer to your question, A - Ask your friends. If you have trouble, you can ask your fellow campers. We have a special chat room specifically for getting help with tools you learn through these Free Code Camp Challenges. Go to https://freecodecamp.slack.com/messages/help/. Keep this chat open while you work on the remaining challenges.",
+ "Then, if you still haven't found an answer to your question, A - Ask your friends. If you have trouble, you can ask your fellow campers. We have a special chat room specifically for getting help with tools you learn through these Free Code Camp Challenges. Go to https://gitter.im/FreeCodeCamp/Help. Keep this chat open while you work on the remaining challenges.",
"Finally, P - Post on Stack Overflow. Before you attempt to do this, read Stack Overflow's guide to asking good questions: http://stackoverflow.com/help/how-to-ask.",
"Here's our detailed field guide on getting help: http://freecodecamp.com/field-guide/how-do-i-get-help-when-i-get-stuck.",
"Now you have a clear algorithm to follow when you need help! Let's start coding! Move on to your next challenge."
@@ -294,7 +294,7 @@
"Cualquier momento en el que te atasques o no sepas que hacer, sigue este simple algoritmo (procedimiento): RSAP (Read, Search, Ask, Post). Que en español vendría a ser Lee, Busca, Pregunta, Publica.",
"Primero, Lee - Lee la documentación o el mensaje de error. El punto fuerte de un buen programador es la habilidad de interpretar y seguir instrucciones.",
"Luego, Busca - Busca en Google. Buenas búsquedas o queries requieren bastante práctica. Cuando búsques en Google, idealmente tienes que incluir el lenguaje o framework que estés usando. También tendrás que limitar los resultados de búsqueda a un periodo reciente.",
- "Ahora, en caso no hayas encontrado la respuesta a tu pregunta, Pregunta - Pregunta a tus amigos. En caso estes en problemas, puedes preguntar a otros campers. Tenemos una sala de chat especificamente para obtener ayuda sobre las herramientas que utilizamos en los desafíos de Free Code Camp. Ingresa a https://freecodecamp.slack.com/messages/help/. Mantén este chat abierto mientras trabajas en los desafíos subsiguientes.",
+ "Ahora, en caso no hayas encontrado la respuesta a tu pregunta, Pregunta - Pregunta a tus amigos. En caso estes en problemas, puedes preguntar a otros campers. Tenemos una sala de chat especificamente para obtener ayuda sobre las herramientas que utilizamos en los desafíos de Free Code Camp. Ingresa a https://gitter.im/FreeCodeCamp/Help. Mantén este chat abierto mientras trabajas en los desafíos subsiguientes.",
"Finalmente, Publica - Publica tu pregunta en Stack Overflow. Antes de hacer esto lee la guía de Stack Overflow para publicar buenas preguntas: http://stackoverflow.com/help/how-to-ask. Tendrás que hacerlo en inglés, en caso no sepas como, pide que te ayuden a traducir tu pregunta en el canal #espanol de Slack.",
"Aquí está nuestra guia detallada en como obtener ayuda: http://freecodecamp.com/field-guide/how-do-i-get-help-when-i-get-stuck.",
"Ahora que tienes en claro el procedimiento a seguir cuando necesites ayuda. ¡Empecémos a programar! Continua con el siguiente desafío."
diff --git a/seed/field-guides.json b/seed/field-guides.json
index 24109b4813..e090157e1a 100644
--- a/seed/field-guides.json
+++ b/seed/field-guides.json
@@ -225,7 +225,7 @@
" ",
" This is the most time-efficient way to handle being stuck, and it's the most respectful of other people's time, too.
",
" Most of the time, you'll solve your problem after just one or two steps of this algorithm.
",
- " We have a special chat room just for getting help: https://freecodecamp.slack.com/messages/help/
",
+ " We have a special chat room just for getting help: https://gitter.im/FreeCodeCamp/Help
",
" Also, if you need to post on Stack Overflow, be sure to read their guide to asking good questions: http://stackoverflow.com/help/how-to-ask.
",
" Learning to code is hard. But it's a lot easier if you ask for help when you need it!
",
""
@@ -492,7 +492,7 @@
"
",
" Click the \"Upload a photo button. To start out, you'll probably just want to use Free Code Camp's banner (download it here), or a scenic shot of your city. Later you can update this with a picture from one of your city's Free Code Camp events.",
"
",
- " Message @quincylarson in Slack with a link to your city's group page and he'll include it here.",
+ " Message @quincylarson on Gitter with a link to your city's group page and he'll include it here.",
" Join our Local Leaders Facebook group, where we share ideas about involving campers in your city.",
" ",
" ",
@@ -634,7 +634,7 @@
" Here are our recommended ways of collaborating:
",
" ",
"
",
- " - • Slack has robust private messaging functionality. It's the main way our team communicates, and we recommend it over email.
",
+ " - • Gitter has robust private messaging functionality. It's the main way our team communicates, and we recommend it over email.
",
" - • Trello is great for managing projects. Work with your stakeholder to create Trello cards, and update these cards regularly as you make progress on them.
",
" - • Screen Hero or Team Viewer - These are the ideal way to pair program. Tools like TMUX are good, but difficult to use. We discourage you from using screen sharing tools where only one person has control of the keyboard and mouse - that isn't real pair programming.
",
" - • Write clear and readable code, commit messages, branch names, and pull request messages.
",
@@ -719,9 +719,9 @@
" ",
" ",
" Free Code Camp should be a harassment-free experience for everyone, regardless of gender, gender identity and expression, age, sexual orientation, disability, physical appearance, body size, race, national origin, or religion (or lack thereof).
",
- " We do not tolerate harassment of campers in any form, anywhere on Free Code Camp's online media (Slack, Twitch, etc.) or during pair programming. Harassment includes sexual language and imagery, deliberate intimidation, stalking, unwelcome sexual attention, libel, and any malicious hacking or social engineering.
",
+ " We do not tolerate harassment of campers in any form, anywhere on Free Code Camp's online media (Gitter, Twitch, etc.) or during pair programming. Harassment includes sexual language and imagery, deliberate intimidation, stalking, unwelcome sexual attention, libel, and any malicious hacking or social engineering.
",
" If a camper engages in harassing behavior, our team will take any action we deem appropriate, up to and including banning them from Free Code Camp.
",
- " We want everyone to feel safe and respected. If you are being harassed or notice that someone else is being harassed, say something! Message @quincylarson, @terakilobyte and @codenonprofit in Slack (preferably with a screen shot of the offending language) so we can take fast action.
",
+ " We want everyone to feel safe and respected. If you are being harassed or notice that someone else is being harassed, say something! Message @quincylarson, @terakilobyte and @codenonprofit on Gitter (preferably with a screen shot of the offending language) so we can take fast action.
",
" If you have questions about this code of conduct, email us at team@freecodecamp.com.
",
""
]
@@ -777,10 +777,10 @@
" We're happy to do a quick interview for your publication or show. Here's whom you should contact about what, and how to best reach them:
",
" ",
"
",
- " - Want to talk to about Free Code Camp's curriculum or long-term vision? Reach out to Quincy Larson. He's @ossia on Twitter and @quincylarson on Slack.
",
- " - Want to talk about Free Code Camp's open source codebase, infrastructure, or JavaScript in general? Talk to Nathan Leniz. He's @terakilobyte on Twitter and @terakilobyte on Slack.
",
- " - Want to explore our efforts to empower nonprofits with code? Michael D. Johnson eats, sleeps and breathes that. He's @figitalboy on Twitter and @codenonprofit on Slack.
",
- " - Want to get a camper's perspective on our community? Talk with Bianca Mihai (@biancamihai on Slack and @bubuslubu on Twitter) or Suzanne Atkinson (@adventurebear on Slack and @steelcitycoach on Twitter).",
+ "
- Want to talk to about Free Code Camp's curriculum or long-term vision? Reach out to Quincy Larson. He's @ossia on Twitter and @quincylarson on Gitter.
",
+ " - Want to talk about Free Code Camp's open source codebase, infrastructure, or JavaScript in general? Talk to Nathan Leniz. He's @terakilobyte on Twitter and @terakilobyte on Gitter.
",
+ " - Want to explore our efforts to empower nonprofits with code? Michael D. Johnson eats, sleeps and breathes that. He's @figitalboy on Twitter and @codenonprofit on Gitter.
",
+ " - Want to get a camper's perspective on our community? Talk with Bianca Mihai (@biancamihai on Gitter and @bubuslubu on Twitter) or Suzanne Atkinson (@adventurebear on Gitter and @steelcitycoach on Twitter).",
"
",
" ",
" We strive to be helpful and transparent in everything we do. We'll do what we can to help you share our community with your audience.
",
@@ -838,7 +838,7 @@
" Contributing to our field guide is a great way to establish your history on GitHub, add to your portfolio, and help other campers. If you have a question about JavaScript or programming in general that you'd like us to add to the field guide, here are two ways to get it into the guide:
",
" ",
"
",
- " - You can message @danraley in Slack with your question.
",
+ " - You can message @danraley on Gitter with your question.
",
" - You can also contribute to this field guide directly via GitHub pull request, by cloning Free Code Camp's main repository and modifying field-guides.json.
",
"
",
" ",
@@ -852,7 +852,7 @@
"description": [
"",
"
Our translation effort is driven by bilingual campers like you.",
- "
If you're able to help us, you can join our Trello board by sending @quincylarson your email address in Slack.
",
+ "
If you're able to help us, you can join our Trello board by sending @quincylarson your email address on Gitter.
",
"
"
]
},
@@ -865,7 +865,7 @@
" Translation is an all-or-nothing proposal.",
"
We won't be able to add new languages to Free Code Camp until all of our challenges are translated into that language.
",
" In addition to translating these initially, we'll also need to maintain the translation as the challenges are gradually updated.
",
- " If you're able to help us, you can join our Trello board by sending @quincylarson your email address in Slack.
",
+ " If you're able to help us, you can join our Trello board by sending @quincylarson your email address on Gitter.
",
""
]
},
From c8c03aae10cdb01860899e3901cb64f8243211e5 Mon Sep 17 00:00:00 2001
From: LumenTeun
Date: Tue, 23 Jun 2015 20:56:25 +0200
Subject: [PATCH 09/17] 'Slack' to 'Gitter or Slack'
---
seed/field-guides.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/seed/field-guides.json b/seed/field-guides.json
index e090157e1a..197ed132d3 100644
--- a/seed/field-guides.json
+++ b/seed/field-guides.json
@@ -625,8 +625,8 @@
"
",
" ",
" It's notoriously difficult to estimate how long building software projects will take, so feel free to ask our volunteer team for help.
",
- " You'll continue to meet with your stakeholder at least twice a month in your project's Slack channel.
",
- " You should also ask questions in your project's Slack channel as they come up throughout the week, and your stakeholder can answer them asynchronously.
",
+ " You'll continue to meet with your stakeholder at least twice a month in your project's Gitter or Slack channel.
",
+ " You should also ask questions in your project's Gitter or Slack channel as they come up throughout the week, and your stakeholder can answer them asynchronously.
",
" Getting \"blocked\" on a task can take away your sense of forward momentum, so be sure to proactively seek answers to any ambiguities you encounter.
",
" Ultimately, the project will be considered complete once both the stakeholder's needs have been met, and you and your pair are happy with the project. Then you can add it to your portfolio!
",
" Working with your Pair
",
From 13590a133160634c41ed4ed5ec3a8f45f7db998b Mon Sep 17 00:00:00 2001
From: Berkeley Martinez
Date: Thu, 25 Jun 2015 15:03:46 -0700
Subject: [PATCH 10/17] refactor rxify stories fixes many bugs
---
public/js/main_0.0.2.js | 5 +-
server/boot/story.js | 696 ++++++++++++++++++++--------------------
server/utils/index.js | 61 ++--
server/utils/rx.js | 15 +-
4 files changed, 380 insertions(+), 397 deletions(-)
diff --git a/public/js/main_0.0.2.js b/public/js/main_0.0.2.js
index 472fe7351d..c4c3a338e2 100644
--- a/public/js/main_0.0.2.js
+++ b/public/js/main_0.0.2.js
@@ -263,10 +263,9 @@ $(document).ready(function() {
.fail(function (xhr, textStatus, errorThrown) {
$('#story-submit').bind('click', storySubmitButtonHandler);
})
- .done(function (data, textStatus, xhr) {
- window.location = '/stories/' + JSON.parse(data).storyLink;
+ .done(function(data, textStatus, xhr) {
+ window.location = '/stories/' + data.storyLink;
});
-
};
$('#story-submit').on('click', storySubmitButtonHandler);
diff --git a/server/boot/story.js b/server/boot/story.js
index b1b1f11b9c..9b34881267 100755
--- a/server/boot/story.js
+++ b/server/boot/story.js
@@ -1,17 +1,84 @@
-var nodemailer = require('nodemailer'),
+var Rx = require('rx'),
+ nodemailer = require('nodemailer'),
+ assign = require('object.assign'),
sanitizeHtml = require('sanitize-html'),
moment = require('moment'),
mongodb = require('mongodb'),
// debug = require('debug')('freecc:cntr:story'),
utils = require('../utils'),
+ observeMethod = require('../utils/rx').observeMethod,
+ saveUser = require('../utils/rx').saveUser,
+ saveInstance = require('../utils/rx').saveInstance,
MongoClient = mongodb.MongoClient,
secrets = require('../../config/secrets');
+var foundationDate = 1413298800000;
+var time48Hours = 172800000;
+
+var unDasherize = utils.unDasherize;
+var dasherize = utils.dasherize;
+var getURLTitle = utils.getURLTitle;
+
+var transporter = nodemailer.createTransport({
+ service: 'Mandrill',
+ auth: {
+ user: secrets.mandrill.user,
+ pass: secrets.mandrill.password
+ }
+});
+
+function sendMailWhillyNilly(mailOptions) {
+ transporter.sendMail(mailOptions, function(err) {
+ if (err) {
+ console.log('err sending mail whilly nilly', err);
+ console.log('logging err but not carring');
+ }
+ });
+}
+
+function hotRank(timeValue, rank) {
+ /*
+ * Hotness ranking algorithm: http://amix.dk/blog/post/19588
+ * tMS = postedOnDate - foundationTime;
+ * Ranking...
+ * f(ts, 1, rank) = log(10)z + (ts)/45000;
+ */
+ var z = Math.log(rank) / Math.log(10);
+ var hotness = z + (timeValue / time48Hours);
+ return hotness;
+}
+
+function sortByRank(a, b) {
+ return hotRank(b.timePosted - foundationDate, b.rank) -
+ hotRank(a.timePosted - foundationDate, a.rank);
+}
+
+function cleanData(data, opts) {
+ var options = assign(
+ {},
+ {
+ allowedTags: [],
+ allowedAttributes: []
+ },
+ opts || {}
+ );
+ return sanitizeHtml(data, options).replace(/";/g, '"');
+}
+
module.exports = function(app) {
var router = app.loopback.Router();
var User = app.models.User;
+ var findUserById = observeMethod(User, 'findById');
+ var findOneUser = observeMethod(User, 'findOne');
+
var Story = app.models.Story;
+ var findStory = observeMethod(Story, 'find');
+ var findOneStory = observeMethod(Story, 'findOne');
+ var findStoryById = observeMethod(Story, 'findById');
+ var countStories = observeMethod(Story, 'count');
+
var Comment = app.models.Comment;
+ var findCommentById = observeMethod(Comment, 'findById');
router.get('/stories/hotStories', hotJSON);
router.get('/stories/comments/:id', comments);
@@ -29,39 +96,19 @@ module.exports = function(app) {
app.use(router);
- function hotRank(timeValue, rank) {
- /*
- * Hotness ranking algorithm: http://amix.dk/blog/post/19588
- * tMS = postedOnDate - foundationTime;
- * Ranking...
- * f(ts, 1, rank) = log(10)z + (ts)/45000;
- */
- var time48Hours = 172800000;
- var hotness;
- var z = Math.log(rank) / Math.log(10);
- hotness = z + (timeValue / time48Hours);
- return hotness;
- }
-
function hotJSON(req, res, next) {
- Story.find({
+ var query = {
order: 'timePosted DESC',
limit: 1000
- }, function(err, stories) {
- if (err) {
- return next(err);
- }
- var foundationDate = 1413298800000;
-
- var sliceVal = stories.length >= 100 ? 100 : stories.length;
- return res.json(stories.map(function(elem) {
- return elem;
- }).sort(function(a, b) {
- return hotRank(b.timePosted - foundationDate, b.rank, b.headline)
- - hotRank(a.timePosted - foundationDate, a.rank, a.headline);
- }).slice(0, sliceVal));
-
- });
+ };
+ findStory(query).subscribe(
+ function(stories) {
+ var sliceVal = stories.length >= 100 ? 100 : stories.length;
+ var data = stories.sort(sortByRank).slice(0, sliceVal);
+ res.json(data);
+ },
+ next
+ );
}
function hot(req, res) {
@@ -78,32 +125,11 @@ module.exports = function(app) {
});
}
- /*
- * no used anywhere
- function search(req, res) {
- return res.render('stories/index', {
- title: 'Search the archives of Camper News',
- page: 'search'
- });
- }
-
- function recent(req, res) {
- return res.render('stories/index', {
- title: 'Recently submitted stories on Camper News',
- page: 'recent'
- });
- }
- */
-
function preSubmit(req, res) {
-
var data = req.query;
- var cleanData = sanitizeHtml(data.url, {
- allowedTags: [],
- allowedAttributes: []
- }).replace(/";/g, '"');
- if (data.url.replace(/&/g, '&') !== cleanData) {
+ var cleanedData = cleanData(data.url);
+ if (data.url.replace(/&/g, '&') !== cleanedData) {
req.flash('errors', {
msg: 'The data for this post is malformed'
});
@@ -125,64 +151,53 @@ module.exports = function(app) {
});
}
-
function returnIndividualStory(req, res, next) {
var dashedName = req.params.storyName;
+ var storyName = unDasherize(dashedName);
- var storyName = dashedName.replace(/\-/g, ' ').trim();
+ findOneStory({ where: { storyLink: storyName } }).subscribe(
+ function(story) {
+ if (!story) {
+ req.flash('errors', {
+ msg: "404: We couldn't find a story with that name. " +
+ 'Please double check the name.'
+ });
- Story.find({ where: { storyLink: storyName } }, function(err, story) {
- if (err) {
- return next(err);
- }
+ var dashedNameFull = story.storyLink.toLowerCase()
+ .replace(/\s+/g, ' ')
+ .replace(/\s/g, '-');
-
- if (story.length < 1) {
- req.flash('errors', {
- msg: "404: We couldn't find a story with that name. " +
- 'Please double check the name.'
- });
-
- return res.redirect('/stories/');
- }
-
- story = story.pop();
- var dashedNameFull = story.storyLink.toLowerCase()
- .replace(/\s+/g, ' ')
- .replace(/\s/g, '-');
- if (dashedNameFull !== dashedName) {
- return res.redirect('../stories/' + dashedNameFull);
- }
-
- var userVoted = false;
- try {
- var votedObj = story.upVotes.filter(function(a) {
- return a['upVotedByUsername'] === req.user['profile']['username'];
- });
- if (votedObj.length > 0) {
- userVoted = true;
+ if (dashedNameFull !== dashedName) {
+ return res.redirect('../stories/' + dashedNameFull);
+ }
+ return res.redirect('/stories/');
}
- } catch(e) {
- userVoted = false;
- }
- res.render('stories/index', {
- title: story.headline,
- link: story.link,
- originalStoryLink: dashedName,
- originalStoryAuthorEmail: story.author.email || '',
- author: story.author,
- description: story.description,
- rank: story.upVotes.length,
- upVotes: story.upVotes,
- comments: story.comments,
- id: story.id,
- timeAgo: moment(story.timePosted).fromNow(),
- image: story.image,
- page: 'show',
- storyMetaDescription: story.metaDescription,
- hasUserVoted: userVoted
- });
- });
+
+ // true if any of votes are made by user
+ var userVoted = story.upVotes.some(function(upvote) {
+ return upvote.upVotedByUsername === req.user.username;
+ });
+
+ res.render('stories/index', {
+ title: story.headline,
+ link: story.link,
+ originalStoryLink: dashedName,
+ originalStoryAuthorEmail: story.author.email || '',
+ author: story.author,
+ description: story.description,
+ rank: story.upVotes.length,
+ upVotes: story.upVotes,
+ comments: story.comments,
+ id: story.id,
+ timeAgo: moment(story.timePosted).fromNow(),
+ image: story.image,
+ page: 'show',
+ storyMetaDescription: story.metaDescription,
+ hasUserVoted: userVoted
+ });
+ },
+ next
+ );
}
function getStories(req, res, next) {
@@ -228,54 +243,50 @@ module.exports = function(app) {
}
function upvote(req, res, next) {
- var data = req.body.data;
- Story.find({ where: { id: data.id } }, function(err, story) {
- if (err) {
- return next(err);
- }
- story = story.pop();
- story.rank += 1;
- story.upVotes.push({
- upVotedBy: req.user.id,
- upVotedByUsername: req.user.username
- });
- story.save();
- // NOTE(Berks): This logic is full of wholes and race conditions
- // this could be the source of many 'can't set headers after
- // they are sent'
- // errors. This needs cleaning
- User.findOne(
- { where: { id: story.author.userId } },
- function(err, user) {
- if (err) { return next(err); }
+ var id = req.body.data.id;
+ var savedStory = findStoryById(id)
+ .flatMap(function(story) {
+ story.rank += 1;
+ story.upVotes.push({
+ upVotedBy: req.user.id,
+ upVotedByUsername: req.user.username
+ });
+ return saveInstance(story);
+ })
+ .shareReplay();
- user.progressTimestamps.push(Date.now() || 0);
- user.save(function (err) {
- req.user.save(function (err) {
- if (err) { return next(err); }
- });
- req.user.progressTimestamps.push(Date.now() || 0);
- if (err) {
- return next(err);
- }
- });
+ savedStory.flatMap(function(story) {
+ // find story author
+ return findUserById(story.author.userId);
+ })
+ .flatMap(function(user) {
+ // if user deletes account then this will not exist
+ if (user) {
+ user.progressTimestamps.push(Date.now());
}
+ return saveUser(user);
+ })
+ .flatMap(function() {
+ req.user.progressTimestamps.push(Date.now());
+ return saveUser(req.user);
+ })
+ .flatMap(savedStory)
+ .subscribe(
+ function(story) {
+ return res.send(story);
+ },
+ next
);
- return res.send(story);
- });
}
function comments(req, res, next) {
- var data = req.params.id;
- Comment.find(
- { where: { id: data } },
- function(err, comment) {
- if (err) {
- return next(err);
- }
- comment = comment.pop();
- return res.send(comment);
- });
+ var id = req.params.id;
+ findCommentById(id).subscribe(
+ function(comment) {
+ res.send(comment);
+ },
+ next
+ );
}
function newStory(req, res, next) {
@@ -283,10 +294,8 @@ module.exports = function(app) {
return next(new Error('Must be logged in'));
}
var url = req.body.data.url;
- var cleanURL = sanitizeHtml(url, {
- allowedTags: [],
- allowedAttributes: []
- }).replace(/"/g, '"');
+ var cleanURL = cleanData(url);
+
if (cleanURL !== url) {
req.flash('errors', {
msg: "The URL you submitted doesn't appear valid"
@@ -300,44 +309,46 @@ module.exports = function(app) {
if (url.search(/^https?:\/\//g) === -1) {
url = 'http://' + url;
}
- Story.find(
- { where: { link: url } },
- function(err, story) {
- if (err) {
- return next(err);
- }
- if (story.length) {
- req.flash('errors', {
- msg: "Someone's already posted that link. Here's the discussion."
- });
- return res.json({
- alreadyPosted: true,
- storyURL: '/stories/' + story.pop().storyLink
- });
- }
- utils.getURLTitle(url, processResponse);
- }
- );
- function processResponse(err, story) {
- if (err) {
- res.json({
+ findStory({ where: { link: url } })
+ .map(function(stories) {
+ if (stories.length) {
+ return {
+ alreadyPosted: true,
+ storyURL: '/stories/' + stories.pop().storyLink
+ };
+ }
+ return {
alreadyPosted: false,
- storyURL: url,
- storyTitle: '',
- storyImage: '',
- storyMetaDescription: ''
- });
- } else {
- res.json({
- alreadyPosted: false,
- storyURL: url,
- storyTitle: story.title,
- storyImage: story.image,
- storyMetaDescription: story.description
- });
- }
- }
+ storyURL: url
+ };
+ })
+ .flatMap(function(data) {
+ if (data.alreadyPosted) {
+ return Rx.Observable.just(data);
+ }
+ return Rx.Observable.fromNodeCallback(getURLTitle)(data.storyURL)
+ .map(function(story) {
+ return {
+ alreadyPosted: false,
+ storyURL: data.storyURL,
+ storyTitle: story.title,
+ storyImage: story.image,
+ storyMetaDescription: story.description
+ };
+ });
+ })
+ .subscribe(
+ function(story) {
+ if (story.alreadyPosted) {
+ req.flash('errors', {
+ msg: "Someone's already posted that link. Here's the discussion."
+ });
+ }
+ res.json(story);
+ },
+ next
+ );
}
function storySubmission(req, res, next) {
@@ -357,66 +368,60 @@ module.exports = function(app) {
link = 'http://' + link;
}
- Story.count({
+ var query = {
storyLink: {
like: ('^' + storyLink + '(?: [0-9]+)?$'),
options: 'i'
}
- }, function (err, storyCount) {
- if (err) {
- return next(err);
- }
+ };
- // if duplicate storyLink add unique number
- storyLink = (storyCount === 0) ? storyLink : storyLink + ' ' + storyCount;
+ var savedStory = countStories(query)
+ .flatMap(function(storyCount) {
+ // if duplicate storyLink add unique number
+ storyLink = (storyCount === 0) ?
+ storyLink :
+ storyLink + ' ' + storyCount;
- var link = data.link;
- if (link.search(/^https?:\/\//g) === -1) {
- link = 'http://' + link;
- }
- var story = new Story({
- headline: sanitizeHtml(data.headline, {
- allowedTags: [],
- allowedAttributes: []
- }).replace(/"/g, '"'),
- timePosted: Date.now(),
- link: link,
- description: sanitizeHtml(data.description, {
- allowedTags: [],
- allowedAttributes: []
- }).replace(/"/g, '"'),
- rank: 1,
- upVotes: [({
- upVotedBy: req.user.id,
- upVotedByUsername: req.user.username
- })],
- author: {
- picture: req.user.picture,
- userId: req.user.id,
- username: req.user.username,
- email: req.user.email
- },
- comments: [],
- image: data.image,
- storyLink: storyLink,
- metaDescription: data.storyMetaDescription,
- originalStoryAuthorEmail: req.user.email
- });
- story.save(function (err) {
- if (err) {
- return next(err);
+ var link = data.link;
+ if (link.search(/^https?:\/\//g) === -1) {
+ link = 'http://' + link;
}
- req.user.progressTimestamps.push(Date.now() || 0);
- req.user.save(function (err) {
- if (err) {
- return next(err);
- }
- res.send(JSON.stringify({
- storyLink: story.storyLink.replace(/\s+/g, '-').toLowerCase()
- }));
+ var newStory = new Story({
+ headline: cleanData(data.headline),
+ timePosted: Date.now(),
+ link: link,
+ description: cleanData(data.description),
+ rank: 1,
+ upVotes: [({
+ upVotedBy: req.user.id,
+ upVotedByUsername: req.user.username
+ })],
+ author: {
+ picture: req.user.picture,
+ userId: req.user.id,
+ username: req.user.username,
+ email: req.user.email
+ },
+ comments: [],
+ image: data.image,
+ storyLink: storyLink,
+ metaDescription: data.storyMetaDescription,
+ originalStoryAuthorEmail: req.user.email
});
+ return saveInstance(newStory);
});
- });
+
+ req.user.progressTimestamps.push(Date.now());
+ return saveUser(req.user)
+ .flatMap(savedStory)
+ .subscribe(
+ function(story) {
+ res.json({
+ storyLink: dasherize(story.storyLink)
+ });
+ },
+ next
+ );
}
function commentSubmit(req, res, next) {
@@ -424,11 +429,8 @@ module.exports = function(app) {
if (!req.user) {
return next(new Error('Not authorized'));
}
- var sanitizedBody = sanitizeHtml(data.body,
- {
- allowedTags: [],
- allowedAttributes: []
- }).replace(/"/g, '"');
+ var sanitizedBody = cleanData(data.body);
+
if (data.body !== sanitizedBody) {
req.flash('errors', {
msg: 'HTML is not allowed'
@@ -453,7 +455,13 @@ module.exports = function(app) {
commentOn: Date.now()
});
- commentSave(comment, Story, res, next);
+ commentSave(comment, findStoryById).subscribe(
+ function() {},
+ next,
+ function() {
+ res.send(true);
+ }
+ );
}
function commentOnCommentSubmit(req, res, next) {
@@ -462,13 +470,7 @@ module.exports = function(app) {
return next(new Error('Not authorized'));
}
- var sanitizedBody = sanitizeHtml(
- data.body,
- {
- allowedTags: [],
- allowedAttributes: []
- }
- ).replace(/"/g, '"');
+ var sanitizedBody = cleanData(data.body);
if (data.body !== sanitizedBody) {
req.flash('errors', {
@@ -494,119 +496,101 @@ module.exports = function(app) {
topLevel: false,
commentOn: Date.now()
});
- commentSave(comment, Comment, res, next);
+ commentSave(comment, findCommentById).subscribe(
+ function() {},
+ next,
+ function() {
+ res.send(true);
+ }
+ );
}
function commentEdit(req, res, next) {
-
- Comment.find({ where: { id: req.params.id } }, function(err, cmt) {
- if (err) {
- return next(err);
- }
- cmt = cmt.pop();
-
- if (!req.user && cmt.author.userId !== req.user.id) {
- return next(new Error('Not authorized'));
- }
-
- var sanitizedBody = sanitizeHtml(req.body.body, {
- allowedTags: [],
- allowedAttributes: []
- }).replace(/"/g, '"');
- if (req.body.body !== sanitizedBody) {
- req.flash('errors', {
- msg: 'HTML is not allowed'
- });
- return res.send(true);
- }
-
- cmt.body = sanitizedBody;
- cmt.commentOn = Date.now();
- cmt.save(function(err) {
- if (err) {
- return next(err);
+ findCommentById(req.params.id)
+ .doOnNext(function(comment) {
+ if (!req.user && comment.author.userId !== req.user.id) {
+ throw new Error('Not authorized');
}
- res.send(true);
- });
-
- });
-
+ })
+ .flatMap(function(comment) {
+ var sanitizedBody = cleanData(req.body.body);
+ if (req.body.body !== sanitizedBody) {
+ req.flash('errors', {
+ msg: 'HTML is not allowed'
+ });
+ }
+ comment.body = sanitizedBody;
+ comment.commentOn = Date.now();
+ return saveInstance(comment);
+ })
+ .subscribe(
+ function() {
+ res.send(true);
+ },
+ next
+ );
}
- function commentSave(comment, Context, res, next) {
- comment.save(function(err, data) {
- if (err) {
- return next(err);
- }
- try {
+ function commentSave(comment, findContextById) {
+ return saveInstance(comment)
+ .flatMap(function(comment) {
// Based on the context retrieve the parent
// object of the comment (Story/Comment)
- Context.find({
- where: { id: data.associatedPost }
- }, function (err, associatedContext) {
- if (err) {
- return next(err);
- }
- associatedContext = associatedContext.pop();
- if (associatedContext) {
- associatedContext.comments.push(data.id);
- associatedContext.save(function (err) {
- if (err) {
- return next(err);
- }
- res.send(true);
- });
- }
- // Find the author of the parent object
- User.findOne({
- username: associatedContext.author.username
- }, function(err, recipient) {
- if (err) {
- return next(err);
- }
- // If the emails of both authors differ,
- // only then proceed with email notification
- if (
- typeof data.author !== 'undefined' &&
- data.author.email &&
- typeof recipient !== 'undefined' &&
- recipient.email &&
- (data.author.email !== recipient.email)
- ) {
- var transporter = nodemailer.createTransport({
- service: 'Mandrill',
- auth: {
- user: secrets.mandrill.user,
- pass: secrets.mandrill.password
- }
- });
+ return findContextById(comment.associatedPost);
+ })
+ .flatMap(function(associatedContext) {
+ if (associatedContext) {
+ associatedContext.comments.push(comment.id);
+ }
+ // NOTE(berks): saveInstance is safe
+ // it will automatically call onNext with null and onCompleted if
+ // argument is falsey or has no method save
+ return saveInstance(associatedContext);
+ })
+ .flatMap(function(associatedContext) {
+ // Find the author of the parent object
+ // if no username
+ var username = associatedContext && associatedContext.author ?
+ associatedContext.author.username :
+ null;
- var mailOptions = {
- to: recipient.email,
- from: 'Team@freecodecamp.com',
- subject: data.author.username +
- ' replied to your post on Camper News',
- text: [
- 'Just a quick heads-up: ',
- data.author.username + ' replied to you on Camper News.',
- 'You can keep this conversation going.',
- 'Just head back to the discussion here: ',
- 'http://freecodecamp.com/stories/' + data.originalStoryLink,
- '- the Free Code Camp Volunteer Team'
- ].join('\n')
- };
-
- transporter.sendMail(mailOptions, function (err) {
- if (err) {
- return err;
- }
- });
- }
+ var query = { where: { username: username } };
+ return findOneUser(query);
+ })
+ // if no user is found we don't want to hit the doOnNext
+ // filter here will call onCompleted without running through the following
+ // steps
+ .filter(function(user) {
+ return !!user;
+ })
+ // if this is called user is guarenteed to exits
+ // this is a side effect, hence we use do/tap observable methods
+ .doOnNext(function(user) {
+ // If the emails of both authors differ,
+ // only then proceed with email notification
+ if (
+ comment.author &&
+ comment.author.email &&
+ user.email &&
+ (comment.author.email !== user.email)
+ ) {
+ sendMailWhillyNilly({
+ to: user.email,
+ from: 'Team@freecodecamp.com',
+ subject: comment.author.username +
+ ' replied to your post on Camper News',
+ text: [
+ 'Just a quick heads-up: ',
+ comment.author.username,
+ ' replied to you on Camper News.',
+ 'You can keep this conversation going.',
+ 'Just head back to the discussion here: ',
+ 'http://freecodecamp.com/stories/',
+ comment.originalStoryLink,
+ '- the Free Code Camp Volunteer Team'
+ ].join('\n')
});
- });
- } catch (e) {
- return next(err);
- }
- });
+ }
+ });
}
};
diff --git a/server/utils/index.js b/server/utils/index.js
index b49a4579f7..ec64435a5f 100644
--- a/server/utils/index.js
+++ b/server/utils/index.js
@@ -20,11 +20,6 @@ var allFieldGuideIds, allFieldGuideNames, allNonprofitNames,
challengeMapWithNames, allChallengeIds,
challengeMapWithDashedNames;
-/**
- * GET /
- * Resources.
- */
-
Array.zip = function(left, right, combinerFunction) {
var counter,
results = [];
@@ -68,7 +63,7 @@ module.exports = {
},
unDasherize: function unDasherize(name) {
- return ('' + name).replace(/\-/g, ' ');
+ return ('' + name).replace(/\-/g, ' ').trim();
},
getChallengeMapForDisplay: function () {
@@ -199,35 +194,37 @@ module.exports = {
return process.env.NODE_ENV;
},
- getURLTitle: function (url, callback) {
- (function () {
- var result = {title: '', image: '', url: '', description: ''};
- request(url, function (error, response, body) {
- if (!error && response.statusCode === 200) {
- var $ = cheerio.load(body);
- var metaDescription = $("meta[name='description']");
- var metaImage = $("meta[property='og:image']");
- var urlImage = metaImage.attr('content') ?
- metaImage.attr('content') :
- '';
+ getURLTitle: function(url, callback) {
+ var result = {
+ title: '',
+ image: '',
+ url: '',
+ description: ''
+ };
+ request(url, function(err, response, body) {
+ if (err || response.statusCode !== 200) {
+ return callback(new Error('failed'));
+ }
+ var $ = cheerio.load(body);
+ var metaDescription = $("meta[name='description']");
+ var metaImage = $("meta[property='og:image']");
+ var urlImage = metaImage.attr('content') ?
+ metaImage.attr('content') :
+ '';
- var metaTitle = $('title');
- var description = metaDescription.attr('content') ?
- metaDescription.attr('content') :
- '';
+ var metaTitle = $('title');
+ var description = metaDescription.attr('content') ?
+ metaDescription.attr('content') :
+ '';
- result.title = metaTitle.text().length < 90 ?
- metaTitle.text() :
- metaTitle.text().slice(0, 87) + '...';
+ result.title = metaTitle.text().length < 90 ?
+ metaTitle.text() :
+ metaTitle.text().slice(0, 87) + '...';
- result.image = urlImage;
- result.description = description;
- callback(null, result);
- } else {
- callback(new Error('failed'));
- }
- });
- })();
+ result.image = urlImage;
+ result.description = description;
+ callback(null, result);
+ });
},
getMDNLinks: function(links) {
diff --git a/server/utils/rx.js b/server/utils/rx.js
index 7e98aa486b..8088e163e0 100644
--- a/server/utils/rx.js
+++ b/server/utils/rx.js
@@ -1,24 +1,27 @@
var Rx = require('rx');
var debug = require('debug')('freecc:rxUtils');
-exports.saveUser = function saveUser(user) {
+exports.saveInstance = function saveInstance(instance) {
return new Rx.Observable.create(function(observer) {
- if (!user || typeof user.save !== 'function') {
- debug('no user or save method');
+ if (!instance || typeof instance.save !== 'function') {
+ debug('no instance or save method');
observer.onNext();
return observer.onCompleted();
}
- user.save(function(err, savedUser) {
+ instance.save(function(err, savedInstance) {
if (err) {
return observer.onError(err);
}
- debug('user saved');
- observer.onNext(savedUser);
+ debug('instance saved');
+ observer.onNext(savedInstance);
observer.onCompleted();
});
});
};
+// alias saveInstance
+exports.saveUser = exports.saveInstance;
+
exports.observableQueryFromModel =
function observableQueryFromModel(Model, method, query) {
return Rx.Observable.fromNodeCallback(Model[method], Model)(query);
From 2422663f3499cf76b354a20f7ce6680c76d1d752 Mon Sep 17 00:00:00 2001
From: Berkeley Martinez
Date: Thu, 25 Jun 2015 21:47:25 -0700
Subject: [PATCH 11/17] add comments migration to loopback script
---
seed/loopbackMigration.js | 46 +++++++++++++++++++++++++++------------
1 file changed, 32 insertions(+), 14 deletions(-)
diff --git a/seed/loopbackMigration.js b/seed/loopbackMigration.js
index 02dd993b9c..1a86352e46 100644
--- a/seed/loopbackMigration.js
+++ b/seed/loopbackMigration.js
@@ -160,28 +160,46 @@ var storyCount = dbObservable
})
.count();
+var commentCount = dbObservable
+ .flatMap(function(db) {
+ return createQuery(db, 'comments', {});
+ })
+ .withLatestFrom(dbObservable, function(comments, db) {
+ return {
+ comments: comments,
+ db: db
+ };
+ })
+ .flatMap(function(dats) {
+ return insertMany(dats.db, 'comment', dats.comments, { w: 1 });
+ })
+ .buffer(20)
+ .count();
+
Rx.Observable.combineLatest(
userIdentityCount,
userSavesCount,
storyCount,
- function(userIdentCount, userCount, storyCount) {
+ commentCount,
+ function(userIdentCount, userCount, storyCount, commentCount) {
return {
userIdentCount: userIdentCount * 20,
userCount: userCount * 20,
- storyCount: storyCount * 20
+ storyCount: storyCount * 20,
+ commentCount: commentCount * 20
};
})
.subscribe(
- function(countObj) {
- console.log('next');
- count = countObj;
- },
- function(err) {
- console.error('an error occured', err, err.stack);
- },
- function() {
+ function(countObj) {
+ console.log('next');
+ count = countObj;
+ },
+ function(err) {
+ console.error('an error occured', err, err.stack);
+ },
+ function() {
- console.log('finished with ', count);
- process.exit(0);
- }
-);
+ console.log('finished with ', count);
+ process.exit(0);
+ }
+ );
From 9c2768591d49bc0b051c52e6f03e2d47c114e4ae Mon Sep 17 00:00:00 2001
From: Berkeley Martinez
Date: Thu, 25 Jun 2015 23:00:49 -0700
Subject: [PATCH 12/17] fix check for user existence
---
server/boot/story.js | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/server/boot/story.js b/server/boot/story.js
index 9b34881267..2f026626c3 100755
--- a/server/boot/story.js
+++ b/server/boot/story.js
@@ -173,9 +173,10 @@ module.exports = function(app) {
return res.redirect('/stories/');
}
+ var username = req.user ? req.user.username : '';
// true if any of votes are made by user
var userVoted = story.upVotes.some(function(upvote) {
- return upvote.upVotedByUsername === req.user.username;
+ return upvote.upVotedByUsername === username;
});
res.render('stories/index', {
From 82c96ec0ba8e2ecf33c40eac38fc0f450d61093d Mon Sep 17 00:00:00 2001
From: Berkeley Martinez
Date: Thu, 25 Jun 2015 23:20:16 -0700
Subject: [PATCH 13/17] bump main.js version
---
public/js/{main_0.0.2.js => main_0.0.3.js} | 0
server/views/partials/universal-head.jade | 2 +-
2 files changed, 1 insertion(+), 1 deletion(-)
rename public/js/{main_0.0.2.js => main_0.0.3.js} (100%)
diff --git a/public/js/main_0.0.2.js b/public/js/main_0.0.3.js
similarity index 100%
rename from public/js/main_0.0.2.js
rename to public/js/main_0.0.3.js
diff --git a/server/views/partials/universal-head.jade b/server/views/partials/universal-head.jade
index 47bc657df6..c31b46da4c 100644
--- a/server/views/partials/universal-head.jade
+++ b/server/views/partials/universal-head.jade
@@ -32,7 +32,7 @@ script.
window.moment || document.write('