From 028feaafc674c91684d45c8386fa89de72df654f Mon Sep 17 00:00:00 2001 From: oaugereau Date: Thu, 12 Jun 2014 09:42:57 +0200 Subject: [PATCH 01/22] Update fr.coffee --- app/locale/fr.coffee | 62 ++++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/app/locale/fr.coffee b/app/locale/fr.coffee index d40d2806e..3b3ca3915 100644 --- a/app/locale/fr.coffee +++ b/app/locale/fr.coffee @@ -16,7 +16,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t play: "Jouer" retry: "Reessayer" watch: "Regarder" -# unwatch: "Unwatch" + unwatch: "Ne plus regarder" submit_patch: "Soumettre un correctif" units: @@ -26,14 +26,14 @@ module.exports = nativeDescription: "français", englishDescription: "French", t minutes: "minutes" hour: "heure" hours: "heures" -# day: "day" -# days: "days" -# week: "week" -# weeks: "weeks" -# month: "month" -# months: "months" -# year: "year" -# years: "years" + day: "jour" + days: "jours" + week: "semaine" + weeks: "semaines" + month: "mois" + months: "mois" + year: "année" + years: "années" modal: close: "Fermer" @@ -128,8 +128,8 @@ module.exports = nativeDescription: "français", englishDescription: "French", t forum_page: "notre forum" forum_suffix: " À la place." send: "Envoyer un commentaire" -# contact_candidate: "Contact Candidate" -# recruitment_reminder: "Use this form to reach out to candidates you are interested in interviewing. Remember that CodeCombat charges 15% of first-year salary. The fee is due upon hiring the employee and is refundable for 90 days if the employee does not remain employed. Part time, remote, and contract employees are free, as are interns." + contact_candidate: "Contacter le candidat" + recruitment_reminder: "Utilisez ce formulaire pour entrer en contact avec le candidat qui vous interesse. Souvenez-vous que CodeCombat facture 15% de la première année de salaire. Ces frais sont dues à l'embauche de l'employé, ils sont remboursable pendant 90 jours si l'employé ne reste pas employé. Les employés à temps partiel, à distance ou contractuel sont gratuits en tant que stagiaires." diplomat_suggestion: title: "Aidez à traduire CodeCombat!" @@ -173,11 +173,11 @@ module.exports = nativeDescription: "français", englishDescription: "French", t email_announcements: "Annonces" email_announcements_description: "Recevoir des mails sur les dernières actualités et sur le développement de CodeCombat." email_notifications: "Notifications" -# email_notifications_summary: "Controls for personalized, automatic email notifications related to your CodeCombat activity." -# email_any_notes: "Any Notifications" + email_notifications_summary: "Commandes pour personaliser les notifications automatiques d'email liées à votre activité sur CodeCombat." + email_any_notes: "Toutes Notifications" email_any_notes_description: "Désactivez pour ne plus recevoir de notifications par e-mail." -# email_recruit_notes: "Job Opportunities" -# email_recruit_notes_description: "If you play really well, we may contact you about getting you a (better) job." + email_recruit_notes: "Offres d'emploi" + email_recruit_notes_description: "Si vous jouez vraiment bien, nous pouvons vous contacter pour vous proposer un (meilleur) emploi." contributor_emails: "Emails des contributeurs" contribute_prefix: "Nous recherchons des personnes pour se joindre à notre groupe! Consultez la " contribute_page: "page de contributions" @@ -186,15 +186,15 @@ module.exports = nativeDescription: "français", englishDescription: "French", t error_saving: "Problème d'enregistrement" saved: "Changements sauvegardés" password_mismatch: "Le mot de passe ne correspond pas." -# job_profile: "Job Profile" -# job_profile_approved: "Your job profile has been approved by CodeCombat. Employers will be able to see it until you either mark it inactive or it has not been changed for four weeks." -# job_profile_explanation: "Hi! Fill this out, and we will get in touch about finding you a software developer job." -# sample_profile: "See a sample profile" + job_profile: "Profil d'emploi" + job_profile_approved: "Votre profil d'emploi a été approuvé par CodeCombat. Les employeurs seront en mesure de voir votre profil jusqu'à ce que vous le marquez inactif ou qu'il n'a pas été changé pendant quatre semaines." + job_profile_explanation: "Salut! Remplissez-le et nous prendrons contact pour vous trouver un emploi de développeur de logiciels." + sample_profile: "Voir un exemple de profil" view_profile: "Voir votre profil" account_profile: edit_settings: "Éditer les préférences" -# done_editing_settings: "Done Editing" + done_editing_settings: "Edition terminée" profile_for_prefix: "Profil pour " profile_for_suffix: "" approved: "Approuvé" @@ -202,17 +202,17 @@ module.exports = nativeDescription: "français", englishDescription: "French", t looking_for: "à la recherche de:" last_updated: "Dernière Mise à jour:" contact: "Contact" -# active: "Looking for interview offers now" -# inactive: "Not looking for offers right now" -# complete: "complete" -# next: "Next" -# next_city: "city?" -# next_country: "pick your country." -# next_name: "name?" -# next_short_description: "summarize yourself at a glance." -# next_long_description: "describe the work you're looking for." -# next_skills: "list at least five skills." -# next_work: "list your work experience." + active: "En recherche d'offres" + inactive: "Ne recherche pas d'offres" + complete: "terminé" + next: "Suivant" + next_city: "ville ?" + next_country: "choisissez votre pays." + next_name: "nom ?" + next_short_description: "résumez votre profil en quelques mots." + next_long_description: "décrivez le travail que vous cherchez." + next_skills: "listez au moins 5 compétances." + next_work: "décrivez votre expérience professionnelle." # next_education: "recount your educational ordeals." # next_projects: "show off up to three projects you've worked on." # next_links: "add any personal or social links." From 0d5e213daa5a9ed15036b32d5b113ec6e095a9a5 Mon Sep 17 00:00:00 2001 From: oaugereau Date: Thu, 12 Jun 2014 14:17:36 +0200 Subject: [PATCH 02/22] Update fr.coffee --- app/locale/fr.coffee | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/app/locale/fr.coffee b/app/locale/fr.coffee index 3b3ca3915..015fa2cff 100644 --- a/app/locale/fr.coffee +++ b/app/locale/fr.coffee @@ -213,21 +213,21 @@ module.exports = nativeDescription: "français", englishDescription: "French", t next_long_description: "décrivez le travail que vous cherchez." next_skills: "listez au moins 5 compétances." next_work: "décrivez votre expérience professionnelle." -# next_education: "recount your educational ordeals." -# next_projects: "show off up to three projects you've worked on." -# next_links: "add any personal or social links." -# next_photo: "add an optional professional photo." -# next_active: "mark yourself open to offers to show up in searches." -# example_blog: "Your Blog" -# example_github: "Your GitHub" -# links_header: "Personal Links" -# links_blurb: "Link any other sites or profiles you want to highlight, like your GitHub, your LinkedIn, or your blog." -# links_name: "Link Name" -# links_name_help: "What are you linking to?" -# links_link_blurb: "Link URL" -# basics_header: "Update basic info" -# basics_active: "Open to Offers" -# basics_active_help: "Want interview offers right now?" + next_education: "raconter votre scolarité." + next_projects: "décrivez jusqu'à 3 projets sur lesquels vous avez travaillé." + next_links: "ajouter des liens internet vers des sites personnels ou des réseaux sociaux." + next_photo: "ajouter une photo professionelle (optionnel)." + next_active: "déclarez vous ouvert aux offres pour apparaitre dans les recherches." + example_blog: "Votre blog" + example_github: "Votre GitHub" + links_header: "Liens personnels" + links_blurb: "Lien vers d'autres sites ou profils que vous souhaitez mettre en avant, comme votre GitHub, LinkedIn ou votre blog." + links_name: "Nom du lien" + links_name_help: "A quoi êtes vous lié ?" + links_link_blurb: "Lien URL" + basics_header: "Mettre à jour les information basiques" + basics_active: "Ouvert aux propositions" + basics_active_help: "Voulez-vous des offres maintenant ?" # "Want interview offers right now?" # basics_job_title: "Desired Job Title" # basics_job_title_help: "What role are you looking for?" # basics_city: "City" @@ -533,7 +533,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t message: "Message" code: "Code" ladder: "Companion" - when: "Lorsuqe" + when: "Quand" opponent: "Adversaire" rank: "Rang" score: "Score" From f7ee40a3a2108a3d169bf96184571105a3c14f64 Mon Sep 17 00:00:00 2001 From: oaugereau Date: Thu, 12 Jun 2014 14:39:41 +0200 Subject: [PATCH 03/22] Update fr.coffee --- app/locale/fr.coffee | 78 ++++++++++++++++++++++---------------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/app/locale/fr.coffee b/app/locale/fr.coffee index 015fa2cff..8f18cae99 100644 --- a/app/locale/fr.coffee +++ b/app/locale/fr.coffee @@ -228,31 +228,31 @@ module.exports = nativeDescription: "français", englishDescription: "French", t basics_header: "Mettre à jour les information basiques" basics_active: "Ouvert aux propositions" basics_active_help: "Voulez-vous des offres maintenant ?" # "Want interview offers right now?" -# basics_job_title: "Desired Job Title" -# basics_job_title_help: "What role are you looking for?" -# basics_city: "City" -# basics_city_help: "City you want to work in (or live in now)." -# basics_country: "Country" -# basics_country_help: "Country you want to work in (or live in now)." -# basics_visa: "US Work Status" -# basics_visa_help: "Are you authorized to work in the US, or do you need visa sponsorship?" -# basics_looking_for: "Looking For" -# basics_looking_for_full_time: "Full-time" -# basics_looking_for_part_time: "Part-time" -# basics_looking_for_remote: "Remote" -# basics_looking_for_contracting: "Contracting" -# basics_looking_for_internship: "Internship" -# basics_looking_for_help: "What kind of developer position do you want?" -# name_header: "Fill in your name" -# name_anonymous: "Anonymous Developer" -# name_help: "Name you want employers to see, like 'Nick Winter'." -# short_description_header: "Write a short description of yourself" + basics_job_title: "Titre du poste souhaité" + basics_job_title_help: "Quel est le rôle que vous cherchez ?" + basics_city: "Ville" + basics_city_help: "Ville dans laquelle vous souhaitez travailler (ou dans laquelle vous vivez actuellement)." + basics_country: "Pays" + basics_country_help: "Pays dans lequel vous souhaitez travailler (ou dans lequel vous vivez actuellement)." + basics_visa: "Status de travail aux Etats-Unis" + basics_visa_help: "Etes vous autorisé à travailler aux Etats-Unis ou avez vous besoin d'un parrainage pour le visa ?" + basics_looking_for: "Recherche" + basics_looking_for_full_time: "Temps plein" + basics_looking_for_part_time: "Temps partiel" + basics_looking_for_remote: "A distance" + basics_looking_for_contracting: "Contrat" + basics_looking_for_internship: "Stage" + basics_looking_for_help: "Quel genre de poste de développeur voulez-vous ?" + name_header: "Remplissez votre nom" + name_anonymous: "Developpeur Anonyme" + name_help: "Le nom que vous souhaitez que l'employeur voie, par exemple 'Chuck Norris'." + short_description_header: "Décrivez vous en quelques mots" # short_description_blurb: "Add a blurb here that will show, at a glance, whether you might be just the developer that an employer is looking for." -# short_description: "Short Description" -# short_description_help: "Who are you, and what are you looking for? 140 characters max." -# skills_header: "Skills" -# skills_help: "Tag relevant developer skills in order of proficiency." -# long_description_header: "Detail your desired position" + short_description: "Description courte" + short_description_help: "Qui êtes vous et que recherchez vous ? 140 caractères max." + skills_header: "Compétences" + skills_help: "Notez vos compétence de développement par ordre de maitrise." + long_description_header: "Détaillez votre poste souhaité" # long_description_blurb_1: "Write a little longer section here to describe the role you would like to pursue next." # long_description_blurb_2: "Talk about how awesome you are and why it would be a good idea to hire you." # long_description: "Description" @@ -322,7 +322,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t goals: "Objectifs" success: "Succès" incomplete: "Imcoplet" -# timed_out: "Ran out of time" + timed_out: "Plus de temps" failing: "Echec" action_timeline: "Action sur la ligne de temps" click_to_select: "Clique sur une unité pour la sélectionner." @@ -465,8 +465,8 @@ module.exports = nativeDescription: "français", englishDescription: "French", t back: "Retour" revert: "Annuler" revert_models: "Annuler les modèles" -# fork_title: "Fork New Version" -# fork_creating: "Creating Fork..." + fork_title: "Fork une nouvelle version" + fork_creating: "Créer un Fork..." more: "Plus" wiki: "Wiki" live_chat: "Chat en live" @@ -744,8 +744,8 @@ module.exports = nativeDescription: "français", englishDescription: "French", t simulation_explanation: "En simulant une partie, tu peux classer ton rang plus rapidement!" simulate_games: "Simuler une Partie!" simulate_all: "REINITIALISER ET SIMULER DES PARTIES" -# games_simulated_by: "Games simulated by you:" -# games_simulated_for: "Games simulated for you:" + games_simulated_by: "Parties que vous avez simulé :" + games_simulated_for: "parties simulées pour vous :" games_simulated: "Partie simulée" games_played: "Parties jouées" ratio: "Moyenne" @@ -776,11 +776,11 @@ module.exports = nativeDescription: "français", englishDescription: "French", t warmup: "Préchauffe" vs: "VS" # friends_playing: "Friends Playing" -# log_in_for_friends: "Log in to play with your friends!" -# social_connect_blurb: "Connect and play against your friends!" + log_in_for_friends: "Connectez vous pour jouer avec vos amis!" + social_connect_blurb: "Connectez vous pour jouer contre vos amis!" # invite_friends_to_battle: "Invite your friends to join you in battle!" -# fight: "Fight!" -# watch_victory: "Watch your victory" + fight: "Combattez !" + watch_victory: "Regardez votre victoire" # defeat_the: "Defeat the" tournament_ends: "Fin du tournoi" tournament_rules: "Règles du tournoi" @@ -835,7 +835,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t unknown: "Erreur inconnue." resources: -# your_sessions: "Your Sessions" + your_sessions: "vos Sessions" level: "Niveau" # social_network_apis: "Social Network APIs" facebook_status: "Statut Facebook" @@ -857,11 +857,11 @@ module.exports = nativeDescription: "français", englishDescription: "French", t # level_session: "Your Session" # opponent_session: "Opponent Session" article: "Article" -# user_names: "User Names" + user_names: "Nom d'utilisateur" # thang_names: "Thang Names" files: "Fichiers" top_simulators: "Top Simulateurs" -# source_document: "Source Document" + source_document: "Document Source" document: "Document" # sprite_sheet: "Sprite Sheet" @@ -869,7 +869,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t added: "Ajouté" modified: "Modifié" deleted: "Supprimé" -# moved_index: "Moved Index" -# text_diff: "Text Diff" -# merge_conflict_with: "MERGE CONFLICT WITH" + moved_index: "Index changé" + text_diff: "Différence de texte" + merge_conflict_with: "Fusionner les conflits avec" no_changes: "Aucuns Changements" From 07515811138f8e701f8fef40567b051a84e65e16 Mon Sep 17 00:00:00 2001 From: Ruben Vereecken Date: Mon, 9 Jun 2014 16:18:26 +0200 Subject: [PATCH 04/22] Search endpoint is now without the /search prefix Intermediate commit for search refactoring --- app/views/editor/level/add_thangs_view.coffee | 4 +- app/views/editor/level/system/add.coffee | 2 +- app/views/editor/level/thangs_tab_view.coffee | 2 +- app/views/kinds/SearchView.coffee | 2 +- .../achievements/achievement_handler.coffee | 6 - server/commons/Handler.coffee | 118 ++++++++++-------- server/routes/db.coffee | 1 - test/server/functional/article.spec.coffee | 31 ++++- 8 files changed, 98 insertions(+), 68 deletions(-) diff --git a/app/views/editor/level/add_thangs_view.coffee b/app/views/editor/level/add_thangs_view.coffee index cf1824201..2a635b2c9 100644 --- a/app/views/editor/level/add_thangs_view.coffee +++ b/app/views/editor/level/add_thangs_view.coffee @@ -4,7 +4,7 @@ ThangType = require 'models/ThangType' CocoCollection = require 'collections/CocoCollection' class ThangTypeSearchCollection extends CocoCollection - url: '/db/thang.type/search?project=true' + url: '/db/thang.type?project=true' model: ThangType addTerm: (term) -> @@ -73,4 +73,4 @@ module.exports = class AddThangsView extends View onEscapePressed: -> @$el.find('input#thang-search').val("") - @runSearch \ No newline at end of file + @runSearch diff --git a/app/views/editor/level/system/add.coffee b/app/views/editor/level/system/add.coffee index 64caa52b4..fb42a866b 100644 --- a/app/views/editor/level/system/add.coffee +++ b/app/views/editor/level/system/add.coffee @@ -5,7 +5,7 @@ LevelSystem = require 'models/LevelSystem' CocoCollection = require 'collections/CocoCollection' class LevelSystemSearchCollection extends CocoCollection - url: '/db/level_system/search' + url: '/db/level_system' model: LevelSystem module.exports = class LevelSystemAddView extends View diff --git a/app/views/editor/level/thangs_tab_view.coffee b/app/views/editor/level/thangs_tab_view.coffee index 22a504480..fb5443460 100644 --- a/app/views/editor/level/thangs_tab_view.coffee +++ b/app/views/editor/level/thangs_tab_view.coffee @@ -21,7 +21,7 @@ componentOriginals = "physics.Physical" : "524b75ad7fc0f6d519000001" class ThangTypeSearchCollection extends CocoCollection - url: '/db/thang.type/search?project=original,name,version,slug,kind,components' + url: '/db/thang.type?project=original,name,version,slug,kind,components' model: ThangType module.exports = class ThangsTabView extends View diff --git a/app/views/kinds/SearchView.coffee b/app/views/kinds/SearchView.coffee index c8f0bc077..8f7a17420 100644 --- a/app/views/kinds/SearchView.coffee +++ b/app/views/kinds/SearchView.coffee @@ -5,7 +5,7 @@ app = require('application') class SearchCollection extends Backbone.Collection initialize: (modelURL, @model, @term, @projection) -> - @url = "#{modelURL}/search?project=" + @url = "#{modelURL}?project=" if @projection? and not (@projection == []) @url += projection[0] @url += ',' + projected for projected in projection[1..] diff --git a/server/achievements/achievement_handler.coffee b/server/achievements/achievement_handler.coffee index 317a6f1b8..fe5f814ba 100644 --- a/server/achievements/achievement_handler.coffee +++ b/server/achievements/achievement_handler.coffee @@ -11,11 +11,5 @@ class AchievementHandler extends Handler hasAccess: (req) -> req.method is 'GET' or req.user?.isAdmin() - get: (req, res) -> - query = @modelClass.find({}) - query.exec (err, documents) => - return @sendDatabaseError(res, err) if err - documents = (@formatEntity(req, doc) for doc in documents) - @sendSuccess(res, documents) module.exports = new AchievementHandler() diff --git a/server/commons/Handler.coffee b/server/commons/Handler.coffee index e8fdbd668..eb8d73ca3 100644 --- a/server/commons/Handler.coffee +++ b/server/commons/Handler.coffee @@ -63,26 +63,72 @@ module.exports = class Handler # generic handlers get: (req, res) -> - # by default, ordinary users never get unfettered access to the database - return @sendUnauthorizedError(res) unless req.user?.isAdmin() + @sendUnauthorizedError(res) if not @hasAccess(req) - # admins can send any sort of query down the wire, though - conditions = JSON.parse(req.query.conditions || '[]') - query = @modelClass.find() + specialParameters = ['term', 'project', 'conditions'] - try - for condition in conditions - name = condition[0] - f = query[name] - args = condition[1..] - query = query[name](args...) - catch e - return @sendError(res, 422, 'Badly formed conditions.') + # If the model uses coco search it's probably a text search + if @modelClass.schema.uses_coco_search + term = req.query.term + matchedObjects = [] + filters = if @modelClass.schema.uses_coco_versions or @modelClass.schema.uses_coco_permissions then [filter: {index: true}] else [filter: {}] + if @modelClass.schema.uses_coco_permissions and req.user + filters.push {filter: {index: req.user.get('id')}} + projection = null + if req.query.project is 'true' + projection = PROJECT + else if req.query.project + if @modelClass.className is 'User' + projection = PROJECT + log.warn "Whoa, we haven't yet thought about public properties for User projection yet." + else + projection = {} + projection[field] = 1 for field in req.query.project.split(',') + for filter in filters + callback = (err, results) => + return @sendDatabaseError(res, err) if err + for r in results.results ? results + obj = r.obj ? r + continue if obj in matchedObjects # TODO: probably need a better equality check + matchedObjects.push obj + filters.pop() # doesn't matter which one + unless filters.length + res.send matchedObjects + res.end() + if term + filter.project = projection + @modelClass.textSearch term, filter, callback + else + args = [filter.filter] + args.push projection if projection + @modelClass.find(args...).limit(FETCH_LIMIT).exec callback + # if it's not a text search but the user is an admin, let him try stuff anyway + else if req.user?.isAdmin() + # admins can send any sort of query down the wire + filter = {} + filter[key] = (val for own key, val of req.query.filter when key not in specialParameters) if 'filter' of req.query + + query = @modelClass.find(filter) + + # Conditions are chained query functions, for example: query.find().limit(20).sort('-dateCreated') + conditions = JSON.parse(req.query.conditions || '[]') + try + for condition in conditions + name = condition[0] + f = query[name] + args = condition[1..] + query = query[name](args...) + catch e + return @sendError(res, 422, 'Badly formed conditions.') + + query.exec (err, documents) => + return @sendDatabaseError(res, err) if err + documents = (@formatEntity(req, doc) for doc in documents) + @sendSuccess(res, documents) + # regular users are only allowed text searches for now, without any additional filters or sorting + else + return @sendUnauthorizedError(res) - query.exec (err, documents) => - return @sendDatabaseError(res, err) if err - documents = (@formatEntity(req, doc) for doc in documents) - @sendSuccess(res, documents) getById: (req, res, id) -> # return @sendNotFoundError(res) # for testing @@ -153,44 +199,6 @@ module.exports = class Handler return @sendDatabaseError(res, err) if err @sendSuccess(res, @formatEntity(req, document)) - # project=true or project=name,description,slug for example - search: (req, res) -> - unless @modelClass.schema.uses_coco_search - return @sendNotFoundError(res) - term = req.query.term - matchedObjects = [] - filters = if @modelClass.schema.uses_coco_versions or @modelClass.schema.uses_coco_permissions then [filter: {index: true}] else [filter: {}] - if @modelClass.schema.uses_coco_permissions and req.user - filters.push {filter: {index: req.user.get('id')}} - projection = null - if req.query.project is 'true' - projection = PROJECT - else if req.query.project - if @modelClass.className is 'User' - projection = PROJECT - log.warn "Whoa, we haven't yet thought about public properties for User projection yet." - else - projection = {} - projection[field] = 1 for field in req.query.project.split(',') - for filter in filters - callback = (err, results) => - return @sendDatabaseError(res, err) if err - for r in results.results ? results - obj = r.obj ? r - continue if obj in matchedObjects # TODO: probably need a better equality check - matchedObjects.push obj - filters.pop() # doesn't matter which one - unless filters.length - res.send matchedObjects - res.end() - if term - filter.project = projection - @modelClass.textSearch term, filter, callback - else - args = [filter.filter] - args.push projection if projection - @modelClass.find(args...).limit(FETCH_LIMIT).exec callback - versions: (req, res, id) -> # TODO: a flexible system for doing GAE-like cursors for these sort of paginating queries # Keeping it simple for now and just allowing access to the first FETCH_LIMIT results. diff --git a/server/routes/db.coffee b/server/routes/db.coffee index f65a56744..cddc70592 100644 --- a/server/routes/db.coffee +++ b/server/routes/db.coffee @@ -34,7 +34,6 @@ module.exports.setup = (app) -> return handler.getLatestVersion(req, res, parts[1], parts[3]) if parts[2] is 'version' return handler.versions(req, res, parts[1]) if parts[2] is 'versions' return handler.files(req, res, parts[1]) if parts[2] is 'files' - return handler.search(req, res) if req.route.method is 'get' and parts[1] is 'search' return handler.getNamesByIDs(req, res) if req.route.method in ['get', 'post'] and parts[1] is 'names' return handler.getByRelationship(req, res, parts[1..]...) if parts.length > 2 return handler.getById(req, res, parts[1]) if req.route.method is 'get' and parts[1]? diff --git a/test/server/functional/article.spec.coffee b/test/server/functional/article.spec.coffee index a3973007b..84c414a86 100644 --- a/test/server/functional/article.spec.coffee +++ b/test/server/functional/article.spec.coffee @@ -92,4 +92,33 @@ describe '/db/article', -> expect(res.statusCode).toBe(422) done() - \ No newline at end of file + it 'allows regular users to get all articles', (done) -> + loginJoe -> + request.get {uri:url}, (err, res, body) -> + expect(res.statusCode).toBe(200) + expect(body.length).toBe(2) + + it 'allows regular users to get articles and use projection', (done) -> + loginJoe -> + # default projection + request.get {uri:url + '?project=true'}, (err, res, body) -> + expect(res.statusCode).toBe(200) + expect(body.length).toBe(2) + expect(body[0].created?).toBeUndefined() + expect(body[0].version?).toBeDefined() + + # custom projection + request.get {uri:url + '?project=original'}, (err, res, body) -> + expect(res.statusCode).toBe(200) + expect(body.length).toBe(2) + expect(Object.keys(body[0]).length).toBe(2) + expect(body[0].original).toBeDefined() + done() + + it 'allows regular users to perform a text search', (done) -> + loginJoe -> + request.get {uri:url + 'term="friend"'}, (err, res, body) -> + expect(res.statusCode).toBe(200) + expect(body.length).toBe(1) + # expect name blabla + done() From fef72d82737c7560d18b63c4a4d3939bec8a0709 Mon Sep 17 00:00:00 2001 From: Ruben Vereecken Date: Thu, 12 Jun 2014 19:19:18 +0200 Subject: [PATCH 05/22] Finished Article tests to demonstrate search functionality --- server/plugins/plugins.coffee | 2 +- test/server/functional/article.spec.coffee | 37 +++++++++++++++------- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/server/plugins/plugins.coffee b/server/plugins/plugins.coffee index 77290ad90..25674f688 100644 --- a/server/plugins/plugins.coffee +++ b/server/plugins/plugins.coffee @@ -23,7 +23,7 @@ module.exports.PatchablePlugin = (schema) -> schema.is_patchable = true schema.index({'target.original':1, 'status':'1', 'created':-1}) -RESERVED_NAMES = ['search', 'names'] +RESERVED_NAMES = ['names'] module.exports.NamedPlugin = (schema) -> schema.uses_coco_names = true diff --git a/test/server/functional/article.spec.coffee b/test/server/functional/article.spec.coffee index 84c414a86..b96df0f4c 100644 --- a/test/server/functional/article.spec.coffee +++ b/test/server/functional/article.spec.coffee @@ -8,6 +8,8 @@ describe '/db/article', -> done() article = {name: 'Yo', body:'yo ma'} + article2 = {name: 'Original', body:'yo daddy'} + url = getURL('/db/article') articles = {} @@ -27,11 +29,22 @@ describe '/db/article', -> expect(body.original).toBeDefined() expect(body.creator).toBeDefined() articles[0] = body - done() + + # Having two articles allow for testing article search and such + request.post {uri:url, json:article2}, (err, res, body) -> + expect(res.statusCode).toBe(200) + expect(body.slug).toBeDefined() + expect(body.body).toBeDefined() + expect(body.name).toBeDefined() + expect(body.original).toBeDefined() + expect(body.creator).toBeDefined() + articles[0] = body + + done() it 'allows admins to make new minor versions', (done) -> new_article = _.clone(articles[0]) - new_article.body = '...' + new_article.body = 'yo daddy' request.post {uri:url, json:new_article}, (err, res, body) -> expect(res.statusCode).toBe(200) expect(body.version.major).toBe(0) @@ -61,7 +74,6 @@ describe '/db/article', -> expect(res.statusCode).toBe(200) expect(body.body).toBe(articles[0].body) done() - it 'does not allow regular users to make new versions', (done) -> new_article = _.clone(articles[2]) @@ -87,28 +99,29 @@ describe '/db/article', -> it 'does not allow naming an article a reserved word', (done) -> loginAdmin -> - new_article = {name: 'Search', body:'is a reserved word'} + new_article = {name: 'Names', body:'is a reserved word'} request.post {uri:url, json:new_article}, (err, res, body) -> expect(res.statusCode).toBe(422) done() it 'allows regular users to get all articles', (done) -> loginJoe -> - request.get {uri:url}, (err, res, body) -> + request.get {uri:url, json:{}}, (err, res, body) -> expect(res.statusCode).toBe(200) expect(body.length).toBe(2) + done() it 'allows regular users to get articles and use projection', (done) -> loginJoe -> # default projection - request.get {uri:url + '?project=true'}, (err, res, body) -> + request.get {uri:url + '?project=true', json:{}}, (err, res, body) -> expect(res.statusCode).toBe(200) expect(body.length).toBe(2) - expect(body[0].created?).toBeUndefined() - expect(body[0].version?).toBeDefined() + expect(body[0].created).toBeUndefined() + expect(body[0].version).toBeDefined() # custom projection - request.get {uri:url + '?project=original'}, (err, res, body) -> + request.get {uri:url + '?project=original', json:{}}, (err, res, body) -> expect(res.statusCode).toBe(200) expect(body.length).toBe(2) expect(Object.keys(body[0]).length).toBe(2) @@ -117,8 +130,10 @@ describe '/db/article', -> it 'allows regular users to perform a text search', (done) -> loginJoe -> - request.get {uri:url + 'term="friend"'}, (err, res, body) -> + request.get {uri:url + '?term="daddy"', json:{}}, (err, res, body) -> expect(res.statusCode).toBe(200) expect(body.length).toBe(1) - # expect name blabla + expect(body[0].name).toBe(article2.name) + expect(body[0].body).toBe(article2.body) done() + From b0666ace0ded0d179ccc3000615434ab99cc95ff Mon Sep 17 00:00:00 2001 From: Ruben Vereecken Date: Tue, 20 May 2014 12:46:37 +0200 Subject: [PATCH 06/22] Refactorings performed necessary to have searchable for more uses --- app/locale/en.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/locale/en.coffee b/app/locale/en.coffee index a96706069..ee7e618e0 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -507,7 +507,7 @@ new_thang_title_login: "Log In to Create a New Thang Type" new_level_title_login: "Log In to Create a New Level" new_achievement_title: "Create a New Achievement" - new_achievement_title_login: "Sign Up to Create a New Achievement" + new_achievement_title_login: "Log In to Create a New Achievement" article_search_title: "Search Articles Here" thang_search_title: "Search Thang Types Here" level_search_title: "Search Levels Here" From 2c04025a5a49d5295541c906e92fa7c235fa9888 Mon Sep 17 00:00:00 2001 From: Ruben Vereecken Date: Thu, 29 May 2014 11:10:57 +0200 Subject: [PATCH 07/22] Created a test environment for repeatables --- app/schemas/models/achievement.coffee | 5 ----- app/schemas/models/user.coffee | 2 ++ app/views/kinds/RootView.coffee | 11 +++++++++++ server/users/user_handler.coffee | 3 ++- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/app/schemas/models/achievement.coffee b/app/schemas/models/achievement.coffee index c064e969f..a6bf3ec74 100644 --- a/app/schemas/models/achievement.coffee +++ b/app/schemas/models/achievement.coffee @@ -57,17 +57,12 @@ _.extend(AchievementSchema.properties, type: 'object' properties: a: {type: 'number', default: 1}, - required: ['a'] - description: 'f(x) = a * x' logarithmic: type:'object' properties: a: {type: 'number', default: 1} b: {type: 'number', default: 1} - required: ['a', 'b'] - description: 'f(x) = a * ln(1/b * (x + b))' ] - default: linear: a: 1 ) AchievementSchema.definitions = {} diff --git a/app/schemas/models/user.coffee b/app/schemas/models/user.coffee index 623cf58f4..a64b7fdb7 100644 --- a/app/schemas/models/user.coffee +++ b/app/schemas/models/user.coffee @@ -127,4 +127,6 @@ c.definitions = lastSent: c.date() count: {type: 'integer'} +UserSchema.additionalProperties = true # TODO Ruben seriously remove this + module.exports = UserSchema diff --git a/app/views/kinds/RootView.coffee b/app/views/kinds/RootView.coffee index 5cd547c8a..c84d1a596 100644 --- a/app/views/kinds/RootView.coffee +++ b/app/views/kinds/RootView.coffee @@ -8,6 +8,7 @@ locale = require 'locale/locale' Achievement = require '../../models/Achievement' User = require '../../models/User' +# TODO remove filterKeyboardEvents = (allowedEvents, func) -> return (splat...) -> @@ -28,6 +29,16 @@ module.exports = class RootView extends CocoView initialize: -> $ => # TODO Ruben remove this. Allows for easy testing right now though + btn = $('Increment') + input = $('') + $('body').append(btn) + $('body').append(input) + btn.on 'click', (e) => + val = input.val() + me.set(val, me.get(val) + 1) + console.debug me.get(val) + me.save() + #test = new Achievement(_id:'537ce4855c91b8d1dda7fda8') #test.fetch(success:@showNewAchievement) diff --git a/server/users/user_handler.coffee b/server/users/user_handler.coffee index 5ffa68c06..db4ede7be 100644 --- a/server/users/user_handler.coffee +++ b/server/users/user_handler.coffee @@ -24,8 +24,9 @@ candidateProperties = [ UserHandler = class UserHandler extends Handler modelClass: User + # TODO Ruben change this back! Really really do. Don't you dare forget. NO COOKIES editableProperties: [ - 'name', 'photoURL', 'password', 'anonymous', 'wizardColor1', 'volume', + 'name', 'photoURL', 'password', 'anonymous', 'wizardColor1', 'volume', 'simulatedBy', 'firstName', 'lastName', 'gender', 'facebookID', 'gplusID', 'emails', 'testGroupNumber', 'music', 'hourOfCode', 'hourOfCodeComplete', 'preferredLanguage', 'wizard', 'aceConfig', 'autocastDelay', 'lastLevel', 'jobProfile' From 5aa211cabde98fea873ed24dcd59a90e5a906229 Mon Sep 17 00:00:00 2001 From: Ruben Vereecken Date: Sat, 31 May 2014 23:19:55 +0200 Subject: [PATCH 08/22] Got repeatables working flawlessly and improved the achievement popup by a great deal --- app/schemas/models/achievement.coffee | 5 +++++ app/schemas/models/user.coffee | 2 -- app/styles/notify.sass | 3 +++ app/views/editor/achievement/edit.coffee | 1 + app/views/editor/achievement/home.coffee | 7 +++++++ app/views/kinds/RootView.coffee | 5 ++--- 6 files changed, 18 insertions(+), 5 deletions(-) diff --git a/app/schemas/models/achievement.coffee b/app/schemas/models/achievement.coffee index a6bf3ec74..c064e969f 100644 --- a/app/schemas/models/achievement.coffee +++ b/app/schemas/models/achievement.coffee @@ -57,12 +57,17 @@ _.extend(AchievementSchema.properties, type: 'object' properties: a: {type: 'number', default: 1}, + required: ['a'] + description: 'f(x) = a * x' logarithmic: type:'object' properties: a: {type: 'number', default: 1} b: {type: 'number', default: 1} + required: ['a', 'b'] + description: 'f(x) = a * ln(1/b * (x + b))' ] + default: linear: a: 1 ) AchievementSchema.definitions = {} diff --git a/app/schemas/models/user.coffee b/app/schemas/models/user.coffee index a64b7fdb7..623cf58f4 100644 --- a/app/schemas/models/user.coffee +++ b/app/schemas/models/user.coffee @@ -127,6 +127,4 @@ c.definitions = lastSent: c.date() count: {type: 'integer'} -UserSchema.additionalProperties = true # TODO Ruben seriously remove this - module.exports = UserSchema diff --git a/app/styles/notify.sass b/app/styles/notify.sass index 0a0b47cf1..abf50bb70 100644 --- a/app/styles/notify.sass +++ b/app/styles/notify.sass @@ -45,3 +45,6 @@ font-family: Bangers font-size: 16px float: right + +.progress-bar-white + background-color: white diff --git a/app/views/editor/achievement/edit.coffee b/app/views/editor/achievement/edit.coffee index 186eb206d..6caaa2ae5 100644 --- a/app/views/editor/achievement/edit.coffee +++ b/app/views/editor/achievement/edit.coffee @@ -46,6 +46,7 @@ module.exports = class AchievementEditView extends View @treema = @$el.find('#achievement-treema').treema(options) @treema.build() + console.log @treema pushChangesToPreview: => 'TODO' # TODO might want some intrinsic preview thing diff --git a/app/views/editor/achievement/home.coffee b/app/views/editor/achievement/home.coffee index 37bcb2795..789da6e60 100644 --- a/app/views/editor/achievement/home.coffee +++ b/app/views/editor/achievement/home.coffee @@ -8,6 +8,13 @@ module.exports = class AchievementSearchView extends SearchView tableTemplate: require 'templates/editor/achievement/table' projection: ['name', 'description', 'collection', 'slug'] + initialize: -> + console.log me.isAdmin() + unless me.isAdmin() + NotFoundView = require '../../not_found' + return new NotFoundView + else super() + getRenderData: -> context = super() context.currentEditor = 'editor.achievement_title' diff --git a/app/views/kinds/RootView.coffee b/app/views/kinds/RootView.coffee index c84d1a596..6e78823c0 100644 --- a/app/views/kinds/RootView.coffee +++ b/app/views/kinds/RootView.coffee @@ -38,9 +38,8 @@ module.exports = class RootView extends CocoView me.set(val, me.get(val) + 1) console.debug me.get(val) me.save() - - #test = new Achievement(_id:'537ce4855c91b8d1dda7fda8') - #test.fetch(success:@showNewAchievement) + test = new Achievement(_id:'537ce4855c91b8d1dda7fda8') + test.fetch(success:@showNewAchievement) showNewAchievement: (achievement) -> currentLevel = me.level() From 7096a07ce81a81db83397755e3921216e934a5bc Mon Sep 17 00:00:00 2001 From: Ruben Vereecken Date: Sat, 31 May 2014 23:55:26 +0200 Subject: [PATCH 09/22] Improved logging (winston) and admin-only'd the achievement editor --- app/templates/kinds/search.jade | 1 + app/views/editor/achievement/edit.coffee | 1 - app/views/editor/achievement/home.coffee | 7 ------- app/views/kinds/RootView.coffee | 4 ++-- 4 files changed, 3 insertions(+), 10 deletions(-) diff --git a/app/templates/kinds/search.jade b/app/templates/kinds/search.jade index bbf7de514..59a9c880c 100644 --- a/app/templates/kinds/search.jade +++ b/app/templates/kinds/search.jade @@ -37,6 +37,7 @@ block content h3(data-i18n="play_level.tip_reticulating") Reticulating Splines... .progress.progress-striped.active .progress-bar + else .alert.alert-danger span Admin only. Turn around. diff --git a/app/views/editor/achievement/edit.coffee b/app/views/editor/achievement/edit.coffee index 6caaa2ae5..186eb206d 100644 --- a/app/views/editor/achievement/edit.coffee +++ b/app/views/editor/achievement/edit.coffee @@ -46,7 +46,6 @@ module.exports = class AchievementEditView extends View @treema = @$el.find('#achievement-treema').treema(options) @treema.build() - console.log @treema pushChangesToPreview: => 'TODO' # TODO might want some intrinsic preview thing diff --git a/app/views/editor/achievement/home.coffee b/app/views/editor/achievement/home.coffee index 789da6e60..37bcb2795 100644 --- a/app/views/editor/achievement/home.coffee +++ b/app/views/editor/achievement/home.coffee @@ -8,13 +8,6 @@ module.exports = class AchievementSearchView extends SearchView tableTemplate: require 'templates/editor/achievement/table' projection: ['name', 'description', 'collection', 'slug'] - initialize: -> - console.log me.isAdmin() - unless me.isAdmin() - NotFoundView = require '../../not_found' - return new NotFoundView - else super() - getRenderData: -> context = super() context.currentEditor = 'editor.achievement_title' diff --git a/app/views/kinds/RootView.coffee b/app/views/kinds/RootView.coffee index 6e78823c0..994b85b40 100644 --- a/app/views/kinds/RootView.coffee +++ b/app/views/kinds/RootView.coffee @@ -38,8 +38,8 @@ module.exports = class RootView extends CocoView me.set(val, me.get(val) + 1) console.debug me.get(val) me.save() - test = new Achievement(_id:'537ce4855c91b8d1dda7fda8') - test.fetch(success:@showNewAchievement) + #test = new Achievement(_id:'537ce4855c91b8d1dda7fda8') + #test.fetch(success:@showNewAchievement) showNewAchievement: (achievement) -> currentLevel = me.level() From a61d0e5569bd89142633a282fd180af2628f4b3d Mon Sep 17 00:00:00 2001 From: Ruben Vereecken Date: Tue, 3 Jun 2014 12:40:47 +0200 Subject: [PATCH 10/22] Added support for diminished exp for repeatables. Needs tweaking though. --- app/lib/utils.coffee | 9 ++++++++ app/models/Achievement.coffee | 10 +++++++- app/schemas/models/achievement.coffee | 19 ++++++--------- app/schemas/models/earned_achievement.coffee | 19 ++++++--------- app/views/kinds/RootView.coffee | 23 ++++++++++--------- server/achievements/Achievement.coffee | 11 +++++++-- server/achievements/EarnedAchievement.coffee | 6 ++++- .../achievements/achievement_handler.coffee | 2 +- server/plugins/achievements.coffee | 11 +++++---- 9 files changed, 66 insertions(+), 44 deletions(-) diff --git a/app/lib/utils.coffee b/app/lib/utils.coffee index 78816990f..5a8ae8915 100644 --- a/app/lib/utils.coffee +++ b/app/lib/utils.coffee @@ -75,3 +75,12 @@ module.exports.getByPath = (target, path) -> return undefined unless piece of obj obj = obj[piece] obj + +module.exports.round = _.curry (digits, n) -> + n = +n.toFixed(digits) + +module.exports.createLinearFunc = (params) -> + (x) -> (params.a or 1) * x + (params.b or 0) + +module.exports.createLogFunc = (params) -> + (x) -> (params.a or 1) * Math.log((params.b or 1) * (x + (params.c or 0))) diff --git a/app/models/Achievement.coffee b/app/models/Achievement.coffee index 5777f7bd5..b2a6978ab 100644 --- a/app/models/Achievement.coffee +++ b/app/models/Achievement.coffee @@ -1,4 +1,5 @@ CocoModel = require './CocoModel' +util = require '../lib/utils' module.exports = class Achievement extends CocoModel @className: 'Achievement' @@ -6,4 +7,11 @@ module.exports = class Achievement extends CocoModel urlRoot: '/db/achievement' isRepeatable: -> - @get('proportionalTo')? \ No newline at end of file + @get('proportionalTo')? + + # TODO logic is duplicated in Mongoose Achievement schema + getExpFunction: -> + kind = @get('function')?.kind or @schema.function.default.kind + parameters = @get('function')?.parameters or @schema.function.default.parameters + funcCreator = if kind is 'linear' then util.createLinearFunc else if kind is 'logarithmic' then utils.createLogFunc + return funcCreator(parameters) if funcCreator? diff --git a/app/schemas/models/achievement.coffee b/app/schemas/models/achievement.coffee index c064e969f..d02e9c8fd 100644 --- a/app/schemas/models/achievement.coffee +++ b/app/schemas/models/achievement.coffee @@ -52,22 +52,17 @@ _.extend(AchievementSchema.properties, description: 'For repeatables only. Denotes the field a repeatable achievement needs for its calculations' function: type: 'object' - oneOf: [ - linear: + properties: + kind: {enum: ['linear', 'logarithmic'], default: 'linear'} + parameters: type: 'object' - properties: - a: {type: 'number', default: 1}, - required: ['a'] - description: 'f(x) = a * x' - logarithmic: - type:'object' properties: a: {type: 'number', default: 1} b: {type: 'number', default: 1} - required: ['a', 'b'] - description: 'f(x) = a * ln(1/b * (x + b))' - ] - default: linear: a: 1 + c: {type: 'number', default: 1} + default: {kind: 'linear', parameters: a: 1} + required: ['kind', 'parameters'] + additionalProperties: false ) AchievementSchema.definitions = {} diff --git a/app/schemas/models/earned_achievement.coffee b/app/schemas/models/earned_achievement.coffee index 9b2c50c19..f976be719 100644 --- a/app/schemas/models/earned_achievement.coffee +++ b/app/schemas/models/earned_achievement.coffee @@ -20,15 +20,10 @@ module.exports = href: '/db/achievement/{($)}' } ] - collection: - type: 'string' - achievementName: - type: 'string' - created: - type: 'date' - changed: - type: 'date' - achievedAmount: - type: 'number' - notified: - type: 'boolean' \ No newline at end of file + collection: type: 'string' + achievementName: type: 'string' + created: type: 'date' + changed: type: 'date' + achievedAmount: type: 'number' + previouslyAchievedAmount: {type: 'number', default: 0} + notified: type: 'boolean' diff --git a/app/views/kinds/RootView.coffee b/app/views/kinds/RootView.coffee index 994b85b40..4824113b5 100644 --- a/app/views/kinds/RootView.coffee +++ b/app/views/kinds/RootView.coffee @@ -41,20 +41,23 @@ module.exports = class RootView extends CocoView #test = new Achievement(_id:'537ce4855c91b8d1dda7fda8') #test.fetch(success:@showNewAchievement) - showNewAchievement: (achievement) -> + showNewAchievement: (achievement, earnedAchievement) -> currentLevel = me.level() nextLevel = currentLevel + 1 currentLevelExp = User.expForLevel(currentLevel) nextLevelExp = User.expForLevel(nextLevel) totalExpNeeded = nextLevelExp - currentLevelExp + expFunction = achievement.getExpFunction() currentExp = me.get('points') - worth = achievement.get('worth') - leveledUp = currentExp - worth < currentLevelExp - alreadyAchievedPercentage = 100 * (currentExp - currentLevelExp - worth) / totalExpNeeded - newlyAchievedPercentage = if currentLevelExp is currentExp then 0 else 100 * worth / totalExpNeeded + previousExp = currentExp - achievement.get('worth') + previousExp = expFunction(earnedAchievement.get('previouslyAchievedAmount')) * achievement.get('worth') if achievement.isRepeatable() + achievedExp = currentExp - previousExp + leveledUp = currentExp - achievedExp < currentLevelExp + alreadyAchievedPercentage = 100 * (previousExp - currentLevelExp) / totalExpNeeded + newlyAchievedPercentage = if leveledUp then 100 * (currentExp - currentLevelExp) / totalExpNeeded else 100 * achievedExp / totalExpNeeded console.debug "Current level is #{currentLevel} (#{currentLevelExp} xp), next level is #{nextLevel} (#{nextLevelExp} xp)." - console.debug "Need a total of #{nextLevelExp - currentLevelExp}, already had #{currentExp - currentLevelExp - worth} and just now earned #{worth} totalling on #{currentExp}" + console.debug "Need a total of #{nextLevelExp - currentLevelExp}, already had #{previousExp} and just now earned #{achievedExp} totalling on #{currentExp}" alreadyAchievedBar = $("
") newlyAchievedBar = $("
") @@ -63,7 +66,7 @@ module.exports = class RootView extends CocoView message = if (currentLevel isnt 1) and leveledUp then "Reached level #{currentLevel}!" else null alreadyAchievedBar.tooltip(title: "#{currentExp} XP in total") - newlyAchievedBar.tooltip(title: "#{worth} XP earned") + newlyAchievedBar.tooltip(title: "#{achievedExp} XP earned") emptyBar.tooltip(title: "#{nextLevelExp - currentExp} XP until level #{nextLevel}") # TODO a default should be linked here @@ -73,7 +76,7 @@ module.exports = class RootView extends CocoView image: $("") description: achievement.get('description') progressBar: progressBar - earnedExp: "+ #{worth} XP" + earnedExp: "+ #{achievedExp} XP" message: message options = @@ -87,13 +90,11 @@ module.exports = class RootView extends CocoView $.notify( data, options ) handleNewAchievements: (earnedAchievements) -> - console.debug 'Got new earned achievements' - # TODO performance? _.each(earnedAchievements.models, (earnedAchievement) => achievement = new Achievement(_id: earnedAchievement.get('achievement')) console.log achievement achievement.fetch( - success: @showNewAchievement + success: (achievement) => @showNewAchievement(achievement, earnedAchievement) ) ) diff --git a/server/achievements/Achievement.coffee b/server/achievements/Achievement.coffee index 2f99443b5..5dcc19d8c 100644 --- a/server/achievements/Achievement.coffee +++ b/server/achievements/Achievement.coffee @@ -1,6 +1,7 @@ mongoose = require('mongoose') jsonschema = require('../../app/schemas/models/achievement') log = require 'winston' +util = require '../../app/lib/utils' # `pre` and `post` are not called for update operations executed directly on the database, # including `Model.update`,`.findByIdAndUpdate`,`.findOneAndUpdate`, `.findOneAndRemove`,and `.findByIdAndRemove`.order @@ -11,16 +12,22 @@ AchievementSchema = new mongoose.Schema({ userField: String }, {strict: false}) -AchievementSchema.methods.objectifyQuery = () -> +AchievementSchema.methods.objectifyQuery = -> try @set('query', JSON.parse(@get('query'))) if typeof @get('query') == "string" catch error log.error "Couldn't convert query string to object because of #{error}" @set('query', {}) -AchievementSchema.methods.stringifyQuery = () -> +AchievementSchema.methods.stringifyQuery = -> @set('query', JSON.stringify(@get('query'))) if typeof @get('query') != "string" +AchievementSchema.methods.getExpFunction = -> + kind = @get('function.kind') or jsonschema.function.default.kind + parameters = @get('function.parameters') or jsonschema.function.default.parameters + funcCreator = if kind is 'linear' then util.createLinearFunc else if kind is 'logarithmic' then util.createLogFunc + return funcCreator(parameters) if funcCreator? + AchievementSchema.post('init', (doc) -> doc.objectifyQuery()) AchievementSchema.pre('save', (next) -> diff --git a/server/achievements/EarnedAchievement.coffee b/server/achievements/EarnedAchievement.coffee index c4f017e38..685a502c6 100644 --- a/server/achievements/EarnedAchievement.coffee +++ b/server/achievements/EarnedAchievement.coffee @@ -13,7 +13,11 @@ EarnedAchievementSchema = new mongoose.Schema({ default: false }, {strict:false}) +EarnedAchievementSchema.pre 'save', (next) -> + @set('changed', Date.now()) + next() + EarnedAchievementSchema.index({user: 1, achievement: 1}, {unique: true, name: 'earned achievement index'}) EarnedAchievementSchema.index({user: 1, changed: -1}, {name: 'latest '}) -module.exports = EarnedAchievement = mongoose.model('EarnedAchievement', EarnedAchievementSchema) \ No newline at end of file +module.exports = EarnedAchievement = mongoose.model('EarnedAchievement', EarnedAchievementSchema) diff --git a/server/achievements/achievement_handler.coffee b/server/achievements/achievement_handler.coffee index 317a6f1b8..fb344742c 100644 --- a/server/achievements/achievement_handler.coffee +++ b/server/achievements/achievement_handler.coffee @@ -5,7 +5,7 @@ class AchievementHandler extends Handler modelClass: Achievement # Used to determine which properties requests may edit - editableProperties: ['name', 'query', 'worth', 'collection', 'description', 'userField', 'proportionalTo', 'icon'] + editableProperties: ['name', 'query', 'worth', 'collection', 'description', 'userField', 'proportionalTo', 'icon', 'function'] jsonSchema = require '../../app/schemas/models/achievement.coffee' hasAccess: (req) -> diff --git a/server/plugins/achievements.coffee b/server/plugins/achievements.coffee index 2292e81e4..a684b2384 100644 --- a/server/plugins/achievements.coffee +++ b/server/plugins/achievements.coffee @@ -54,6 +54,8 @@ module.exports = AchievablePlugin = (schema, options) -> achievement: achievement._id.toHexString() achievementName: achievement.get 'name' } + + worth = achievement.get('worth') earnedPoints = 0 wrapUp = -> # Update user's experience points @@ -68,22 +70,23 @@ module.exports = AchievablePlugin = (schema, options) -> newAmount = docObj[proportionalTo] if originalAmount isnt newAmount + expFunction = achievement.getExpFunction() earned.notified = false earned.achievedAmount = newAmount - earned.changed = Date.now() + earned.previouslyAchievedAmount = originalAmount EarnedAchievement.findOneAndUpdate({achievement:earned.achievement, user:earned.user}, earned, upsert:true, (err, docs) -> return log.debug err if err? ) - earnedPoints = achievement.get('worth') * (newAmount - originalAmount) + earnedPoints = (expFunction(newAmount) - expFunction(originalAmount)) * worth + log.debug earnedPoints wrapUp() else # not alreadyAchieved log.debug 'Creating a new earned achievement called \'' + (achievement.get 'name') + '\' for ' + userID (new EarnedAchievement(earned)).save (err, doc) -> return log.debug err if err? - - earnedPoints = achievement.get('worth') + earnedPoints = worth wrapUp() delete before[doc.id] unless isNew # This assumes everything we patch has a _id From bea751eed9e94f5aabdcb52d1b74c0f84328cbfa Mon Sep 17 00:00:00 2001 From: Ruben Vereecken Date: Tue, 3 Jun 2014 16:14:10 +0200 Subject: [PATCH 11/22] Laid down the basics for an admin server endpoint --- server/achievements/EarnedAchievement.coffee | 6 +++++ .../earned_achievement_handler.coffee | 4 ++++ server/commons/mapping.coffee | 1 + server/routes/admin.coffee | 24 +++++++++++++++++++ 4 files changed, 35 insertions(+) create mode 100644 server/routes/admin.coffee diff --git a/server/achievements/EarnedAchievement.coffee b/server/achievements/EarnedAchievement.coffee index 685a502c6..088a919b3 100644 --- a/server/achievements/EarnedAchievement.coffee +++ b/server/achievements/EarnedAchievement.coffee @@ -20,4 +20,10 @@ EarnedAchievementSchema.pre 'save', (next) -> EarnedAchievementSchema.index({user: 1, achievement: 1}, {unique: true, name: 'earned achievement index'}) EarnedAchievementSchema.index({user: 1, changed: -1}, {name: 'latest '}) +EarnedAchievementSchema.static 'recalculate', (callback) -> + callback('pass') + module.exports = EarnedAchievement = mongoose.model('EarnedAchievement', EarnedAchievementSchema) + + + diff --git a/server/achievements/earned_achievement_handler.coffee b/server/achievements/earned_achievement_handler.coffee index ebab1e45d..8869f5c6d 100644 --- a/server/achievements/earned_achievement_handler.coffee +++ b/server/achievements/earned_achievement_handler.coffee @@ -1,3 +1,4 @@ +log = require 'winston' mongoose = require('mongoose') EarnedAchievement = require './EarnedAchievement' Handler = require '../commons/Handler' @@ -9,4 +10,7 @@ class EarnedAchievementHandler extends Handler hasAccess: (req) -> req.method is 'GET' + recalculate: (req, res) -> + EarnedAchievement.recalculate (data) => @sendSuccess(res, data) + module.exports = new EarnedAchievementHandler() diff --git a/server/commons/mapping.coffee b/server/commons/mapping.coffee index 98efbad6a..4b859632c 100644 --- a/server/commons/mapping.coffee +++ b/server/commons/mapping.coffee @@ -14,6 +14,7 @@ module.exports.handlers = module.exports.routes = [ + 'routes/admin' 'routes/auth' 'routes/contact' 'routes/db' diff --git a/server/routes/admin.coffee b/server/routes/admin.coffee new file mode 100644 index 000000000..012f0d865 --- /dev/null +++ b/server/routes/admin.coffee @@ -0,0 +1,24 @@ +log = require 'winston' +errors = require '../commons/errors' +handlers = require('../commons/mapping').handlers + +module.exports.setup = (app) -> + app.all '/admin/*', (req, res) -> + res.setHeader('Content-Type', 'application/json') + + module = req.path[7..] + parts = module.split('/') + module = parts[0] + + return errors.unauthorized(res, 'Must be admin to access this area.') unless req.user?.isAdmin() + + try + moduleName = module.replace '.', '_' + name = handlers[moduleName] + handler = require('../' + name) + + return handler[parts[1]](req, res, parts[2..]...) if parts[1] of handler + + catch error + log.error("Error trying db method '#{req.route.method}' route '#{parts}' from #{name}: #{error}") + errors.notFound(res, "Route #{req.path} not found.") From bd2b2899438aaf0d55398fc103fd0aab4d0efa0b Mon Sep 17 00:00:00 2001 From: Ruben Vereecken Date: Tue, 3 Jun 2014 22:54:56 +0200 Subject: [PATCH 12/22] Recalculate script mainly finished --- app/schemas/models/earned_achievement.coffee | 1 + server/achievements/EarnedAchievement.coffee | 3 +- .../earned_achievement_handler.coffee | 94 ++++++++++++++++++- server/plugins/achievements.coffee | 9 +- server/routes/admin.coffee | 5 +- 5 files changed, 102 insertions(+), 10 deletions(-) diff --git a/app/schemas/models/earned_achievement.coffee b/app/schemas/models/earned_achievement.coffee index f976be719..e250834d4 100644 --- a/app/schemas/models/earned_achievement.coffee +++ b/app/schemas/models/earned_achievement.coffee @@ -25,5 +25,6 @@ module.exports = created: type: 'date' changed: type: 'date' achievedAmount: type: 'number' + earnedPoints: type: 'number' previouslyAchievedAmount: {type: 'number', default: 0} notified: type: 'boolean' diff --git a/server/achievements/EarnedAchievement.coffee b/server/achievements/EarnedAchievement.coffee index 088a919b3..e02174c29 100644 --- a/server/achievements/EarnedAchievement.coffee +++ b/server/achievements/EarnedAchievement.coffee @@ -1,5 +1,6 @@ mongoose = require 'mongoose' jsonschema = require '../../app/schemas/models/earned_achievement' +User = require '../users/User' EarnedAchievementSchema = new mongoose.Schema({ created: @@ -20,8 +21,6 @@ EarnedAchievementSchema.pre 'save', (next) -> EarnedAchievementSchema.index({user: 1, achievement: 1}, {unique: true, name: 'earned achievement index'}) EarnedAchievementSchema.index({user: 1, changed: -1}, {name: 'latest '}) -EarnedAchievementSchema.static 'recalculate', (callback) -> - callback('pass') module.exports = EarnedAchievement = mongoose.model('EarnedAchievement', EarnedAchievementSchema) diff --git a/server/achievements/earned_achievement_handler.coffee b/server/achievements/earned_achievement_handler.coffee index 8869f5c6d..80570b8d0 100644 --- a/server/achievements/earned_achievement_handler.coffee +++ b/server/achievements/earned_achievement_handler.coffee @@ -1,16 +1,104 @@ log = require 'winston' -mongoose = require('mongoose') +mongoose = require 'mongoose' +async = require 'async' +Achievement = require './Achievement' EarnedAchievement = require './EarnedAchievement' +User = require '../users/User' Handler = require '../commons/Handler' +LocalMongo = require '../../app/lib/LocalMongo' class EarnedAchievementHandler extends Handler modelClass: EarnedAchievement # Don't allow POSTs or anything yet hasAccess: (req) -> - req.method is 'GET' + req.method is 'GET' # or req.user.isAdmin() recalculate: (req, res) -> - EarnedAchievement.recalculate (data) => @sendSuccess(res, data) + onSuccess = (data) => @sendSuccess(res, data) + if 'achievements' of req.query # Support both slugs and IDs separated by commas + achievementSlugsOrIDs = req.query.id.split(',') + EarnedAchievementHandler.recalculate achievementSlugsOrIDs, onSuccess + else + EarnedAchievementHandler.recalculate onSuccess + @sendSuccess res + + # Returns success: boolean + @recalculate: (callbackOrSlugsOrIDs, callback) -> + if _.isArray callbackOrSlugsOrIDs + achievementSlugs = (thing unless Handler.isID(thing) for thing in callbackOrSlugsOrIDs) + achievementIDs = (thing if Handler.isID(thing) for thing in callbackOrSlugsOrIDs) + else + callback = callbackOrSlugsOrIDs + + filter = {} + filter.$or = [ + _id: $in: achievementIDs + slug: $in: achievementSlugs + ] if achievementSlugs? or achievementIDs? + + Achievement.find filter, (err, achievements) -> + return false and log.error err if err? + User.find {}, (err, users) -> + _.each users, (user) -> + # Keep track of a user's already achieved so as to set the notified values correctly + userID = user.get('_id').toHexString() + EarnedAchievement.find {user: userID}, (err, alreadyEarned) -> + alreadyEarnedIDs = [] + previousPoints = 0 + _.each alreadyEarned, (earned) -> + alreadyEarnedIDs.push earned.get('achievement') + previousPoints += earned.get 'earnedPoints' + + # TODO maybe also delete earned? Make sure you don't delete too many + + newTotalPoints = 0 + + earnedAchievementSaverGenerator = (achievement) -> (callback) -> + log.debug 'Checking out tha fancy achievement' + isRepeatable = achievement.get('proportionalTo')? + model = mongoose.model(achievement.get('collection')) + if not model? + log.error "Model #{achievement.get 'collection'} doesn't even exist." + return callback() + + model.findOne achievement.query, (err, something) -> + return callback() unless something + + log.debug "Matched an achievement: #{achievement.get 'name'}" + + earned = + user: userID + achievement: achievement._id.toHexString() + achievementName: achievement.get 'name' + notified: achievement._id in alreadyEarnedIDs + + if isRepeatable + earned.achievedAmount = something.get(achievement.get 'proportionalTo') + earned.previouslyAchievedAmount = 0 + + expFunction = achievement.getExpFunction() + newTotalPoints += expFunction(earned.achievedAmount) * achievement.get('worth') + else + newTotalPoints += achievement.get 'worth' + + EarnedAchievement.update {achievement:earned.achievement, user:earned.user}, earned, {upsert: true}, (err) -> + log.error err if err? + callback() + + saveUserPoints = (callback) -> + # In principle it is enough to deduct the old amount of points and add the new amount, + # but just to be entirely safe let's start from 0 in case we're updating all of a user's achievements + log.debug "Matched a total of #{newTotalPoints} new points" + if _.isEmpty filter # Completely clean + User.update {_id: userID}, {$set: points: newTotalPoints}, {}, (err) -> log.error err if err? + else + User.update {_id: userID}, {$inc: points: newTotalPoints - previousPoints}, {}, (err) -> log.error err if err? + + earnedAchievementSavers = (earnedAchievementSaverGenerator(achievement) for achievement in achievements) + earnedAchievementSavers.push saveUserPoints + + async.series earnedAchievementSavers + module.exports = new EarnedAchievementHandler() diff --git a/server/plugins/achievements.coffee b/server/plugins/achievements.coffee index a684b2384..c68136983 100644 --- a/server/plugins/achievements.coffee +++ b/server/plugins/achievements.coffee @@ -73,17 +73,18 @@ module.exports = AchievablePlugin = (schema, options) -> expFunction = achievement.getExpFunction() earned.notified = false earned.achievedAmount = newAmount + earned.earnedPoints = (expFunction(newAmount) - expFunction(originalAmount)) * worth earned.previouslyAchievedAmount = originalAmount - EarnedAchievement.findOneAndUpdate({achievement:earned.achievement, user:earned.user}, earned, upsert:true, (err, docs) -> - return log.debug err if err? - ) + EarnedAchievement.update {achievement:earned.achievement, user:earned.user}, earned, {upsert: true}, (err) -> + return log.debug err if err? - earnedPoints = (expFunction(newAmount) - expFunction(originalAmount)) * worth + earnedPoints = earned.earnedPoints log.debug earnedPoints wrapUp() else # not alreadyAchieved log.debug 'Creating a new earned achievement called \'' + (achievement.get 'name') + '\' for ' + userID + earned.earnedPoints = worth (new EarnedAchievement(earned)).save (err, doc) -> return log.debug err if err? earnedPoints = worth diff --git a/server/routes/admin.coffee b/server/routes/admin.coffee index 012f0d865..c2ae7612a 100644 --- a/server/routes/admin.coffee +++ b/server/routes/admin.coffee @@ -2,8 +2,11 @@ log = require 'winston' errors = require '../commons/errors' handlers = require('../commons/mapping').handlers +mongoose = require('mongoose') + module.exports.setup = (app) -> - app.all '/admin/*', (req, res) -> + app.post '/admin/*', (req, res) -> + # TODO apparently I can leave this out as long as I use res.send res.setHeader('Content-Type', 'application/json') module = req.path[7..] From 509be067c31683e49ca4f930cb3392ce8ca58a57 Mon Sep 17 00:00:00 2001 From: Ruben Vereecken Date: Wed, 4 Jun 2014 20:47:32 +0200 Subject: [PATCH 13/22] Added support for the operator in LocalMongo --- app/lib/LocalMongo.coffee | 7 +- .../earned_achievement_handler.coffee | 29 ++++--- test/app/lib/local_mongo.spec.coffee | 86 ++++++++++--------- 3 files changed, 67 insertions(+), 55 deletions(-) diff --git a/app/lib/LocalMongo.coffee b/app/lib/LocalMongo.coffee index cdd8fd50e..2027e1c70 100644 --- a/app/lib/LocalMongo.coffee +++ b/app/lib/LocalMongo.coffee @@ -18,6 +18,7 @@ doQuerySelector = (value, operatorObj) -> when '$ne' then return false if mapred value, body, (l, r) -> l == r when '$in' then return false unless _.reduce value, ((result, val) -> result or val in body), false when '$nin' then return false if _.reduce value, ((result, val) -> result or val in body), false + when '$exists' then return false if value[0]? isnt body[0] else return false true @@ -34,11 +35,13 @@ matchesQuery = (target, queryObj) -> pieces = prop.split('.') obj = target for piece in pieces - return false unless piece of obj + unless piece of obj + obj = null + break obj = obj[piece] if typeof query != 'object' or _.isArray query return false unless obj == query or (query in obj if _.isArray obj) else return false unless doQuerySelector obj, query true -LocalMongo.matchesQuery = matchesQuery \ No newline at end of file +LocalMongo.matchesQuery = matchesQuery diff --git a/server/achievements/earned_achievement_handler.coffee b/server/achievements/earned_achievement_handler.coffee index 80570b8d0..0fefc5842 100644 --- a/server/achievements/earned_achievement_handler.coffee +++ b/server/achievements/earned_achievement_handler.coffee @@ -17,45 +17,45 @@ class EarnedAchievementHandler extends Handler recalculate: (req, res) -> onSuccess = (data) => @sendSuccess(res, data) if 'achievements' of req.query # Support both slugs and IDs separated by commas - achievementSlugsOrIDs = req.query.id.split(',') + achievementSlugsOrIDs = req.query.achievements.split(',') EarnedAchievementHandler.recalculate achievementSlugsOrIDs, onSuccess else EarnedAchievementHandler.recalculate onSuccess - @sendSuccess res + @sendSuccess res # Returns success: boolean @recalculate: (callbackOrSlugsOrIDs, callback) -> if _.isArray callbackOrSlugsOrIDs - achievementSlugs = (thing unless Handler.isID(thing) for thing in callbackOrSlugsOrIDs) - achievementIDs = (thing if Handler.isID(thing) for thing in callbackOrSlugsOrIDs) + achievementSlugs = (thing for thing in callbackOrSlugsOrIDs when not Handler.isID(thing)) + achievementIDs = (thing for thing in callbackOrSlugsOrIDs when Handler.isID(thing)) else callback = callbackOrSlugsOrIDs filter = {} filter.$or = [ - _id: $in: achievementIDs - slug: $in: achievementSlugs + {_id: $in: achievementIDs}, + {slug: $in: achievementSlugs} ] if achievementSlugs? or achievementIDs? Achievement.find filter, (err, achievements) -> return false and log.error err if err? User.find {}, (err, users) -> _.each users, (user) -> - # Keep track of a user's already achieved so as to set the notified values correctly + # Keep track of a user's already achieved in order to set the notified values correctly userID = user.get('_id').toHexString() EarnedAchievement.find {user: userID}, (err, alreadyEarned) -> alreadyEarnedIDs = [] previousPoints = 0 _.each alreadyEarned, (earned) -> - alreadyEarnedIDs.push earned.get('achievement') - previousPoints += earned.get 'earnedPoints' + if (_.find achievements, (single) -> earned.get('achievement') is single.get('_id').toHexString()) + alreadyEarnedIDs.push earned.get('achievement') + previousPoints += earned.get 'earnedPoints' # TODO maybe also delete earned? Make sure you don't delete too many newTotalPoints = 0 earnedAchievementSaverGenerator = (achievement) -> (callback) -> - log.debug 'Checking out tha fancy achievement' isRepeatable = achievement.get('proportionalTo')? model = mongoose.model(achievement.get('collection')) if not model? @@ -78,9 +78,12 @@ class EarnedAchievementHandler extends Handler earned.previouslyAchievedAmount = 0 expFunction = achievement.getExpFunction() - newTotalPoints += expFunction(earned.achievedAmount) * achievement.get('worth') + newPoints = expFunction(earned.achievedAmount) * achievement.get('worth') else - newTotalPoints += achievement.get 'worth' + newPoints = achievement.get 'worth' + + earned.earnedPoints = newPoints + newTotalPoints += newPoints EarnedAchievement.update {achievement:earned.achievement, user:earned.user}, earned, {upsert: true}, (err) -> log.error err if err? @@ -93,11 +96,13 @@ class EarnedAchievementHandler extends Handler if _.isEmpty filter # Completely clean User.update {_id: userID}, {$set: points: newTotalPoints}, {}, (err) -> log.error err if err? else + log.debug "Incrementing score for these achievements with #{newTotalPoints - previousPoints}" User.update {_id: userID}, {$inc: points: newTotalPoints - previousPoints}, {}, (err) -> log.error err if err? earnedAchievementSavers = (earnedAchievementSaverGenerator(achievement) for achievement in achievements) earnedAchievementSavers.push saveUserPoints + # We need to have all these database updates chained so we know the final score async.series earnedAchievementSavers diff --git a/test/app/lib/local_mongo.spec.coffee b/test/app/lib/local_mongo.spec.coffee index 6aa22a8f1..f3719021e 100644 --- a/test/app/lib/local_mongo.spec.coffee +++ b/test/app/lib/local_mongo.spec.coffee @@ -2,7 +2,7 @@ describe 'Local Mongo queries', -> LocalMongo = require 'lib/LocalMongo' beforeEach -> - this.fixture1 = + @fixture1 = 'id': 'somestring' 'value': 9000 'levels': [3, 8, 21] @@ -10,68 +10,72 @@ describe 'Local Mongo queries', -> 'type': 'unicorn' 'likes': ['poptarts', 'popsicles', 'popcorn'] - this.fixture2 = this: is: so: 'deep' + @fixture2 = this: is: so: 'deep' it 'regular match of a property', -> - expect(LocalMongo.matchesQuery(this.fixture1, 'gender': 'unicorn')).toBeFalsy() - expect(LocalMongo.matchesQuery(this.fixture1, 'type':'unicorn')).toBeTruthy() - expect(LocalMongo.matchesQuery(this.fixture1, 'type':'zebra')).toBeFalsy() - expect(LocalMongo.matchesQuery(this.fixture1, 'type':'unicorn', 'id':'somestring')).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'gender': 'unicorn')).toBeFalsy() + expect(LocalMongo.matchesQuery(@fixture1, 'type':'unicorn')).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'type':'zebra')).toBeFalsy() + expect(LocalMongo.matchesQuery(@fixture1, 'type':'unicorn', 'id':'somestring')).toBeTruthy() it 'array match of a property', -> - expect(LocalMongo.matchesQuery(this.fixture1, 'likes':'poptarts')).toBeTruthy() - expect(LocalMongo.matchesQuery(this.fixture1, 'likes':'walks on the beach')).toBeFalsy() + expect(LocalMongo.matchesQuery(@fixture1, 'likes':'poptarts')).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'likes':'walks on the beach')).toBeFalsy() it 'nested match', -> - expect(LocalMongo.matchesQuery(this.fixture2, 'this.is.so':'deep')).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture2, 'this.is.so':'deep')).toBeTruthy() it '$gt selector', -> - expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$gt': 8000)).toBeTruthy() - expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$gt': [8000, 10000])).toBeTruthy() - expect(LocalMongo.matchesQuery(this.fixture1, 'levels': '$gt': [10, 20, 30])).toBeTruthy() - expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$gt': 9000)).toBeFalsy() - expect(LocalMongo.matchesQuery(this.fixture1, 'value': {'$gt': 8000}, 'worth': {'$gt': 5})).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'value': '$gt': 8000)).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'value': '$gt': [8000, 10000])).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'levels': '$gt': [10, 20, 30])).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'value': '$gt': 9000)).toBeFalsy() + expect(LocalMongo.matchesQuery(@fixture1, 'value': {'$gt': 8000}, 'worth': {'$gt': 5})).toBeTruthy() it '$gte selector', -> - expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$gte': 9001)).toBeFalsy() - expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$gte': 9000)).toBeTruthy() - expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$gte': [9000, 10000])).toBeTruthy() - expect(LocalMongo.matchesQuery(this.fixture1, 'levels': '$gte': [21, 30])).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'value': '$gte': 9001)).toBeFalsy() + expect(LocalMongo.matchesQuery(@fixture1, 'value': '$gte': 9000)).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'value': '$gte': [9000, 10000])).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'levels': '$gte': [21, 30])).toBeTruthy() it '$lt selector', -> - expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$lt': 9001)).toBeTruthy() - expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$lt': 9000)).toBeFalsy() - expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$lt': [9001, 9000])).toBeTruthy() - expect(LocalMongo.matchesQuery(this.fixture1, 'levels': '$lt': [10, 20, 30])).toBeTruthy() - expect(LocalMongo.matchesQuery(this.fixture1, 'value': {'$lt': 9001}, 'worth': {'$lt': 7})).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'value': '$lt': 9001)).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'value': '$lt': 9000)).toBeFalsy() + expect(LocalMongo.matchesQuery(@fixture1, 'value': '$lt': [9001, 9000])).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'levels': '$lt': [10, 20, 30])).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'value': {'$lt': 9001}, 'worth': {'$lt': 7})).toBeTruthy() it '$lte selector', -> - expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$lte': 9000)).toBeTruthy() - expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$lte': 8000)).toBeFalsy() - expect(LocalMongo.matchesQuery(this.fixture1, 'value': {'$lte': 9000}, 'worth': {'$lte': [6, 5]})).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'value': '$lte': 9000)).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'value': '$lte': 8000)).toBeFalsy() + expect(LocalMongo.matchesQuery(@fixture1, 'value': {'$lte': 9000}, 'worth': {'$lte': [6, 5]})).toBeTruthy() it '$ne selector', -> - expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$ne': 9000)).toBeFalsy() - expect(LocalMongo.matchesQuery(this.fixture1, 'id': '$ne': 'otherstring')).toBeTruthy() - expect(LocalMongo.matchesQuery(this.fixture1, 'id': '$ne': ['otherstring', 'somestring'])).toBeFalsy() - expect(LocalMongo.matchesQuery(this.fixture1, 'likes': '$ne': ['popcorn', 'chicken'])).toBeFalsy() + expect(LocalMongo.matchesQuery(@fixture1, 'value': '$ne': 9000)).toBeFalsy() + expect(LocalMongo.matchesQuery(@fixture1, 'id': '$ne': 'otherstring')).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'id': '$ne': ['otherstring', 'somestring'])).toBeFalsy() + expect(LocalMongo.matchesQuery(@fixture1, 'likes': '$ne': ['popcorn', 'chicken'])).toBeFalsy() it '$in selector', -> - expect(LocalMongo.matchesQuery(this.fixture1, 'type': '$in': ['unicorn', 'zebra'])).toBeTruthy() - expect(LocalMongo.matchesQuery(this.fixture1, 'type': '$in': ['cats', 'dogs'])).toBeFalsy() - expect(LocalMongo.matchesQuery(this.fixture1, 'likes': '$in': ['popcorn', 'chicken'])).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'type': '$in': ['unicorn', 'zebra'])).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'type': '$in': ['cats', 'dogs'])).toBeFalsy() + expect(LocalMongo.matchesQuery(@fixture1, 'likes': '$in': ['popcorn', 'chicken'])).toBeTruthy() it '$nin selector', -> - expect(LocalMongo.matchesQuery(this.fixture1, 'type': '$nin': ['unicorn', 'zebra'])).toBeFalsy() - expect(LocalMongo.matchesQuery(this.fixture1, 'type': '$nin': ['cats', 'dogs'])).toBeTruthy() - expect(LocalMongo.matchesQuery(this.fixture1, 'likes': '$nin': ['popcorn', 'chicken'])).toBeFalsy() + expect(LocalMongo.matchesQuery(@fixture1, 'type': '$nin': ['unicorn', 'zebra'])).toBeFalsy() + expect(LocalMongo.matchesQuery(@fixture1, 'type': '$nin': ['cats', 'dogs'])).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, 'likes': '$nin': ['popcorn', 'chicken'])).toBeFalsy() it '$or operator', -> - expect(LocalMongo.matchesQuery(this.fixture1, $or: [{value:9000}, {type:'zebra'}])).toBeTruthy() - expect(LocalMongo.matchesQuery(this.fixture1, $or: [{value:9001}, {worth:$lt:10}])).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, $or: [{value:9000}, {type:'zebra'}])).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, $or: [{value:9001}, {worth:$lt:10}])).toBeTruthy() it '$and operator', -> - expect(LocalMongo.matchesQuery(this.fixture1, $and: [{value:9000}, {type:'zebra'}])).toBeFalsy() - expect(LocalMongo.matchesQuery(this.fixture1, $and: [{value:9000}, {type:'unicorn'}])).toBeTruthy() - expect(LocalMongo.matchesQuery(this.fixture1, $and: [{value:$gte:9000}, {worth:$lt:10}])).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, $and: [{value:9000}, {type:'zebra'}])).toBeFalsy() + expect(LocalMongo.matchesQuery(@fixture1, $and: [{value:9000}, {type:'unicorn'}])).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, $and: [{value:$gte:9000}, {worth:$lt:10}])).toBeTruthy() + + it '$exists operator', -> + expect(LocalMongo.matchesQuery(@fixture1, type: $exists: true)).toBeTruthy() + expect(LocalMongo.matchesQuery(@fixture1, interesting: $exists: false)).toBeTruthy() From 2a5c2020cac68e1b3f0e4a6a384d7f53230f1faf Mon Sep 17 00:00:00 2001 From: Ruben Vereecken Date: Mon, 9 Jun 2014 00:33:06 +0200 Subject: [PATCH 14/22] Added recalculation button to the achievement editor --- app/styles/editor/achievement/edit.sass | 12 +++++++ app/templates/editor/achievement/edit.jade | 18 +++++----- app/templates/modal/confirm.jade | 11 +++++++ app/templates/modal/modal_base.jade | 2 +- app/views/editor/achievement/edit.coffee | 33 +++++++++++++++++++ app/views/modal/confirm.coffee | 30 +++++++++++++++++ .../earned_achievement_handler.coffee | 20 +++++++---- 7 files changed, 110 insertions(+), 16 deletions(-) create mode 100644 app/styles/editor/achievement/edit.sass create mode 100644 app/templates/modal/confirm.jade create mode 100644 app/views/modal/confirm.coffee diff --git a/app/styles/editor/achievement/edit.sass b/app/styles/editor/achievement/edit.sass new file mode 100644 index 000000000..7177978d3 --- /dev/null +++ b/app/styles/editor/achievement/edit.sass @@ -0,0 +1,12 @@ +#editor-achievement-edit-view + .treema-root + margin: 28px 0px 20px + + button + float: right + margin-top: 15px + margin-left: 10px + + textarea + width: 92% + height: 300px diff --git a/app/templates/editor/achievement/edit.jade b/app/templates/editor/achievement/edit.jade index 2cee658bc..42b59de17 100644 --- a/app/templates/editor/achievement/edit.jade +++ b/app/templates/editor/achievement/edit.jade @@ -11,19 +11,21 @@ block content li.active | #{achievement.attributes.name} - button(data-i18n="common.save", disabled=authorized === true ? undefined : "true").btn.btn-primary#save-button Save + button(data-i18n="", disabled=me.isAdmin() === true ? undefined : "true").btn.btn-primary#recalculate-button Recalculate + button(data-i18n="common.save", disabled=me.isAdmin() === true ? undefined : "true").btn.btn-primary#save-button Save - h3(data-i18n="achievement.edit_achievement_title") Edit Achievement - span - |: "#{achievement.attributes.name}" + h3(data-i18n="achievement.edit_achievement_title") Edit Achievement + span + |: "#{achievement.attributes.name}" - #achievement-treema + #achievement-treema - #achievement-view + #achievement-view - hr + hr + + div#error-view - div#error-view else .alert.alert-danger span Admin only. Turn around. diff --git a/app/templates/modal/confirm.jade b/app/templates/modal/confirm.jade new file mode 100644 index 000000000..f7d746de4 --- /dev/null +++ b/app/templates/modal/confirm.jade @@ -0,0 +1,11 @@ +extends /templates/modal/modal_base + +block modal-header-content + h3 #{confirmTitle} + +block modal-body-content + p #{confirmBody} + +block modal-footer-content + button.btn.btn-secondary#decline-button(type="button", data-dismiss="modal") #{confirmDecline} + button.btn.btn-primary#confirm-button(type="button", data-dismiss=closeOnConfirm === true ? "modal" : undefined) #{confirmConfirm} diff --git a/app/templates/modal/modal_base.jade b/app/templates/modal/modal_base.jade index e2c2d527f..c8374033e 100644 --- a/app/templates/modal/modal_base.jade +++ b/app/templates/modal/modal_base.jade @@ -24,4 +24,4 @@ block modal-footer .modal-footer block modal-footer-content - button.btn.btn-primary(type="button", data-dismiss="modal", aria-hidden="true", data-i18n="modal.okay") Okay \ No newline at end of file + button.btn.btn-primary(type="button", data-dismiss="modal", aria-hidden="true", data-i18n="modal.okay") Okay diff --git a/app/views/editor/achievement/edit.coffee b/app/views/editor/achievement/edit.coffee index 186eb206d..76988a586 100644 --- a/app/views/editor/achievement/edit.coffee +++ b/app/views/editor/achievement/edit.coffee @@ -1,6 +1,7 @@ View = require 'views/kinds/RootView' template = require 'templates/editor/achievement/edit' Achievement = require 'models/Achievement' +ConfirmModal = require 'views/modal/confirm' module.exports = class AchievementEditView extends View id: "editor-achievement-edit-view" @@ -9,6 +10,7 @@ module.exports = class AchievementEditView extends View events: 'click #save-button': 'saveAchievement' + 'click #recalculate-button': 'confirmRecalculation' subscriptions: 'save-new': 'saveAchievement' @@ -72,3 +74,34 @@ module.exports = class AchievementEditView extends View res.success => url = "/editor/achievement/#{@achievement.get('slug') or @achievement.id}" document.location.href = url + + confirmRecalculation: (e) -> + renderData = + 'confirmTitle': "Are you really sure?" + 'confirmBody': "This will trigger recalculation of the achievement for all users. Are you really sure you want to go down this path?" + 'confirmDecline': "Not really" + 'confirmConfirm': "Definitely" + + confirmModal = new ConfirmModal(renderData) + confirmModal.onConfirm @recalculateAchievement + @openModalView confirmModal + + recalculateAchievement: => + $.ajax + data: JSON.stringify(achievements: [@achievement.get('slug') or @achievement.get('_id')]) + success: (data, status, jqXHR) -> + noty + timeout: 5000 + text: 'Recalculation process started' + type: 'success' + layout: 'topCenter' + error: (jqXHR, status, error) -> + console.error jqXHR + noty + timeout: 5000 + text: "Starting recalculation process failed with error code #{jqXHR.status}" + type: 'error' + layout: 'topCenter' + url: '/admin/earned.achievement/recalculate' + type: 'POST' + contentType: 'application/json' diff --git a/app/views/modal/confirm.coffee b/app/views/modal/confirm.coffee new file mode 100644 index 000000000..18ded9ed6 --- /dev/null +++ b/app/views/modal/confirm.coffee @@ -0,0 +1,30 @@ +ModalView = require '../kinds/ModalView' +template = require 'templates/modal/confirm' + +module.exports = class ConfirmModal extends ModalView + id: "confirm-modal" + template: template + closeButton: true + closeOnConfirm: true + + events: + 'click #decline-button': 'doDecline' + 'click #confirm-button': 'doConfirm' + + constructor: (@renderData={}, options={}) -> + super(options) + + getRenderData: -> + context = super() + context.closeOnConfirm = @closeOnConfirm + _.extend context, @renderData + + setRenderData: (@renderData) -> + + onDecline: (@decline) -> + + onConfirm: (@confirm) -> + + doConfirm: -> @confirm() if @confirm + + doDecline: -> @decline() if @decline diff --git a/server/achievements/earned_achievement_handler.coffee b/server/achievements/earned_achievement_handler.coffee index 0fefc5842..24615054d 100644 --- a/server/achievements/earned_achievement_handler.coffee +++ b/server/achievements/earned_achievement_handler.coffee @@ -15,21 +15,22 @@ class EarnedAchievementHandler extends Handler req.method is 'GET' # or req.user.isAdmin() recalculate: (req, res) -> - onSuccess = (data) => @sendSuccess(res, data) - if 'achievements' of req.query # Support both slugs and IDs separated by commas - achievementSlugsOrIDs = req.query.achievements.split(',') + onSuccess = (data) => log.debug "Finished recalculating achievements" + if 'achievements' of req.body # Support both slugs and IDs separated by commas + achievementSlugsOrIDs = req.body.achievements EarnedAchievementHandler.recalculate achievementSlugsOrIDs, onSuccess else EarnedAchievementHandler.recalculate onSuccess - @sendSuccess res + @sendSuccess res, {} # Returns success: boolean - @recalculate: (callbackOrSlugsOrIDs, callback) -> + # TODO call onFinished + @recalculate: (callbackOrSlugsOrIDs, onFinished) -> if _.isArray callbackOrSlugsOrIDs achievementSlugs = (thing for thing in callbackOrSlugsOrIDs when not Handler.isID(thing)) achievementIDs = (thing for thing in callbackOrSlugsOrIDs when Handler.isID(thing)) else - callback = callbackOrSlugsOrIDs + onFinished = callbackOrSlugsOrIDs filter = {} filter.$or = [ @@ -37,12 +38,17 @@ class EarnedAchievementHandler extends Handler {slug: $in: achievementSlugs} ] if achievementSlugs? or achievementIDs? + # Fetch all relevant achievements Achievement.find filter, (err, achievements) -> - return false and log.error err if err? + return log.error err if err? + + # Fetch every single user User.find {}, (err, users) -> _.each users, (user) -> # Keep track of a user's already achieved in order to set the notified values correctly userID = user.get('_id').toHexString() + + # Fetch all of a user's earned achievements EarnedAchievement.find {user: userID}, (err, alreadyEarned) -> alreadyEarnedIDs = [] previousPoints = 0 From aecf937722653f1154aad8fb9e7fb04fb3400e8d Mon Sep 17 00:00:00 2001 From: Ruben Vereecken Date: Mon, 9 Jun 2014 11:59:27 +0200 Subject: [PATCH 15/22] Cleaned up achievement testing environment Made sure server sided tests work completely --- app/templates/admin.jade | 8 ++++++++ app/views/admin_view.coffee | 6 ++++++ app/views/kinds/RootView.coffee | 14 -------------- server/achievements/EarnedAchievement.coffee | 1 - server/users/user_handler.coffee | 3 +-- 5 files changed, 15 insertions(+), 17 deletions(-) diff --git a/app/templates/admin.jade b/app/templates/admin.jade index 50cd435e3..11ee67ee8 100644 --- a/app/templates/admin.jade +++ b/app/templates/admin.jade @@ -33,3 +33,11 @@ block content a(href="/admin/base", data-i18n="admin.av_other_debug_base_url") Base (for debugging base.jade) li a(href="/admin/clas", data-i18n="admin.clas") CLAs + + hr + + h3 Achievements + p This is just some stuff for temporary achievement testing. Should be replaced by a demo system. + + input#increment-field(type="text") + a.btn.btn-secondary#increment-button(href="#") Increment diff --git a/app/views/admin_view.coffee b/app/views/admin_view.coffee index 8c93ff616..eab1bbc42 100644 --- a/app/views/admin_view.coffee +++ b/app/views/admin_view.coffee @@ -8,6 +8,7 @@ module.exports = class AdminView extends View events: 'click #enter-espionage-mode': 'enterEspionageMode' + 'click #increment-button': 'incrementUserAttribute' enterEspionageMode: -> userEmail = $("#user-email").val().toLowerCase() @@ -29,3 +30,8 @@ module.exports = class AdminView extends View espionageFailure: (jqxhr, status,error)-> console.log "There was an error entering espionage mode: #{error}" + + incrementUserAttribute: (e) -> + val = $('#increment-field').val() + me.set(val, me.get(val) + 1) + me.save() diff --git a/app/views/kinds/RootView.coffee b/app/views/kinds/RootView.coffee index 4824113b5..2ef78f7f7 100644 --- a/app/views/kinds/RootView.coffee +++ b/app/views/kinds/RootView.coffee @@ -26,20 +26,6 @@ module.exports = class RootView extends CocoView subscriptions: 'achievements:new': 'handleNewAchievements' - initialize: -> - $ => - # TODO Ruben remove this. Allows for easy testing right now though - btn = $('Increment') - input = $('') - $('body').append(btn) - $('body').append(input) - btn.on 'click', (e) => - val = input.val() - me.set(val, me.get(val) + 1) - console.debug me.get(val) - me.save() - #test = new Achievement(_id:'537ce4855c91b8d1dda7fda8') - #test.fetch(success:@showNewAchievement) showNewAchievement: (achievement, earnedAchievement) -> currentLevel = me.level() diff --git a/server/achievements/EarnedAchievement.coffee b/server/achievements/EarnedAchievement.coffee index e02174c29..16738ae78 100644 --- a/server/achievements/EarnedAchievement.coffee +++ b/server/achievements/EarnedAchievement.coffee @@ -1,6 +1,5 @@ mongoose = require 'mongoose' jsonschema = require '../../app/schemas/models/earned_achievement' -User = require '../users/User' EarnedAchievementSchema = new mongoose.Schema({ created: diff --git a/server/users/user_handler.coffee b/server/users/user_handler.coffee index db4ede7be..5ffa68c06 100644 --- a/server/users/user_handler.coffee +++ b/server/users/user_handler.coffee @@ -24,9 +24,8 @@ candidateProperties = [ UserHandler = class UserHandler extends Handler modelClass: User - # TODO Ruben change this back! Really really do. Don't you dare forget. NO COOKIES editableProperties: [ - 'name', 'photoURL', 'password', 'anonymous', 'wizardColor1', 'volume', 'simulatedBy', + 'name', 'photoURL', 'password', 'anonymous', 'wizardColor1', 'volume', 'firstName', 'lastName', 'gender', 'facebookID', 'gplusID', 'emails', 'testGroupNumber', 'music', 'hourOfCode', 'hourOfCodeComplete', 'preferredLanguage', 'wizard', 'aceConfig', 'autocastDelay', 'lastLevel', 'jobProfile' From b66f4984c392093d04ecffad6880ff59b5ef45ac Mon Sep 17 00:00:00 2001 From: Ruben Vereecken Date: Mon, 9 Jun 2014 17:28:35 +0200 Subject: [PATCH 16/22] Finished basic functional tests for achievement --- server/achievements/Achievement.coffee | 7 +- server/commons/Handler.coffee | 5 ++ server/commons/errors.coffee | 7 +- server/routes/file.coffee | 2 +- test/server/common.coffee | 4 +- .../server/functional/achievement.spec.coffee | 86 +++++++++++++++++++ test/server/functional/file.spec.coffee | 11 +++ .../functional/level_component.spec.coffee | 6 +- .../functional/level_system.spec.coffee | 4 +- 9 files changed, 118 insertions(+), 14 deletions(-) create mode 100644 test/server/functional/achievement.spec.coffee diff --git a/server/achievements/Achievement.coffee b/server/achievements/Achievement.coffee index 5dcc19d8c..47e6efdb9 100644 --- a/server/achievements/Achievement.coffee +++ b/server/achievements/Achievement.coffee @@ -2,6 +2,7 @@ mongoose = require('mongoose') jsonschema = require('../../app/schemas/models/achievement') log = require 'winston' util = require '../../app/lib/utils' +plugins = require('../plugins/plugins') # `pre` and `post` are not called for update operations executed directly on the database, # including `Model.update`,`.findByIdAndUpdate`,`.findOneAndUpdate`, `.findOneAndRemove`,and `.findByIdAndRemove`.order @@ -35,9 +36,7 @@ AchievementSchema.pre('save', (next) -> next() ) -module.exports = Achievement = mongoose.model('Achievement', AchievementSchema) - -plugins = require('../plugins/plugins') - AchievementSchema.plugin(plugins.NamedPlugin) AchievementSchema.plugin(plugins.SearchablePlugin, {searchable: ['name']}) + +module.exports = Achievement = mongoose.model('Achievement', AchievementSchema) diff --git a/server/commons/Handler.coffee b/server/commons/Handler.coffee index e8fdbd668..70d669db1 100644 --- a/server/commons/Handler.coffee +++ b/server/commons/Handler.coffee @@ -17,6 +17,7 @@ module.exports = class Handler postEditableProperties: [] jsonSchema: {} waterfallFunctions: [] + allowedMethods: ['GET', 'POST', 'PUT', 'PATCH'] # subclasses should override these methods hasAccess: (req) -> true @@ -420,3 +421,7 @@ module.exports = class Handler dict[document.id] = document res.send dict res.end() + + delete: (req, res) -> @sendMethodNotAllowed res, @allowedMethods, "DELETE not allowed." + + head: (req, res) -> @sendMethodNotAllowed res, @allowedMethods, "HEAD not allowed." diff --git a/server/commons/errors.coffee b/server/commons/errors.coffee index 8af347126..3f60ef852 100644 --- a/server/commons/errors.coffee +++ b/server/commons/errors.coffee @@ -17,8 +17,9 @@ module.exports.notFound = (res, message='Not found.') -> res.send 404, message res.end() -module.exports.badMethod = (res, message='Method Not Allowed') -> - # TODO: The response MUST include an Allow header containing a list of valid methods for the requested resource +module.exports.badMethod = (res, allowed=['GET', 'POST', 'PUT', 'PATCH'], message='Method Not Allowed') -> + allowHeader = _.reduce allowed, ((str, current) -> str += ', ' + current) + res.set 'Allow', allowHeader # TODO not sure if these are always the case res.send 405, message res.end() @@ -40,4 +41,4 @@ module.exports.gatewayTimeoutError = (res, message="Gateway timeout") -> module.exports.clientTimeout = (res, message="The server did not recieve the client response in a timely manner") -> res.send 408, message - res.end() \ No newline at end of file + res.end() diff --git a/server/routes/file.coffee b/server/routes/file.coffee index 1cc0f73d7..e96a2453a 100644 --- a/server/routes/file.coffee +++ b/server/routes/file.coffee @@ -8,7 +8,7 @@ module.exports.setup = (app) -> app.all '/file*', (req, res) -> return fileGet(req, res) if req.route.method is 'get' return filePost(req, res) if req.route.method is 'post' - return errors.badMethod(res) + return errors.badMethod(res, ['GET', 'POST']) fileGet = (req, res) -> diff --git a/test/server/common.coffee b/test/server/common.coffee index d80548aa3..c098650ca 100644 --- a/test/server/common.coffee +++ b/test/server/common.coffee @@ -30,6 +30,8 @@ models_path = [ '../../server/levels/thangs/LevelThangType' '../../server/users/User' '../../server/patches/Patch' + '../../server/achievements/Achievement' + '../../server/achievements/EarnedAchievement' ] for m in models_path @@ -162,4 +164,4 @@ tick = -> mongoose.disconnect() clearTimeout tickInterval -tickInterval = setInterval tick, 1000 \ No newline at end of file +tickInterval = setInterval tick, 1000 diff --git a/test/server/functional/achievement.spec.coffee b/test/server/functional/achievement.spec.coffee new file mode 100644 index 000000000..1ee3572d8 --- /dev/null +++ b/test/server/functional/achievement.spec.coffee @@ -0,0 +1,86 @@ +require '../common' + +describe 'Achievement', -> + + unlockable = + name: 'One Time Only' + description: 'So you did the really cool thing.' + worth: 6.66 + collection: 'level.session' + + repeatable = + name: 'Lots of em' + description: 'Oops you did it again.' + worth: 1 + collection: 'User' + proportionalTo: '_id' + + url = getURL('/db/achievement') + allowHeader = 'GET, POST, PUT, PATCH' + + it 'preparing test: deleting all Achievements first', (done) -> + clearModels [Achievement], (err) -> + expect(err).toBeNull() + done() + + it 'can\'t be created by ordinary users', (done) -> + loginJoe -> + request.post {uri: url, json: unlockable}, (err, res, body) -> + expect(res.statusCode).toBe(403) + done() + + it 'can\'t be updated by ordinary users', (done) -> + loginJoe -> + request.put {uri: url, json:unlockable}, (err, res, body) -> + expect(res.statusCode).toBe(403) + + request {method: 'patch', uri: url, json: unlockable}, (err, res, body) -> + expect(res.statusCode).toBe(403) + done() + + it 'can be created by admins', (done) -> + loginAdmin -> + request.post {uri: url, json: unlockable}, (err, res, body) -> + expect(res.statusCode).toBe(200) + unlockable._id = body._id + done() + + it 'can get all for ordinary users', (done) -> + loginJoe -> + request.get {uri: url, json: unlockable}, (err, res, body) -> + expect(res.statusCode).toBe(200) + expect(body.length).toBe(1) + done() + + it 'can be read by ordinary users', (done) -> + loginJoe -> + request.get {uri: url + '/' + unlockable._id, json: unlockable}, (err, res, body) -> + expect(res.statusCode).toBe(200) + expect(body.name).toBe(unlockable.name) + done() + + it 'can\'t be requested with HTTP HEAD method', (done) -> + loginJoe -> + request.head {uri: url + '/' + unlockable._id}, (err, res, body) -> + expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) + done() + + it 'can\'t be requested with HTTP DEL method', (done) -> + loginJoe -> + request.del {uri: url + '/' + unlockable._id}, (err, res, body) -> + expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) + done() + + it 'get schema', (done) -> + request.get {uri:url + '/schema'}, (err, res, body) -> + expect(res.statusCode).toBe(200) + body = JSON.parse(body) + expect(body.type).toBeDefined() + done() + + it 'cleaning up test: deleting all Achievements', (done) -> + clearModels [Achievement], (err) -> + expect(err).toBeNull() + done() diff --git a/test/server/functional/file.spec.coffee b/test/server/functional/file.spec.coffee index b22990067..eba3d24e4 100644 --- a/test/server/functional/file.spec.coffee +++ b/test/server/functional/file.spec.coffee @@ -28,6 +28,8 @@ xdescribe '/file', -> my_buffer_url: 'http://fc07.deviantart.net/fs37/f/2008/283/5/1/Chu_Chu_Pikachu_by_angelishi.gif' } + allowHeader = 'GET, POST' + it 'preparing test : deletes all the files first', (done) -> dropGridFS -> done() @@ -147,19 +149,28 @@ xdescribe '/file', -> request.post(options, func) + it ' can\'t be requested with HTTP PATCH method', (done) -> + request {method: 'patch', uri:url}, (err, res) -> + expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) + done() + it ' can\'t be requested with HTTP PUT method', (done) -> request.put {uri:url}, (err, res) -> expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) done() it ' can\'t be requested with HTTP HEAD method', (done) -> request.head {uri:url}, (err, res) -> expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) done() it ' can\'t be requested with HTTP DEL method', (done) -> request.del {uri:url}, (err, res) -> expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) done() # TODO: test server errors, see what they do diff --git a/test/server/functional/level_component.spec.coffee b/test/server/functional/level_component.spec.coffee index 4269eee21..e185657ba 100644 --- a/test/server/functional/level_component.spec.coffee +++ b/test/server/functional/level_component.spec.coffee @@ -130,17 +130,17 @@ describe 'LevelComponent', -> xit ' can\'t be requested with HTTP PUT method', (done) -> request.put {uri:url+'/'+components[0]._id}, (err, res) -> - expect(res.statusCode).toBe(404) + expect(res.statusCode).toBe(405) done() it ' can\'t be requested with HTTP HEAD method', (done) -> request.head {uri:url+'/'+components[0]._id}, (err, res) -> - expect(res.statusCode).toBe(404) + expect(res.statusCode).toBe(405) done() it ' can\'t be requested with HTTP DEL method', (done) -> request.del {uri:url+'/'+components[0]._id}, (err, res) -> - expect(res.statusCode).toBe(404) + expect(res.statusCode).toBe(405) done() it 'get schema', (done) -> diff --git a/test/server/functional/level_system.spec.coffee b/test/server/functional/level_system.spec.coffee index c933eb8bb..a84c75308 100644 --- a/test/server/functional/level_system.spec.coffee +++ b/test/server/functional/level_system.spec.coffee @@ -123,12 +123,12 @@ describe 'LevelSystem', -> it ' can\'t be requested with HTTP HEAD method', (done) -> request.head {uri:url+'/'+systems[0]._id}, (err, res) -> - expect(res.statusCode).toBe(404) + expect(res.statusCode).toBe(405) done() it ' can\'t be requested with HTTP DEL method', (done) -> request.del {uri:url+'/'+systems[0]._id}, (err, res) -> - expect(res.statusCode).toBe(404) + expect(res.statusCode).toBe(405) done() it 'get schema', (done) -> From 925594ffe9a1420e54dd4a4809af9c27737cd9bc Mon Sep 17 00:00:00 2001 From: Ruben Vereecken Date: Mon, 9 Jun 2014 18:01:43 +0200 Subject: [PATCH 17/22] Took a detour to write tests for BadMethod. Queue's tests not working yet. --- server/routes/folder.coffee | 4 +-- server/routes/languages.coffee | 2 +- server/routes/queue.coffee | 4 +-- test/server/functional/folder.spec.coffee | 35 ++++++++++++++++++++ test/server/functional/languages.spec.coffee | 35 ++++++++++++++++++++ test/server/functional/queue.spec.coffee | 25 ++++++++++++++ 6 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 test/server/functional/folder.spec.coffee create mode 100644 test/server/functional/languages.spec.coffee create mode 100644 test/server/functional/queue.spec.coffee diff --git a/server/routes/folder.coffee b/server/routes/folder.coffee index 7ac5e187a..d63701605 100644 --- a/server/routes/folder.coffee +++ b/server/routes/folder.coffee @@ -4,7 +4,7 @@ errors = require '../commons/errors' module.exports.setup = (app) -> app.all '/folder*', (req, res) -> return folderGet(req, res) if req.route.method is 'get' - return errors.badMethod(res) + return errors.badMethod(res, ['GET']) folderGet = (req, res) -> folder = req.path[7..] @@ -15,4 +15,4 @@ folderGet = (req, res) -> mongoose.connection.db.collection 'media.files', (errors, collection) -> collection.find({'metadata.path': folder}).toArray (err, results) -> res.send(results) - res.end() \ No newline at end of file + res.end() diff --git a/server/routes/languages.coffee b/server/routes/languages.coffee index a959e823d..b29cc8fdb 100644 --- a/server/routes/languages.coffee +++ b/server/routes/languages.coffee @@ -11,7 +11,7 @@ module.exports.setup = (app) -> app.all '/languages', (req, res) -> # Now that these are in the client, not sure when we would use this, but hey - return errors.badMethod(res) if req.route.method isnt 'get' + return errors.badMethod(res, ['GET']) if req.route.method isnt 'get' res.send(languages) return res.end() diff --git a/server/routes/queue.coffee b/server/routes/queue.coffee index 388bce4e0..e69ae720e 100644 --- a/server/routes/queue.coffee +++ b/server/routes/queue.coffee @@ -28,7 +28,7 @@ module.exports.setup = (app) -> app.all '/queue/*', (req, res) -> setResponseHeaderToJSONContentType res - + queueName = getQueueNameFromPath req.path try handler = loadQueueHandler queueName @@ -64,7 +64,7 @@ isHTTPMethodPost = (req) -> return req.route.method is 'post' isHTTPMethodPut = (req) -> return req.route.method is 'put' -sendMethodNotSupportedError = (req, res) -> errors.badMethod(res,"Queues do not support the HTTP method used." ) +sendMethodNotSupportedError = (req, res) -> errors.badMethod(res, ['GET', 'POST', 'PUT'], "Queues do not support the HTTP method used." ) sendQueueError = (req,res, error) -> errors.serverError(res, "Route #{req.path} had a problem: #{error}") diff --git a/test/server/functional/folder.spec.coffee b/test/server/functional/folder.spec.coffee new file mode 100644 index 000000000..e79ef6d63 --- /dev/null +++ b/test/server/functional/folder.spec.coffee @@ -0,0 +1,35 @@ +require '../common' + +describe 'folder', -> + url = getURL('/folder') + allowHeader = 'GET' + + it 'can\'t be requested with HTTP POST method', (done) -> + request.post {uri: url}, (err, res, body) -> + expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) + done() + + it 'can\'t be requested with HTTP PUT method', (done) -> + request.put {uri: url}, (err, res, body) -> + expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) + done() + + it 'can\'t be requested with HTTP PATCH method', (done) -> + request {method:'patch', uri: url}, (err, res, body) -> + expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) + done() + + it 'can\'t be requested with HTTP HEAD method', (done) -> + request.head {uri: url}, (err, res, body) -> + expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) + done() + + it 'can\'t be requested with HTTP DELETE method', (done) -> + request.del {uri: url}, (err, res, body) -> + expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) + done() diff --git a/test/server/functional/languages.spec.coffee b/test/server/functional/languages.spec.coffee new file mode 100644 index 000000000..f8ccba0df --- /dev/null +++ b/test/server/functional/languages.spec.coffee @@ -0,0 +1,35 @@ +require '../common' + +describe 'languages', -> + url = getURL('/languages') + allowHeader = 'GET' + + it 'can\'t be requested with HTTP POST method', (done) -> + request.post {uri: url}, (err, res, body) -> + expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) + done() + + it 'can\'t be requested with HTTP PUT method', (done) -> + request.put {uri: url}, (err, res, body) -> + expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) + done() + + it 'can\'t be requested with HTTP PATCH method', (done) -> + request {method:'patch', uri: url}, (err, res, body) -> + expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) + done() + + it 'can\'t be requested with HTTP HEAD method', (done) -> + request.head {uri: url}, (err, res, body) -> + expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) + done() + + it 'can\'t be requested with HTTP DELETE method', (done) -> + request.del {uri: url}, (err, res, body) -> + expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) + done() diff --git a/test/server/functional/queue.spec.coffee b/test/server/functional/queue.spec.coffee new file mode 100644 index 000000000..c59ff409b --- /dev/null +++ b/test/server/functional/queue.spec.coffee @@ -0,0 +1,25 @@ +require '../common' + +describe 'queue', -> + someURL = getURL('/queue/') + allowHeader = 'GET, POST, PUT' + + xit 'can\'t be requested with HTTP PATCH method', (done) -> + request {method:'patch', uri: someURL}, (err, res, body) -> + expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) + done() + + xit 'can\'t be requested with HTTP HEAD method', (done) -> + request.head {uri: someURL}, (err, res, body) -> + expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) + done() + + xit 'can\'t be requested with HTTP DELETE method', (done) -> + request.del {uri: someURL}, (err, res, body) -> + expect(res.statusCode).toBe(405) + expect(res.headers.allow).toBe(allowHeader) + done() + + From fddca03417de45c01b91f4937668c6e77ad27c42 Mon Sep 17 00:00:00 2001 From: Ruben Vereecken Date: Tue, 10 Jun 2014 20:05:32 +0200 Subject: [PATCH 18/22] Worked on achievement tests --- server/achievements/Achievement.coffee | 4 ++ server/plugins/achievements.coffee | 26 ++++---- .../server/functional/achievement.spec.coffee | 64 +++++++++++++------ 3 files changed, 62 insertions(+), 32 deletions(-) diff --git a/server/achievements/Achievement.coffee b/server/achievements/Achievement.coffee index 47e6efdb9..a3168a2e3 100644 --- a/server/achievements/Achievement.coffee +++ b/server/achievements/Achievement.coffee @@ -40,3 +40,7 @@ AchievementSchema.plugin(plugins.NamedPlugin) AchievementSchema.plugin(plugins.SearchablePlugin, {searchable: ['name']}) module.exports = Achievement = mongoose.model('Achievement', AchievementSchema) + +# Reload achievements upon save +AchievablePlugin = require '../plugins/achievements' +AchievementSchema.post 'save', (doc) -> AchievablePlugin.loadAchievements() diff --git a/server/plugins/achievements.coffee b/server/plugins/achievements.coffee index c68136983..a3f0096af 100644 --- a/server/plugins/achievements.coffee +++ b/server/plugins/achievements.coffee @@ -1,5 +1,4 @@ mongoose = require('mongoose') -Achievement = require('../achievements/Achievement') EarnedAchievement = require '../achievements/EarnedAchievement' LocalMongo = require '../../app/lib/LocalMongo' util = require '../../app/lib/utils' @@ -7,18 +6,9 @@ log = require 'winston' achievements = {} -loadAchievements = -> - achievements = {} - query = Achievement.find({}) - query.exec (err, docs) -> - _.each docs, (achievement) -> - category = achievement.get 'collection' - achievements[category] = [] unless category of achievements - achievements[category].push achievement -loadAchievements() - module.exports = AchievablePlugin = (schema, options) -> - User = require '../users/User' + User = require '../users/User' # Avoid mutual inclusion cycles + Achievement = require('../achievements/Achievement') checkForAchievement = (doc) -> collectionName = doc.constructor.modelName @@ -92,3 +82,15 @@ module.exports = AchievablePlugin = (schema, options) -> delete before[doc.id] unless isNew # This assumes everything we patch has a _id return + +module.exports.loadAchievements = -> + achievements = {} + Achievement = require('../achievements/Achievement') + query = Achievement.find({}) + query.exec (err, docs) -> + _.each docs, (achievement) -> + category = achievement.get 'collection' + achievements[category] = [] unless category of achievements + achievements[category].push achievement + +AchievablePlugin.loadAchievements() diff --git a/test/server/functional/achievement.spec.coffee b/test/server/functional/achievement.spec.coffee index 1ee3572d8..793248758 100644 --- a/test/server/functional/achievement.spec.coffee +++ b/test/server/functional/achievement.spec.coffee @@ -1,25 +1,29 @@ require '../common' +unlockable = + name: 'Dungeon Arena Started' + description: 'Started playing Dungeon Arena.' + worth: 3 + collection: 'level.session' + query: "{\"level.original\":\"dungeon-arena\"}" + userField: 'creator' + +repeatable = + name: 'Simulated' + description: 'Simulated Games.' + worth: 1 + collection: 'User' + query: "{\"simulatedBy\":{\"$gt\":\"0\"}}" + userField: '_id' + proportionalTo: 'simulatedBy' + +url = getURL('/db/achievement') + describe 'Achievement', -> - - unlockable = - name: 'One Time Only' - description: 'So you did the really cool thing.' - worth: 6.66 - collection: 'level.session' - - repeatable = - name: 'Lots of em' - description: 'Oops you did it again.' - worth: 1 - collection: 'User' - proportionalTo: '_id' - - url = getURL('/db/achievement') allowHeader = 'GET, POST, PUT, PATCH' it 'preparing test: deleting all Achievements first', (done) -> - clearModels [Achievement], (err) -> + clearModels [Achievement, EarnedAchievement, LevelSession, User], (err) -> expect(err).toBeNull() done() @@ -43,13 +47,17 @@ describe 'Achievement', -> request.post {uri: url, json: unlockable}, (err, res, body) -> expect(res.statusCode).toBe(200) unlockable._id = body._id - done() + + request.post {uri: url, json: repeatable}, (err, res, body) -> + expect(res.statusCode).toBe(200) + repeatable._id = body._id + done() it 'can get all for ordinary users', (done) -> loginJoe -> request.get {uri: url, json: unlockable}, (err, res, body) -> expect(res.statusCode).toBe(200) - expect(body.length).toBe(1) + expect(body.length).toBe(2) done() it 'can be read by ordinary users', (done) -> @@ -80,7 +88,23 @@ describe 'Achievement', -> expect(body.type).toBeDefined() done() - it 'cleaning up test: deleting all Achievements', (done) -> - clearModels [Achievement], (err) -> + +describe 'Achieving Achievements', -> + + it 'allows users to unlock one-time Achievements', (done) -> + loginJoe (joe) -> + levelSession = + creator: joe._id + level: original: 'dungeon-arena' + + request.post {uri:getURL('/db/level.session'), json:levelSession}, (session) -> + + done() + + + xit 'cleaning up test: deleting all Achievements and relates', (done) -> + clearModels [Achievement, EarnedAchievement, LevelSession], (err) -> expect(err).toBeNull() done() + + From dcdcca751f9b13dd65aecd856951530169235fdf Mon Sep 17 00:00:00 2001 From: Ruben Vereecken Date: Thu, 12 Jun 2014 19:39:45 +0200 Subject: [PATCH 19/22] Fiddled with creating xp functions --- app/lib/utils.coffee | 19 ++++++++++++++++--- app/models/Achievement.coffee | 3 +-- server/achievements/Achievement.coffee | 9 ++++----- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/app/lib/utils.coffee b/app/lib/utils.coffee index 5a8ae8915..8dbae8cc2 100644 --- a/app/lib/utils.coffee +++ b/app/lib/utils.coffee @@ -79,8 +79,21 @@ module.exports.getByPath = (target, path) -> module.exports.round = _.curry (digits, n) -> n = +n.toFixed(digits) -module.exports.createLinearFunc = (params) -> +positify = (func) -> (x) -> if x > 0 then func(x) else 0 + +# f(x) = ax + b +createLinearFunc = (params) -> (x) -> (params.a or 1) * x + (params.b or 0) -module.exports.createLogFunc = (params) -> - (x) -> (params.a or 1) * Math.log((params.b or 1) * (x + (params.c or 0))) +# f(x) = ax² + bx + c +createQuadraticFunc = (params) -> + (x) -> (params.a or 1) * x * x + (params.b or 1) * x + (params.c or 0) + +# f(x) = a log(b (x + c)) + d +createLogFunc = (params) -> + (x) -> if x > 0 then (params.a or 1) * Math.log((params.b or 1) * (x + (params.c or 0))) + (params.d or 0) else 0 + +module.exports.functionCreators = + linear: positify(createLinearFunc) + quadratic: positify(createQuadraticFunc) + logarithmic: positify(createLogFunc) diff --git a/app/models/Achievement.coffee b/app/models/Achievement.coffee index b2a6978ab..88603c6ef 100644 --- a/app/models/Achievement.coffee +++ b/app/models/Achievement.coffee @@ -13,5 +13,4 @@ module.exports = class Achievement extends CocoModel getExpFunction: -> kind = @get('function')?.kind or @schema.function.default.kind parameters = @get('function')?.parameters or @schema.function.default.parameters - funcCreator = if kind is 'linear' then util.createLinearFunc else if kind is 'logarithmic' then utils.createLogFunc - return funcCreator(parameters) if funcCreator? + return utils.functionCreators[kind](parameters) if kind of utils.functionCreators diff --git a/server/achievements/Achievement.coffee b/server/achievements/Achievement.coffee index a3168a2e3..83562018e 100644 --- a/server/achievements/Achievement.coffee +++ b/server/achievements/Achievement.coffee @@ -23,11 +23,10 @@ AchievementSchema.methods.objectifyQuery = -> AchievementSchema.methods.stringifyQuery = -> @set('query', JSON.stringify(@get('query'))) if typeof @get('query') != "string" -AchievementSchema.methods.getExpFunction = -> - kind = @get('function.kind') or jsonschema.function.default.kind - parameters = @get('function.parameters') or jsonschema.function.default.parameters - funcCreator = if kind is 'linear' then util.createLinearFunc else if kind is 'logarithmic' then util.createLogFunc - return funcCreator(parameters) if funcCreator? + getExpFunction: -> + kind = @get('function')?.kind or jsonschema.function.default.kind + parameters = @get('function')?.parameters or jsonschema.function.default.parameters + return utils.functionCreators[kind](parameters) if kind of utils.functionCreators AchievementSchema.post('init', (doc) -> doc.objectifyQuery()) From 994e4773cad8ceeaaa97cb5fb666b13663ebd623 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Thu, 12 Jun 2014 13:19:06 -0700 Subject: [PATCH 20/22] Reverting the objectId schema. --- app/schemas/schemas.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/schemas/schemas.coffee b/app/schemas/schemas.coffee index a1a8ed366..85e35d935 100644 --- a/app/schemas/schemas.coffee +++ b/app/schemas/schemas.coffee @@ -17,7 +17,7 @@ me.shortString = (ext) -> combine({type: 'string', maxLength: 100}, ext) me.pct = (ext) -> combine({type: 'number', maximum: 1.0, minimum: 0.0}, ext) me.date = (ext) -> combine({type: ['object', 'string'], format: 'date-time'}, ext) # should just be string (Mongo ID), but sometimes mongoose turns them into objects representing those, so we are lenient -me.objectId = (ext) -> schema = combine(['object', 'string'], ext) +me.objectId = (ext) -> schema = combine({type: ['object', 'string'] }, ext) me.url = (ext) -> combine({type: 'string', format: 'url', pattern: urlPattern}, ext) PointSchema = me.object {title: "Point", description: "An {x, y} coordinate point.", format: "point2d", required: ["x", "y"]}, From cdea4befd924d9282fecc49f54262a6f9e542368 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Thu, 12 Jun 2014 13:35:39 -0700 Subject: [PATCH 21/22] Giving the test server more time to spin up. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e7020f786..0e998a59e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,7 @@ before_script: - "mkdir mongo" - "mongod --dbpath=./mongo --fork --logpath ./mongodb.log" - "node index.js --unittest &" - - "sleep 5" # to give node a chance to start + - "sleep 10" # to give node a chance to start script: - "./node_modules/jasmine-node/bin/jasmine-node test/server/ --coffee --captureExceptions" From 152b860a235888fe7ad8c4a977f24950957366df Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Thu, 12 Jun 2014 14:44:55 -0700 Subject: [PATCH 22/22] Hotfix for the profile view. --- app/views/account/profile_view.coffee | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/views/account/profile_view.coffee b/app/views/account/profile_view.coffee index 0c2c95b17..ef49b583c 100644 --- a/app/views/account/profile_view.coffee +++ b/app/views/account/profile_view.coffee @@ -224,8 +224,11 @@ module.exports = class ProfileView extends View links = ($.extend(true, {}, link) for link in links) link.icon = @iconForLink link for link in links context.profileLinks = _.sortBy links, (link) -> not link.icon # icons first - context.sessions = (s.attributes for s in @sessions.models when (s.get('submitted') or s.get('level-id') is 'gridmancer')) - context.sessions.sort (a, b) -> (b.playtime ? 0) - (a.playtime ? 0) + if @sessions + context.sessions = (s.attributes for s in @sessions.models when (s.get('submitted') or s.get('level-id') is 'gridmancer')) + context.sessions.sort (a, b) -> (b.playtime ? 0) - (a.playtime ? 0) + else + context.sessions = [] context afterRender: ->