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 000000000..0345e26c8 Binary files /dev/null and b/src/assets/img/user-avatar.png differ diff --git a/src/core/fileuploader/providers/delegate.ts b/src/core/fileuploader/providers/delegate.ts index f266ea6e1..30ee687d7 100644 --- a/src/core/fileuploader/providers/delegate.ts +++ b/src/core/fileuploader/providers/delegate.ts @@ -196,14 +196,14 @@ export class CoreFileUploaderDelegate { if (mimetypes) { if (!handler.getSupportedMimetypes) { // Handler doesn't implement a required function, don't add it. - return; + continue; } supportedMimetypes = handler.getSupportedMimetypes(mimetypes); if (!supportedMimetypes.length) { // Handler doesn't support any mimetype, don't add it. - return; + continue; } } diff --git a/src/core/user/lang/en.json b/src/core/user/lang/en.json index 0e0dcd235..d04f99898 100644 --- a/src/core/user/lang/en.json +++ b/src/core/user/lang/en.json @@ -1,3 +1,11 @@ { - + "details": "Details", + "detailsnotavailable": "The details of this user are not available to you.", + "editingteacher": "Teacher", + "errorloaduser": "Error loading user.", + "manager": "Manager", + "newpicture": "New picture", + "roles": "Roles", + "student": "Student", + "teacher": "Non-editing teacher" } \ No newline at end of file diff --git a/src/core/user/pages/profile/profile.html b/src/core/user/pages/profile/profile.html index d3bf591b3..2aec9846a 100644 --- a/src/core/user/pages/profile/profile.html +++ b/src/core/user/pages/profile/profile.html @@ -4,4 +4,35 @@ + + + + + + + + + + + + + + + {{ 'core.user.roles' | translate}}{{'core.labelsep' | translate}} + + + + + + + {{ 'core.user.details' | 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 {}
+ {{ 'core.user.roles' | translate}}{{'core.labelsep' | translate}} + +