mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-02 03:47:09 -05:00
Merge branch 'master' into production
This commit is contained in:
commit
6f9371af02
50 changed files with 857 additions and 301 deletions
|
@ -18,7 +18,7 @@ before_script:
|
||||||
- "mkdir mongo"
|
- "mkdir mongo"
|
||||||
- "mongod --dbpath=./mongo --fork --logpath ./mongodb.log"
|
- "mongod --dbpath=./mongo --fork --logpath ./mongodb.log"
|
||||||
- "node index.js --unittest &"
|
- "node index.js --unittest &"
|
||||||
- "sleep 5" # to give node a chance to start
|
- "sleep 10" # to give node a chance to start
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- "./node_modules/jasmine-node/bin/jasmine-node test/server/ --coffee --captureExceptions"
|
- "./node_modules/jasmine-node/bin/jasmine-node test/server/ --coffee --captureExceptions"
|
||||||
|
|
|
@ -18,6 +18,7 @@ doQuerySelector = (value, operatorObj) ->
|
||||||
when '$ne' then return false if mapred value, body, (l, r) -> l == r
|
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 '$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 '$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
|
else return false
|
||||||
true
|
true
|
||||||
|
|
||||||
|
@ -34,7 +35,9 @@ matchesQuery = (target, queryObj) ->
|
||||||
pieces = prop.split('.')
|
pieces = prop.split('.')
|
||||||
obj = target
|
obj = target
|
||||||
for piece in pieces
|
for piece in pieces
|
||||||
return false unless piece of obj
|
unless piece of obj
|
||||||
|
obj = null
|
||||||
|
break
|
||||||
obj = obj[piece]
|
obj = obj[piece]
|
||||||
if typeof query != 'object' or _.isArray query
|
if typeof query != 'object' or _.isArray query
|
||||||
return false unless obj == query or (query in obj if _.isArray obj)
|
return false unless obj == query or (query in obj if _.isArray obj)
|
||||||
|
|
|
@ -75,3 +75,25 @@ module.exports.getByPath = (target, path) ->
|
||||||
return undefined unless piece of obj
|
return undefined unless piece of obj
|
||||||
obj = obj[piece]
|
obj = obj[piece]
|
||||||
obj
|
obj
|
||||||
|
|
||||||
|
module.exports.round = _.curry (digits, n) ->
|
||||||
|
n = +n.toFixed(digits)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
|
@ -507,7 +507,7 @@
|
||||||
new_thang_title_login: "Log In to Create a New Thang Type"
|
new_thang_title_login: "Log In to Create a New Thang Type"
|
||||||
new_level_title_login: "Log In to Create a New Level"
|
new_level_title_login: "Log In to Create a New Level"
|
||||||
new_achievement_title: "Create a New Achievement"
|
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"
|
article_search_title: "Search Articles Here"
|
||||||
thang_search_title: "Search Thang Types Here"
|
thang_search_title: "Search Thang Types Here"
|
||||||
level_search_title: "Search Levels Here"
|
level_search_title: "Search Levels Here"
|
||||||
|
|
|
@ -16,7 +16,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
|
||||||
play: "Jouer"
|
play: "Jouer"
|
||||||
retry: "Reessayer"
|
retry: "Reessayer"
|
||||||
watch: "Regarder"
|
watch: "Regarder"
|
||||||
# unwatch: "Unwatch"
|
unwatch: "Ne plus regarder"
|
||||||
submit_patch: "Soumettre un correctif"
|
submit_patch: "Soumettre un correctif"
|
||||||
|
|
||||||
units:
|
units:
|
||||||
|
@ -26,14 +26,14 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
|
||||||
minutes: "minutes"
|
minutes: "minutes"
|
||||||
hour: "heure"
|
hour: "heure"
|
||||||
hours: "heures"
|
hours: "heures"
|
||||||
# day: "day"
|
day: "jour"
|
||||||
# days: "days"
|
days: "jours"
|
||||||
# week: "week"
|
week: "semaine"
|
||||||
# weeks: "weeks"
|
weeks: "semaines"
|
||||||
# month: "month"
|
month: "mois"
|
||||||
# months: "months"
|
months: "mois"
|
||||||
# year: "year"
|
year: "année"
|
||||||
# years: "years"
|
years: "années"
|
||||||
|
|
||||||
modal:
|
modal:
|
||||||
close: "Fermer"
|
close: "Fermer"
|
||||||
|
@ -128,8 +128,8 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
|
||||||
forum_page: "notre forum"
|
forum_page: "notre forum"
|
||||||
forum_suffix: " À la place."
|
forum_suffix: " À la place."
|
||||||
send: "Envoyer un commentaire"
|
send: "Envoyer un commentaire"
|
||||||
# contact_candidate: "Contact Candidate"
|
contact_candidate: "Contacter le candidat"
|
||||||
# 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."
|
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:
|
diplomat_suggestion:
|
||||||
title: "Aidez à traduire CodeCombat!"
|
title: "Aidez à traduire CodeCombat!"
|
||||||
|
@ -173,11 +173,11 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
|
||||||
email_announcements: "Annonces"
|
email_announcements: "Annonces"
|
||||||
email_announcements_description: "Recevoir des mails sur les dernières actualités et sur le développement de CodeCombat."
|
email_announcements_description: "Recevoir des mails sur les dernières actualités et sur le développement de CodeCombat."
|
||||||
email_notifications: "Notifications"
|
email_notifications: "Notifications"
|
||||||
# email_notifications_summary: "Controls for personalized, automatic email notifications related to your CodeCombat activity."
|
email_notifications_summary: "Commandes pour personaliser les notifications automatiques d'email liées à votre activité sur CodeCombat."
|
||||||
# email_any_notes: "Any Notifications"
|
email_any_notes: "Toutes Notifications"
|
||||||
email_any_notes_description: "Désactivez pour ne plus recevoir de notifications par e-mail."
|
email_any_notes_description: "Désactivez pour ne plus recevoir de notifications par e-mail."
|
||||||
# email_recruit_notes: "Job Opportunities"
|
email_recruit_notes: "Offres d'emploi"
|
||||||
# email_recruit_notes_description: "If you play really well, we may contact you about getting you a (better) job."
|
email_recruit_notes_description: "Si vous jouez vraiment bien, nous pouvons vous contacter pour vous proposer un (meilleur) emploi."
|
||||||
contributor_emails: "Emails des contributeurs"
|
contributor_emails: "Emails des contributeurs"
|
||||||
contribute_prefix: "Nous recherchons des personnes pour se joindre à notre groupe! Consultez la "
|
contribute_prefix: "Nous recherchons des personnes pour se joindre à notre groupe! Consultez la "
|
||||||
contribute_page: "page de contributions"
|
contribute_page: "page de contributions"
|
||||||
|
@ -186,15 +186,15 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
|
||||||
error_saving: "Problème d'enregistrement"
|
error_saving: "Problème d'enregistrement"
|
||||||
saved: "Changements sauvegardés"
|
saved: "Changements sauvegardés"
|
||||||
password_mismatch: "Le mot de passe ne correspond pas."
|
password_mismatch: "Le mot de passe ne correspond pas."
|
||||||
# job_profile: "Job Profile"
|
job_profile: "Profil d'emploi"
|
||||||
# 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_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: "Hi! Fill this out, and we will get in touch about finding you a software developer job."
|
job_profile_explanation: "Salut! Remplissez-le et nous prendrons contact pour vous trouver un emploi de développeur de logiciels."
|
||||||
# sample_profile: "See a sample profile"
|
sample_profile: "Voir un exemple de profil"
|
||||||
view_profile: "Voir votre profil"
|
view_profile: "Voir votre profil"
|
||||||
|
|
||||||
account_profile:
|
account_profile:
|
||||||
edit_settings: "Éditer les préférences"
|
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_prefix: "Profil pour "
|
||||||
profile_for_suffix: ""
|
profile_for_suffix: ""
|
||||||
approved: "Approuvé"
|
approved: "Approuvé"
|
||||||
|
@ -202,57 +202,57 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
|
||||||
looking_for: "à la recherche de:"
|
looking_for: "à la recherche de:"
|
||||||
last_updated: "Dernière Mise à jour:"
|
last_updated: "Dernière Mise à jour:"
|
||||||
contact: "Contact"
|
contact: "Contact"
|
||||||
# active: "Looking for interview offers now"
|
active: "En recherche d'offres"
|
||||||
# inactive: "Not looking for offers right now"
|
inactive: "Ne recherche pas d'offres"
|
||||||
# complete: "complete"
|
complete: "terminé"
|
||||||
# next: "Next"
|
next: "Suivant"
|
||||||
# next_city: "city?"
|
next_city: "ville ?"
|
||||||
# next_country: "pick your country."
|
next_country: "choisissez votre pays."
|
||||||
# next_name: "name?"
|
next_name: "nom ?"
|
||||||
# next_short_description: "summarize yourself at a glance."
|
next_short_description: "résumez votre profil en quelques mots."
|
||||||
# next_long_description: "describe the work you're looking for."
|
next_long_description: "décrivez le travail que vous cherchez."
|
||||||
# next_skills: "list at least five skills."
|
next_skills: "listez au moins 5 compétances."
|
||||||
# next_work: "list your work experience."
|
next_work: "décrivez votre expérience professionnelle."
|
||||||
# next_education: "recount your educational ordeals."
|
next_education: "raconter votre scolarité."
|
||||||
# next_projects: "show off up to three projects you've worked on."
|
next_projects: "décrivez jusqu'à 3 projets sur lesquels vous avez travaillé."
|
||||||
# next_links: "add any personal or social links."
|
next_links: "ajouter des liens internet vers des sites personnels ou des réseaux sociaux."
|
||||||
# next_photo: "add an optional professional photo."
|
next_photo: "ajouter une photo professionelle (optionnel)."
|
||||||
# next_active: "mark yourself open to offers to show up in searches."
|
next_active: "déclarez vous ouvert aux offres pour apparaitre dans les recherches."
|
||||||
# example_blog: "Your Blog"
|
example_blog: "Votre blog"
|
||||||
# example_github: "Your GitHub"
|
example_github: "Votre GitHub"
|
||||||
# links_header: "Personal Links"
|
links_header: "Liens personnels"
|
||||||
# links_blurb: "Link any other sites or profiles you want to highlight, like your GitHub, your LinkedIn, or your blog."
|
links_blurb: "Lien vers d'autres sites ou profils que vous souhaitez mettre en avant, comme votre GitHub, LinkedIn ou votre blog."
|
||||||
# links_name: "Link Name"
|
links_name: "Nom du lien"
|
||||||
# links_name_help: "What are you linking to?"
|
links_name_help: "A quoi êtes vous lié ?"
|
||||||
# links_link_blurb: "Link URL"
|
links_link_blurb: "Lien URL"
|
||||||
# basics_header: "Update basic info"
|
basics_header: "Mettre à jour les information basiques"
|
||||||
# basics_active: "Open to Offers"
|
basics_active: "Ouvert aux propositions"
|
||||||
# basics_active_help: "Want interview offers right now?"
|
basics_active_help: "Voulez-vous des offres maintenant ?" # "Want interview offers right now?"
|
||||||
# basics_job_title: "Desired Job Title"
|
basics_job_title: "Titre du poste souhaité"
|
||||||
# basics_job_title_help: "What role are you looking for?"
|
basics_job_title_help: "Quel est le rôle que vous cherchez ?"
|
||||||
# basics_city: "City"
|
basics_city: "Ville"
|
||||||
# basics_city_help: "City you want to work in (or live in now)."
|
basics_city_help: "Ville dans laquelle vous souhaitez travailler (ou dans laquelle vous vivez actuellement)."
|
||||||
# basics_country: "Country"
|
basics_country: "Pays"
|
||||||
# basics_country_help: "Country you want to work in (or live in now)."
|
basics_country_help: "Pays dans lequel vous souhaitez travailler (ou dans lequel vous vivez actuellement)."
|
||||||
# basics_visa: "US Work Status"
|
basics_visa: "Status de travail aux Etats-Unis"
|
||||||
# basics_visa_help: "Are you authorized to work in the US, or do you need visa sponsorship?"
|
basics_visa_help: "Etes vous autorisé à travailler aux Etats-Unis ou avez vous besoin d'un parrainage pour le visa ?"
|
||||||
# basics_looking_for: "Looking For"
|
basics_looking_for: "Recherche"
|
||||||
# basics_looking_for_full_time: "Full-time"
|
basics_looking_for_full_time: "Temps plein"
|
||||||
# basics_looking_for_part_time: "Part-time"
|
basics_looking_for_part_time: "Temps partiel"
|
||||||
# basics_looking_for_remote: "Remote"
|
basics_looking_for_remote: "A distance"
|
||||||
# basics_looking_for_contracting: "Contracting"
|
basics_looking_for_contracting: "Contrat"
|
||||||
# basics_looking_for_internship: "Internship"
|
basics_looking_for_internship: "Stage"
|
||||||
# basics_looking_for_help: "What kind of developer position do you want?"
|
basics_looking_for_help: "Quel genre de poste de développeur voulez-vous ?"
|
||||||
# name_header: "Fill in your name"
|
name_header: "Remplissez votre nom"
|
||||||
# name_anonymous: "Anonymous Developer"
|
name_anonymous: "Developpeur Anonyme"
|
||||||
# name_help: "Name you want employers to see, like 'Nick Winter'."
|
name_help: "Le nom que vous souhaitez que l'employeur voie, par exemple 'Chuck Norris'."
|
||||||
# short_description_header: "Write a short description of yourself"
|
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_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: "Description courte"
|
||||||
# short_description_help: "Who are you, and what are you looking for? 140 characters max."
|
short_description_help: "Qui êtes vous et que recherchez vous ? 140 caractères max."
|
||||||
# skills_header: "Skills"
|
skills_header: "Compétences"
|
||||||
# skills_help: "Tag relevant developer skills in order of proficiency."
|
skills_help: "Notez vos compétence de développement par ordre de maitrise."
|
||||||
# long_description_header: "Detail your desired position"
|
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_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_blurb_2: "Talk about how awesome you are and why it would be a good idea to hire you."
|
||||||
# long_description: "Description"
|
# long_description: "Description"
|
||||||
|
@ -322,7 +322,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
|
||||||
goals: "Objectifs"
|
goals: "Objectifs"
|
||||||
success: "Succès"
|
success: "Succès"
|
||||||
incomplete: "Imcoplet"
|
incomplete: "Imcoplet"
|
||||||
# timed_out: "Ran out of time"
|
timed_out: "Plus de temps"
|
||||||
failing: "Echec"
|
failing: "Echec"
|
||||||
action_timeline: "Action sur la ligne de temps"
|
action_timeline: "Action sur la ligne de temps"
|
||||||
click_to_select: "Clique sur une unité pour la sélectionner."
|
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"
|
back: "Retour"
|
||||||
revert: "Annuler"
|
revert: "Annuler"
|
||||||
revert_models: "Annuler les modèles"
|
revert_models: "Annuler les modèles"
|
||||||
# fork_title: "Fork New Version"
|
fork_title: "Fork une nouvelle version"
|
||||||
# fork_creating: "Creating Fork..."
|
fork_creating: "Créer un Fork..."
|
||||||
more: "Plus"
|
more: "Plus"
|
||||||
wiki: "Wiki"
|
wiki: "Wiki"
|
||||||
live_chat: "Chat en live"
|
live_chat: "Chat en live"
|
||||||
|
@ -533,7 +533,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
|
||||||
message: "Message"
|
message: "Message"
|
||||||
code: "Code"
|
code: "Code"
|
||||||
ladder: "Companion"
|
ladder: "Companion"
|
||||||
when: "Lorsuqe"
|
when: "Quand"
|
||||||
opponent: "Adversaire"
|
opponent: "Adversaire"
|
||||||
rank: "Rang"
|
rank: "Rang"
|
||||||
score: "Score"
|
score: "Score"
|
||||||
|
@ -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!"
|
simulation_explanation: "En simulant une partie, tu peux classer ton rang plus rapidement!"
|
||||||
simulate_games: "Simuler une Partie!"
|
simulate_games: "Simuler une Partie!"
|
||||||
simulate_all: "REINITIALISER ET SIMULER DES PARTIES"
|
simulate_all: "REINITIALISER ET SIMULER DES PARTIES"
|
||||||
# games_simulated_by: "Games simulated by you:"
|
games_simulated_by: "Parties que vous avez simulé :"
|
||||||
# games_simulated_for: "Games simulated for you:"
|
games_simulated_for: "parties simulées pour vous :"
|
||||||
games_simulated: "Partie simulée"
|
games_simulated: "Partie simulée"
|
||||||
games_played: "Parties jouées"
|
games_played: "Parties jouées"
|
||||||
ratio: "Moyenne"
|
ratio: "Moyenne"
|
||||||
|
@ -776,11 +776,11 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
|
||||||
warmup: "Préchauffe"
|
warmup: "Préchauffe"
|
||||||
vs: "VS"
|
vs: "VS"
|
||||||
# friends_playing: "Friends Playing"
|
# friends_playing: "Friends Playing"
|
||||||
# log_in_for_friends: "Log in to play with your friends!"
|
log_in_for_friends: "Connectez vous pour jouer avec vos amis!"
|
||||||
# social_connect_blurb: "Connect and play against your friends!"
|
social_connect_blurb: "Connectez vous pour jouer contre vos amis!"
|
||||||
# invite_friends_to_battle: "Invite your friends to join you in battle!"
|
# invite_friends_to_battle: "Invite your friends to join you in battle!"
|
||||||
# fight: "Fight!"
|
fight: "Combattez !"
|
||||||
# watch_victory: "Watch your victory"
|
watch_victory: "Regardez votre victoire"
|
||||||
# defeat_the: "Defeat the"
|
# defeat_the: "Defeat the"
|
||||||
tournament_ends: "Fin du tournoi"
|
tournament_ends: "Fin du tournoi"
|
||||||
tournament_rules: "Règles du tournoi"
|
tournament_rules: "Règles du tournoi"
|
||||||
|
@ -835,7 +835,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
|
||||||
unknown: "Erreur inconnue."
|
unknown: "Erreur inconnue."
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
# your_sessions: "Your Sessions"
|
your_sessions: "vos Sessions"
|
||||||
level: "Niveau"
|
level: "Niveau"
|
||||||
# social_network_apis: "Social Network APIs"
|
# social_network_apis: "Social Network APIs"
|
||||||
facebook_status: "Statut Facebook"
|
facebook_status: "Statut Facebook"
|
||||||
|
@ -857,11 +857,11 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
|
||||||
# level_session: "Your Session"
|
# level_session: "Your Session"
|
||||||
# opponent_session: "Opponent Session"
|
# opponent_session: "Opponent Session"
|
||||||
article: "Article"
|
article: "Article"
|
||||||
# user_names: "User Names"
|
user_names: "Nom d'utilisateur"
|
||||||
# thang_names: "Thang Names"
|
# thang_names: "Thang Names"
|
||||||
files: "Fichiers"
|
files: "Fichiers"
|
||||||
top_simulators: "Top Simulateurs"
|
top_simulators: "Top Simulateurs"
|
||||||
# source_document: "Source Document"
|
source_document: "Document Source"
|
||||||
document: "Document"
|
document: "Document"
|
||||||
# sprite_sheet: "Sprite Sheet"
|
# sprite_sheet: "Sprite Sheet"
|
||||||
|
|
||||||
|
@ -869,7 +869,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
|
||||||
added: "Ajouté"
|
added: "Ajouté"
|
||||||
modified: "Modifié"
|
modified: "Modifié"
|
||||||
deleted: "Supprimé"
|
deleted: "Supprimé"
|
||||||
# moved_index: "Moved Index"
|
moved_index: "Index changé"
|
||||||
# text_diff: "Text Diff"
|
text_diff: "Différence de texte"
|
||||||
# merge_conflict_with: "MERGE CONFLICT WITH"
|
merge_conflict_with: "Fusionner les conflits avec"
|
||||||
no_changes: "Aucuns Changements"
|
no_changes: "Aucuns Changements"
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
CocoModel = require './CocoModel'
|
CocoModel = require './CocoModel'
|
||||||
|
util = require '../lib/utils'
|
||||||
|
|
||||||
module.exports = class Achievement extends CocoModel
|
module.exports = class Achievement extends CocoModel
|
||||||
@className: 'Achievement'
|
@className: 'Achievement'
|
||||||
|
@ -7,3 +8,9 @@ module.exports = class Achievement extends CocoModel
|
||||||
|
|
||||||
isRepeatable: ->
|
isRepeatable: ->
|
||||||
@get('proportionalTo')?
|
@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
|
||||||
|
return utils.functionCreators[kind](parameters) if kind of utils.functionCreators
|
||||||
|
|
|
@ -51,23 +51,18 @@ _.extend(AchievementSchema.properties,
|
||||||
type: 'string'
|
type: 'string'
|
||||||
description: 'For repeatables only. Denotes the field a repeatable achievement needs for its calculations'
|
description: 'For repeatables only. Denotes the field a repeatable achievement needs for its calculations'
|
||||||
function:
|
function:
|
||||||
type: 'object'
|
|
||||||
oneOf: [
|
|
||||||
linear:
|
|
||||||
type: 'object'
|
type: 'object'
|
||||||
properties:
|
properties:
|
||||||
a: {type: 'number', default: 1},
|
kind: {enum: ['linear', 'logarithmic'], default: 'linear'}
|
||||||
required: ['a']
|
parameters:
|
||||||
description: 'f(x) = a * x'
|
|
||||||
logarithmic:
|
|
||||||
type: 'object'
|
type: 'object'
|
||||||
properties:
|
properties:
|
||||||
a: {type: 'number', default: 1}
|
a: {type: 'number', default: 1}
|
||||||
b: {type: 'number', default: 1}
|
b: {type: 'number', default: 1}
|
||||||
required: ['a', 'b']
|
c: {type: 'number', default: 1}
|
||||||
description: 'f(x) = a * ln(1/b * (x + b))'
|
default: {kind: 'linear', parameters: a: 1}
|
||||||
]
|
required: ['kind', 'parameters']
|
||||||
default: linear: a: 1
|
additionalProperties: false
|
||||||
)
|
)
|
||||||
|
|
||||||
AchievementSchema.definitions = {}
|
AchievementSchema.definitions = {}
|
||||||
|
|
|
@ -20,15 +20,11 @@ module.exports =
|
||||||
href: '/db/achievement/{($)}'
|
href: '/db/achievement/{($)}'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
collection:
|
collection: type: 'string'
|
||||||
type: 'string'
|
achievementName: type: 'string'
|
||||||
achievementName:
|
created: type: 'date'
|
||||||
type: 'string'
|
changed: type: 'date'
|
||||||
created:
|
achievedAmount: type: 'number'
|
||||||
type: 'date'
|
earnedPoints: type: 'number'
|
||||||
changed:
|
previouslyAchievedAmount: {type: 'number', default: 0}
|
||||||
type: 'date'
|
notified: type: 'boolean'
|
||||||
achievedAmount:
|
|
||||||
type: 'number'
|
|
||||||
notified:
|
|
||||||
type: 'boolean'
|
|
||||||
|
|
|
@ -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.pct = (ext) -> combine({type: 'number', maximum: 1.0, minimum: 0.0}, ext)
|
||||||
me.date = (ext) -> combine({type: ['object', 'string'], format: 'date-time'}, 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
|
# 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)
|
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"]},
|
PointSchema = me.object {title: "Point", description: "An {x, y} coordinate point.", format: "point2d", required: ["x", "y"]},
|
||||||
|
|
12
app/styles/editor/achievement/edit.sass
Normal file
12
app/styles/editor/achievement/edit.sass
Normal file
|
@ -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
|
|
@ -45,3 +45,6 @@
|
||||||
font-family: Bangers
|
font-family: Bangers
|
||||||
font-size: 16px
|
font-size: 16px
|
||||||
float: right
|
float: right
|
||||||
|
|
||||||
|
.progress-bar-white
|
||||||
|
background-color: white
|
||||||
|
|
|
@ -33,3 +33,11 @@ block content
|
||||||
a(href="/admin/base", data-i18n="admin.av_other_debug_base_url") Base (for debugging base.jade)
|
a(href="/admin/base", data-i18n="admin.av_other_debug_base_url") Base (for debugging base.jade)
|
||||||
li
|
li
|
||||||
a(href="/admin/clas", data-i18n="admin.clas") CLAs
|
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
|
||||||
|
|
|
@ -11,7 +11,8 @@ block content
|
||||||
li.active
|
li.active
|
||||||
| #{achievement.attributes.name}
|
| #{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
|
h3(data-i18n="achievement.edit_achievement_title") Edit Achievement
|
||||||
span
|
span
|
||||||
|
@ -24,6 +25,7 @@ block content
|
||||||
hr
|
hr
|
||||||
|
|
||||||
div#error-view
|
div#error-view
|
||||||
|
|
||||||
else
|
else
|
||||||
.alert.alert-danger
|
.alert.alert-danger
|
||||||
span Admin only. Turn around.
|
span Admin only. Turn around.
|
||||||
|
|
|
@ -37,6 +37,7 @@ block content
|
||||||
h3(data-i18n="play_level.tip_reticulating") Reticulating Splines...
|
h3(data-i18n="play_level.tip_reticulating") Reticulating Splines...
|
||||||
.progress.progress-striped.active
|
.progress.progress-striped.active
|
||||||
.progress-bar
|
.progress-bar
|
||||||
|
|
||||||
else
|
else
|
||||||
.alert.alert-danger
|
.alert.alert-danger
|
||||||
span Admin only. Turn around.
|
span Admin only. Turn around.
|
||||||
|
|
11
app/templates/modal/confirm.jade
Normal file
11
app/templates/modal/confirm.jade
Normal file
|
@ -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}
|
|
@ -224,8 +224,11 @@ module.exports = class ProfileView extends View
|
||||||
links = ($.extend(true, {}, link) for link in links)
|
links = ($.extend(true, {}, link) for link in links)
|
||||||
link.icon = @iconForLink link for link in links
|
link.icon = @iconForLink link for link in links
|
||||||
context.profileLinks = _.sortBy links, (link) -> not link.icon # icons first
|
context.profileLinks = _.sortBy links, (link) -> not link.icon # icons first
|
||||||
|
if @sessions
|
||||||
context.sessions = (s.attributes for s in @sessions.models when (s.get('submitted') or s.get('level-id') is 'gridmancer'))
|
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)
|
context.sessions.sort (a, b) -> (b.playtime ? 0) - (a.playtime ? 0)
|
||||||
|
else
|
||||||
|
context.sessions = []
|
||||||
context
|
context
|
||||||
|
|
||||||
afterRender: ->
|
afterRender: ->
|
||||||
|
|
|
@ -8,6 +8,7 @@ module.exports = class AdminView extends View
|
||||||
|
|
||||||
events:
|
events:
|
||||||
'click #enter-espionage-mode': 'enterEspionageMode'
|
'click #enter-espionage-mode': 'enterEspionageMode'
|
||||||
|
'click #increment-button': 'incrementUserAttribute'
|
||||||
|
|
||||||
enterEspionageMode: ->
|
enterEspionageMode: ->
|
||||||
userEmail = $("#user-email").val().toLowerCase()
|
userEmail = $("#user-email").val().toLowerCase()
|
||||||
|
@ -29,3 +30,8 @@ module.exports = class AdminView extends View
|
||||||
|
|
||||||
espionageFailure: (jqxhr, status,error)->
|
espionageFailure: (jqxhr, status,error)->
|
||||||
console.log "There was an error entering espionage mode: #{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()
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
View = require 'views/kinds/RootView'
|
View = require 'views/kinds/RootView'
|
||||||
template = require 'templates/editor/achievement/edit'
|
template = require 'templates/editor/achievement/edit'
|
||||||
Achievement = require 'models/Achievement'
|
Achievement = require 'models/Achievement'
|
||||||
|
ConfirmModal = require 'views/modal/confirm'
|
||||||
|
|
||||||
module.exports = class AchievementEditView extends View
|
module.exports = class AchievementEditView extends View
|
||||||
id: "editor-achievement-edit-view"
|
id: "editor-achievement-edit-view"
|
||||||
|
@ -9,6 +10,7 @@ module.exports = class AchievementEditView extends View
|
||||||
|
|
||||||
events:
|
events:
|
||||||
'click #save-button': 'saveAchievement'
|
'click #save-button': 'saveAchievement'
|
||||||
|
'click #recalculate-button': 'confirmRecalculation'
|
||||||
|
|
||||||
subscriptions:
|
subscriptions:
|
||||||
'save-new': 'saveAchievement'
|
'save-new': 'saveAchievement'
|
||||||
|
@ -72,3 +74,34 @@ module.exports = class AchievementEditView extends View
|
||||||
res.success =>
|
res.success =>
|
||||||
url = "/editor/achievement/#{@achievement.get('slug') or @achievement.id}"
|
url = "/editor/achievement/#{@achievement.get('slug') or @achievement.id}"
|
||||||
document.location.href = url
|
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'
|
||||||
|
|
|
@ -4,7 +4,7 @@ ThangType = require 'models/ThangType'
|
||||||
CocoCollection = require 'collections/CocoCollection'
|
CocoCollection = require 'collections/CocoCollection'
|
||||||
|
|
||||||
class ThangTypeSearchCollection extends CocoCollection
|
class ThangTypeSearchCollection extends CocoCollection
|
||||||
url: '/db/thang.type/search?project=true'
|
url: '/db/thang.type?project=true'
|
||||||
model: ThangType
|
model: ThangType
|
||||||
|
|
||||||
addTerm: (term) ->
|
addTerm: (term) ->
|
||||||
|
|
|
@ -5,7 +5,7 @@ LevelSystem = require 'models/LevelSystem'
|
||||||
CocoCollection = require 'collections/CocoCollection'
|
CocoCollection = require 'collections/CocoCollection'
|
||||||
|
|
||||||
class LevelSystemSearchCollection extends CocoCollection
|
class LevelSystemSearchCollection extends CocoCollection
|
||||||
url: '/db/level_system/search'
|
url: '/db/level_system'
|
||||||
model: LevelSystem
|
model: LevelSystem
|
||||||
|
|
||||||
module.exports = class LevelSystemAddView extends View
|
module.exports = class LevelSystemAddView extends View
|
||||||
|
|
|
@ -21,7 +21,7 @@ componentOriginals =
|
||||||
"physics.Physical" : "524b75ad7fc0f6d519000001"
|
"physics.Physical" : "524b75ad7fc0f6d519000001"
|
||||||
|
|
||||||
class ThangTypeSearchCollection extends CocoCollection
|
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
|
model: ThangType
|
||||||
|
|
||||||
module.exports = class ThangsTabView extends View
|
module.exports = class ThangsTabView extends View
|
||||||
|
|
|
@ -8,6 +8,7 @@ locale = require 'locale/locale'
|
||||||
|
|
||||||
Achievement = require '../../models/Achievement'
|
Achievement = require '../../models/Achievement'
|
||||||
User = require '../../models/User'
|
User = require '../../models/User'
|
||||||
|
# TODO remove
|
||||||
|
|
||||||
filterKeyboardEvents = (allowedEvents, func) ->
|
filterKeyboardEvents = (allowedEvents, func) ->
|
||||||
return (splat...) ->
|
return (splat...) ->
|
||||||
|
@ -25,26 +26,24 @@ module.exports = class RootView extends CocoView
|
||||||
subscriptions:
|
subscriptions:
|
||||||
'achievements:new': 'handleNewAchievements'
|
'achievements:new': 'handleNewAchievements'
|
||||||
|
|
||||||
initialize: ->
|
|
||||||
$ =>
|
|
||||||
# TODO Ruben remove this. Allows for easy testing right now though
|
|
||||||
#test = new Achievement(_id:'537ce4855c91b8d1dda7fda8')
|
|
||||||
#test.fetch(success:@showNewAchievement)
|
|
||||||
|
|
||||||
showNewAchievement: (achievement) ->
|
showNewAchievement: (achievement, earnedAchievement) ->
|
||||||
currentLevel = me.level()
|
currentLevel = me.level()
|
||||||
nextLevel = currentLevel + 1
|
nextLevel = currentLevel + 1
|
||||||
currentLevelExp = User.expForLevel(currentLevel)
|
currentLevelExp = User.expForLevel(currentLevel)
|
||||||
nextLevelExp = User.expForLevel(nextLevel)
|
nextLevelExp = User.expForLevel(nextLevel)
|
||||||
totalExpNeeded = nextLevelExp - currentLevelExp
|
totalExpNeeded = nextLevelExp - currentLevelExp
|
||||||
|
expFunction = achievement.getExpFunction()
|
||||||
currentExp = me.get('points')
|
currentExp = me.get('points')
|
||||||
worth = achievement.get('worth')
|
previousExp = currentExp - achievement.get('worth')
|
||||||
leveledUp = currentExp - worth < currentLevelExp
|
previousExp = expFunction(earnedAchievement.get('previouslyAchievedAmount')) * achievement.get('worth') if achievement.isRepeatable()
|
||||||
alreadyAchievedPercentage = 100 * (currentExp - currentLevelExp - worth) / totalExpNeeded
|
achievedExp = currentExp - previousExp
|
||||||
newlyAchievedPercentage = if currentLevelExp is currentExp then 0 else 100 * worth / totalExpNeeded
|
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 "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 = $("<div class='progress-bar progress-bar-warning' style='width:#{alreadyAchievedPercentage}%'></div>")
|
alreadyAchievedBar = $("<div class='progress-bar progress-bar-warning' style='width:#{alreadyAchievedPercentage}%'></div>")
|
||||||
newlyAchievedBar = $("<div data-toggle='tooltip' class='progress-bar progress-bar-success' style='width:#{newlyAchievedPercentage}%'></div>")
|
newlyAchievedBar = $("<div data-toggle='tooltip' class='progress-bar progress-bar-success' style='width:#{newlyAchievedPercentage}%'></div>")
|
||||||
|
@ -53,7 +52,7 @@ module.exports = class RootView extends CocoView
|
||||||
message = if (currentLevel isnt 1) and leveledUp then "Reached level #{currentLevel}!" else null
|
message = if (currentLevel isnt 1) and leveledUp then "Reached level #{currentLevel}!" else null
|
||||||
|
|
||||||
alreadyAchievedBar.tooltip(title: "#{currentExp} XP in total")
|
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}")
|
emptyBar.tooltip(title: "#{nextLevelExp - currentExp} XP until level #{nextLevel}")
|
||||||
|
|
||||||
# TODO a default should be linked here
|
# TODO a default should be linked here
|
||||||
|
@ -63,7 +62,7 @@ module.exports = class RootView extends CocoView
|
||||||
image: $("<img src='#{imageURL}' />")
|
image: $("<img src='#{imageURL}' />")
|
||||||
description: achievement.get('description')
|
description: achievement.get('description')
|
||||||
progressBar: progressBar
|
progressBar: progressBar
|
||||||
earnedExp: "+ #{worth} XP"
|
earnedExp: "+ #{achievedExp} XP"
|
||||||
message: message
|
message: message
|
||||||
|
|
||||||
options =
|
options =
|
||||||
|
@ -77,13 +76,11 @@ module.exports = class RootView extends CocoView
|
||||||
$.notify( data, options )
|
$.notify( data, options )
|
||||||
|
|
||||||
handleNewAchievements: (earnedAchievements) ->
|
handleNewAchievements: (earnedAchievements) ->
|
||||||
console.debug 'Got new earned achievements'
|
|
||||||
# TODO performance?
|
|
||||||
_.each(earnedAchievements.models, (earnedAchievement) =>
|
_.each(earnedAchievements.models, (earnedAchievement) =>
|
||||||
achievement = new Achievement(_id: earnedAchievement.get('achievement'))
|
achievement = new Achievement(_id: earnedAchievement.get('achievement'))
|
||||||
console.log achievement
|
console.log achievement
|
||||||
achievement.fetch(
|
achievement.fetch(
|
||||||
success: @showNewAchievement
|
success: (achievement) => @showNewAchievement(achievement, earnedAchievement)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ app = require('application')
|
||||||
|
|
||||||
class SearchCollection extends Backbone.Collection
|
class SearchCollection extends Backbone.Collection
|
||||||
initialize: (modelURL, @model, @term, @projection) ->
|
initialize: (modelURL, @model, @term, @projection) ->
|
||||||
@url = "#{modelURL}/search?project="
|
@url = "#{modelURL}?project="
|
||||||
if @projection? and not (@projection == [])
|
if @projection? and not (@projection == [])
|
||||||
@url += projection[0]
|
@url += projection[0]
|
||||||
@url += ',' + projected for projected in projection[1..]
|
@url += ',' + projected for projected in projection[1..]
|
||||||
|
|
30
app/views/modal/confirm.coffee
Normal file
30
app/views/modal/confirm.coffee
Normal file
|
@ -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
|
|
@ -1,6 +1,8 @@
|
||||||
mongoose = require('mongoose')
|
mongoose = require('mongoose')
|
||||||
jsonschema = require('../../app/schemas/models/achievement')
|
jsonschema = require('../../app/schemas/models/achievement')
|
||||||
log = require 'winston'
|
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,
|
# `pre` and `post` are not called for update operations executed directly on the database,
|
||||||
# including `Model.update`,`.findByIdAndUpdate`,`.findOneAndUpdate`, `.findOneAndRemove`,and `.findByIdAndRemove`.order
|
# including `Model.update`,`.findByIdAndUpdate`,`.findOneAndUpdate`, `.findOneAndRemove`,and `.findByIdAndRemove`.order
|
||||||
|
@ -11,16 +13,21 @@ AchievementSchema = new mongoose.Schema({
|
||||||
userField: String
|
userField: String
|
||||||
}, {strict: false})
|
}, {strict: false})
|
||||||
|
|
||||||
AchievementSchema.methods.objectifyQuery = () ->
|
AchievementSchema.methods.objectifyQuery = ->
|
||||||
try
|
try
|
||||||
@set('query', JSON.parse(@get('query'))) if typeof @get('query') == "string"
|
@set('query', JSON.parse(@get('query'))) if typeof @get('query') == "string"
|
||||||
catch error
|
catch error
|
||||||
log.error "Couldn't convert query string to object because of #{error}"
|
log.error "Couldn't convert query string to object because of #{error}"
|
||||||
@set('query', {})
|
@set('query', {})
|
||||||
|
|
||||||
AchievementSchema.methods.stringifyQuery = () ->
|
AchievementSchema.methods.stringifyQuery = ->
|
||||||
@set('query', JSON.stringify(@get('query'))) if typeof @get('query') != "string"
|
@set('query', JSON.stringify(@get('query'))) if typeof @get('query') != "string"
|
||||||
|
|
||||||
|
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())
|
AchievementSchema.post('init', (doc) -> doc.objectifyQuery())
|
||||||
|
|
||||||
AchievementSchema.pre('save', (next) ->
|
AchievementSchema.pre('save', (next) ->
|
||||||
|
@ -28,9 +35,11 @@ AchievementSchema.pre('save', (next) ->
|
||||||
next()
|
next()
|
||||||
)
|
)
|
||||||
|
|
||||||
module.exports = Achievement = mongoose.model('Achievement', AchievementSchema)
|
|
||||||
|
|
||||||
plugins = require('../plugins/plugins')
|
|
||||||
|
|
||||||
AchievementSchema.plugin(plugins.NamedPlugin)
|
AchievementSchema.plugin(plugins.NamedPlugin)
|
||||||
AchievementSchema.plugin(plugins.SearchablePlugin, {searchable: ['name']})
|
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()
|
||||||
|
|
|
@ -13,7 +13,15 @@ EarnedAchievementSchema = new mongoose.Schema({
|
||||||
default: false
|
default: false
|
||||||
}, {strict: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, achievement: 1}, {unique: true, name: 'earned achievement index'})
|
||||||
EarnedAchievementSchema.index({user: 1, changed: -1}, {name: 'latest '})
|
EarnedAchievementSchema.index({user: 1, changed: -1}, {name: 'latest '})
|
||||||
|
|
||||||
|
|
||||||
module.exports = EarnedAchievement = mongoose.model('EarnedAchievement', EarnedAchievementSchema)
|
module.exports = EarnedAchievement = mongoose.model('EarnedAchievement', EarnedAchievementSchema)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,17 +5,11 @@ class AchievementHandler extends Handler
|
||||||
modelClass: Achievement
|
modelClass: Achievement
|
||||||
|
|
||||||
# Used to determine which properties requests may edit
|
# 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'
|
jsonSchema = require '../../app/schemas/models/achievement.coffee'
|
||||||
|
|
||||||
hasAccess: (req) ->
|
hasAccess: (req) ->
|
||||||
req.method is 'GET' or req.user?.isAdmin()
|
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()
|
module.exports = new AchievementHandler()
|
||||||
|
|
|
@ -1,12 +1,115 @@
|
||||||
mongoose = require('mongoose')
|
log = require 'winston'
|
||||||
|
mongoose = require 'mongoose'
|
||||||
|
async = require 'async'
|
||||||
|
Achievement = require './Achievement'
|
||||||
EarnedAchievement = require './EarnedAchievement'
|
EarnedAchievement = require './EarnedAchievement'
|
||||||
|
User = require '../users/User'
|
||||||
Handler = require '../commons/Handler'
|
Handler = require '../commons/Handler'
|
||||||
|
LocalMongo = require '../../app/lib/LocalMongo'
|
||||||
|
|
||||||
class EarnedAchievementHandler extends Handler
|
class EarnedAchievementHandler extends Handler
|
||||||
modelClass: EarnedAchievement
|
modelClass: EarnedAchievement
|
||||||
|
|
||||||
# Don't allow POSTs or anything yet
|
# Don't allow POSTs or anything yet
|
||||||
hasAccess: (req) ->
|
hasAccess: (req) ->
|
||||||
req.method is 'GET'
|
req.method is 'GET' # or req.user.isAdmin()
|
||||||
|
|
||||||
|
recalculate: (req, res) ->
|
||||||
|
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, {}
|
||||||
|
|
||||||
|
# Returns success: boolean
|
||||||
|
# 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
|
||||||
|
onFinished = callbackOrSlugsOrIDs
|
||||||
|
|
||||||
|
filter = {}
|
||||||
|
filter.$or = [
|
||||||
|
{_id: $in: achievementIDs},
|
||||||
|
{slug: $in: achievementSlugs}
|
||||||
|
] if achievementSlugs? or achievementIDs?
|
||||||
|
|
||||||
|
# Fetch all relevant achievements
|
||||||
|
Achievement.find filter, (err, achievements) ->
|
||||||
|
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
|
||||||
|
_.each alreadyEarned, (earned) ->
|
||||||
|
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) ->
|
||||||
|
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()
|
||||||
|
newPoints = expFunction(earned.achievedAmount) * achievement.get('worth')
|
||||||
|
else
|
||||||
|
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?
|
||||||
|
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
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
module.exports = new EarnedAchievementHandler()
|
module.exports = new EarnedAchievementHandler()
|
||||||
|
|
|
@ -17,6 +17,7 @@ module.exports = class Handler
|
||||||
postEditableProperties: []
|
postEditableProperties: []
|
||||||
jsonSchema: {}
|
jsonSchema: {}
|
||||||
waterfallFunctions: []
|
waterfallFunctions: []
|
||||||
|
allowedMethods: ['GET', 'POST', 'PUT', 'PATCH']
|
||||||
|
|
||||||
# subclasses should override these methods
|
# subclasses should override these methods
|
||||||
hasAccess: (req) -> true
|
hasAccess: (req) -> true
|
||||||
|
@ -63,13 +64,55 @@ module.exports = class Handler
|
||||||
|
|
||||||
# generic handlers
|
# generic handlers
|
||||||
get: (req, res) ->
|
get: (req, res) ->
|
||||||
# by default, ordinary users never get unfettered access to the database
|
@sendUnauthorizedError(res) if not @hasAccess(req)
|
||||||
return @sendUnauthorizedError(res) unless req.user?.isAdmin()
|
|
||||||
|
|
||||||
# admins can send any sort of query down the wire, though
|
specialParameters = ['term', 'project', '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 || '[]')
|
conditions = JSON.parse(req.query.conditions || '[]')
|
||||||
query = @modelClass.find()
|
|
||||||
|
|
||||||
try
|
try
|
||||||
for condition in conditions
|
for condition in conditions
|
||||||
name = condition[0]
|
name = condition[0]
|
||||||
|
@ -83,6 +126,10 @@ module.exports = class Handler
|
||||||
return @sendDatabaseError(res, err) if err
|
return @sendDatabaseError(res, err) if err
|
||||||
documents = (@formatEntity(req, doc) for doc in documents)
|
documents = (@formatEntity(req, doc) for doc in documents)
|
||||||
@sendSuccess(res, documents)
|
@sendSuccess(res, documents)
|
||||||
|
# regular users are only allowed text searches for now, without any additional filters or sorting
|
||||||
|
else
|
||||||
|
return @sendUnauthorizedError(res)
|
||||||
|
|
||||||
|
|
||||||
getById: (req, res, id) ->
|
getById: (req, res, id) ->
|
||||||
# return @sendNotFoundError(res) # for testing
|
# return @sendNotFoundError(res) # for testing
|
||||||
|
@ -153,44 +200,6 @@ module.exports = class Handler
|
||||||
return @sendDatabaseError(res, err) if err
|
return @sendDatabaseError(res, err) if err
|
||||||
@sendSuccess(res, @formatEntity(req, document))
|
@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) ->
|
versions: (req, res, id) ->
|
||||||
# TODO: a flexible system for doing GAE-like cursors for these sort of paginating queries
|
# 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.
|
# Keeping it simple for now and just allowing access to the first FETCH_LIMIT results.
|
||||||
|
@ -420,3 +429,7 @@ module.exports = class Handler
|
||||||
dict[document.id] = document
|
dict[document.id] = document
|
||||||
res.send dict
|
res.send dict
|
||||||
res.end()
|
res.end()
|
||||||
|
|
||||||
|
delete: (req, res) -> @sendMethodNotAllowed res, @allowedMethods, "DELETE not allowed."
|
||||||
|
|
||||||
|
head: (req, res) -> @sendMethodNotAllowed res, @allowedMethods, "HEAD not allowed."
|
||||||
|
|
|
@ -17,8 +17,9 @@ module.exports.notFound = (res, message='Not found.') ->
|
||||||
res.send 404, message
|
res.send 404, message
|
||||||
res.end()
|
res.end()
|
||||||
|
|
||||||
module.exports.badMethod = (res, message='Method Not Allowed') ->
|
module.exports.badMethod = (res, allowed=['GET', 'POST', 'PUT', 'PATCH'], message='Method Not Allowed') ->
|
||||||
# TODO: The response MUST include an Allow header containing a list of valid methods for the requested resource
|
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.send 405, message
|
||||||
res.end()
|
res.end()
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ module.exports.handlers =
|
||||||
|
|
||||||
module.exports.routes =
|
module.exports.routes =
|
||||||
[
|
[
|
||||||
|
'routes/admin'
|
||||||
'routes/auth'
|
'routes/auth'
|
||||||
'routes/contact'
|
'routes/contact'
|
||||||
'routes/db'
|
'routes/db'
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
mongoose = require('mongoose')
|
mongoose = require('mongoose')
|
||||||
Achievement = require('../achievements/Achievement')
|
|
||||||
EarnedAchievement = require '../achievements/EarnedAchievement'
|
EarnedAchievement = require '../achievements/EarnedAchievement'
|
||||||
LocalMongo = require '../../app/lib/LocalMongo'
|
LocalMongo = require '../../app/lib/LocalMongo'
|
||||||
util = require '../../app/lib/utils'
|
util = require '../../app/lib/utils'
|
||||||
|
@ -7,18 +6,9 @@ log = require 'winston'
|
||||||
|
|
||||||
achievements = {}
|
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) ->
|
module.exports = AchievablePlugin = (schema, options) ->
|
||||||
User = require '../users/User'
|
User = require '../users/User' # Avoid mutual inclusion cycles
|
||||||
|
Achievement = require('../achievements/Achievement')
|
||||||
|
|
||||||
checkForAchievement = (doc) ->
|
checkForAchievement = (doc) ->
|
||||||
collectionName = doc.constructor.modelName
|
collectionName = doc.constructor.modelName
|
||||||
|
@ -54,6 +44,8 @@ module.exports = AchievablePlugin = (schema, options) ->
|
||||||
achievement: achievement._id.toHexString()
|
achievement: achievement._id.toHexString()
|
||||||
achievementName: achievement.get 'name'
|
achievementName: achievement.get 'name'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
worth = achievement.get('worth')
|
||||||
earnedPoints = 0
|
earnedPoints = 0
|
||||||
wrapUp = ->
|
wrapUp = ->
|
||||||
# Update user's experience points
|
# Update user's experience points
|
||||||
|
@ -68,23 +60,37 @@ module.exports = AchievablePlugin = (schema, options) ->
|
||||||
newAmount = docObj[proportionalTo]
|
newAmount = docObj[proportionalTo]
|
||||||
|
|
||||||
if originalAmount isnt newAmount
|
if originalAmount isnt newAmount
|
||||||
|
expFunction = achievement.getExpFunction()
|
||||||
earned.notified = false
|
earned.notified = false
|
||||||
earned.achievedAmount = newAmount
|
earned.achievedAmount = newAmount
|
||||||
earned.changed = Date.now()
|
earned.earnedPoints = (expFunction(newAmount) - expFunction(originalAmount)) * worth
|
||||||
EarnedAchievement.findOneAndUpdate({achievement:earned.achievement, user:earned.user}, earned, upsert:true, (err, docs) ->
|
earned.previouslyAchievedAmount = originalAmount
|
||||||
|
EarnedAchievement.update {achievement:earned.achievement, user:earned.user}, earned, {upsert: true}, (err) ->
|
||||||
return log.debug err if err?
|
return log.debug err if err?
|
||||||
)
|
|
||||||
|
|
||||||
earnedPoints = achievement.get('worth') * (newAmount - originalAmount)
|
earnedPoints = earned.earnedPoints
|
||||||
|
log.debug earnedPoints
|
||||||
wrapUp()
|
wrapUp()
|
||||||
|
|
||||||
else # not alreadyAchieved
|
else # not alreadyAchieved
|
||||||
log.debug 'Creating a new earned achievement called \'' + (achievement.get 'name') + '\' for ' + userID
|
log.debug 'Creating a new earned achievement called \'' + (achievement.get 'name') + '\' for ' + userID
|
||||||
|
earned.earnedPoints = worth
|
||||||
(new EarnedAchievement(earned)).save (err, doc) ->
|
(new EarnedAchievement(earned)).save (err, doc) ->
|
||||||
return log.debug err if err?
|
return log.debug err if err?
|
||||||
|
earnedPoints = worth
|
||||||
earnedPoints = achievement.get('worth')
|
|
||||||
wrapUp()
|
wrapUp()
|
||||||
|
|
||||||
delete before[doc.id] unless isNew # This assumes everything we patch has a _id
|
delete before[doc.id] unless isNew # This assumes everything we patch has a _id
|
||||||
return
|
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()
|
||||||
|
|
|
@ -23,7 +23,7 @@ module.exports.PatchablePlugin = (schema) ->
|
||||||
schema.is_patchable = true
|
schema.is_patchable = true
|
||||||
schema.index({'target.original':1, 'status':'1', 'created':-1})
|
schema.index({'target.original':1, 'status':'1', 'created':-1})
|
||||||
|
|
||||||
RESERVED_NAMES = ['search', 'names']
|
RESERVED_NAMES = ['names']
|
||||||
|
|
||||||
module.exports.NamedPlugin = (schema) ->
|
module.exports.NamedPlugin = (schema) ->
|
||||||
schema.uses_coco_names = true
|
schema.uses_coco_names = true
|
||||||
|
|
27
server/routes/admin.coffee
Normal file
27
server/routes/admin.coffee
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
log = require 'winston'
|
||||||
|
errors = require '../commons/errors'
|
||||||
|
handlers = require('../commons/mapping').handlers
|
||||||
|
|
||||||
|
mongoose = require('mongoose')
|
||||||
|
|
||||||
|
module.exports.setup = (app) ->
|
||||||
|
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..]
|
||||||
|
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.")
|
|
@ -34,7 +34,6 @@ module.exports.setup = (app) ->
|
||||||
return handler.getLatestVersion(req, res, parts[1], parts[3]) if parts[2] is 'version'
|
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.versions(req, res, parts[1]) if parts[2] is 'versions'
|
||||||
return handler.files(req, res, parts[1]) if parts[2] is 'files'
|
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.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.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]?
|
return handler.getById(req, res, parts[1]) if req.route.method is 'get' and parts[1]?
|
||||||
|
|
|
@ -8,7 +8,7 @@ module.exports.setup = (app) ->
|
||||||
app.all '/file*', (req, res) ->
|
app.all '/file*', (req, res) ->
|
||||||
return fileGet(req, res) if req.route.method is 'get'
|
return fileGet(req, res) if req.route.method is 'get'
|
||||||
return filePost(req, res) if req.route.method is 'post'
|
return filePost(req, res) if req.route.method is 'post'
|
||||||
return errors.badMethod(res)
|
return errors.badMethod(res, ['GET', 'POST'])
|
||||||
|
|
||||||
|
|
||||||
fileGet = (req, res) ->
|
fileGet = (req, res) ->
|
||||||
|
|
|
@ -4,7 +4,7 @@ errors = require '../commons/errors'
|
||||||
module.exports.setup = (app) ->
|
module.exports.setup = (app) ->
|
||||||
app.all '/folder*', (req, res) ->
|
app.all '/folder*', (req, res) ->
|
||||||
return folderGet(req, res) if req.route.method is 'get'
|
return folderGet(req, res) if req.route.method is 'get'
|
||||||
return errors.badMethod(res)
|
return errors.badMethod(res, ['GET'])
|
||||||
|
|
||||||
folderGet = (req, res) ->
|
folderGet = (req, res) ->
|
||||||
folder = req.path[7..]
|
folder = req.path[7..]
|
||||||
|
|
|
@ -11,7 +11,7 @@ module.exports.setup = (app) ->
|
||||||
|
|
||||||
app.all '/languages', (req, res) ->
|
app.all '/languages', (req, res) ->
|
||||||
# Now that these are in the client, not sure when we would use this, but hey
|
# 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)
|
res.send(languages)
|
||||||
return res.end()
|
return res.end()
|
||||||
|
|
||||||
|
|
|
@ -64,7 +64,7 @@ isHTTPMethodPost = (req) -> return req.route.method is 'post'
|
||||||
isHTTPMethodPut = (req) -> return req.route.method is 'put'
|
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}")
|
sendQueueError = (req,res, error) -> errors.serverError(res, "Route #{req.path} had a problem: #{error}")
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ describe 'Local Mongo queries', ->
|
||||||
LocalMongo = require 'lib/LocalMongo'
|
LocalMongo = require 'lib/LocalMongo'
|
||||||
|
|
||||||
beforeEach ->
|
beforeEach ->
|
||||||
this.fixture1 =
|
@fixture1 =
|
||||||
'id': 'somestring'
|
'id': 'somestring'
|
||||||
'value': 9000
|
'value': 9000
|
||||||
'levels': [3, 8, 21]
|
'levels': [3, 8, 21]
|
||||||
|
@ -10,68 +10,72 @@ describe 'Local Mongo queries', ->
|
||||||
'type': 'unicorn'
|
'type': 'unicorn'
|
||||||
'likes': ['poptarts', 'popsicles', 'popcorn']
|
'likes': ['poptarts', 'popsicles', 'popcorn']
|
||||||
|
|
||||||
this.fixture2 = this: is: so: 'deep'
|
@fixture2 = this: is: so: 'deep'
|
||||||
|
|
||||||
it 'regular match of a property', ->
|
it 'regular match of a property', ->
|
||||||
expect(LocalMongo.matchesQuery(this.fixture1, 'gender': 'unicorn')).toBeFalsy()
|
expect(LocalMongo.matchesQuery(@fixture1, 'gender': 'unicorn')).toBeFalsy()
|
||||||
expect(LocalMongo.matchesQuery(this.fixture1, 'type':'unicorn')).toBeTruthy()
|
expect(LocalMongo.matchesQuery(@fixture1, 'type':'unicorn')).toBeTruthy()
|
||||||
expect(LocalMongo.matchesQuery(this.fixture1, 'type':'zebra')).toBeFalsy()
|
expect(LocalMongo.matchesQuery(@fixture1, 'type':'zebra')).toBeFalsy()
|
||||||
expect(LocalMongo.matchesQuery(this.fixture1, 'type':'unicorn', 'id':'somestring')).toBeTruthy()
|
expect(LocalMongo.matchesQuery(@fixture1, 'type':'unicorn', 'id':'somestring')).toBeTruthy()
|
||||||
|
|
||||||
it 'array match of a property', ->
|
it 'array match of a property', ->
|
||||||
expect(LocalMongo.matchesQuery(this.fixture1, 'likes':'poptarts')).toBeTruthy()
|
expect(LocalMongo.matchesQuery(@fixture1, 'likes':'poptarts')).toBeTruthy()
|
||||||
expect(LocalMongo.matchesQuery(this.fixture1, 'likes':'walks on the beach')).toBeFalsy()
|
expect(LocalMongo.matchesQuery(@fixture1, 'likes':'walks on the beach')).toBeFalsy()
|
||||||
|
|
||||||
it 'nested match', ->
|
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', ->
|
it '$gt selector', ->
|
||||||
expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$gt': 8000)).toBeTruthy()
|
expect(LocalMongo.matchesQuery(@fixture1, 'value': '$gt': 8000)).toBeTruthy()
|
||||||
expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$gt': [8000, 10000])).toBeTruthy()
|
expect(LocalMongo.matchesQuery(@fixture1, 'value': '$gt': [8000, 10000])).toBeTruthy()
|
||||||
expect(LocalMongo.matchesQuery(this.fixture1, 'levels': '$gt': [10, 20, 30])).toBeTruthy()
|
expect(LocalMongo.matchesQuery(@fixture1, 'levels': '$gt': [10, 20, 30])).toBeTruthy()
|
||||||
expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$gt': 9000)).toBeFalsy()
|
expect(LocalMongo.matchesQuery(@fixture1, 'value': '$gt': 9000)).toBeFalsy()
|
||||||
expect(LocalMongo.matchesQuery(this.fixture1, 'value': {'$gt': 8000}, 'worth': {'$gt': 5})).toBeTruthy()
|
expect(LocalMongo.matchesQuery(@fixture1, 'value': {'$gt': 8000}, 'worth': {'$gt': 5})).toBeTruthy()
|
||||||
|
|
||||||
it '$gte selector', ->
|
it '$gte selector', ->
|
||||||
expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$gte': 9001)).toBeFalsy()
|
expect(LocalMongo.matchesQuery(@fixture1, 'value': '$gte': 9001)).toBeFalsy()
|
||||||
expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$gte': 9000)).toBeTruthy()
|
expect(LocalMongo.matchesQuery(@fixture1, 'value': '$gte': 9000)).toBeTruthy()
|
||||||
expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$gte': [9000, 10000])).toBeTruthy()
|
expect(LocalMongo.matchesQuery(@fixture1, 'value': '$gte': [9000, 10000])).toBeTruthy()
|
||||||
expect(LocalMongo.matchesQuery(this.fixture1, 'levels': '$gte': [21, 30])).toBeTruthy()
|
expect(LocalMongo.matchesQuery(@fixture1, 'levels': '$gte': [21, 30])).toBeTruthy()
|
||||||
|
|
||||||
it '$lt selector', ->
|
it '$lt selector', ->
|
||||||
expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$lt': 9001)).toBeTruthy()
|
expect(LocalMongo.matchesQuery(@fixture1, 'value': '$lt': 9001)).toBeTruthy()
|
||||||
expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$lt': 9000)).toBeFalsy()
|
expect(LocalMongo.matchesQuery(@fixture1, 'value': '$lt': 9000)).toBeFalsy()
|
||||||
expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$lt': [9001, 9000])).toBeTruthy()
|
expect(LocalMongo.matchesQuery(@fixture1, 'value': '$lt': [9001, 9000])).toBeTruthy()
|
||||||
expect(LocalMongo.matchesQuery(this.fixture1, 'levels': '$lt': [10, 20, 30])).toBeTruthy()
|
expect(LocalMongo.matchesQuery(@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}, 'worth': {'$lt': 7})).toBeTruthy()
|
||||||
|
|
||||||
it '$lte selector', ->
|
it '$lte selector', ->
|
||||||
expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$lte': 9000)).toBeTruthy()
|
expect(LocalMongo.matchesQuery(@fixture1, 'value': '$lte': 9000)).toBeTruthy()
|
||||||
expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$lte': 8000)).toBeFalsy()
|
expect(LocalMongo.matchesQuery(@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}, 'worth': {'$lte': [6, 5]})).toBeTruthy()
|
||||||
|
|
||||||
it '$ne selector', ->
|
it '$ne selector', ->
|
||||||
expect(LocalMongo.matchesQuery(this.fixture1, 'value': '$ne': 9000)).toBeFalsy()
|
expect(LocalMongo.matchesQuery(@fixture1, 'value': '$ne': 9000)).toBeFalsy()
|
||||||
expect(LocalMongo.matchesQuery(this.fixture1, 'id': '$ne': 'otherstring')).toBeTruthy()
|
expect(LocalMongo.matchesQuery(@fixture1, 'id': '$ne': 'otherstring')).toBeTruthy()
|
||||||
expect(LocalMongo.matchesQuery(this.fixture1, 'id': '$ne': ['otherstring', 'somestring'])).toBeFalsy()
|
expect(LocalMongo.matchesQuery(@fixture1, 'id': '$ne': ['otherstring', 'somestring'])).toBeFalsy()
|
||||||
expect(LocalMongo.matchesQuery(this.fixture1, 'likes': '$ne': ['popcorn', 'chicken'])).toBeFalsy()
|
expect(LocalMongo.matchesQuery(@fixture1, 'likes': '$ne': ['popcorn', 'chicken'])).toBeFalsy()
|
||||||
|
|
||||||
it '$in selector', ->
|
it '$in selector', ->
|
||||||
expect(LocalMongo.matchesQuery(this.fixture1, 'type': '$in': ['unicorn', 'zebra'])).toBeTruthy()
|
expect(LocalMongo.matchesQuery(@fixture1, 'type': '$in': ['unicorn', 'zebra'])).toBeTruthy()
|
||||||
expect(LocalMongo.matchesQuery(this.fixture1, 'type': '$in': ['cats', 'dogs'])).toBeFalsy()
|
expect(LocalMongo.matchesQuery(@fixture1, 'type': '$in': ['cats', 'dogs'])).toBeFalsy()
|
||||||
expect(LocalMongo.matchesQuery(this.fixture1, 'likes': '$in': ['popcorn', 'chicken'])).toBeTruthy()
|
expect(LocalMongo.matchesQuery(@fixture1, 'likes': '$in': ['popcorn', 'chicken'])).toBeTruthy()
|
||||||
|
|
||||||
it '$nin selector', ->
|
it '$nin selector', ->
|
||||||
expect(LocalMongo.matchesQuery(this.fixture1, 'type': '$nin': ['unicorn', 'zebra'])).toBeFalsy()
|
expect(LocalMongo.matchesQuery(@fixture1, 'type': '$nin': ['unicorn', 'zebra'])).toBeFalsy()
|
||||||
expect(LocalMongo.matchesQuery(this.fixture1, 'type': '$nin': ['cats', 'dogs'])).toBeTruthy()
|
expect(LocalMongo.matchesQuery(@fixture1, 'type': '$nin': ['cats', 'dogs'])).toBeTruthy()
|
||||||
expect(LocalMongo.matchesQuery(this.fixture1, 'likes': '$nin': ['popcorn', 'chicken'])).toBeFalsy()
|
expect(LocalMongo.matchesQuery(@fixture1, 'likes': '$nin': ['popcorn', 'chicken'])).toBeFalsy()
|
||||||
|
|
||||||
it '$or operator', ->
|
it '$or operator', ->
|
||||||
expect(LocalMongo.matchesQuery(this.fixture1, $or: [{value:9000}, {type:'zebra'}])).toBeTruthy()
|
expect(LocalMongo.matchesQuery(@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:9001}, {worth:$lt:10}])).toBeTruthy()
|
||||||
|
|
||||||
it '$and operator', ->
|
it '$and operator', ->
|
||||||
expect(LocalMongo.matchesQuery(this.fixture1, $and: [{value:9000}, {type:'zebra'}])).toBeFalsy()
|
expect(LocalMongo.matchesQuery(@fixture1, $and: [{value:9000}, {type:'zebra'}])).toBeFalsy()
|
||||||
expect(LocalMongo.matchesQuery(this.fixture1, $and: [{value:9000}, {type:'unicorn'}])).toBeTruthy()
|
expect(LocalMongo.matchesQuery(@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:$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()
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,8 @@ models_path = [
|
||||||
'../../server/levels/thangs/LevelThangType'
|
'../../server/levels/thangs/LevelThangType'
|
||||||
'../../server/users/User'
|
'../../server/users/User'
|
||||||
'../../server/patches/Patch'
|
'../../server/patches/Patch'
|
||||||
|
'../../server/achievements/Achievement'
|
||||||
|
'../../server/achievements/EarnedAchievement'
|
||||||
]
|
]
|
||||||
|
|
||||||
for m in models_path
|
for m in models_path
|
||||||
|
|
110
test/server/functional/achievement.spec.coffee
Normal file
110
test/server/functional/achievement.spec.coffee
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
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', ->
|
||||||
|
allowHeader = 'GET, POST, PUT, PATCH'
|
||||||
|
|
||||||
|
it 'preparing test: deleting all Achievements first', (done) ->
|
||||||
|
clearModels [Achievement, EarnedAchievement, LevelSession, User], (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
|
||||||
|
|
||||||
|
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(2)
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,8 @@ describe '/db/article', ->
|
||||||
done()
|
done()
|
||||||
|
|
||||||
article = {name: 'Yo', body:'yo ma'}
|
article = {name: 'Yo', body:'yo ma'}
|
||||||
|
article2 = {name: 'Original', body:'yo daddy'}
|
||||||
|
|
||||||
url = getURL('/db/article')
|
url = getURL('/db/article')
|
||||||
articles = {}
|
articles = {}
|
||||||
|
|
||||||
|
@ -27,11 +29,22 @@ describe '/db/article', ->
|
||||||
expect(body.original).toBeDefined()
|
expect(body.original).toBeDefined()
|
||||||
expect(body.creator).toBeDefined()
|
expect(body.creator).toBeDefined()
|
||||||
articles[0] = body
|
articles[0] = body
|
||||||
|
|
||||||
|
# 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()
|
done()
|
||||||
|
|
||||||
it 'allows admins to make new minor versions', (done) ->
|
it 'allows admins to make new minor versions', (done) ->
|
||||||
new_article = _.clone(articles[0])
|
new_article = _.clone(articles[0])
|
||||||
new_article.body = '...'
|
new_article.body = 'yo daddy'
|
||||||
request.post {uri:url, json:new_article}, (err, res, body) ->
|
request.post {uri:url, json:new_article}, (err, res, body) ->
|
||||||
expect(res.statusCode).toBe(200)
|
expect(res.statusCode).toBe(200)
|
||||||
expect(body.version.major).toBe(0)
|
expect(body.version.major).toBe(0)
|
||||||
|
@ -62,7 +75,6 @@ describe '/db/article', ->
|
||||||
expect(body.body).toBe(articles[0].body)
|
expect(body.body).toBe(articles[0].body)
|
||||||
done()
|
done()
|
||||||
|
|
||||||
|
|
||||||
it 'does not allow regular users to make new versions', (done) ->
|
it 'does not allow regular users to make new versions', (done) ->
|
||||||
new_article = _.clone(articles[2])
|
new_article = _.clone(articles[2])
|
||||||
request.post {uri:url, json:new_article}, (err, res, body) ->
|
request.post {uri:url, json:new_article}, (err, res, body) ->
|
||||||
|
@ -87,9 +99,41 @@ describe '/db/article', ->
|
||||||
|
|
||||||
it 'does not allow naming an article a reserved word', (done) ->
|
it 'does not allow naming an article a reserved word', (done) ->
|
||||||
loginAdmin ->
|
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) ->
|
request.post {uri:url, json:new_article}, (err, res, body) ->
|
||||||
expect(res.statusCode).toBe(422)
|
expect(res.statusCode).toBe(422)
|
||||||
done()
|
done()
|
||||||
|
|
||||||
|
it 'allows regular users to get all articles', (done) ->
|
||||||
|
loginJoe ->
|
||||||
|
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', json:{}}, (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', json:{}}, (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="daddy"', json:{}}, (err, res, body) ->
|
||||||
|
expect(res.statusCode).toBe(200)
|
||||||
|
expect(body.length).toBe(1)
|
||||||
|
expect(body[0].name).toBe(article2.name)
|
||||||
|
expect(body[0].body).toBe(article2.body)
|
||||||
|
done()
|
||||||
|
|
|
@ -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'
|
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) ->
|
it 'preparing test : deletes all the files first', (done) ->
|
||||||
dropGridFS ->
|
dropGridFS ->
|
||||||
done()
|
done()
|
||||||
|
@ -147,19 +149,28 @@ xdescribe '/file', ->
|
||||||
|
|
||||||
request.post(options, func)
|
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) ->
|
it ' can\'t be requested with HTTP PUT method', (done) ->
|
||||||
request.put {uri:url}, (err, res) ->
|
request.put {uri:url}, (err, res) ->
|
||||||
expect(res.statusCode).toBe(405)
|
expect(res.statusCode).toBe(405)
|
||||||
|
expect(res.headers.allow).toBe(allowHeader)
|
||||||
done()
|
done()
|
||||||
|
|
||||||
it ' can\'t be requested with HTTP HEAD method', (done) ->
|
it ' can\'t be requested with HTTP HEAD method', (done) ->
|
||||||
request.head {uri:url}, (err, res) ->
|
request.head {uri:url}, (err, res) ->
|
||||||
expect(res.statusCode).toBe(405)
|
expect(res.statusCode).toBe(405)
|
||||||
|
expect(res.headers.allow).toBe(allowHeader)
|
||||||
done()
|
done()
|
||||||
|
|
||||||
it ' can\'t be requested with HTTP DEL method', (done) ->
|
it ' can\'t be requested with HTTP DEL method', (done) ->
|
||||||
request.del {uri:url}, (err, res) ->
|
request.del {uri:url}, (err, res) ->
|
||||||
expect(res.statusCode).toBe(405)
|
expect(res.statusCode).toBe(405)
|
||||||
|
expect(res.headers.allow).toBe(allowHeader)
|
||||||
done()
|
done()
|
||||||
|
|
||||||
# TODO: test server errors, see what they do
|
# TODO: test server errors, see what they do
|
||||||
|
|
35
test/server/functional/folder.spec.coffee
Normal file
35
test/server/functional/folder.spec.coffee
Normal file
|
@ -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()
|
35
test/server/functional/languages.spec.coffee
Normal file
35
test/server/functional/languages.spec.coffee
Normal file
|
@ -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()
|
|
@ -130,17 +130,17 @@ describe 'LevelComponent', ->
|
||||||
|
|
||||||
xit ' can\'t be requested with HTTP PUT method', (done) ->
|
xit ' can\'t be requested with HTTP PUT method', (done) ->
|
||||||
request.put {uri:url+'/'+components[0]._id}, (err, res) ->
|
request.put {uri:url+'/'+components[0]._id}, (err, res) ->
|
||||||
expect(res.statusCode).toBe(404)
|
expect(res.statusCode).toBe(405)
|
||||||
done()
|
done()
|
||||||
|
|
||||||
it ' can\'t be requested with HTTP HEAD method', (done) ->
|
it ' can\'t be requested with HTTP HEAD method', (done) ->
|
||||||
request.head {uri:url+'/'+components[0]._id}, (err, res) ->
|
request.head {uri:url+'/'+components[0]._id}, (err, res) ->
|
||||||
expect(res.statusCode).toBe(404)
|
expect(res.statusCode).toBe(405)
|
||||||
done()
|
done()
|
||||||
|
|
||||||
it ' can\'t be requested with HTTP DEL method', (done) ->
|
it ' can\'t be requested with HTTP DEL method', (done) ->
|
||||||
request.del {uri:url+'/'+components[0]._id}, (err, res) ->
|
request.del {uri:url+'/'+components[0]._id}, (err, res) ->
|
||||||
expect(res.statusCode).toBe(404)
|
expect(res.statusCode).toBe(405)
|
||||||
done()
|
done()
|
||||||
|
|
||||||
it 'get schema', (done) ->
|
it 'get schema', (done) ->
|
||||||
|
|
|
@ -123,12 +123,12 @@ describe 'LevelSystem', ->
|
||||||
|
|
||||||
it ' can\'t be requested with HTTP HEAD method', (done) ->
|
it ' can\'t be requested with HTTP HEAD method', (done) ->
|
||||||
request.head {uri:url+'/'+systems[0]._id}, (err, res) ->
|
request.head {uri:url+'/'+systems[0]._id}, (err, res) ->
|
||||||
expect(res.statusCode).toBe(404)
|
expect(res.statusCode).toBe(405)
|
||||||
done()
|
done()
|
||||||
|
|
||||||
it ' can\'t be requested with HTTP DEL method', (done) ->
|
it ' can\'t be requested with HTTP DEL method', (done) ->
|
||||||
request.del {uri:url+'/'+systems[0]._id}, (err, res) ->
|
request.del {uri:url+'/'+systems[0]._id}, (err, res) ->
|
||||||
expect(res.statusCode).toBe(404)
|
expect(res.statusCode).toBe(405)
|
||||||
done()
|
done()
|
||||||
|
|
||||||
it 'get schema', (done) ->
|
it 'get schema', (done) ->
|
||||||
|
|
25
test/server/functional/queue.spec.coffee
Normal file
25
test/server/functional/queue.spec.coffee
Normal file
|
@ -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()
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue