diff --git a/app/core/utils.coffee b/app/core/utils.coffee index ec954ef32b9..70062a02dee 100644 --- a/app/core/utils.coffee +++ b/app/core/utils.coffee @@ -1,3 +1,5 @@ +slugify = _.str?.slugify ? _.string?.slugify # TODO: why _.string on client and _.str on server? + clone = (obj) -> return obj if obj is null or typeof (obj) isnt 'object' temp = obj.constructor() @@ -24,24 +26,25 @@ countries = [ {country: 'brazil', countryCode: 'BR'} # Loosely ordered by decreasing traffic as measured 2016-09-01 - 2016-11-07 - {country: 'united-kingdom', countryCode: 'GB'} + # TODO: switch to alphabetical ordering + {country: 'united-kingdom', countryCode: 'GB', inEU: true, ageOfConsent: 13} {country: 'russia', countryCode: 'RU'} {country: 'australia', countryCode: 'AU'} {country: 'canada', countryCode: 'CA'} - {country: 'france', countryCode: 'FR'} + {country: 'france', countryCode: 'FR', inEU: true, ageOfConsent: 16} {country: 'taiwan', countryCode: 'TW'} {country: 'ukraine', countryCode: 'UA'} - {country: 'poland', countryCode: 'PL'} - {country: 'spain', countryCode: 'ES'} - {country: 'germany', countryCode: 'DE'} - {country: 'netherlands', countryCode: 'NL'} - {country: 'hungary', countryCode: 'HU'} + {country: 'poland', countryCode: 'PL', inEU: true, ageOfConsent: 13} + {country: 'spain', countryCode: 'ES', inEU: true, ageOfConsent: 13} + {country: 'germany', countryCode: 'DE', inEU: true, ageOfConsent: 16} + {country: 'netherlands', countryCode: 'NL', inEU: true, ageOfConsent: 16} + {country: 'hungary', countryCode: 'HU', inEU: true, ageOfConsent: 16} {country: 'japan', countryCode: 'JP'} {country: 'turkey', countryCode: 'TR'} {country: 'south-africa', countryCode: 'ZA'} {country: 'indonesia', countryCode: 'ID'} {country: 'new-zealand', countryCode: 'NZ'} - {country: 'finland', countryCode: 'FI'} + {country: 'finland', countryCode: 'FI', inEU: true, ageOfConsent: 13} {country: 'south-korea', countryCode: 'KR'} {country: 'mexico', countryCode: 'MX'} {country: 'vietnam', countryCode: 'VN'} @@ -49,44 +52,52 @@ countries = [ {country: 'colombia', countryCode: 'CO'} {country: 'india', countryCode: 'IN'} {country: 'thailand', countryCode: 'TH'} - {country: 'belgium', countryCode: 'BE'} - {country: 'sweden', countryCode: 'SE'} - {country: 'denmark', countryCode: 'DK'} - {country: 'czech-republic', countryCode: 'CZ'} + {country: 'belgium', countryCode: 'BE', inEU: true} + {country: 'sweden', countryCode: 'SE', inEU: true, ageOfConsent: 13} + {country: 'denmark', countryCode: 'DK', inEU: true, ageOfConsent: 13} + {country: 'czech-republic', countryCode: 'CZ', inEU: true, ageOfConsent: 13} {country: 'hong-kong', countryCode: 'HK'} - {country: 'italy', countryCode: 'IT'} - {country: 'romania', countryCode: 'RO'} + {country: 'italy', countryCode: 'IT', inEU: true} + {country: 'romania', countryCode: 'RO', inEU: true} {country: 'belarus', countryCode: 'BY'} {country: 'norway', countryCode: 'NO'} {country: 'philippines', countryCode: 'PH'} - {country: 'lithuania', countryCode: 'LT'} + {country: 'lithuania', countryCode: 'LT', inEU: true, ageOfConsent: 16} {country: 'argentina', countryCode: 'AR'} {country: 'malaysia', countryCode: 'MY'} {country: 'pakistan', countryCode: 'PK'} {country: 'serbia', countryCode: 'RS'} - {country: 'greece', countryCode: 'GR'} - {country: 'israel', countryCode: 'IL'} - {country: 'portugal', countryCode: 'PT'} - {country: 'slovakia', countryCode: 'SK'} - {country: 'ireland', countryCode: 'IE'} + {country: 'greece', countryCode: 'GR', inEU: true, ageOfConsent: 15} + {country: 'israel', countryCode: 'IL', inEU: true} + {country: 'portugal', countryCode: 'PT', inEU: true} + {country: 'slovakia', countryCode: 'SK', inEU: true, ageOfConsent: 16} + {country: 'ireland', countryCode: 'IE', inEU: true, ageOfConsent: 13} {country: 'switzerland', countryCode: 'CH'} {country: 'peru', countryCode: 'PE'} - {country: 'bulgaria', countryCode: 'BG'} + {country: 'bulgaria', countryCode: 'BG', inEU: true} {country: 'venezuela', countryCode: 'VE'} - {country: 'austria', countryCode: 'AT'} - {country: 'croatia', countryCode: 'HR'} + {country: 'austria', countryCode: 'AT', inEU: true, ageOfConsent: 14} + {country: 'croatia', countryCode: 'HR', inEU: true} {country: 'saudia-arabia', countryCode: 'SA'} {country: 'chile', countryCode: 'CL'} {country: 'united-arab-emirates', countryCode: 'AE'} {country: 'kazakhstan', countryCode: 'KZ'} - {country: 'estonia', countryCode: 'EE'} + {country: 'estonia', countryCode: 'EE', inEU: true} {country: 'iran', countryCode: 'IR'} {country: 'egypt', countryCode: 'EG'} {country: 'ecuador', countryCode: 'EC'} - {country: 'slovenia', countryCode: 'SI'} + {country: 'slovenia', countryCode: 'SI', inEU: true} {country: 'macedonia', countryCode: 'MK'} + {country: 'cyprus', countryCode: 'CY', inEU: true} + {country: 'latvia', countryCode: 'LV', inEU: true, ageOfConsent: 13} + {country: 'luxembourg', countryCode: 'LU', inEU: true, ageOfConsent: 16} + {country: 'malta', countryCode: 'MT', inEU: true} ] +inEU = (country) -> !!countries.find((c) => c.country is slugify(country))?.inEU + +ageOfConsent = (country) -> countries.find((c) => c.country is slugify(country))?.ageOfConsent ? 16 + courseIDs = INTRODUCTION_TO_COMPUTER_SCIENCE: '560f1a9f22961295f9427742' GAME_DEVELOPMENT_1: '5789587aad86a6efb573701e' @@ -652,6 +663,7 @@ isValidEmail = (email) -> emailRegex.test(email?.trim().toLowerCase()) module.exports = { + ageOfConsent capitalLanguages clone combineAncestralObject @@ -679,6 +691,7 @@ module.exports = { hslToHex i18n injectCSS + inEU isID isRegionalSubscription isSmokeTestEmail diff --git a/app/models/User.coffee b/app/models/User.coffee index 8ab39bd3394..f0fefe63a10 100644 --- a/app/models/User.coffee +++ b/app/models/User.coffee @@ -35,6 +35,8 @@ module.exports = class User extends CocoModel displayName: -> @get('name', true) broadName: -> User.broadName(@attributes) + inEU: -> unless @get('country') then true else utils.inEU(@get('country')) + getPhotoURL: (size=80) -> return '' if application.testing return "/db/user/#{@id}/avatar?s=#{size}" diff --git a/server/models/User.coffee b/server/models/User.coffee index 95bb2eea297..556e48a4b41 100644 --- a/server/models/User.coffee +++ b/server/models/User.coffee @@ -118,6 +118,8 @@ UserSchema.methods.getUserInfo = -> id: @get('_id') email: if @get('anonymous') then 'Unregistered User' else @get('email') +UserSchema.methods.inEU = -> unless @get('country') then true else core_utils.inEU(@get('country')) + UserSchema.methods.removeFromClassrooms = -> userID = @get('_id') yield Classroom.update( diff --git a/spec/server/unit/user.spec.coffee b/spec/server/unit/user.spec.coffee index 0d2ffddda40..f50f56c70ad 100644 --- a/spec/server/unit/user.spec.coffee +++ b/spec/server/unit/user.spec.coffee @@ -319,4 +319,14 @@ describe 'User', -> expect(args[1]?.status).toBe('unsubscribed') done() - + describe 'inEU', -> + it 'true if in EU country', utils.wrap -> + u = yield utils.initUser({country: 'germany'}) + expect(u.inEU()).toEqual(true) + it 'false if not in EU country', utils.wrap -> + u = yield utils.initUser({country: 'mexico'}) + expect(u.inEU()).toEqual(false) + it 'true if not defined', utils.wrap -> + u = yield utils.initUser() + expect(u.get('country')).toBeUndefined() + expect(u.inEU()).toEqual(true) diff --git a/test/app/core/utils.spec.coffee b/test/app/core/utils.spec.coffee index 7eaf0250d30..c334c670f3d 100644 --- a/test/app/core/utils.spec.coffee +++ b/test/app/core/utils.spec.coffee @@ -1,5 +1,5 @@ describe 'Utility library', -> - utils = require 'core/utils' + utils = require '../../../app/core/utils' describe 'getQueryVariable(param, defaultValue)', -> beforeEach -> @@ -65,6 +65,26 @@ describe 'Utility library', -> it 'i18n can fall forward if a general language is not found', -> expect(utils.i18n(this.fixture1, 'text', 'pt')).toEqual(this.fixture1.i18n['pt-BR'].text) + describe 'inEU', -> + it 'EU countries return true', -> + euCountries = ['Austria', 'Belgium', 'Bulgaria', 'Croatia', 'Cyprus', 'Czech Republic', 'Denmark', 'Estonia', 'Finland', 'France', 'Germany', 'Greece', 'Hungary', 'Ireland', 'Italy', 'Latvia', 'Lithuania', 'Luxembourg', 'Malta', 'Netherlands', 'Poland', 'Portugal', 'Romania', 'Slovakia', 'Slovenia', 'Spain', 'Sweden', 'United Kingdom'] + euCountries.forEach((c) -> expect(utils.inEU(c)).toEqual(true)) + it 'non-EU countries return false', -> + nonEuCountries = ['united-states', 'peru', 'vietnam'] + nonEuCountries.forEach((c) -> expect(utils.inEU(c)).toEqual(false)) + + describe 'ageOfConsent', -> + it 'Latvia is 13', -> + expect(utils.ageOfConsent('latvia')).toEqual(13) + it 'Austria is 14', -> + expect(utils.ageOfConsent('austria')).toEqual(14) + it 'Greece is 15', -> + expect(utils.ageOfConsent('greece')).toEqual(15) + it 'Slovakia is 16', -> + expect(utils.ageOfConsent('slovakia')).toEqual(16) + it 'default is 16', -> + expect(utils.ageOfConsent('codecombat')).toEqual(16) + describe 'createLevelNumberMap', -> # r=required p=practice it 'returns correct map for r', -> diff --git a/test/app/models/User.spec.coffee b/test/app/models/User.spec.coffee index 5953a1fb9bc..eba3db351f2 100644 --- a/test/app/models/User.spec.coffee +++ b/test/app/models/User.spec.coffee @@ -24,7 +24,7 @@ describe 'UserModel', -> expect(defaultEmails.anyNotes.enabled).toBe(true) expect(defaultEmails.generalNews.enabled).toBe(true) expect(defaultEmails.recruitNotes.enabled).toBe(true) - + it 'maintains defaults of other emails when one is explicitly set', -> u = new User() u.setEmailSubscription('recruitNotes', false) @@ -32,7 +32,7 @@ describe 'UserModel', -> expect(defaultEmails.anyNotes?.enabled).toBe(true) expect(defaultEmails.generalNews?.enabled).toBe(true) expect(defaultEmails.recruitNotes.enabled).toBe(false) - + it 'does not populate raw data for other emails when one is explicitly set', -> u = new User() u.setEmailSubscription('recruitNotes', false) @@ -44,7 +44,7 @@ describe 'UserModel', -> describe 'validate', -> it 'returns undefined if the user is valid', -> expect(new User().validate()).toBeUndefined() - + it 'returns an array of errors if the user is invalid', -> res = new User({invalidProp:'...'}).validate() expect(_.isArray(res)).toBe(true) @@ -56,3 +56,15 @@ describe 'UserModel', -> expect(user.validate()).toBeUndefined() user.set('newInvalidProp', '...') expect(_.isArray(user.validate())).toBe(true) + + describe 'inEU', -> + it 'true if in EU country', -> + u = new User({country: 'germany'}) + expect(u.inEU()).toEqual(true) + it 'false if not in EU country', -> + u = new User({country: 'mexico'}) + expect(u.inEU()).toEqual(false) + it 'true if not defined', -> + u = new User() + expect(u.get('country')).toBeUndefined() + expect(u.inEU()).toEqual(true)