From 2b19b5172d52af7fab572ca611acb77ebbbafa14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 12 Jan 2018 16:24:24 +0100 Subject: [PATCH] MOBILE-2317 user: Implement user profile page --- src/app/app.scss | 3 +- src/assets/img/user-avatar.png | Bin 0 -> 1233 bytes src/core/fileuploader/providers/delegate.ts | 4 +- src/core/user/lang/en.json | 10 +- src/core/user/pages/profile/profile.html | 31 +++ src/core/user/pages/profile/profile.module.ts | 4 + src/core/user/pages/profile/profile.scss | 7 +- src/core/user/pages/profile/profile.ts | 132 ++++++++++- src/core/user/providers/helper.ts | 70 ++++++ src/core/user/providers/user.ts | 211 +++++++++++++++++- src/core/user/user.module.ts | 4 +- 11 files changed, 468 insertions(+), 8 deletions(-) create mode 100644 src/assets/img/user-avatar.png create mode 100644 src/core/user/providers/helper.ts diff --git a/src/app/app.scss b/src/app/app.scss index 862c6232a..79b3808c7 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -115,7 +115,8 @@ } > img:first-child, - ion-avatar img { + ion-avatar img, + img { display: block; margin: auto; width: 90px; diff --git a/src/assets/img/user-avatar.png b/src/assets/img/user-avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..0345e26c8279b7917ad8f76c2854c965a2fd680f GIT binary patch literal 1233 zcmV;?1TOoDP)CJP_~QSd zef>L6egs_T8rMhf+j;{(EhfKt^8b1AODq4Cky#|s_UeEC3$!A?05AZQCwa!z;nI%| zkeA!<m=H2rJB#rXF85ainPvdfr-Eeu!t1Kyp?!?y#7Xs~At@_3mXXm=ArD9nCif&h2lPc%t0MujJ9Xci9&8HWOER9F+#|brJB*3GpqrV7iEzP2_`q_u^?47IfK4T%e3V-o;_<-S zcLFI)J>3mOVEG z@+3(FmgvpB92dHH3gfWQChnk!#%eBl-w`PHV#7+mu2nd9;;HsGnX@-B?! zx2sdAON_|i!$Az@nDazgJRz)5@6tQd@-`0FL+==uZs&|92A1F5wyHBC!%^7s$1+b) z2c#Z{1Zx25sB7OWSTxk5r+9aM&W6L)Ic;FvcWE>+2KgdF3E zWW>f617{eK(N`PoIB_S*J$nsQXh__(bpuuYz&P$gxY&3^Z-QL^_;UX_h0z8i$%qOI z#!x-vgJNCG7Q5riI)?~P+Cs#5Is^u&uZsN{&sk}cfacCF(_OM4T(a{%1Y!nUy7P`7 z3*~z#KxX80 z;IViO-(bvzOOs21Q+a~0e}989+lZ>Iw(HlJC#n!EVrJ^pfS}tOOMVTRu@Gw*5yg-v z2XL7h#0t%<_oK5*cHBO7bwW(k8v^uAVdV8XMrw{ewPG?+EX@;P^3g{tYlXy?J;&i< z=Q*fM4V;-Dt_Q`fO`!pL4B_faqU_m*-J{{+2INLB^NPE&UJI_e^ocuF(q73(vjLB| vQzh-NhKm{KO7|b!B`KKJ$rJUyu6N@ + + + + + + +
+ {{ 'core.pictureof' | translate:{$a: user.fullname} }} + {{ 'core.pictureof' | translate:{$a: user.fullname} }} + +
+

+

+

+ {{ 'core.user.roles' | translate}}{{'core.labelsep' | translate}} + +

+
+ + + + + +
+ + + +
\ No newline at end of file diff --git a/src/core/user/pages/profile/profile.module.ts b/src/core/user/pages/profile/profile.module.ts index 0e0780fee..f8a6d8922 100644 --- a/src/core/user/pages/profile/profile.module.ts +++ b/src/core/user/pages/profile/profile.module.ts @@ -14,8 +14,10 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; import { CoreUserProfilePage } from './profile'; import { CoreDirectivesModule } from '../../../../directives/directives.module'; +import { CoreComponentsModule } from '../../../../components/components.module'; @NgModule({ declarations: [ @@ -23,7 +25,9 @@ import { CoreDirectivesModule } from '../../../../directives/directives.module'; ], imports: [ CoreDirectivesModule, + CoreComponentsModule, IonicPageModule.forChild(CoreUserProfilePage), + TranslateModule.forChild() ], }) export class CoreUserProfilePageModule {} diff --git a/src/core/user/pages/profile/profile.scss b/src/core/user/pages/profile/profile.scss index 76f53dc77..a5844f405 100644 --- a/src/core/user/pages/profile/profile.scss +++ b/src/core/user/pages/profile/profile.scss @@ -1,3 +1,8 @@ page-core-user-profile { - + .core-icon-foreground { + position: relative; + left: 60px; + bottom: 30px; + font-size: 24px; + } } \ No newline at end of file diff --git a/src/core/user/pages/profile/profile.ts b/src/core/user/pages/profile/profile.ts index 983092276..3d7bd28c7 100644 --- a/src/core/user/pages/profile/profile.ts +++ b/src/core/user/pages/profile/profile.ts @@ -13,8 +13,16 @@ // limitations under the License. import { Component } from '@angular/core'; -import { IonicPage } from 'ionic-angular'; +import { IonicPage, NavParams } from 'ionic-angular'; import { CoreUserProvider } from '../../providers/user'; +import { CoreUserHelperProvider } from '../../providers/helper'; +import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreCoursesProvider } from '../../../courses/providers/courses'; +import { CoreEventsProvider } from '../../../../providers/events'; +import { CoreSitesProvider } from '../../../../providers/sites'; +import { CoreMimetypeUtilsProvider } from '../../../../providers/utils/mimetype'; +import { CoreFileUploaderHelperProvider } from '../../../fileuploader/providers/helper'; /** * Page that displays an user profile page. @@ -25,4 +33,126 @@ import { CoreUserProvider } from '../../providers/user'; templateUrl: 'profile.html', }) export class CoreUserProfilePage { + protected courseId: number; + protected userId: number; + protected site; + protected obsProfileRefreshed: any; + + userLoaded: boolean = false; + isLoadingHandlers: boolean = false; + user: any = {}; + title: string; + isDeleted: boolean = false; + canChangeProfilePicture: boolean = false; + + constructor(private navParams: NavParams, private userProvider: CoreUserProvider, private userHelper: CoreUserHelperProvider, + private domUtils: CoreDomUtilsProvider, private translate: TranslateService, private eventsProvider: CoreEventsProvider, + private coursesProvider: CoreCoursesProvider, private sitesProvider: CoreSitesProvider, + private mimetypeUtils: CoreMimetypeUtilsProvider, private fileUploaderHelper: CoreFileUploaderHelperProvider) { + this.userId = navParams.get('userId'); + this.courseId = navParams.get('courseId'); + + this.site = this.sitesProvider.getCurrentSite(); + + // Allow to change the profile image only in the app profile page. + this.canChangeProfilePicture = + (!this.courseId || this.courseId == this.site.getSiteHomeId()) && + this.userId == this.site.getUserId() && + this.site.canUploadFiles() && + this.site.wsAvailable('core_user_update_picture') && + !this.userProvider.isUpdatePictureDisabledInSite(this.site); + + this.obsProfileRefreshed = eventsProvider.on(CoreUserProvider.PROFILE_REFRESHED, (data) => { + if (typeof data.user != "undefined") { + this.user.email = data.user.email; + this.user.address = this.userHelper.formatAddress("", data.user.city, data.user.country); + } + }, sitesProvider.getCurrentSiteId()); + } + + /** + * View loaded. + */ + ionViewDidLoad() { + this.fetchUser().then(() => { + return this.userProvider.logView(this.userId, this.courseId).catch((error) => { + this.isDeleted = error === this.translate.instant('core.userdeleted'); + }); + }).finally(() => { + this.userLoaded = true; + }); + } + + /** + * Fetches the user and updates the view. + */ + fetchUser() : Promise { + return this.userProvider.getProfile(this.userId, this.courseId).then((user) => { + + user.address = this.userHelper.formatAddress("", user.city, user.country); + user.roles = this.userHelper.formatRoleList(user.roles); + + this.user = user; + this.title = user.fullname; + + this.isLoadingHandlers = true; + + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'core.user.errorloaduser', true); + }); + } + + + /** + * Opens dialog to change profile picture. + */ + changeProfilePicture(){ + let maxSize = -1, + title = this.translate.instant('core.user.newpicture'), + mimetypes = this.mimetypeUtils.getGroupMimeInfo('image', 'mimetypes'); + + return this.fileUploaderHelper.selectAndUploadFile(maxSize, title, mimetypes).then((result) => { + let modal = this.domUtils.showModalLoading('core.sending', true); + + return this.userProvider.changeProfilePicture(result.itemid, this.userId).then((profileImageURL) => { + this.eventsProvider.trigger(CoreUserProvider.PROFILE_PICTURE_UPDATED, {userId: this.userId, picture: profileImageURL}); + this.sitesProvider.updateSiteInfo(this.site.getId()); + this.refreshUser(); + }).finally(function() { + modal.dismiss(); + }); + }).catch((message) => { + if (message) { + this.domUtils.showErrorModal(message); + } + }); + } + + /** + * Refresh the user. + * + * @param {any} refresher Refresher. + */ + refreshUser(refresher?: any) { + let promises = []; + + promises.push(this.userProvider.invalidateUserCache(this.userId)); + promises.push(this.coursesProvider.invalidateUserNavigationOptions()); + promises.push(this.coursesProvider.invalidateUserAdministrationOptions()); + + Promise.all(promises).finally(() => { + this.fetchUser().finally(() => { + this.eventsProvider.trigger(CoreUserProvider.PROFILE_REFRESHED, {courseId: this.courseId, userId: this.userId, + user: this.user}, this.site.getId()); + refresher && refresher.complete(); + }); + }); + } + + /** + * Page destroyed. + */ + ngOnDestroy() { + this.obsProfileRefreshed && this.obsProfileRefreshed.off(); + } } \ No newline at end of file diff --git a/src/core/user/providers/helper.ts b/src/core/user/providers/helper.ts new file mode 100644 index 000000000..e09c223d5 --- /dev/null +++ b/src/core/user/providers/helper.ts @@ -0,0 +1,70 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CoreLoggerProvider } from '../../../providers/logger'; +import { TranslateService } from '@ngx-translate/core'; + +/** + * Service that provides some features regarding users information. + */ +@Injectable() +export class CoreUserHelperProvider { + protected logger; + + constructor(logger: CoreLoggerProvider, private translate: TranslateService) { + this.logger = logger.getInstance('CoreUserHelperProvider'); + } + + /** + * Formats a user address, concatenating address, city and country. + * + * @param {string} address Address. + * @param {string} city City. + * @param {string} country Country. + * @return {string} Formatted address. + */ + formatAddress(address: string, city: string, country: string) : string { + let separator = this.translate.instant('core.listsep'), + values = [address, city, country]; + + values = values.filter((value) => { + return value && value.length > 0; + }); + + return values.join(separator + " "); + } + + /** + * Formats a user role list, translating and concatenating them. + * + * @param {any[]} [roles] List of user roles. + * @return {string} The formatted roles. + */ + formatRoleList(roles?: any[]) : string { + if (!roles || roles.length <= 0) { + return ""; + } + + let separator = this.translate.instant('core.listsep'); + + roles.map((value) => { + console.error(value); + let translation = this.translate.instant('core.user.' + value.shortname); + return translation.indexOf('core.user.') < 0 ? translation : value.shortname; + }); + + return roles.join(separator + " "); + } +} diff --git a/src/core/user/providers/user.ts b/src/core/user/providers/user.ts index 772ade7eb..b243db4c9 100644 --- a/src/core/user/providers/user.ts +++ b/src/core/user/providers/user.ts @@ -13,12 +13,221 @@ // limitations under the License. import { Injectable } from '@angular/core'; +import { CoreLoggerProvider } from '../../../providers/logger'; +import { CoreSite } from '../../../classes/site'; +import { CoreSitesProvider } from '../../../providers/sites'; +import { CoreUtilsProvider } from '../../../providers/utils/utils'; /** * Service to provide user functionalities. */ @Injectable() export class CoreUserProvider { + public static PROFILE_REFRESHED = 'CoreUserProfileRefreshed'; + public static PROFILE_PICTURE_UPDATED = 'CoreUserProfilePictureUpdated'; + protected ROOT_CACHE_KEY = 'mmUser:'; - constructor() {} + // Variables for database. + protected USERS_TABLE = 'users'; + protected tablesSchema = [ + { + name: this.USERS_TABLE, + columns: [ + { + name: 'id', + type: 'INTEGER', + primaryKey: true + }, + { + name: 'fullname', + type: 'TEXT' + }, + { + name: 'profileimageurl', + type: 'TEXT' + } + ] + } + ]; + + protected logger; + + constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider) { + this.logger = logger.getInstance('CoreUserProvider'); + this.sitesProvider.createTablesFromSchema(this.tablesSchema); + } + + /** + * Change the given user profile picture. + * + * @param {number} draftItemId New picture draft item id. + * @param {number} id User ID. + * @return {Promise} Promise resolve with the new profileimageurl + */ + changeProfilePicture(draftItemId: number, userId: number): Promise { + var data = { + 'draftitemid': draftItemId, + 'delete': 0, + 'userid': userId + }; + + return this.sitesProvider.getCurrentSite().write('core_user_update_picture', data).then((result) => { + if (!result.success) { + return Promise.reject(null); + } + return result.profileimageurl; + }); + } + + /** + * Get user profile. The type of profile retrieved depends on the params. + * + * @param {number} userId User's ID. + * @param {number} [courseId] Course ID to get course profile, undefined or 0 to get site profile. + * @param {boolean} [forceLocal] True to retrieve the user data from local DB, false to retrieve it from WS. + * @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site. + * @return {Promise} Promise resolved with the user data. + */ + getProfile(userId: number, courseId: number, forceLocal = false, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + if (forceLocal) { + return this.getUserFromLocalDb(userId, siteId).catch(function() { + return this.getUserFromWS(userId, courseId, siteId); + }); + } + return this.getUserFromWS(userId, courseId, siteId).catch(function() { + return this.getUserFromLocalDb(userId, siteId); + }); + } + + /** + * Invalidates user WS calls. + * + * @param {number} userId User ID. + * @return {string} Cache key. + */ + protected getUserCacheKey(userId): string { + return this.ROOT_CACHE_KEY + 'data:' + userId; + } + + /** + * Get user basic information from local DB. + * + * @param {number} userId User ID. + * @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site. + * @return {Promise} Promise resolve when the user is retrieved. + */ + protected getUserFromLocalDb(userId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecord(this.USERS_TABLE, {id: userId}); + }); + } + + /** + * Get user profile from WS. + * + * @param {number} userId User ID. + * @param {number} [courseId] Course ID to get course profile, undefined or 0 to get site profile. + * @param {string} [siteId] ID of the site. If not defined, use current site. + * @return {Promise} Promise resolve when the user is retrieved. + */ + protected getUserFromWS(userId: number, courseId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + let presets = { + cacheKey: this.getUserCacheKey(userId) + }, + wsName, data; + + // Determine WS and data to use. + if (courseId && courseId != site.getSiteHomeId()) { + this.logger.debug(`Get participant with ID '${userId}' in course '${courseId}`); + wsName = 'core_user_get_course_user_profiles'; + data = { + "userlist[0][userid]": userId, + "userlist[0][courseid]": courseId + }; + } else { + this.logger.debug(`Get user with ID '${userId}'`); + wsName = 'core_user_get_users_by_field'; + data = { + 'field': 'id', + 'values[0]': userId + }; + } + + return site.read(wsName, data, presets).then((users) => { + if (users.length == 0) { + return Promise.reject('Cannot retrieve user info.'); + } + + var user = users.shift(); + if (user.country) { + user.country = this.utils.getCountryName(user.country); + } + this.storeUser(user.id, user.fullname, user.profileimageurl); + return user; + }); + + }); + } + + /** + * Invalidates user WS calls. + * + * @param {number} userId User ID. + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateUserCache(userId: number, siteId?: string) : Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getUserCacheKey(userId)); + }); + }; + + /** + * Check if update profile picture is disabled in a certain site. + * + * @param {CoreSite} [site] Site. If not defined, use current site. + * @return {boolean} True if disabled, false otherwise. + */ + isUpdatePictureDisabledInSite(site?: CoreSite) : boolean { + site = site || this.sitesProvider.getCurrentSite(); + return site.isFeatureDisabled('$mmUserDelegate_picture'); + }; + + + /** + * Log User Profile View in Moodle. + * @param {number} userId User ID. + * @param {number} courseId Course ID. + * @return {Promise} Promise resolved when done. + */ + logView(userId: number, courseId?: number) : Promise { + return this.sitesProvider.getCurrentSite().write('core_user_view_user_profile', { + userid: userId, + courseid: courseId + }); + } + + /** + * Store user basic information in local DB to be retrieved if the WS call fails. + * + * @param {number} userId User ID. + * @param {string} fullname User full name. + * @param {string} avatar User avatar URL. + * @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site. + * @return {Promise} Promise resolve when the user is stored. + */ + protected storeUser(userId: number, fullname: string, avatar: string, siteId?: string) { + return this.sitesProvider.getSite(siteId).then((site) => { + let userRecord = { + id: userId, + fullname: fullname, + profileimageurl: avatar + }; + + return site.getDb().insertOrUpdateRecord(this.USERS_TABLE, userRecord, {id: userId}); + }); + } } diff --git a/src/core/user/user.module.ts b/src/core/user/user.module.ts index 18296d3ef..2aeae9a9e 100644 --- a/src/core/user/user.module.ts +++ b/src/core/user/user.module.ts @@ -15,6 +15,7 @@ import { NgModule } from '@angular/core'; import { CoreUserDelegate } from './providers/delegate'; import { CoreUserProvider } from './providers/user'; +import { CoreUserHelperProvider } from './providers/helper'; @NgModule({ declarations: [ @@ -23,7 +24,8 @@ import { CoreUserProvider } from './providers/user'; ], providers: [ CoreUserDelegate, - CoreUserProvider + CoreUserProvider, + CoreUserHelperProvider ] }) export class CoreUserModule {}