diff --git a/services/web/app/views/user/settings.pug b/services/web/app/views/user/settings.pug index 9ff94f4714..0c4b8ce81d 100644 --- a/services/web/app/views/user/settings.pug +++ b/services/web/app/views/user/settings.pug @@ -117,36 +117,51 @@ block content ) #{translate("change")} hr - div( + form( ng-controller="UserAffiliationsController" + novalidate + name="affiliationsForm" ) - h3 Affiliations + h3 Emails and affiliations + p Do you use multiple e-mail addresses? If so, add those here. + ul + li You'll retain access to your account even if you lose access to one of your emails + li Others users will be able to find you under different email addresses when sharing projects + li If your e-mail is affiliated with an institution with a group license, your account will be associated with that institution (and you'll gain acess to any benefits the institution may have). - //- p Do you use multiple e-mail addresses? If so, add those here. - - //- p Why should I do this? - - //- p Others users will be able to find you under different e-mail address when sharing projects. - - //- p If your e-mail is an institutional e-mail, your account will be associated with that institution (and gain acess to any benefits the institution may have). - - .affiliations-form-row + pre(style="font-size: 0.8em; position: fixed; left: 80px; top: 80px; color: black;") {{ newAffiliation | json }} + .affiliations-row .affiliation-col-4 label(for="affiliations-email") #{translate("email")} input-suggestions( ng-model="newAffiliation.email" + ng-model-options="{ allowInvalid: true }" get-suggestion="getEmailSuggestion(userInput)" + on-blur="handleEmailInputBlur()" input-id="affilitations-email" + input-name="affilitationsEmail" input-placeholder="e.g. johndoe@mit.edu" + input-type="email" + input-required="true" ) .affiliation-col-8( - ng-if="newAffiliation.autoDetected" + ng-show="newAffiliation.autoDetectMode" ) - p(ng-if="newAffiliation.university") {{ newAffiliation.university }} - p(ng-if="!newAffiliation.university") Start by adding your institutional email address. - + p.affiliation-input-feedback( + ng-if="newAffiliation.university" + ) + | {{ newAffiliation.university.name }} ( + a( + href + ng-click="selectUniversityManually();" + ) change + | ) + p.affiliation-input-feedback( + ng-if="!newAffiliation.university" + ) Start by adding your email address. + .affiliations-row .affiliation-col-4( - ng-if="!newAffiliation.autoDetected" + ng-if="!newAffiliation.autoDetectMode" ) label Institution ui-select( @@ -161,7 +176,7 @@ block content span( ng-bind="country.name" ) - .affiliation-col-4(ng-if="!newAffiliation.autoDetected") + .affiliation-col-4(ng-if="!newAffiliation.autoDetectMode") ui-select( ng-model="newAffiliation.university" ng-disabled="!newAffiliation.country" @@ -177,6 +192,9 @@ block content span( ng-bind="university.name" ) + .affiliations-row( + ng-if="newAffiliation.university" + ) .affiliation-col-4 label(for="affiliations-role") Role input.form-control( @@ -193,9 +211,14 @@ block content placeholder="e.g. Mathematics" ng-model="newAffiliation.department" ) - .affiliation-col-4.affiliation-col-align-right - button.btn.btn-primary( - ) Add new affiliation + .affiliations-row + .affiliation-col-4 + input.btn.btn-primary( + type="submit" + ng-disabled="affiliationsForm.$invalid" + ng-click="handleAffiliationFormSubmit()" + value="Add new email" + ) //- p Your current affiliations diff --git a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee index d07b656624..b26122f523 100644 --- a/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee +++ b/services/web/public/coffee/main/affiliations/controllers/UserAffiliationsController.coffee @@ -12,34 +12,80 @@ define [ department: null autoDetectMode: true - EMAIL_REGEXP = /([^@]+)@(.+)/ + LOCAL_AND_DOMAIN_REGEX = /([^@]+)@(.+)/ + EMAIL_REGEX = /^([A-Za-z0-9_\-\.]+)@([^\.]+)\.([A-Za-z]+)$/ - _matchEmail = (email) -> - match = email.match EMAIL_REGEXP + _matchLocalAndDomain = (userEmailInput) -> + match = userEmailInput?.match LOCAL_AND_DOMAIN_REGEX if match? { local: match[1], domain: match[2] } else { local: null, domain: null } + UserAffiliationsDataService.getUserEmails() $scope.addUniversityToSelection = (universityName) -> - { name: universityName, country_code: $scope.newAffiliation.country.code } + { name: universityName, isUserSuggested: true } $scope.getEmailSuggestion = (userInput) -> - matchedEmail = _matchEmail(userInput) - if matchedEmail.domain? - UserAffiliationsDataService.getUniversityDomainFromPartialDomainInput(matchedEmail.domain) - .then (universityDomain) -> - $scope.newAffiliation.university = universityDomain.university.name - $scope.newAffiliation.department = universityDomain.department - $q.resolve "#{matchedEmail.local}@#{universityDomain.hostname}" + userInputLocalAndDomain = _matchLocalAndDomain(userInput) + if userInputLocalAndDomain.domain? + UserAffiliationsDataService.getUniversityDomainFromPartialDomainInput(userInputLocalAndDomain.domain) + .then (universityDomain) -> + $scope.newAffiliation.autoDetectMode = true + if userInputLocalAndDomain.domain == universityDomain.hostname + $scope.newAffiliation.university = universityDomain.university + $scope.newAffiliation.department = universityDomain.department + else + $scope.newAffiliation.university = null + $scope.newAffiliation.department = null + $q.resolve "#{userInputLocalAndDomain.local}@#{universityDomain.hostname}" .catch () -> $scope.newAffiliation.university = null $scope.newAffiliation.department = null + # If the input is already a full e-mail and we have no suggestions, then the user + # will need to manually select his institution. + if userInput.match EMAIL_REGEX + $scope.newAffiliation.autoDetectMode = false $q.reject null - else + else + $scope.newAffiliation.university = null + $scope.newAffiliation.department = null $q.resolve null + $scope.handleEmailInputBlur = () -> + if $scope.newAffiliation.autoDetectMode and !$scope.newAffiliation.university and $scope.newAffiliation.email?.match EMAIL_REGEX + $scope.newAffiliation.autoDetectMode = false + + $scope.selectUniversityManually = () -> + $scope.newAffiliation.university = null + $scope.newAffiliation.department = null + $scope.newAffiliation.autoDetectMode = false + + $scope.handleAffiliationFormSubmit = () -> + if !$scope.newAffiliation.university? + UserAffiliationsDataService.addUserEmail( + $scope.newAffiliation.email, + $scope.newAffiliation.role, + $scope.newAffiliation.department + ) + else + if $scope.newAffiliation.university.isUserSuggested + UserAffiliationsDataService.addUserAffiliationWithUnknownUniversity( + $scope.newAffiliation.email, + $scope.newAffiliation.university.name, + $scope.newAffiliation.country.code, + $scope.newAffiliation.role, + $scope.newAffiliation.department + ) + else + UserAffiliationsDataService.addUserAffiliation( + $scope.newAffiliation.email, + $scope.newAffiliation.university.id + $scope.newAffiliation.role, + $scope.newAffiliation.department + ) + UserAffiliationsDataService .getCountries() .then (countries) -> $scope.countries = countries diff --git a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee index c90ff65baf..76827273c0 100644 --- a/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee +++ b/services/web/public/coffee/main/affiliations/factories/UserAffiliationsDataService.coffee @@ -3,12 +3,26 @@ define [ ], (App) -> # TODO Get actual countries list from the DB countriesList = [{ code : "af", name: "Afghanistan" }, { code : "ax", name: "Åland Islands" }, { code : "al", name: "Albania" }, { code : "dz", name: "Algeria" }, { code : "as", name: "American Samoa" }, { code : "ad", name: "Andorra" }, { code : "ao", name: "Angola" }, { code : "ai", name: "Anguilla" }, { code : "aq", name: "Antarctica" }, { code : "ag", name: "Antigua and Barbuda" }, { code : "ar", name: "Argentina" }, { code : "am", name: "Armenia" }, { code : "aw", name: "Aruba" }, { code : "au", name: "Australia" }, { code : "at", name: "Austria" }, { code : "az", name: "Azerbaijan" }, { code : "bs", name: "Bahamas" }, { code : "bh", name: "Bahrain" }, { code : "bd", name: "Bangladesh" }, { code : "bb", name: "Barbados" }, { code : "by", name: "Belarus" }, { code : "be", name: "Belgium" }, { code : "bz", name: "Belize" }, { code : "bj", name: "Benin" }, { code : "bm", name: "Bermuda" }, { code : "bt", name: "Bhutan" }, { code : "bo", name: "Bolivia" }, { code : "bq", name: "Bonaire, Saint Eustatius and Saba" }, { code : "ba", name: "Bosnia and Herzegovina" }, { code : "bw", name: "Botswana" }, { code : "bv", name: "Bouvet Island" }, { code : "br", name: "Brazil" }, { code : "io", name: "British Indian Ocean Territory" }, { code : "vg", name: "British Virgin Islands" }, { code : "bn", name: "Brunei" }, { code : "bg", name: "Bulgaria" }, { code : "bf", name: "Burkina Faso" }, { code : "bi", name: "Burundi" }, { code : "kh", name: "Cambodia" }, { code : "cm", name: "Cameroon" }, { code : "ca", name: "Canada" }, { code : "cv", name: "Cabo Verde" }, { code : "ky", name: "Cayman Islands" }, { code : "cf", name: "Central African Republic" }, { code : "td", name: "Chad" }, { code : "cl", name: "Chile" }, { code : "cn", name: "China" }, { code : "cx", name: "Christmas Island" }, { code : "cc", name: "Cocos (Keeling) Islands" }, { code : "co", name: "Colombia" }, { code : "km", name: "Comoros" }, { code : "cg", name: "Congo" }, { code : "ck", name: "Cook Islands" }, { code : "cr", name: "Costa Rica" }, { code : "ci", name: "Côte d'Ivoire" }, { code : "hr", name: "Croatia" }, { code : "cu", name: "Cuba" }, { code : "cw", name: "Curaçao" }, { code : "cy", name: "Cyprus" }, { code : "cz", name: "Czech Republic" }, { code : "kp", name: "Democratic People's Republic of Korea" }, { code : "cd", name: "Democratic Republic of the Congo" }, { code : "dk", name: "Denmark" }, { code : "dj", name: "Djibouti" }, { code : "dm", name: "Dominica" }, { code : "do", name: "Dominican Republic" }, { code : "ec", name: "Ecuador" }, { code : "eg", name: "Egypt" }, { code : "sv", name: "El Salvador" }, { code : "gq", name: "Equatorial Guinea" }, { code : "er", name: "Eritrea" }, { code : "ee", name: "Estonia" }, { code : "et", name: "Ethiopia" }, { code : "fk", name: "Falkland Islands (Malvinas)" }, { code : "fo", name: "Faroe Islands" }, { code : "fj", name: "Fiji" }, { code : "fi", name: "Finland" }, { code : "fr", name: "France" }, { code : "gf", name: "French Guiana" }, { code : "pf", name: "French Polynesia" }, { code : "tf", name: "French Southern Territories" }, { code : "ga", name: "Gabon" }, { code : "gm", name: "Gambia" }, { code : "ge", name: "Georgia" }, { code : "de", name: "Germany" }, { code : "gh", name: "Ghana" }, { code : "gi", name: "Gibraltar" }, { code : "gr", name: "Greece" }, { code : "gl", name: "Greenland" }, { code : "gd", name: "Grenada" }, { code : "gp", name: "Guadeloupe" }, { code : "gu", name: "Guam" }, { code : "gt", name: "Guatemala" }, { code : "gg", name: "Guernsey" }, { code : "gn", name: "Guinea" }, { code : "gw", name: "Guinea-Bissau" }, { code : "gy", name: "Guyana" }, { code : "ht", name: "Haiti" }, { code : "hm", name: "Heard Island and McDonald Islands" }, { code : "va", name: "Holy See (Vatican City)" }, { code : "hn", name: "Honduras" }, { code : "hk", name: "Hong Kong" }, { code : "hu", name: "Hungary" }, { code : "is", name: "Iceland" }, { code : "in", name: "India" }, { code : "id", name: "Indonesia" }, { code : "ir", name: "Iran" }, { code : "iq", name: "Iraq" }, { code : "ie", name: "Ireland" }, { code : "im", name: "Isle of Man" }, { code : "il", name: "Israel" }, { code : "it", name: "Italy" }, { code : "jm", name: "Jamaica" }, { code : "jp", name: "Japan" }, { code : "je", name: "Jersey" }, { code : "jo", name: "Jordan" }, { code : "kz", name: "Kazakhstan" }, { code : "ke", name: "Kenya" }, { code : "ki", name: "Kiribati" }, { code : "xk", name: "Kosovo" }, { code : "kw", name: "Kuwait" }, { code : "kg", name: "Kyrgyzstan" }, { code : "la", name: "Laos" }, { code : "lv", name: "Latvia" }, { code : "lb", name: "Lebanon" }, { code : "ls", name: "Lesotho" }, { code : "lr", name: "Liberia" }, { code : "ly", name: "Libya" }, { code : "li", name: "Liechtenstein" }, { code : "lt", name: "Lithuania" }, { code : "lu", name: "Luxembourg" }, { code : "mo", name: "Macao" }, { code : "mk", name: "Macedonia" }, { code : "mg", name: "Madagascar" }, { code : "mw", name: "Malawi" }, { code : "my", name: "Malaysia" }, { code : "mv", name: "Maldives" }, { code : "ml", name: "Mali" }, { code : "mt", name: "Malta" }, { code : "mh", name: "Marshall Islands" }, { code : "mq", name: "Martinique" }, { code : "mr", name: "Mauritania" }, { code : "mu", name: "Mauritius" }, { code : "yt", name: "Mayotte" }, { code : "mx", name: "Mexico" }, { code : "fm", name: "Micronesia" }, { code : "md", name: "Moldova" }, { code : "mc", name: "Monaco" }, { code : "mn", name: "Mongolia" }, { code : "me", name: "Montenegro" }, { code : "ms", name: "Montserrat" }, { code : "ma", name: "Morocco" }, { code : "mz", name: "Mozambique" }, { code : "mm", name: "Myanmar" }, { code : "na", name: "Namibia" }, { code : "nr", name: "Nauru" }, { code : "np", name: "Nepal" }, { code : "nl", name: "Netherlands" }, { code : "an", name: "Netherlands Antilles" }, { code : "nc", name: "New Caledonia" }, { code : "nz", name: "New Zealand" }, { code : "ni", name: "Nicaragua" }, { code : "ne", name: "Niger" }, { code : "ng", name: "Nigeria" }, { code : "nu", name: "Niue" }, { code : "nf", name: "Norfolk Island" }, { code : "mp", name: "Northern Mariana Islands" }, { code : "no", name: "Norway" }, { code : "om", name: "Oman" }, { code : "pk", name: "Pakistan" }, { code : "pw", name: "Palau" }, { code : "ps", name: "Palestine" }, { code : "pa", name: "Panama" }, { code : "pg", name: "Papua New Guinea" }, { code : "py", name: "Paraguay" }, { code : "pe", name: "Peru" }, { code : "ph", name: "Philippines" }, { code : "pn", name: "Pitcairn" }, { code : "pl", name: "Poland" }, { code : "pt", name: "Portugal" }, { code : "pr", name: "Puerto Rico" }, { code : "qa", name: "Qatar" }, { code : "kr", name: "Republic of Korea" }, { code : "re", name: "Réunion" }, { code : "ro", name: "Romania" }, { code : "ru", name: "Russia" }, { code : "rw", name: "Rwanda" }, { code : "bl", name: "Saint Barthélemy" }, { code : "sh", name: "Saint Helena, Ascension and Tristan da Cunha" }, { code : "kn", name: "Saint Kitts and Nevis" }, { code : "lc", name: "Saint Lucia" }, { code : "mf", name: "Saint Martin" }, { code : "pm", name: "Saint Pierre and Miquelon" }, { code : "vc", name: "Saint Vincent and the Grenadines" }, { code : "ws", name: "Samoa" }, { code : "sm", name: "San Marino" }, { code : "st", name: "Sao Tome and Principe" }, { code : "sa", name: "Saudi Arabia" }, { code : "sn", name: "Senegal" }, { code : "rs", name: "Serbia" }, { code : "sc", name: "Seychelles" }, { code : "sl", name: "Sierra Leone" }, { code : "sg", name: "Singapore" }, { code : "sx", name: "Sint Maarten" }, { code : "sk", name: "Slovakia" }, { code : "si", name: "Slovenia" }, { code : "sb", name: "Solomon Islands" }, { code : "so", name: "Somalia" }, { code : "za", name: "South Africa" }, { code : "gs", name: "South Georgia and the South Sandwich Islands" }, { code : "ss", name: "South Sudan" }, { code : "es", name: "Spain" }, { code : "lk", name: "Sri Lanka" }, { code : "sd", name: "Sudan" }, { code : "sr", name: "Suriname" }, { code : "sj", name: "Svalbard and Jan Mayen" }, { code : "sz", name: "Swaziland" }, { code : "se", name: "Sweden" }, { code : "ch", name: "Switzerland" }, { code : "sy", name: "Syria" }, { code : "tw", name: "Taiwan" }, { code : "tj", name: "Tajikistan" }, { code : "tz", name: "Tanzania" }, { code : "th", name: "Thailand" }, { code : "tl", name: "Timor-Leste" }, { code : "tg", name: "Togo" }, { code : "tk", name: "Tokelau" }, { code : "to", name: "Tonga" }, { code : "tt", name: "Trinidad and Tobago" }, { code : "tn", name: "Tunisia" }, { code : "tr", name: "Turkey" }, { code : "tm", name: "Turkmenistan" }, { code : "tc", name: "Turks and Caicos Islands" }, { code : "tv", name: "Tuvalu" }, { code : "vi", name: "U.S. Virgin Islands" }, { code : "ug", name: "Uganda" }, { code : "ua", name: "Ukraine" }, { code : "ae", name: "United Arab Emirates" }, { code : "gb", name: "United Kingdom" }, { code : "us", name: "United States of America" }, { code : "um", name: "United States Minor Outlying Islands" }, { code : "uy", name: "Uruguay" }, { code : "uz", name: "Uzbekistan" }, { code : "vu", name: "Vanuatu" }, { code : "ve", name: "Venezuela" }, { code : "vn", name: "Vietnam" }, { code : "wf", name: "Wallis and Futuna" }, { code : "eh", name: "Western Sahara" }, { code : "ye", name: "Yemen" }, { code : "zm", name: "Zambia" }, { code : "zw", name: "Zimbabwe" } ] + universities = {} universitiesByDomain = {} + + domainsBlackList = { "overleaf.com" : true } + commonTLDs = [ "br", "cn", "co", "co.jp", "co.uk", "com", "com.au", "de","fr", "in", "info", "io", "net", "no", "ru", "se", "us", "com.tw", "com.br", "pl", "it", "co.in", "com.mx" ] + commonDomains = [ "gmail", "googlemail", "icloud", "me", "yahoo", "ymail", "yahoomail", "hotmail", "live", "msn", "outlook", "gmx", "mail", "aol", "163", "mac", "qq", "o2", "libero", "126" ] + + for domain in commonDomains + for tld in commonTLDs + domainsBlackList["#{domain}.#{tld}"] = true + App.factory "UserAffiliationsDataService", ["$http", "$q", "_", ($http, $q, _) -> getCountries = () -> $q.resolve(countriesList) + getUserEmails = () -> + $http.get "/user/emails" + .then (response) -> response.data + getUniversitiesFromCountry = (country) -> if universities[country.code]? universitiesFromCountry = universities[country.code] @@ -31,9 +45,41 @@ define [ else $q.reject null + addUserEmail = (email) -> + $http.post "/user/emails", { + email + } + + addUserAffiliationWithUnknownUniversity = (email, unknownUniversityName, unknownUniversityCountryCode, role, department) -> + $http.post "/user/emails", { + email, + university: + name: unknownUniversityName + country_code: unknownUniversityCountryCode + role, + department + } + + addUserAffiliation = (email, universityId, role, department) -> + $http.post "/user/emails", { + email, + university: + id: universityId + role, + department + } + + isDomainBlacklisted = (domain) -> + domain.toLowerCase() of domainsBlackList + return { getCountries + getUserEmails getUniversitiesFromCountry getUniversityDomainFromPartialDomainInput + addUserEmail + addUserAffiliationWithUnknownUniversity + addUserAffiliation + isDomainBlacklisted } ] \ No newline at end of file diff --git a/services/web/public/stylesheets/app/account-settings.less b/services/web/public/stylesheets/app/account-settings.less index 6fdf398d8b..95f92aa97a 100644 --- a/services/web/public/stylesheets/app/account-settings.less +++ b/services/web/public/stylesheets/app/account-settings.less @@ -12,9 +12,8 @@ } -.affiliations-form-row { +.affiliations-row { display: flex; - flex-wrap: wrap; margin-left: -(@grid-gutter-width / 2); margin-right: -(@grid-gutter-width / 2); @media (min-width: @screen-sm-min) { @@ -23,7 +22,9 @@ } .affiliation-col-4, .affiliation-col-8 { - flex: 0 0 100%; + width: 100%; + flex-grow: 0; + flex-shrink: 0; padding-left: (@grid-gutter-width / 2); padding-right: (@grid-gutter-width / 2); margin-bottom: (@grid-gutter-width / 2); @@ -31,13 +32,13 @@ .affiliation-col-4 { @media (min-width: @screen-sm-min) { - flex-basis: (4 / 12) * 100%;; + width: (4 / 12) * 100%; } } .affiliation-col-8 { @media (min-width: @screen-sm-min) { - flex-basis: (8 / 12) * 100%; + width: (8 / 12) * 100%; } } @@ -47,3 +48,9 @@ justify-content: flex-end; } } + + .affiliation-input-feedback { + height: @input-height-base; + padding: @padding-base-vertical; + margin: 0; + } \ No newline at end of file