From ce07fb25b4080f88fdbc2d93865d67949cc11931 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Sun, 22 May 2016 16:55:54 -0700 Subject: [PATCH 1/5] Add my-profile route Allows the user to view their own profile information for school membership, cohorts, and learner groups. --- app/components/my-profile.js | 17 +++ app/components/user-profile.js | 23 +--- app/models/user.js | 11 +- app/router.js | 1 + app/routes/myprofile.js | 11 ++ app/styles/mixins.scss | 1 + app/styles/mixins/profile.scss | 65 +++++++++++ app/styles/newcomponents.scss | 1 + app/styles/newcomponents/my-profile.scss | 3 + app/templates/components/my-profile.hbs | 104 ++++++++++++++++++ app/templates/myprofile.hbs | 1 + .../integration/components/my-profile-test.js | 24 ++++ tests/unit/models/user-test.js | 26 +++++ tests/unit/routes/myprofile-test.js | 11 ++ 14 files changed, 278 insertions(+), 21 deletions(-) create mode 100644 app/components/my-profile.js create mode 100644 app/routes/myprofile.js create mode 100644 app/styles/mixins/profile.scss create mode 100644 app/styles/newcomponents/my-profile.scss create mode 100644 app/templates/components/my-profile.hbs create mode 100644 app/templates/myprofile.hbs create mode 100644 tests/integration/components/my-profile-test.js create mode 100644 tests/unit/routes/myprofile-test.js diff --git a/app/components/my-profile.js b/app/components/my-profile.js new file mode 100644 index 0000000000..544074b4ce --- /dev/null +++ b/app/components/my-profile.js @@ -0,0 +1,17 @@ +import Ember from 'ember'; + +const { computed, RSVP } = Ember; +const { Promise } = RSVP; + +export default Ember.Component.extend({ + classNames: ['my-profile'], + user: null, + roles: computed('user.roles.[]', function(){ + const user = this.get('user'); + return new Promise(resolve => { + user.get('roles').then(roles => { + resolve(roles.mapBy('title')); + }); + }) + }), +}); diff --git a/app/components/user-profile.js b/app/components/user-profile.js index f8940f4535..354031a4a7 100644 --- a/app/components/user-profile.js +++ b/app/components/user-profile.js @@ -5,9 +5,9 @@ import ValidationErrorDisplay from 'ilios/mixins/validation-error-display'; import { task } from 'ember-concurrency'; const { computed, Component, inject, RSVP } = Ember; -const { PromiseObject, PromiseArray } = DS; +const { PromiseObject } = DS; const { service } = inject; -const { sort } = computed; +const { sort, reads } = computed; const Validations = buildValidations({ firstName: [ @@ -112,24 +112,7 @@ export default Component.extend(ValidationErrorDisplay, Validations, { } }).readOnly(), - secondaryCohorts: computed('user.primaryCohort', 'user.cohorts.[]', { - get() { - const user = this.get('user'); - - let promise = user.get('cohorts').then((cohorts) => { - return user.get('primaryCohort').then((primaryCohort) => { - if (!primaryCohort) { - return cohorts; - } - return cohorts.filter(cohort => { - return cohort.get('id') !== primaryCohort.get('id'); - }); - }); - }); - - return PromiseArray.create({ promise }); - } - }).readOnly(), + secondaryCohorts: reads('user.secondaryCohorts'), cohortSorting: [ 'programYear.program.school.title:asc', diff --git a/app/models/user.js b/app/models/user.js index 22a282bc13..fa403f851a 100644 --- a/app/models/user.js +++ b/app/models/user.js @@ -228,7 +228,16 @@ var User = DS.Model.extend({ }); }); - } + }, + secondaryCohorts: computed('primaryCohort', 'cohorts.[]', function(){ + return new Promise(resolve => { + this.get('cohorts').then((cohorts) => { + this.get('primaryCohort').then((primaryCohort) => { + resolve(cohorts.filter(cohort => cohort !== primaryCohort)); + }); + }); + }); + }) }); export default User; diff --git a/app/router.js b/app/router.js index 5f18ca630f..27b3682bda 100644 --- a/app/router.js +++ b/app/router.js @@ -56,6 +56,7 @@ Router.map(function() { this.route('schools'); this.route('school', { path: 'schools/:school_id'}); this.route('assign-students', {path: '/admin/assignstudents'}); + this.route('myprofile'); }); export default Router; diff --git a/app/routes/myprofile.js b/app/routes/myprofile.js new file mode 100644 index 0000000000..ddaad68ca7 --- /dev/null +++ b/app/routes/myprofile.js @@ -0,0 +1,11 @@ +import Ember from 'ember'; +import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin'; + +const { service } = Ember.inject; + +export default Ember.Route.extend(AuthenticatedRouteMixin, { + currentUser: service(), + model(){ + return this.get('currentUser').get('model'); + } +}); diff --git a/app/styles/mixins.scss b/app/styles/mixins.scss index 96db21bfb0..fbd6635257 100644 --- a/app/styles/mixins.scss +++ b/app/styles/mixins.scss @@ -1 +1,2 @@ @import 'mixins/ilios-form'; +@import 'mixins/profile'; diff --git a/app/styles/mixins/profile.scss b/app/styles/mixins/profile.scss new file mode 100644 index 0000000000..3ecbb6fe0c --- /dev/null +++ b/app/styles/mixins/profile.scss @@ -0,0 +1,65 @@ +@mixin profile () { + .name { + margin: 1.5em 0 0; + text-align: center; + } + + .is-student { + color: $ilios-green; + text-align: center; + } + + .details { + @include fill-parent; + margin-top: 2em; + + .permissions { + @include span-columns(4); + background: $header-grey; + border-radius: 1em; + min-height: 300px; + padding: 1em; + text-align: center; + vertical-align: top; + + .permissions-header { + font-size: 1.5em; + margin: 0 0 1em; + text-align: center; + } + + .permissions-body { + display: inline-block; + text-align: left; + } + + .permissions-row { + i { + margin-right: 1em; + } + } + + hr { + border: 1px solid $moderate-grey; + margin: 1em 0; + } + } + + .info { + @include shift(1); + @include span-columns(7); + + .row { + @include fill-parent; + + .title { + font-weight: bold; + } + + ul { + padding-left: 1em; + } + } + } + } +} diff --git a/app/styles/newcomponents.scss b/app/styles/newcomponents.scss index a15925b4eb..8d7f7c43e2 100644 --- a/app/styles/newcomponents.scss +++ b/app/styles/newcomponents.scss @@ -1 +1,2 @@ @import 'newcomponents/new-user'; +@import 'newcomponents/my-profile'; diff --git a/app/styles/newcomponents/my-profile.scss b/app/styles/newcomponents/my-profile.scss new file mode 100644 index 0000000000..dce77d232f --- /dev/null +++ b/app/styles/newcomponents/my-profile.scss @@ -0,0 +1,3 @@ +.my-profile { + @include profile; +} diff --git a/app/templates/components/my-profile.hbs b/app/templates/components/my-profile.hbs new file mode 100644 index 0000000000..381c2544a3 --- /dev/null +++ b/app/templates/components/my-profile.hbs @@ -0,0 +1,104 @@ +

+ {{user.fullName}} +

+ +{{#if user.isStudent.content}} + {{t 'general.student'}} +{{/if}} + +
+
+

{{t 'user.userRoles'}}

+ +
+ {{#if (is-fulfilled roles)}} +
+ + {{fa-icon (if (contains 'Course Director' (await roles)) 'check' 'ban') class=(if (contains 'Course Director' (await roles)) 'yes' 'no')}} + {{t 'user.courseDirector'}} + +
+
+ + {{fa-icon (if (contains 'Faculty' (await roles)) 'check' 'ban') class=(if (contains 'Faculty' (await roles)) 'yes' 'no')}} + {{t 'user.instructor'}} + +
+
+ + {{fa-icon (if (contains 'Developer' (await roles)) 'check' 'ban') class=(if (contains 'Developer' (await roles)) 'yes' 'no')}} + {{t 'user.developer'}} + +
+
+ + {{fa-icon (if (contains 'Former Student' (await roles)) 'check' 'ban') class=(if (contains 'Former Student' (await roles)) 'yes' 'no')}} + {{t 'user.formerStudent'}} + +
+
+ +
+ + {{fa-icon (if user.syncIgnore 'check' 'ban') class=(if user.syncIgnore 'yes' 'no')}} + {{t 'user.excludeFromSync'}} + +
+ {{/if}} +
+
+ +
+
+ {{t 'general.primarySchool'}}: + + {{#if (await user.school)}} + {{get (await user.school) 'title'}} + {{else}} + {{t 'general.unassigned'}} + {{/if}} + +
+
+ {{t 'general.primaryCohort'}}: + + {{#if (await user.primaryCohort)}} + {{get (await user.primaryCohort) 'title'}} + {{else}} + {{t 'general.unassigned'}} + {{/if}} + +
+
+ {{t 'general.secondaryCohorts'}}: + + {{#if (get (await user.secondaryCohorts) 'length')}} +
    + {{#each (sort-by 'title' (await user.secondaryCohorts)) as |cohort|}} +
  • + {{cohort.title}} + {{cohort.programYear.program.title}} +
  • + {{/each}} +
+ {{else}} + {{t 'general.unassigned'}} + {{/if}} +
+
+
+ {{t 'general.learnerGroups'}}: + + {{#if (await user.learnerGroups)}} +
    + {{#each (sort-by 'title' (await user.learnerGroups)) as |group|}} +
  • {{group.title}} ({{group.cohort.title}} {{group.cohort.programYear.program.title}})
  • + {{/each}} +
+ {{else}} + {{t 'general.unassigned'}} + {{/if}} +
+
+
+
diff --git a/app/templates/myprofile.hbs b/app/templates/myprofile.hbs new file mode 100644 index 0000000000..9b63d3e3bf --- /dev/null +++ b/app/templates/myprofile.hbs @@ -0,0 +1 @@ +{{my-profile user=model}} diff --git a/tests/integration/components/my-profile-test.js b/tests/integration/components/my-profile-test.js new file mode 100644 index 0000000000..8d0f17f772 --- /dev/null +++ b/tests/integration/components/my-profile-test.js @@ -0,0 +1,24 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; + +moduleForComponent('my-profile', 'Integration | Component | my profile', { + integration: true +}); + +test('it renders', function(assert) { + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.on('myAction', function(val) { ... }); + + this.render(hbs`{{my-profile}}`); + + assert.equal(this.$().text().trim(), ''); + + // Template block usage: + this.render(hbs` + {{#my-profile}} + template block text + {{/my-profile}} + `); + + assert.equal(this.$().text().trim(), 'template block text'); +}); diff --git a/tests/unit/models/user-test.js b/tests/unit/models/user-test.js index d1ec2468be..5dedd4071d 100644 --- a/tests/unit/models/user-test.js +++ b/tests/unit/models/user-test.js @@ -427,3 +427,29 @@ test('return null when there is no group in the tree', function(assert) { }); }); + +test('gets secondary cohorts (all cohorts not the primary cohort)', function(assert) { + let model = this.subject(); + let store = this.store(); + Ember.run(()=>{ + let primaryCohort = store.createRecord('cohort', { + users: [model] + }); + let secondaryCohort = store.createRecord('cohort', { + users: [model] + }); + let anotherCohort = store.createRecord('cohort', { + users: [model] + }); + model.set('primaryCohort', primaryCohort); + model.set('cohorts', [primaryCohort, secondaryCohort, anotherCohort]); + + model.get('secondaryCohorts').then(cohorts => { + assert.equal(cohorts.length, 2); + assert.ok(cohorts.contains(secondaryCohort)); + assert.ok(cohorts.contains(anotherCohort)); + assert.notOk(cohorts.contains(primaryCohort)); + }); + }); + +}); diff --git a/tests/unit/routes/myprofile-test.js b/tests/unit/routes/myprofile-test.js new file mode 100644 index 0000000000..ceb98be48a --- /dev/null +++ b/tests/unit/routes/myprofile-test.js @@ -0,0 +1,11 @@ +import { moduleFor, test } from 'ember-qunit'; + +moduleFor('route:myprofile', 'Unit | Route | myprofile', { + // Specify the other units that are required for this test. + // needs: ['controller:foo'] +}); + +test('it exists', function(assert) { + let route = this.subject(); + assert.ok(route); +}); From 5a25b5c227ced420459e7191bf02cf6c8e5d391d Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Mon, 23 May 2016 13:19:46 -0700 Subject: [PATCH 2/5] Add my profile link to header and split our translations Header links now in two menus. One for translations and one for user tasks. --- app/components/ilios-header.js | 7 +- app/locales/en/translations.js | 1 + app/locales/es/translations.js | 1 + app/locales/fr/translations.js | 1 + app/styles/app.scss | 1 - app/styles/components/_action-menu.scss | 15 ---- app/styles/components/_header.scss | 71 ------------------ app/styles/mixins.scss | 1 + app/styles/mixins/header.scss | 84 ++++++++++++++++++++++ app/styles/newcomponents.scss | 1 + app/styles/newcomponents/ilios-header.scss | 3 + app/templates/components/action-menu.hbs | 2 +- app/templates/components/ilios-header.hbs | 45 ++++++------ 13 files changed, 118 insertions(+), 115 deletions(-) delete mode 100644 app/styles/components/_header.scss create mode 100644 app/styles/mixins/header.scss create mode 100644 app/styles/newcomponents/ilios-header.scss diff --git a/app/components/ilios-header.js b/app/components/ilios-header.js index f98ba692d9..d4f5610301 100644 --- a/app/components/ilios-header.js +++ b/app/components/ilios-header.js @@ -1,17 +1,14 @@ import Ember from 'ember'; const { Component, computed, inject } = Ember; -const { oneWay, equal } = computed; const { service } = inject; export default Component.extend({ session: service(), currentUser: Ember.inject.service(), i18n: Ember.inject.service(), - userName: oneWay('currentUser.model.fullName'), - inEnglish: equal('i18n.locale', 'en'), - inSpanish: equal('i18n.locale', 'es'), - inFrench: equal('i18n.locale', 'fr'), + classNames: ['ilios-header'], + tagName: 'header', locales: computed('i18n.locales', 'i18n.locale', function() { return this.get('i18n.locales').map(locale => { return { id: locale, text: this.get('i18n').t('language.select.' + locale) }; diff --git a/app/locales/en/translations.js b/app/locales/en/translations.js index eabeef81b7..78b7c3bfb0 100644 --- a/app/locales/en/translations.js +++ b/app/locales/en/translations.js @@ -462,6 +462,7 @@ export default { 'saved': 'New User Saved Successfully', 'username': 'Username', 'password': 'Password', + 'myProfile': 'My Profile', }, 'language': { 'select': { diff --git a/app/locales/es/translations.js b/app/locales/es/translations.js index 113d4e032b..7b17df9956 100644 --- a/app/locales/es/translations.js +++ b/app/locales/es/translations.js @@ -462,6 +462,7 @@ export default { 'saved': 'Nuevo Usuario Salvó con Éxito', 'username': 'Nombre de Usuario', 'password': 'Contraseña', + 'myProfile': 'Mi Perfil', }, 'language': { 'select': { diff --git a/app/locales/fr/translations.js b/app/locales/fr/translations.js index 756e0ea550..933373311d 100755 --- a/app/locales/fr/translations.js +++ b/app/locales/fr/translations.js @@ -462,6 +462,7 @@ export default { 'saved': 'Nouvel utilisateur enregistré avec succès', 'username': 'Nom d’utilisateur', 'password': 'Mot de passe', + 'myProfile': 'Mon profil', }, 'language': { 'select': { diff --git a/app/styles/app.scss b/app/styles/app.scss index 66665ffaa8..808513300c 100644 --- a/app/styles/app.scss +++ b/app/styles/app.scss @@ -7,7 +7,6 @@ @import 'components/reset'; @import 'components/global'; @import 'components/action-menu'; -@import 'components/header'; @import 'components/navigation'; @import 'components/table'; @import 'components/searchbox'; diff --git a/app/styles/components/_action-menu.scss b/app/styles/components/_action-menu.scss index 800de4ba37..961966eac6 100644 --- a/app/styles/components/_action-menu.scss +++ b/app/styles/components/_action-menu.scss @@ -48,21 +48,6 @@ $dropdown-distance-from-menu: $dropdown-padding * 2; } } - &.user-menu .button { - background-color: transparent; - border: 0; - border-radius: $base-border-radius; - color: $white; - font-weight: normal; - padding: .5em 2em .5em 1em; - vertical-align: middle; - - &:hover { - background-color: $white; - color: $ilios-orange; - } - } - .dropdown-menu { @include transition (all .2s ease-in-out); background-color: $white; diff --git a/app/styles/components/_header.scss b/app/styles/components/_header.scss deleted file mode 100644 index f8bc819f54..0000000000 --- a/app/styles/components/_header.scss +++ /dev/null @@ -1,71 +0,0 @@ -// scss-lint:disable QualifyingElement -header.main { - $header-padding: 1em; - $header-background: $ilios-orange; - $header-color: $white; - $header-height: 60px; - $horizontal-bar-mode: $large-screen; - $header-box-width: 250px; - - background-color: $header-background; - border-bottom: 1px solid darken($header-background, 10); - height: $header-height; - text-align: center; - vertical-align: bottom; - width: 100%; - z-index: 100; - - .logo { - float: left; - max-height: $header-height; - padding-left: $header-padding; - - @include media($large-screen) { - width: $header-box-width; - } - - span.image { - background-image: url('images/ilios-logo.png'); - background-repeat: no-repeat; - display: block; - height: 42px; - padding: .8em 0; - width: 84px; - } - } - - h1 { - color: $header-color; - display: inline-block; - font-size: $base-font-size * 1.5; - margin-bottom: 1em; - } - - // Elements on the far right - .tools { - background: transparent; - display: block; - float: right; - padding-left: $header-padding/2; - padding-right: $header-padding; - - @include media($large-screen) { - width: $header-box-width; - } - - .user { - @include transition (all .2s ease-in-out); - color: $header-color; - display: inline; - float: left; - font-size: $base-font-size * .8; - font-weight: 800; - margin-top: 1.2em; - padding: .4em .5em; - - @include media($large-screen) { - padding: .5em 1em; - } - } - } -} diff --git a/app/styles/mixins.scss b/app/styles/mixins.scss index fbd6635257..ec257f568a 100644 --- a/app/styles/mixins.scss +++ b/app/styles/mixins.scss @@ -1,2 +1,3 @@ @import 'mixins/ilios-form'; @import 'mixins/profile'; +@import 'mixins/header'; diff --git a/app/styles/mixins/header.scss b/app/styles/mixins/header.scss new file mode 100644 index 0000000000..439e4e3f7d --- /dev/null +++ b/app/styles/mixins/header.scss @@ -0,0 +1,84 @@ +@mixin header () { + $header-height: 60px; + + background-color: $ilios-orange; + border-bottom: 1px solid darken($ilios-orange, 10); + height: $header-height; + min-width: 250px; + text-align: center; + vertical-align: bottom; + width: 100%; + z-index: 100; + + .logo { + float: left; + max-height: $header-height; + padding-left: 1em; + + .image { + background-image: url('images/ilios-logo.png'); + background-repeat: no-repeat; + display: block; + height: 42px; + padding: .8em 0; + width: 84px; + } + } + + h1 { + color: $white; + display: inline-block; + font-size: $base-font-size * .75; + margin-top: 2em; + + @include media($medium-screen) { + font-size: $base-font-size * 1.5; + margin: .5em; + } + } + + // Elements on the far right + .tools { + background: transparent; + display: block; + float: right; + padding-left: .5em; + padding-right: 1em; + + .user, + .locales { + color: $ilios-orange; + display: inline; + float: left; + font-size: $base-font-size * .8; + font-weight: 800; + margin-top: 1.2em; + padding: .4em .5em; + + .button { + background-color: transparent; + border: 0; + border-radius: $base-border-radius; + color: $white; + font-weight: normal; + padding: .5em 2em .5em 1em; + vertical-align: middle; + + span { + display: none; + + @include media($medium-screen) { + display: inline; + } + } + + &:hover { + background-color: $white; + color: $ilios-orange; + } + + + } + } + } +} diff --git a/app/styles/newcomponents.scss b/app/styles/newcomponents.scss index 8d7f7c43e2..a538e941ba 100644 --- a/app/styles/newcomponents.scss +++ b/app/styles/newcomponents.scss @@ -1,2 +1,3 @@ @import 'newcomponents/new-user'; @import 'newcomponents/my-profile'; +@import 'newcomponents/ilios-header'; diff --git a/app/styles/newcomponents/ilios-header.scss b/app/styles/newcomponents/ilios-header.scss new file mode 100644 index 0000000000..4179eb147f --- /dev/null +++ b/app/styles/newcomponents/ilios-header.scss @@ -0,0 +1,3 @@ +.ilios-header { + @include header; +} diff --git a/app/templates/components/action-menu.hbs b/app/templates/components/action-menu.hbs index 9909296b9c..0c84bff4bd 100644 --- a/app/templates/components/action-menu.hbs +++ b/app/templates/components/action-menu.hbs @@ -2,7 +2,7 @@ {{#if icon}} {{fa-icon icon}} {{/if}} - {{title}} + {{title}}