MOBILE-2317 user: Implement user profile page

main
Pau Ferrer Ocaña 2018-01-12 16:24:24 +01:00
parent c2946cc797
commit 2b19b5172d
11 changed files with 468 additions and 8 deletions

View File

@ -115,7 +115,8 @@
}
> img:first-child,
ion-avatar img {
ion-avatar img,
img {
display: block;
margin: auto;
width: 90px;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -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;
}
}

View File

@ -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"
}

View File

@ -4,4 +4,35 @@
</ion-navbar>
</ion-header>
<ion-content>
<ion-refresher [enabled]="userLoaded" (ionRefresh)="refreshUser($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="userLoaded" class="core-loading-center">
<ion-list *ngIf="user && !isDeleted">
<ion-item text-center>
<div class="item-avatar-center">
<img *ngIf="user.profileimageurl" class="avatar" [src]="user.profileimageurl" core-external-content alt="{{ 'core.pictureof' | translate:{$a: user.fullname} }}" role="presentation">
<img *ngIf="!user.profileimageurl" class="avatar" src="assets/img/user-avatar.png" alt="{{ 'core.pictureof' | translate:{$a: user.fullname} }}" role="presentation">
<ion-icon name="create" class="core-icon-foreground" *ngIf="canChangeProfilePicture" (click)="changeProfilePicture()"></ion-icon>
</div>
<h2><core-format-text [text]="user.fullname"></core-format-text></h2>
<p><core-format-text *ngIf="user.address" [text]="user.address"></core-format-text></p>
<p *ngIf="user.roles">
<strong>{{ 'core.user.roles' | translate}}</strong>{{'core.labelsep' | translate}}
<core-format-text [text]="user.roles"></core-format-text>
</p>
</ion-item>
<a ion-item text-wrap class="core-user-profile-handler" [navPush]="CoreUserAboutPage" [navParams]="{courseId: courseId, userId: userId}" title="{{ 'core.user.details' | translate }}">
<ion-icon name="person" item-start></ion-icon>
<h2>{{ 'core.user.details' | translate }}</h2>
</a>
<ion-item text-center *ngIf="isLoadingHandlers">
<ion-spinner></ion-spinner>
</ion-item>
</ion-list>
<core-empty-box *ngIf="!user && !isDeleted" icon="person" [message]=" 'core.user.detailsnotavailable' | translate"></core-empty-box>
<core-empty-box *ngIf="isDeleted" icon="person" [message]="'core.userdeleted' | translate"></core-empty-box>
</core-loading>
</ion-content>

View File

@ -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 {}

View File

@ -1,3 +1,8 @@
page-core-user-profile {
.core-icon-foreground {
position: relative;
left: 60px;
bottom: 30px;
font-size: 24px;
}
}

View File

@ -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<any> {
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();
}
}

View File

@ -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 + " ");
}
}

View File

@ -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<any>} Promise resolve with the new profileimageurl
*/
changeProfilePicture(draftItemId: number, userId: number): Promise<any> {
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<any>} Promise resolved with the user data.
*/
getProfile(userId: number, courseId: number, forceLocal = false, siteId?: string): Promise<any> {
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<any>} Promise resolve when the user is retrieved.
*/
protected getUserFromLocalDb(userId: number, siteId?: string): Promise<any> {
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<any>} Promise resolve when the user is retrieved.
*/
protected getUserFromWS(userId: number, courseId: number, siteId?: string): Promise<any> {
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<any>} Promise resolved when the data is invalidated.
*/
invalidateUserCache(userId: number, siteId?: string) : Promise<any> {
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<any>} Promise resolved when done.
*/
logView(userId: number, courseId?: number) : Promise<any> {
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<any>} 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});
});
}
}

View File

@ -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 {}