To receive notifications about scheduled maintenance, please subscribe to the mailing-list gitlab-operations@sympa.ethz.ch. You can subscribe to the mailing-list at https://sympa.ethz.ch

Commit c9f7edc6 authored by Sandro Lutz's avatar Sandro Lutz Committed by lic
Browse files

General Overhaul of the `Profile` Page

parent c3efcc1c
......@@ -73,6 +73,9 @@
"profile.rfid_error": "6 Ziffern erforderlich. Siehe auf der Rückseite deiner Legi.",
"profile.newsletter_unsubscribe": "Vom Newsletter abmelden",
"profile.newsletter_subscribe": "Newsletter abonnieren",
"profile.loading_sessions": "Lade Sitzungsdaten",
"profile.no_active_sessions": "Du hast keine anderen aktiven Sitzungen.",
"profile.active_sessions": "Beende alle anderen %{count} aktiven Sitzungen.",
"profile.search_groups": "Gruppen durchsuchen",
"profile.expire_on": "läuft am %{date} ab",
"studydocs.not_found": "Dokument nicht gefunden",
......
......@@ -73,6 +73,9 @@
"profile.rfid_error": "6 digits required. See on the back of your legi.",
"profile.newsletter_unsubscribe": "Unsubscribe from Newsletter",
"profile.newsletter_subscribe": "subscribe to Newsletter",
"profile.loading_sessions": "Loading session data",
"profile.no_active_sessions": "You have no other active sessions",
"profile.active_sessions": "Terminate all other %{count} active sessions",
"profile.search_groups": "Search groups",
"profile.expire_on": "expires on %{date}",
"studydocs.not_found": "Document not found",
......
import m from 'mithril';
import { apiUrl } from 'config';
import { getToken, getUserId } from './auth';
import PaginationController from './pagination';
import { error } from './log';
import Query from './query';
/**
* GroupMembershipsController class (inherited from `PaginationController`)
*
* Used to handle the list of group memberships of the authenticated user.
*/
export default class GroupMembershipsController extends PaginationController {
constructor(query = {}, additionalQuery = {}) {
super(
'groupmemberships',
query,
Query.merge(additionalQuery, {
embedded: { group: 1 },
})
);
}
/**
* Enroll the authenticated user to a group
*
* @param {String} groupId
* @return {Promise} exports for additional response handling
*/
enroll(groupId) {
return m
.request({
method: 'POST',
url: `${apiUrl}/groupmemberships`,
headers: getToken()
? {
Authorization: `Token ${getToken()}`,
}
: {},
data: { group: groupId, user: getUserId() },
})
.then(() => {
this.loadAll();
})
.catch(e => {
error(e.message);
});
}
/**
* Withdraw membership of the authenticated user from a group.
*
* @param {String} groupMembershipId groupmembership id
* @param {String} etag value given by AMIV API to be used as `If-Match` header.
* @return {Promise} exports for additional response handling
*/
withdraw(groupMembershipId, etag) {
return m
.request({
method: 'DELETE',
url: `${apiUrl}/groupmemberships/${groupMembershipId}`,
headers: getToken()
? {
Authorization: `Token ${getToken()}`,
'If-Match': etag,
}
: { 'If-Match': etag },
})
.then(() => {
this.loadAll();
})
.catch(e => {
error(e.message);
});
}
}
import m from 'mithril';
import { apiUrl } from 'config';
import { getToken, getUserId } from './auth';
import { error } from './log';
import Query from './query';
let querySaved = '';
import PaginationController from './pagination';
/**
* Get the loaded list of groups.
* GroupsController class (inherited from `PaginationController`)
*
* @return {array} `group` objects returned by the AMIV API.
* Used to handle a list of groups.
*/
export function getList() {
if (this.groups === undefined) {
return [];
export default class GroupsController extends PaginationController {
constructor(query = {}, additionalQuery = {}) {
super('groups', query, additionalQuery);
}
return this.groups;
}
/**
* Get the memberships for the authenticated user.
*
* @return {array} `groupmembership` objects with embedded groups returned by the AMIV API.
*/
export function getMemberships() {
if (this.memberships === undefined) {
return [];
}
return this.memberships;
}
/**
* Enroll the authenticated user to a group
*
* @param {String} groupId
* @return {Promise} exports for additional response handling
*/
export function enroll(groupId) {
return m
.request({
method: 'POST',
url: `${apiUrl}/groupmemberships`,
headers: getToken()
? {
Authorization: `Token ${getToken()}`,
}
: {},
data: { group: groupId, user: getUserId() },
})
.then(result => {
const membership = result;
const group = this.groups.find(item => item._id === membership.group);
if (group === undefined) {
membership.group = membership.group;
} else {
membership.group = group;
}
this.memberships.push(membership);
})
.catch(e => {
error(e.message);
});
}
/**
* Withdraw membership of the authenticated user from a group.
*
* @param {String} groupMembershipId groupmembership id
* @param {String} etag value given by AMIV API to be used as `If-Match` header.
* @return {Promise} exports for additional response handling
*/
export function withdraw(groupMembershipId, etag) {
return m
.request({
method: 'DELETE',
url: `${apiUrl}/groupmemberships/${groupMembershipId}`,
headers: getToken()
? {
Authorization: `Token ${getToken()}`,
'If-Match': etag,
}
: { 'If-Match': etag },
})
.then(() => {
this.memberships = this.memberships.filter(item => item._id !== groupMembershipId);
})
.catch(e => {
error(e.message);
});
}
/**
* Load groups from the AMIV API.
*
* @param {*} query filter and sort query for the API request.
* @return {Promise} exports for additional response handling
*/
export function load(query = {}) {
const queryEncoded = Query.buildQueryString({ where: query });
querySaved = query;
return m
.request({
method: 'GET',
url: `${apiUrl}/groups?${queryEncoded}`,
headers: getToken()
? {
Authorization: `Token ${getToken()}`,
}
: {},
})
.then(result => {
this.groups = result._items;
})
.catch(e => {
error(e.message);
});
}
/**
* Load groupmemberships of the authenticated user from the AMIV API.
*
* @return {Promise} exports for additional response handling
*/
export function loadMemberships() {
const queryEncoded = Query.buildQueryString({
where: { user: getUserId() },
embedded: { group: 1 },
});
return m
.request({
method: 'GET',
url: `${apiUrl}/groupmemberships?${queryEncoded}`,
headers: getToken()
? {
Authorization: `Token ${getToken()}`,
}
: {},
})
.then(result => {
this.memberships = result._items;
})
.catch(e => {
error(e.message);
});
}
/**
* Reload event list with the same query as before.
*
* @return {Promise} exports for additional response handling
*/
export function reload() {
return load(querySaved);
}
......@@ -89,6 +89,20 @@ export default class PaginationController {
return this._pages.map(page => callback(page.items));
}
/**
* Test whether some element passes the test implemented by the provided function.
*
* @param {function} test function implementing a test
* @public
*/
some(test) {
let result = false;
this._pages.forEach(page => {
result = result || page.items.some(test);
});
return result;
}
/**
* Get number of loaded items
*
......
......@@ -4,65 +4,143 @@ import { getToken, getUserId } from './auth';
import { error } from './log';
/**
* Update data of the authenticated user.
* If a specific token should be used, specify it as the second parameter.
* User class
*
* @param {Object} options any subset of user properties.
* @param {string} token API token (optional)
* @return {Promise} exports for additional response handling
* Managing data of authenticated user.
*/
export function update(options, token) {
return m
.request({
method: 'PATCH',
url: `${apiUrl}/users/${getUserId()}`,
export default class UserController {
constructor() {
this._sessionCount = 0;
}
/**
* Load user data of the authenticated user from the AMIV API.
*
* @return {Promise} exports for additional response handling
*/
async load() {
try {
const promiseList = [];
promiseList.push(
m
.request({
method: 'GET',
url: `${apiUrl}/users/${getUserId()}`,
headers: {
Authorization: getToken(),
},
})
.then(result => {
this._user = result;
})
);
promiseList.push(this._loadSessionPage(1));
await Promise.all(promiseList);
} catch (err) {
error(err.message);
}
}
/**
* Get available user data of the authenticated user.
*
* @return {Object} `user` object returned by the AMIV API.
*/
get user() {
if (typeof this._user === 'undefined') {
return {};
}
return this._user;
}
/**
* Get total number of active sessions for the authenticated user.
*
* @return {int} number of sessions
*/
get sessionCount() {
return this._sessionCount;
}
/**
* Update data of the authenticated user.
* If a specific token should be used, specify it as the second parameter.
*
* @param {Object} options any subset of user properties.
* @param {string} token API token (optional)
* @return {Promise} exports for additional response handling
*/
update(options, token) {
return m
.request({
method: 'PATCH',
url: `${apiUrl}/users/${getUserId()}`,
headers: {
Authorization: token || getToken(),
'If-Match': this._user._etag,
},
data: options,
})
.then(result => {
this._user = result;
})
.catch(e => {
error(e.message);
});
}
/**
* Terminates all other active sessions of the authenticated user.
*/
async clearOtherSessions() {
const sessions = [];
const totalPages = Math.ceil(this._sessionCount / 20);
let promiseList = [];
let currentPage = 1;
// Load all sessions
while (currentPage <= totalPages) {
promiseList.push(
this._loadSessionPage(currentPage).then(result => {
sessions.push(...result);
})
);
currentPage += 1;
}
await Promise.all(promiseList);
// Delete all sessions (except the currently used one)
promiseList = [];
sessions.forEach(session => {
if (session.token !== getToken()) {
promiseList.push(this.constructor._deleteSession(session));
}
});
await Promise.all(promiseList);
this._sessionCount = 1;
}
// helper function to load session page
async _loadSessionPage(pageNum) {
const response = await m.request({
method: 'GET',
url: `${apiUrl}/sessions?where={"user":"${getUserId()}"}&max_results=20&page=${pageNum}`,
headers: {
Authorization: token || getToken(),
'If-Match': this.user._etag,
Authorization: getToken(),
},
data: options,
})
.then(result => {
this.user = result;
})
.catch(e => {
error(e.message);
});
}
/**
* Get available user data of the authenticated user.
*
* @return {Object} `user` object returned by the AMIV API.
*/
export function get() {
if (typeof this.user === 'undefined') {
return {};
this._sessionCount = response._meta.total;
return response._items;
}
return this.user;
}
// load information of logged in user
/**
* Load user data of the authenticated user from the AMIV API.
*
* @return {Promise} exports for additional response handling
*/
export function load() {
return m
.request({
method: 'GET',
url: `${apiUrl}/users/${getUserId()}`,
headers: getToken()
? {
Authorization: `Token ${getToken()}`,
}
: {},
})
.then(result => {
this.user = result;
})
.catch(e => {
error(e.message);
// helper function to delete a session
static async _deleteSession(session) {
return m.request({
method: 'DELETE',
url: `${apiUrl}/sessions/${session._id}`,
headers: {
Authorization: getToken(),
'If-Match': session._etag,
},
});
}
}
@import './board.less';
@import './colors.less';
@import './commissions.less';
@import './dimensions.less';
@import './mediaquery.less';
@import './infobox.less';
@import './errors.less';
@import './eventDetails.less';
@import './eventList.less';
@import './filteredListPage.less';
@import './footer.less';
@import './frontpage.less';
@import './board.less';
@import './commissions.less';
@import './studydocList.less';
@import './legalNotice.less';
@import './header.less';
@import './footer.less';
@import './filteredListPage.less';
@import './eventList.less';
@import './eventDetails.less';
@import './studydocList.less';
@import './infobox.less';
@import './jobofferList.less';
@import './legalNotice.less';
@import './mediaquery.less';
@import './profile.less';
@import './studydocList.less';
html,body {
width: 100%;
......
#profile-container {
display: grid;
grid-template-columns: repeat(12, 1fr);
grid-gap: 30px;
align-content: center;
text-align: center;
border: none;
#user-info {
grid-column: ~'1 / 13';
grid-row: ~'1 / 2';
background-color: #ddd;
margin: 10px;
padding: 10px;
}
#change-password {
grid-column: ~'1 / 7';
grid-row: ~'2 / 5';
}
#rfid {
grid-column: ~'1 / 7';
grid-row: ~'5 / 6';
}
#announce-subscription {
grid-column: ~'7 / 13';
grid-row: ~'2 / 3';
}
#sessions {
grid-column: ~'7 / 13';
grid-row: ~'3 / 4';
}
#groupmemberships {
grid-column: ~'7 / 13';
grid-row: ~'4 / 6';
display: grid;
grid-template-columns: repeat(3, 1fr);
}
#groups {
grid-column: ~'7 / 13';
grid-row: ~'6 / 8';
display: grid;
grid-template-columns: repeat(3, 1fr);
}
}
#group-search {
grid-row: ~'1 / 2';
grid-column: ~'1 / 4';
}
#group-list {
grid-row: ~'2 / 3';
grid-column: ~'1 / -1';
.group-entry {
grid-column: ~'1 / -1';
display: grid;
grid-template-columns: repeat(3, 1fr);
.group-name {
grid-column: ~'1 / 2';
}
.group-expiry {
grid-column: ~'2 / 3';
}
.group-button {
grid-column: ~'3 / 4';
}
}
}
import m from 'mithril';
import { apiUrl } from 'config';
import { error } from '../models/log';
import * as user from '../models/user';
import * as groups from '../models/groups';
// import { Button, InputGroupForm } from '../components';
import { Button, TextField } from '../components';
import { i18n } from '../models/language';
// shows all relevant user information
class showUserInfo {
static view() {
let freeBeerNotice;
if (user.get().membership !== 'none') {
if (user.get().rfid !== undefined && user.get().rfid.length === 6) {
freeBeerNotice = m('div', i18n('profile.free_beer'));
} else {
freeBeerNotice = m('div', i18n('profile.set_rfid'));
}
}
return m('div', [
m('div', [
m('span', `${i18n('profile.membership')}: `),
m('span', i18n(`${user.get().membership}_member`)),
]),
freeBeerNotice,