diff --git a/app/locale/ar.coffee b/app/locale/ar.coffee index e3e9a0caf..00b13df29 100644 --- a/app/locale/ar.coffee +++ b/app/locale/ar.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "العربية", englishDescription: "Arabi # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." # customize_wizard: "Customize Wizard" # game_menu: diff --git a/app/locale/bg.coffee b/app/locale/bg.coffee index d3332d086..9784c59a2 100644 --- a/app/locale/bg.coffee +++ b/app/locale/bg.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "български език", englishDescri # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." # customize_wizard: "Customize Wizard" # game_menu: diff --git a/app/locale/ca.coffee b/app/locale/ca.coffee index 94f614363..c838b2a69 100644 --- a/app/locale/ca.coffee +++ b/app/locale/ca.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "Català", englishDescription: "Catalan", tr # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." customize_wizard: "Personalitza el teu bruixot" game_menu: diff --git a/app/locale/cs.coffee b/app/locale/cs.coffee index 2b3281da2..1b59c262f 100644 --- a/app/locale/cs.coffee +++ b/app/locale/cs.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "čeština", englishDescription: "Czech", tr # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." customize_wizard: "Upravit Kouzelníka" game_menu: diff --git a/app/locale/da.coffee b/app/locale/da.coffee index ed5ce5386..29131e1bf 100644 --- a/app/locale/da.coffee +++ b/app/locale/da.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "dansk", englishDescription: "Danish", trans # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." customize_wizard: "Tilpas troldmand" game_menu: diff --git a/app/locale/de-AT.coffee b/app/locale/de-AT.coffee index 0087e1ea0..d34585229 100644 --- a/app/locale/de-AT.coffee +++ b/app/locale/de-AT.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "Deutsch (Österreich)", englishDescription: # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." customize_wizard: "Bearbeite den Zauberer" game_menu: diff --git a/app/locale/de-CH.coffee b/app/locale/de-CH.coffee index d2bd3ad7e..7b6b1451b 100644 --- a/app/locale/de-CH.coffee +++ b/app/locale/de-CH.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "Deutsch (Schweiz)", englishDescription: "Ge # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." customize_wizard: "Zauberer apasse" game_menu: diff --git a/app/locale/de-DE.coffee b/app/locale/de-DE.coffee index c16e3adee..0a3388ea5 100644 --- a/app/locale/de-DE.coffee +++ b/app/locale/de-DE.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription: tip_hofstadters_law: "Hofstadter's Gesetz: Es dauert immer länger als erwartet, auch wenn du Hofstadter's Gesetz anwendest." tip_premature_optimization: "Vorzeitige Optimierung ist die Wurzel allen Übels (oder mindestens des meisten) bei der Programmierung - Donald Knuth" tip_brute_force: "Verwende im Zweifelsfall rohe Gewalt. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." customize_wizard: "Bearbeite den Zauberer" game_menu: diff --git a/app/locale/el.coffee b/app/locale/el.coffee index 3947e9500..1ed8c7f4a 100644 --- a/app/locale/el.coffee +++ b/app/locale/el.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "Ελληνικά", englishDescription: "Gre # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." customize_wizard: "Προσαρμογή Μάγου" game_menu: diff --git a/app/locale/en-AU.coffee b/app/locale/en-AU.coffee index d4492aa88..e49714380 100644 --- a/app/locale/en-AU.coffee +++ b/app/locale/en-AU.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "English (AU)", englishDescription: "English # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." # customize_wizard: "Customize Wizard" # game_menu: diff --git a/app/locale/en-GB.coffee b/app/locale/en-GB.coffee index d9ed831b1..57028b941 100644 --- a/app/locale/en-GB.coffee +++ b/app/locale/en-GB.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "English (UK)", englishDescription: "English # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." customize_wizard: "Customise Wizard" # game_menu: diff --git a/app/locale/en-US.coffee b/app/locale/en-US.coffee index eb1fb863c..b2d0a4bec 100644 --- a/app/locale/en-US.coffee +++ b/app/locale/en-US.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "English (US)", englishDescription: "English # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." # customize_wizard: "Customize Wizard" # game_menu: diff --git a/app/locale/en.coffee b/app/locale/en.coffee index e13714240..e7f15c8c5 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -296,6 +296,7 @@ tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" tip_brute_force: "When in doubt, use brute force. - Ken Thompson" + tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." customize_wizard: "Customize Wizard" game_menu: diff --git a/app/locale/es-419.coffee b/app/locale/es-419.coffee index a415e6c9f..05054a66c 100644 --- a/app/locale/es-419.coffee +++ b/app/locale/es-419.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "español (América Latina)", englishDescrip tip_hofstadters_law: "Ley de Hofstadter: Siempre toma más tiempo del que esperas, inclso cuando tienes en cuenta la ley de Hofstadter." tip_premature_optimization: "La optimización prematura es la raíz de la maldad. - Donald Knuth" tip_brute_force: "Cuando tengas duda, usa la fuerza bruta. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." customize_wizard: "Personalizar Hechicero" game_menu: diff --git a/app/locale/es-ES.coffee b/app/locale/es-ES.coffee index 3a03d1d82..62ce99731 100644 --- a/app/locale/es-ES.coffee +++ b/app/locale/es-ES.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." tip_premature_optimization: "La optimizacion prematura es la raiz de todo mal. - Donald Knuth" tip_brute_force: "Cuando haya dudas, usa la fuerza bruta. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." customize_wizard: "Personalizar Mago" game_menu: diff --git a/app/locale/fa.coffee b/app/locale/fa.coffee index 5139f5a84..7de7917b8 100644 --- a/app/locale/fa.coffee +++ b/app/locale/fa.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "فارسی", englishDescription: "Persian", # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." # customize_wizard: "Customize Wizard" # game_menu: diff --git a/app/locale/fi.coffee b/app/locale/fi.coffee index b0fe0d705..5d474dca0 100644 --- a/app/locale/fi.coffee +++ b/app/locale/fi.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "suomi", englishDescription: "Finnish", tran # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." # customize_wizard: "Customize Wizard" # game_menu: diff --git a/app/locale/fr.coffee b/app/locale/fr.coffee index 972aab0c2..f43a79559 100644 --- a/app/locale/fr.coffee +++ b/app/locale/fr.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t tip_hofstadters_law: "Loi de Hofstadter: Il faut toujours plus de temps que prévu, même si vous prenez en compte la loi de Hofstadter." tip_premature_optimization: "L'optimisation prématurée est la racine de tous les maux. - Donald Knuth" tip_brute_force: "En cas de doute, utiliser la force brute. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." customize_wizard: "Personnaliser le magicien" game_menu: diff --git a/app/locale/gl.coffee b/app/locale/gl.coffee index 85f86d333..b969cb6c6 100644 --- a/app/locale/gl.coffee +++ b/app/locale/gl.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "Galego", englishDescription: "Galician", tr # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." tip_premature_optimization: "A optimizacion prematura é a raíz de todo mal. - Donald Knuth" tip_brute_force: "Cando hai dúbidas, usa a forza bruta. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." customize_wizard: "Persoalizar Mago" game_menu: diff --git a/app/locale/he.coffee b/app/locale/he.coffee index a4693e33c..dea375e6b 100644 --- a/app/locale/he.coffee +++ b/app/locale/he.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "עברית", englishDescription: "Hebrew", # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." # customize_wizard: "Customize Wizard" # game_menu: diff --git a/app/locale/hi.coffee b/app/locale/hi.coffee index 7a1fbf1e4..a810e327c 100644 --- a/app/locale/hi.coffee +++ b/app/locale/hi.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "मानक हिन्दी", englishDe # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." # customize_wizard: "Customize Wizard" # game_menu: diff --git a/app/locale/hu.coffee b/app/locale/hu.coffee index f7e1a85fa..18afa2096 100644 --- a/app/locale/hu.coffee +++ b/app/locale/hu.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "magyar", englishDescription: "Hungarian", t # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." customize_wizard: "Varázsló testreszabása" game_menu: diff --git a/app/locale/id.coffee b/app/locale/id.coffee index 6618d5b7a..896eb4f39 100644 --- a/app/locale/id.coffee +++ b/app/locale/id.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "Bahasa Indonesia", englishDescription: "Ind # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." # customize_wizard: "Customize Wizard" # game_menu: diff --git a/app/locale/it.coffee b/app/locale/it.coffee index c0ca2f6b1..fcffa53e8 100644 --- a/app/locale/it.coffee +++ b/app/locale/it.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." customize_wizard: "Personalizza il mago" game_menu: diff --git a/app/locale/ja.coffee b/app/locale/ja.coffee index a47d573e4..a3e89d596 100644 --- a/app/locale/ja.coffee +++ b/app/locale/ja.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "日本語", englishDescription: "Japanese", # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." customize_wizard: "魔法使いの設定" game_menu: diff --git a/app/locale/ko.coffee b/app/locale/ko.coffee index 619fc2875..c3e1b92fe 100644 --- a/app/locale/ko.coffee +++ b/app/locale/ko.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "한국어", englishDescription: "Korean", t # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." customize_wizard: "사용자 정의 마법사" game_menu: diff --git a/app/locale/lt.coffee b/app/locale/lt.coffee index 1cc498d2e..dd4b93dc4 100644 --- a/app/locale/lt.coffee +++ b/app/locale/lt.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "lietuvių kalba", englishDescription: "Lith # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." # customize_wizard: "Customize Wizard" # game_menu: diff --git a/app/locale/ms.coffee b/app/locale/ms.coffee index d94a7d2d7..7753985d6 100644 --- a/app/locale/ms.coffee +++ b/app/locale/ms.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "Bahasa Melayu", englishDescription: "Bahasa # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." # customize_wizard: "Customize Wizard" # game_menu: diff --git a/app/locale/nb.coffee b/app/locale/nb.coffee index ff0688d5f..39492f06a 100644 --- a/app/locale/nb.coffee +++ b/app/locale/nb.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "Norsk Bokmål", englishDescription: "Norweg tip_hofstadters_law: "Hofstadters Lov: Ting tar alltid lenger tid enn du tror, selv når du tar Hofstadters Lov med i beregningen." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." customize_wizard: "Tilpass Trollmann" game_menu: diff --git a/app/locale/nl-BE.coffee b/app/locale/nl-BE.coffee index a78c4997b..34683351d 100644 --- a/app/locale/nl-BE.coffee +++ b/app/locale/nl-BE.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "Nederlands (België)", englishDescription: # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." customize_wizard: "Pas Tovenaar aan" game_menu: diff --git a/app/locale/nl-NL.coffee b/app/locale/nl-NL.coffee index 01fbe4cef..cbab9b4c3 100644 --- a/app/locale/nl-NL.coffee +++ b/app/locale/nl-NL.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." customize_wizard: "Pas Tovenaar aan" game_menu: diff --git a/app/locale/nn.coffee b/app/locale/nn.coffee index 47d5b45a0..d2710c50d 100644 --- a/app/locale/nn.coffee +++ b/app/locale/nn.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "Norwegian Nynorsk", englishDescription: "No # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." # customize_wizard: "Customize Wizard" # game_menu: diff --git a/app/locale/no.coffee b/app/locale/no.coffee index 0be828d6f..1c332da56 100644 --- a/app/locale/no.coffee +++ b/app/locale/no.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "Norsk", englishDescription: "Norwegian", tr # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." customize_wizard: "Tilpass trollmann" game_menu: diff --git a/app/locale/pl.coffee b/app/locale/pl.coffee index 0608423e1..60d5e3fb5 100644 --- a/app/locale/pl.coffee +++ b/app/locale/pl.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "język polski", englishDescription: "Polish # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." customize_wizard: "Spersonalizuj czarodzieja" game_menu: diff --git a/app/locale/pt-BR.coffee b/app/locale/pt-BR.coffee index bf8c18f41..986ab87aa 100644 --- a/app/locale/pt-BR.coffee +++ b/app/locale/pt-BR.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "português do Brasil", englishDescription: tip_hofstadters_law: "Lei de Hofstadter: Sempre demora mais do que você espera, mesmo quando você leva em consideração a Lei de Hofstadter." tip_premature_optimization: "Uma otimização permatura é a raíz de todos os males. - Donald Knuth" tip_brute_force: "Na dúvida, utilize força bruta. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." customize_wizard: "Personalize o feiticeiro" game_menu: diff --git a/app/locale/pt-PT.coffee b/app/locale/pt-PT.coffee index 7ed91940b..2b163f12a 100644 --- a/app/locale/pt-PT.coffee +++ b/app/locale/pt-PT.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription: tip_hofstadters_law: "Lei de Hofstadter: Tudo demora sempre mais do que pensas, mesmo quando levas em conta a Lei de Hofstadter." tip_premature_optimization: "Uma otimização permatura é a raíz de todo o mal. - Donald Knuth" tip_brute_force: "Quando em dúvida, usa a força bruta. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." customize_wizard: "Personalizar Feiticeiro" game_menu: diff --git a/app/locale/ro.coffee b/app/locale/ro.coffee index 67cff3737..c2db60ea9 100644 --- a/app/locale/ro.coffee +++ b/app/locale/ro.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "limba română", englishDescription: "Roman # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." customize_wizard: "Personalizează Wizard-ul" game_menu: diff --git a/app/locale/ru.coffee b/app/locale/ru.coffee index 7756018fc..f356631c7 100644 --- a/app/locale/ru.coffee +++ b/app/locale/ru.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi tip_hofstadters_law: "Закон Хофштадтера: Любое дело всегда длится дольше, чем ожидается, даже если учесть закон Хофштадтера." tip_premature_optimization: "Поспешная оптимизация - корень всех зол. - Donald Knuth" tip_brute_force: "Когда сомневаешься используй грубую силу. - Кен Томпсон" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." customize_wizard: "Настройки волшебника" game_menu: @@ -376,13 +377,13 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi blocks: "Блокирует" # As in "this shield blocks this much damage" skills: "Умения" available_for_purchase: "Доступно для покупки" - level_to_unlock: "Разблокируется на уровне:" # ToDo: check in interface + level_to_unlock: "Разблокируется на уровне:" restricted_to_certain_heroes: "Только определенные герои могут играть этот уровень." skill_docs: # writable: "writable" # Hover over "attack" in Your Skills while playing a level to see most of this # read_only: "read-only" - action_name: "имя" # ToDo: check in interface + action_name: "имя" action_cooldown: "Применяется" action_specific_cooldown: "Восстановление" action_damage: "Повреждения" diff --git a/app/locale/sk.coffee b/app/locale/sk.coffee index 6172439c4..69108f840 100644 --- a/app/locale/sk.coffee +++ b/app/locale/sk.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "slovenčina", englishDescription: "Slovak", # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." # customize_wizard: "Customize Wizard" # game_menu: diff --git a/app/locale/sl.coffee b/app/locale/sl.coffee index c6dbdc816..46fb45a39 100644 --- a/app/locale/sl.coffee +++ b/app/locale/sl.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "slovenščina", englishDescription: "Sloven # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." # customize_wizard: "Customize Wizard" # game_menu: diff --git a/app/locale/sr.coffee b/app/locale/sr.coffee index 1aab0fdcf..da99b70eb 100644 --- a/app/locale/sr.coffee +++ b/app/locale/sr.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." customize_wizard: "Прилагоди Чаробњака" game_menu: diff --git a/app/locale/sv.coffee b/app/locale/sv.coffee index e6737e009..f049ee2ef 100644 --- a/app/locale/sv.coffee +++ b/app/locale/sv.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "Svenska", englishDescription: "Swedish", tr # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." customize_wizard: "Skräddarsy trollkarl" game_menu: diff --git a/app/locale/th.coffee b/app/locale/th.coffee index 96652cba4..6dcef569a 100644 --- a/app/locale/th.coffee +++ b/app/locale/th.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "ไทย", englishDescription: "Thai", tra # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." # customize_wizard: "Customize Wizard" game_menu: diff --git a/app/locale/tr.coffee b/app/locale/tr.coffee index cee9ae923..f01928092 100644 --- a/app/locale/tr.coffee +++ b/app/locale/tr.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "Türkçe", englishDescription: "Turkish", t # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." customize_wizard: "Sihirbazı Düzenle" game_menu: diff --git a/app/locale/uk.coffee b/app/locale/uk.coffee index 5de3e9a20..d5bd191fa 100644 --- a/app/locale/uk.coffee +++ b/app/locale/uk.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "українська мова", englishDesc # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." customize_wizard: "Налаштування персонажа" game_menu: diff --git a/app/locale/ur.coffee b/app/locale/ur.coffee index fe4a58ce2..bda5d5e9c 100644 --- a/app/locale/ur.coffee +++ b/app/locale/ur.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "اُردُو", englishDescription: "Urdu", # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." # customize_wizard: "Customize Wizard" # game_menu: diff --git a/app/locale/vi.coffee b/app/locale/vi.coffee index 1cc502633..25185e13e 100644 --- a/app/locale/vi.coffee +++ b/app/locale/vi.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." customize_wizard: "Tùy chỉnh Wizard" game_menu: diff --git a/app/locale/zh-HANS.coffee b/app/locale/zh-HANS.coffee index 8173dce3d..ec3058605 100644 --- a/app/locale/zh-HANS.coffee +++ b/app/locale/zh-HANS.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese tip_hofstadters_law: "侯世达定律:做事所花费的时间总是比你预期的要长,即使你的预期中考虑了侯世达定律。" tip_premature_optimization: "过早的优化是万恶之源。 - 高德纳" tip_brute_force: "拿不准时就用穷举法。 - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." customize_wizard: "自定义向导" game_menu: diff --git a/app/locale/zh-HANT.coffee b/app/locale/zh-HANT.coffee index 057020f25..493124707 100644 --- a/app/locale/zh-HANT.coffee +++ b/app/locale/zh-HANT.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "繁体中文", englishDescription: "Chinese # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." customize_wizard: "自定義巫師" game_menu: diff --git a/app/locale/zh-WUU-HANS.coffee b/app/locale/zh-WUU-HANS.coffee index 325404d2b..d71e50457 100644 --- a/app/locale/zh-WUU-HANS.coffee +++ b/app/locale/zh-WUU-HANS.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "吴语", englishDescription: "Wuu (Simplifi # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." # customize_wizard: "Customize Wizard" # game_menu: diff --git a/app/locale/zh-WUU-HANT.coffee b/app/locale/zh-WUU-HANT.coffee index e80869dc5..8c9b91c53 100644 --- a/app/locale/zh-WUU-HANT.coffee +++ b/app/locale/zh-WUU-HANT.coffee @@ -296,6 +296,7 @@ module.exports = nativeDescription: "吳語", englishDescription: "Wuu (Traditio # tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law." # tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth" # tip_brute_force: "When in doubt, use brute force. - Ken Thompson" +# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..." customize_wizard: "自設定獻路人" game_menu: diff --git a/app/schemas/models/user.coffee b/app/schemas/models/user.coffee index 015145768..1d6396ac0 100644 --- a/app/schemas/models/user.coffee +++ b/app/schemas/models/user.coffee @@ -278,6 +278,9 @@ _.extend UserSchema.properties, planID: { enum: ['basic'] } subscriptionID: { type: 'string' } token: { type: 'string' } + couponID: { type: 'string' } + discountID: { type: 'string' } + free: { type: ['boolean', 'string'], format: 'date-time' } } c.extendBasicProperties UserSchema, 'user' diff --git a/app/styles/play/level/tome/spell.sass b/app/styles/play/level/tome/spell.sass index b7d2b3169..964288585 100644 --- a/app/styles/play/level/tome/spell.sass +++ b/app/styles/play/level/tome/spell.sass @@ -107,6 +107,9 @@ .executed background-color: rgba(110, 110, 110, 0.12) + .locked-code + border: 1px dashed rgba(53, 45, 34, 0.5) + +keyframes(pulseRedBackground) from background-color: rgba(255, 45, 27, 0.4) diff --git a/app/templates/play/level/level_loading.jade b/app/templates/play/level/level_loading.jade index 97babe75c..d2590a7ec 100644 --- a/app/templates/play/level/level_loading.jade +++ b/app/templates/play/level/level_loading.jade @@ -52,6 +52,7 @@ strong.tip.rare(data-i18n='play_level.tip_hofstadters_law') Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law. strong.tip.rare(data-i18n='play_level.tip_premature_optimization') Premature optimization is the root of all evil - Donald Knuth strong.tip.rare(data-i18n='play_level.tip_brute_force') When in doubt, use brute force. - Ken Thompson + strong.tip.rare(data-i18n='play_level.tip_extrapolation') There are only two kinds of people: those that can extrapolate from incomplete data... strong.tip.rare span(data-i18n='play_level.tip_harry') Yer a Wizard, span= me.get('name', true) diff --git a/app/views/account/PaymentsView.coffee b/app/views/account/PaymentsView.coffee index 8ec4d888e..931b825a9 100644 --- a/app/views/account/PaymentsView.coffee +++ b/app/views/account/PaymentsView.coffee @@ -30,6 +30,7 @@ module.exports = class PaymentsView extends RootView onClickStartSubscription: (e) -> @openModalView new SubscribeModal() window.tracker?.trackEvent 'Show subscription modal', category: 'Subscription', label: 'payments view' + window.tracker?.trackPageView "subscription/show-modal", ['Google Analytics'] onSubscribed: -> document.location.reload() diff --git a/app/views/play/WorldMapView.coffee b/app/views/play/WorldMapView.coffee index 7afe43cad..90e8b05e2 100644 --- a/app/views/play/WorldMapView.coffee +++ b/app/views/play/WorldMapView.coffee @@ -124,6 +124,7 @@ module.exports = class WorldMapView extends RootView @openModalView? new modal() unless window.currentModal if modal is SubscribeModal window.tracker?.trackEvent 'Show subscription modal', category: 'Subscription', label: 'world map loadded' + window.tracker?.trackPageView "subscription/show-modal", ['Google Analytics'] , 2000 onSubscribed: -> @@ -214,6 +215,7 @@ module.exports = class WorldMapView extends RootView @openModalView new modal() if modal is SubscribeModal window.tracker?.trackEvent 'Show subscription modal', category: 'Subscription', label: 'map level clicked' + window.tracker?.trackPageView "subscription/show-modal", ['Google Analytics'] else if $(e.target).attr('disabled') Backbone.Mediator.publish 'router:navigate', route: '/contribute/adventurer' return @@ -222,11 +224,13 @@ module.exports = class WorldMapView extends RootView else @startLevel levelElement window.tracker?.trackEvent 'Clicked Level', category: 'World Map', levelID: levelID, ['Google Analytics'] + window.tracker?.trackPageView "world-map/clicked-level/#{levelID}", ['Google Analytics'] onClickStartLevel: (e) -> levelElement = $(e.target).parents('.level-info-container') @startLevel levelElement window.tracker?.trackEvent 'Clicked Start Level', category: 'World Map', levelID: levelElement.data('level-id'), ['Google Analytics'] + window.tracker?.trackPageView "world-map/clicked-start-level/#{levelElement.data('level-id')}", ['Google Analytics'] startLevel: (levelElement) -> @setupManager?.destroy() diff --git a/app/views/play/level/LevelLoadingView.coffee b/app/views/play/level/LevelLoadingView.coffee index 04955721f..99d8150ca 100644 --- a/app/views/play/level/LevelLoadingView.coffee +++ b/app/views/play/level/LevelLoadingView.coffee @@ -114,6 +114,7 @@ module.exports = class LevelLoadingView extends CocoView @openModalView new modal() if modal is SubscribeModal window.tracker?.trackEvent 'Show subscription modal', category: 'Subscription', label: 'level loading' + window.tracker?.trackPageView "subscription/show-modal", ['Google Analytics'] onSubscribed: -> document.location.reload() diff --git a/app/views/play/level/tome/SpellView.coffee b/app/views/play/level/tome/SpellView.coffee index 0582b9fb8..06d948f6d 100644 --- a/app/views/play/level/tome/SpellView.coffee +++ b/app/views/play/level/tome/SpellView.coffee @@ -273,6 +273,15 @@ module.exports = class SpellView extends CocoView return true for range in @readOnlyRanges when leftRange.intersects(range) false + intersectsRight = => + rightRange = @ace.getSelectionRange().clone() + if rightRange.end.column < @aceDoc.getLine(rightRange.end.row).length + rightRange.setEnd rightRange.end.row, rightRange.end.column + 1 + else if rightRange.start.row < @aceDoc.getLength() - 1 + rightRange.setEnd rightRange.end.row + 1, 0 + return true for range in @readOnlyRanges when rightRange.intersects(range) + false + preventReadonly = (next) -> return true if intersects() next?() @@ -284,27 +293,16 @@ module.exports = class SpellView extends CocoView wrapper => orig.apply obj, args obj[method] - finishRange = (row, startRow, startColumn) => - range = new Range startRow, startColumn, row, @aceSession.getLine(row).length - 1 - range.start = @aceDoc.createAnchor range.start - range.end = @aceDoc.createAnchor range.end - range.end.$insertRight = true - @readOnlyRanges.push range + if @lockedCodeMarkerID? + @aceSession.removeMarker @lockedCodeMarkerID + @lockedCodeMarkerID = null - # Create a read-only range for each chunk of text not separated by an empty line @readOnlyRanges = [] - startRow = startColumn = null - for row in [0...@aceSession.getLength()] - unless /^\s*$/.test @aceSession.getLine(row) - unless startRow? and startColumn? - startRow = row - startColumn = 0 - else - if startRow? and startColumn? - finishRange row - 1, startRow, startColumn - startRow = startColumn = null - if startRow? and startColumn? - finishRange @aceSession.getLength() - 1, startRow, startColumn + lines = @aceDoc.getAllLines() + lastRow = row for line, row in lines when not /^\s*$/.test(line) + if lastRow? + @readOnlyRanges.push new Range 0, 0, lastRow, lines[lastRow].length - 1 + @lockedCodeMarkerID = @aceSession.addMarker @readOnlyRanges[0], 'locked-code', 'fullLine' # Override write operations that intersect with default code interceptCommand @ace, 'onPaste', preventReadonly @@ -319,6 +317,9 @@ module.exports = class SpellView extends CocoView if e.command.name in ['Backspace', 'throttle-backspaces'] and intersectsLeft() @zatanna?.off?() return false + if e.command.name is 'del' and intersectsRight() + @zatanna?.off?() + return false if e.command.name in ['enter-skip-delimiters', 'Enter', 'Return'] if intersects() e.editor.navigateDown 1 diff --git a/app/views/play/modal/SubscribeModal.coffee b/app/views/play/modal/SubscribeModal.coffee index 59b34338b..cc8af4bb7 100644 --- a/app/views/play/modal/SubscribeModal.coffee +++ b/app/views/play/modal/SubscribeModal.coffee @@ -45,10 +45,12 @@ module.exports = class SubscribeModal extends ModalView container: @$el ).on 'shown.bs.popover', => application.tracker?.trackEvent 'Subscription parent hover', {} + application.tracker?.trackPageView "subscription/parent-hover", ['Google Analytics'] onClickPurchaseButton: (e) -> @playSound 'menu-button-click' application.tracker?.trackEvent 'Started subscription purchase', {} + application.tracker?.trackPageView "subscription/start-purchase", ['Google Analytics'] stripeHandler.open({ description: $.i18n.t('subscribe.stripe_description') amount: @product.amount @@ -69,6 +71,7 @@ module.exports = class SubscribeModal extends ModalView onSubscriptionSuccess: -> application.tracker?.trackEvent 'Finished subscription purchase', {} + application.tracker?.trackPageView "subscription/finish-purchase", ['Google Analytics'] Backbone.Mediator.publish 'subscribe-modal:subscribed', {} @playSound 'victory' @hide() diff --git a/server/payments/discount_handler.coffee b/server/payments/discount_handler.coffee new file mode 100644 index 000000000..64e865c94 --- /dev/null +++ b/server/payments/discount_handler.coffee @@ -0,0 +1,48 @@ +# Not paired with a document in the DB, just handles coordinating between +# the stripe property in the user with what's being stored in Stripe. + +Handler = require '../commons/Handler' +config = require '../../server_config' +stripe = require('stripe')(config.stripe.secretKey) + +class DiscountHandler extends Handler + logDiscountError: (req, msg) -> + console.warn "Discount Error: #{req.user.get('slug')} (#{req.user._id}): '#{msg}'" + + discountUser: (req, user, done) -> + if (not user) or user.isAnonymous() + return done({res: 'User must not be anonymous.', code: 403}) + + couponID = req.body.stripe.couponID + if not couponID + @logDiscountError(req, 'Missing couponID.') + return done({res: 'Missing couponID.', code: 422}) + + stripe.coupons.retrieve couponID, (err, coupon) => + if (err) + return done({res: 'No coupon with id '+couponID, code: 404}) + + if customerID = user.get('stripe')?.customerID + options = { coupon: coupon.id } + stripe.customers.update customerID, options, (err, customer) => + if err + @logDiscountError(req, 'Error applying coupon to customer'+customerID) + return done({res: 'Error applying coupon to customer.', code: 500}) + done() + + else + # couponID will be set on the user by the handler + done() + + removeDiscountFromCustomer: (req, user, done) -> + customerID = user.get('stripe').customerID + return done() unless customerID + + stripe.customers.deleteDiscount customerID, (err, customer) => + if err + console.log 'err?', err + @logDiscountError(req, 'Error removing coupon from customer ' + customerID) + return done({res: 'Error applying coupon to customer.', code: 500}) + done() + +module.exports = new DiscountHandler() \ No newline at end of file diff --git a/server/payments/payment_handler.coffee b/server/payments/payment_handler.coffee index f2e4f2bd4..cc077994b 100644 --- a/server/payments/payment_handler.coffee +++ b/server/payments/payment_handler.coffee @@ -166,29 +166,34 @@ PaymentHandler = class PaymentHandler extends Handler handleStripePaymentPost: (req, res, timestamp, productID, token) -> # First, make sure we save the payment info as a Customer object, if we haven't already. - if not req.user.get('stripe')?.customerID - stripe.customers.create({ - card: token - email: req.user.get('email') - metadata: { - id: req.user._id + '' - slug: req.user.get('slug') - } - }).then(((customer) => - stripeInfo = _.cloneDeep(req.user.get('stripe') ? {}) - stripeInfo.customerID = customer.id - req.user.set('stripe', stripeInfo) - req.user.save((err) => - if err - @logPaymentError(req, 'Stripe customer id save db error. '+err) - return @sendDatabaseError(res, err) + if token + customerID = req.user.get('stripe')?.customerID + + if customerID + # old customer, new token. Save it. + stripe.customers.update customerID, { card: token }, (err, customer) => @beginStripePayment(req, res, timestamp, productID) - ) - ), - (err) => - @logPaymentError(req, 'Stripe customer creation error. '+err) - return @sendDatabaseError(res, err) - ) + + else + newCustomer = { + card: token + email: req.user.get('email') + metadata: { id: req.user._id + '', slug: req.user.get('slug') } + } + + stripe.customers.create newCustomer, (err, customer) => + if err + @logPaymentError(req, 'Stripe customer creation error. '+err) + return @sendDatabaseError(res, err) + + stripeInfo = _.cloneDeep(req.user.get('stripe') ? {}) + stripeInfo.customerID = customer.id + req.user.set('stripe', stripeInfo) + req.user.save (err) => + if err + @logPaymentError(req, 'Stripe customer id save db error. '+err) + return @sendDatabaseError(res, err) + @beginStripePayment(req, res, timestamp, productID) else @beginStripePayment(req, res, timestamp, productID) diff --git a/server/payments/subscription_handler.coffee b/server/payments/subscription_handler.coffee index 231f059d4..f1d0531e5 100644 --- a/server/payments/subscription_handler.coffee +++ b/server/payments/subscription_handler.coffee @@ -4,6 +4,7 @@ Handler = require '../commons/Handler' config = require '../../server_config' stripe = require('stripe')(config.stripe.secretKey) +discountHandler = require './discount_handler' subscriptions = { basic: { @@ -19,61 +20,52 @@ class SubscriptionHandler extends Handler if (not req.user) or req.user.isAnonymous() return done({res: 'You must be signed in to subscribe.', code: 403}) - stripeToken = req.body.stripe?.token - extantCustomerID = user.get('stripe')?.customerID - if not (stripeToken or extantCustomerID) + token = req.body.stripe?.token + customerID = user.get('stripe')?.customerID + if not (token or customerID) @logSubscriptionError(req, 'Missing stripe token or customer ID.') return done({res: 'Missing stripe token or customer ID.', code: 422}) - if stripeToken - stripe.customers.create({ - card: stripeToken - email: req.user.get('email') - metadata: { - id: req.user._id + '' - slug: req.user.get('slug') + if token + if customerID + stripe.customers.update customerID, { card: token }, (err, customer) => + @checkForExistingSubscription(req, user, customer, done) + + else + newCustomer = { + card: token + email: req.user.get('email') + metadata: { id: req.user._id + '', slug: req.user.get('slug') } } - }).then(((customer) => + + stripe.customers.create newCustomer, (err, customer) => + if err + if err.type in ['StripeCardError', 'StripeInvalidRequestError'] + return done({res: 'Card error', code: 402}) + else + @logSubscriptionError(req, 'Stripe customer creation error. '+err) + return done({res: 'Database error.', code: 500}) + stripeInfo = _.cloneDeep(req.user.get('stripe') ? {}) stripeInfo.customerID = customer.id req.user.set('stripe', stripeInfo) - req.user.save((err) => + req.user.save (err) => if err @logSubscriptionError(req, 'Stripe customer id save db error. '+err) return done({res: 'Database error.', code: 500}) @checkForExistingSubscription(req, user, customer, done) - ) - ), - (err) => - if err.type in ['StripeCardError', 'StripeInvalidRequestError'] - done({res: 'Card error', code: 402}) - else - @logSubscriptionError(req, 'Stripe customer creation error. '+err) - return done({res: 'Database error.', code: 500}) - ) else - stripe.customers.retrieve(extantCustomerID, (err, customer) => + stripe.customers.retrieve(customerID, (err, customer) => if err @logSubscriptionError(req, 'Stripe customer creation error. '+err) return done({res: 'Database error.', code: 500}) - else if not customer - # TODO: what actually happens when you try to retrieve a customer and it DNE? - @logSubscriptionError(req, 'Stripe customer id is missing! '+err) - stripeInfo = _.cloneDeep(req.user.get('stripe') ? {}) - delete stripeInfo.customerID - req.user.set('stripe', stripeInfo) - req.user.save (err) => - if err - @logSubscriptionError(req, 'Stripe customer id delete db error. '+err) - return done({res: 'Database error.', code: 500}) - @subscribeUser(req, done) - else - @checkForExistingSubscription(req, user, customer, done) + @checkForExistingSubscription(req, user, customer, done) ) checkForExistingSubscription: (req, user, customer, done) -> + couponID = user.get('stripe')?.couponID if subscription = customer.subscriptions?.data?[0] if subscription.cancel_at_period_end @@ -87,30 +79,34 @@ class SubscriptionHandler extends Handler return done({res: 'Database error.', code: 500}) options = { plan: 'basic', trial_end: subscription.current_period_end } + options.coupon = couponID if couponID stripe.customers.update req.user.get('stripe').customerID, options, (err, customer) => if err @logSubscriptionError(req, 'Stripe customer plan setting error. '+err) return done({res: 'Database error.', code: 500}) - @updateUser(req, user, customer.subscriptions.data[0], false, done) + @updateUser(req, user, customer, false, done) else # can skip creating the subscription - return @updateUser(req, user, customer.subscriptions.data[0], false, done) + return @updateUser(req, user, customer, false, done) else - stripe.customers.update req.user.get('stripe').customerID, { plan: 'basic' }, (err, customer) => + options = { plan: 'basic' } + options.coupon = couponID if couponID + stripe.customers.update req.user.get('stripe').customerID, options, (err, customer) => if err @logSubscriptionError(req, 'Stripe customer plan setting error. '+err) return done({res: 'Database error.', code: 500}) - @updateUser(req, user, customer.subscriptions.data[0], true, done) + @updateUser(req, user, customer, true, done) - updateUser: (req, user, subscription, increment, done) -> + updateUser: (req, user, customer, increment, done) -> + subscription = customer.subscriptions.data[0] stripeInfo = _.cloneDeep(user.get('stripe') ? {}) stripeInfo.planID = 'basic' stripeInfo.subscriptionID = subscription.id - stripeInfo.customerID = subscription.customer + stripeInfo.customerID = customer.id req.body.stripe = stripeInfo # to make sure things work for admins, who are mad with power user.set('stripe', stripeInfo) diff --git a/server/users/user_handler.coffee b/server/users/user_handler.coffee index 083b91ab4..c413d93da 100644 --- a/server/users/user_handler.coffee +++ b/server/users/user_handler.coffee @@ -12,6 +12,7 @@ moment = require 'moment' LevelSession = require '../levels/sessions/LevelSession' LevelSessionHandler = require '../levels/sessions/level_session_handler' SubscriptionHandler = require '../payments/subscription_handler' +DiscountHandler = require '../payments/discount_handler' EarnedAchievement = require '../achievements/EarnedAchievement' UserRemark = require './remarks/UserRemark' {isID} = require '../lib/utils' @@ -123,6 +124,25 @@ UserHandler = class UserHandler extends Handler return callback(err) if err return callback(null, req, user) ) + + # Discount setting + (req, user, callback) -> + return callback(null, req, user) unless req.user?.isAdmin() + hasCoupon = user.get('stripe')?.couponID + wantsCoupon = req.body.stripe?.couponID + + return callback(null, req, user) if hasCoupon is wantsCoupon + if wantsCoupon and (hasCoupon isnt wantsCoupon) + DiscountHandler.discountUser(req, user, (err) -> + return callback(err) if err + return callback(null, req, user) + ) + else if hasCoupon and not wantsCoupon + DiscountHandler.removeDiscountFromCustomer(req, user, (err) -> + return callback(err) if err + return callback(null, req, user) + ) + ] getById: (req, res, id) -> diff --git a/server_setup.coffee b/server_setup.coffee index ad056068b..ae76032c2 100644 --- a/server_setup.coffee +++ b/server_setup.coffee @@ -56,7 +56,6 @@ setupExpressMiddleware = (app) -> express.logger.format('prod', productionLogging) app.use(express.logger('prod')) app.use express.compress filter: (req, res) -> - return false if req.headers.host is 'codecombat.com' # CloudFlare will gzip it for us on codecombat.com # But now it's disabled. compressible res.getHeader('Content-Type') else express.logger.format('dev', developmentLogging) diff --git a/test/server/functional/discount_handler.spec.coffee b/test/server/functional/discount_handler.spec.coffee new file mode 100644 index 000000000..909b97f41 --- /dev/null +++ b/test/server/functional/discount_handler.spec.coffee @@ -0,0 +1,114 @@ + +config = require '../../../server_config' +require '../common' + +# sample data that comes in through the webhook when you subscribe + + +describe '/db/user, editing stripe.couponID property', -> + + stripe = require('stripe')(config.stripe.secretKey) + userURL = getURL('/db/user') + webhookURL = getURL('/stripe/webhook') + + it 'clears the db first', (done) -> + clearModels [User, Payment], (err) -> + throw err if err + done() + + #- shared data between tests + joeData = null + firstSubscriptionID = null + + it 'does not work for non-admins', (done) -> + loginJoe (joe) -> + joeData = joe.toObject() + expect(joeData.stripe).toBeUndefined() + joeData.stripe = { couponID: '20pct' } + request.put {uri: userURL, json: joeData }, (err, res, body) -> + expect(res.statusCode).toBe(200) # fails silently + expect(res.body.stripe).toBeUndefined() # but still fails + done() + + it 'does not work with invalid coupons', (done) -> + loginAdmin (admin) -> + joeData.stripe = { couponID: 'DNE' } + request.put {uri: userURL, json: joeData }, (err, res, body) -> + expect(res.statusCode).toBe(404) + done() + + it 'sets the couponID on a user without an existing stripe object', (done) -> + joeData.stripe = { couponID: '20pct' } + request.put {uri: userURL, json: joeData }, (err, res, body) -> + joeData = body + expect(res.statusCode).toBe(200) + expect(body.stripe.couponID).toBe('20pct') + done() + + it 'just updates the couponID when it changes and there is no existing subscription', (done) -> + joeData.stripe.couponID = '500off' + request.put {uri: userURL, json: joeData }, (err, res, body) -> + expect(res.statusCode).toBe(200) + expect(body.stripe.couponID).toBe('500off') + done() + + it 'removes the couponID from the user when the admin makes it so', (done) -> + delete joeData.stripe.couponID + request.put {uri: userURL, json: joeData }, (err, res, body) -> + joeData = body + expect(res.statusCode).toBe(200) + expect(body.stripe).toBeUndefined() + done() + + it 'puts the coupon back', (done) -> + joeData.stripe = {couponID: '500off'} + request.put {uri: userURL, json: joeData }, (err, res, body) -> + expect(res.statusCode).toBe(200) + expect(body.stripe.couponID).toBe('500off') + done() + + it 'applies a discount to the newly created customer when a plan is set', (done) -> + stripe.tokens.create { + card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' } + }, (err, token) -> + stripeTokenID = token.id + loginJoe (joe) -> + joeData.stripe.token = stripeTokenID + joeData.stripe.planID = 'basic' + request.put {uri: userURL, json: joeData }, (err, res, body) -> + joeData = body + expect(res.statusCode).toBe(200) + stripe.customers.retrieve joeData.stripe.customerID, (err, customer) -> + expect(customer.discount).toBeDefined() + expect(customer.discount.coupon.id).toBe('500off') + done() + + + it 'updates the discount on the customer when an admin changes the couponID', (done) -> + loginAdmin (admin) -> + joeData.stripe.couponID = '20pct' + request.put {uri: userURL, json: joeData }, (err, res, body) -> + expect(res.statusCode).toBe(200) + expect(body.stripe.couponID).toBe('20pct') + stripe.customers.retrieve joeData.stripe.customerID, (err, customer) -> + expect(customer.discount.coupon.id).toBe('20pct') + done() + + it 'removes discounts from the customer when an admin removes the couponID', (done) -> + delete joeData.stripe.couponID + request.put {uri: userURL, json: joeData }, (err, res, body) -> + expect(res.statusCode).toBe(200) + expect(body.stripe.couponID).toBeUndefined() + stripe.customers.retrieve joeData.stripe.customerID, (err, customer) -> + expect(customer.discount).toBe(null) + done() + + it 'adds a discount to the customer when an admin adds the couponID', (done) -> + joeData.stripe.couponID = '20pct' + request.put {uri: userURL, json: joeData }, (err, res, body) -> + expect(res.statusCode).toBe(200) + expect(body.stripe.couponID).toBe('20pct') + stripe.customers.retrieve joeData.stripe.customerID, (err, customer) -> + expect(customer.discount.coupon.id).toBe('20pct') + done() + diff --git a/test/server/functional/payment.spec.coffee b/test/server/functional/payment.spec.coffee index 26cb4cb44..f8fb257cf 100644 --- a/test/server/functional/payment.spec.coffee +++ b/test/server/functional/payment.spec.coffee @@ -82,6 +82,7 @@ describe '/db/payment', -> joeID = null timestamp = new Date().getTime() stripeTokenID = null + joeData = null it 'clears the db first', (done) -> clearModels [User, Payment], (err) -> @@ -131,6 +132,34 @@ describe '/db/payment', -> done() ) ) + + it 'allows a new charge on the existing customer', (done) -> + data = { productID: 'gems_5', stripe: { timestamp: new Date().getTime() } } + request.post {uri: paymentURL, json: data }, (err, res, body) -> + expect(res.statusCode).toBe 201 + Payment.count {}, (err, count) -> + expect(count).toBe(2) + User.findById joeID, (err, user) -> + joeData = user.toObject() + expect(user.get('purchased').gems).toBe(10000) + done() + + it "updates the customer's card when you submit a new token", (done) -> + stripe.customers.retrieve joeData.stripe.customerID, (err, customer) -> + originalCustomerID = customer.id + originalCardID = customer.cards.data[0].id + stripe.tokens.create { + card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' } + }, (err, token) -> + data = { productID: 'gems_5', stripe: { timestamp: new Date().getTime(), token: token.id } } + request.post {uri: paymentURL, json: data }, (err, res, body) -> + expect(res.statusCode).toBe(201) + User.findById joeID, (err, user) -> + joeData = user.toObject() + expect(joeData.stripe.customerID).toBe(originalCustomerID) + stripe.customers.retrieve joeData.stripe.customerID, (err, customer) -> + expect(customer.cards.data[0].id).not.toBe(originalCardID) + done() it 'clears the db', (done) -> clearModels [User, Payment], (err) ->