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 43e8aa10 authored by Hermann's avatar Hermann Committed by boian
Browse files

Simplify Config and parse more settings from the openapi schema

parent 3ac07c1f
......@@ -7,7 +7,7 @@ module.exports = {
"browser": true,
},
"rules": {
"no-console": 0,
"no-console": 1,
"class-methods-use-this": 0,
"prefer-destructuring": 1,
"no-underscore-dangle": 0,
......
......@@ -5,13 +5,13 @@ import { Snackbar } from 'polythene-mithril';
// eslint-disable-next-line import/extensions
import { apiUrl, ownUrl, oAuthID } from 'networkConfig';
import * as localStorage from './localStorage';
import config from './resourceConfig.json';
// Object which stores the current login-state
const APISession = {
authenticated: false,
token: '',
userID: null,
schema: null,
rights: {
users: [],
joboffers: [],
......@@ -19,6 +19,10 @@ const APISession = {
},
};
if (!APISession.schema) {
m.request(`${apiUrl}/docs/api-docs`).then((schema) => { APISession.schema = schema; });
}
const amivapi = axios.create({
baseURL: apiUrl,
headers: { 'Content-Type': 'application/json' },
......@@ -59,14 +63,12 @@ export function checkAuthenticated() {
else {
// let's see if we have a stored token
const token = localStorage.get('token');
console.log(`found this token: ${token}`);
if (token !== '') {
// check of token is valid
checkToken(token).then((session) => {
APISession.token = token;
APISession.authenticated = true;
APISession.userID = session.user;
console.log(APISession);
amivapi.get('/', {
headers: { 'Content-Type': 'application/json', Authorization: token },
}).then((response) => {
......@@ -128,6 +130,22 @@ export function getUserRights() {
return APISession.rights;
}
export function getSchema() {
return APISession.schema;
}
// Mapper for resource vs schema-object names
const objectNameForResource = {
users: 'User',
groupmemberships: 'Group Membership',
groups: 'Group',
eventsignups: 'Event Signup',
events: 'Event',
studydocuments: 'Study Document',
joboffers: 'Job Offer',
blacklist: 'Blacklist',
};
export class ResourceHandler {
/* Handler to get and manipulate resource items
*
......@@ -139,16 +157,24 @@ export class ResourceHandler {
constructor(resource, searchKeys = false) {
this.resource = resource;
this.rights = [];
// special case for users
this.schema = JSON.parse(JSON.stringify(getSchema().definitions[
objectNameForResource[this.resource]]));
// readOnly fields should be removed before patch
this.noPatchKeys = Object.keys(this.schema.properties).filter(key =>
this.schema.properties[key].readOnly);
// any field that is a string can be searched
const possibleSearchKeys = Object.keys(this.schema.properties).filter((key) => {
const field = this.schema.properties[key];
return field.type === 'string' && field.format !== 'objectid' &&
field.format !== 'date-time' && !key.startsWith('_');
});
// special case for users, we don't allow reverse search by legi or rfid
if (resource === 'users') this.searchKeys = ['firstname', 'lastname', 'nethz'];
else this.searchKeys = searchKeys || config[resource].searchKeys;
this.noPatchKeys = [
'_etag', '_id', '_created', '_links', '_updated',
...(config[resource].notPatchableKeys || [])];
else this.searchKeys = searchKeys || possibleSearchKeys;
checkAuthenticated().then(() => {
// again special case for users
if (resource === 'users' && APISession.isUserAdmin) {
this.searchKeys = searchKeys || config[resource].searchKeys;
this.searchKeys = searchKeys || possibleSearchKeys;
}
});
}
......@@ -217,6 +243,7 @@ export class ResourceHandler {
}
networkError(e) {
// eslint-disable-next-line no-console
console.log(e);
Snackbar.show({ title: 'Network error, try again.', style: { color: 'red' } });
}
......
......@@ -2,7 +2,6 @@ import m from 'mithril';
import { TextField } from 'polythene-mithril';
import { ListSelect, DatalistController } from 'amiv-web-ui-components';
import { ResourceHandler } from '../auth';
import { loadingScreen } from '../layout';
import EditView from '../views/editView';
class NanoController {
......@@ -48,7 +47,6 @@ export default class NewBlacklist extends EditView {
}
view() {
if (!this.form.schema) return m(loadingScreen);
return this.layout([
m('div', { style: { display: 'flex' } }, [
m(TextField, { label: 'User: ', disabled: true, style: { width: '160px' } }),
......@@ -57,7 +55,7 @@ export default class NewBlacklist extends EditView {
selection: this.form.data.user,
listTileAttrs: user => Object.assign({}, { title: `${user.firstname} ${user.lastname}` }),
selectedText: user => `${user.firstname} ${user.lastname}`,
onSelect: (data) => { console.log('data'); this.form.data.user = data; },
onSelect: (data) => { this.form.data.user = data; },
})),
]),
...this.form.renderSchema(['reason', 'start_time', 'price']),
......
......@@ -9,7 +9,6 @@ import { TabsCSS, ButtonCSS } from 'polythene-css';
import { apiUrl, ownUrl } from 'networkConfig';
import { ResourceHandler } from '../auth';
import { colors } from '../style';
import { loadingScreen } from '../layout';
import { icons } from '../views/elements';
import EditView from '../views/editView';
......@@ -69,7 +68,6 @@ export default class newEvent extends EditView {
// proposition URL-link decoder
if (this.rightSubmit && m.route.param('proposition')) {
const data = JSON.parse(window.atob(m.route.param('proposition')));
console.log(data);
this.form.data = data;
}
if (this.form.data.priority === 10) this.form.data.high_priority = true;
......@@ -243,8 +241,6 @@ export default class newEvent extends EditView {
}
view() {
if (!this.form.schema) return m(loadingScreen);
// load image urls from the API data
['thumbnail', 'poster', 'infoscreen'].forEach((key) => {
const img = this.form.data[`img_${key}`];
......@@ -254,8 +250,6 @@ export default class newEvent extends EditView {
}
});
console.log(this.form.errors, this.form.valid);
// Define the number of Tabs and their titles
const titles = ['Event Description', 'When and Where?', 'Signups', 'Internal Info'];
if (this.rightSubmit) titles.push('Images');
......
......@@ -6,7 +6,6 @@ import { DropdownCard, DatalistController, Chip } from 'amiv-web-ui-components';
// eslint-disable-next-line import/extensions
import { apiUrl } from 'networkConfig';
import ItemView from '../views/itemView';
import { eventsignups as signupConfig } from '../resourceConfig.json';
import TableView from '../views/tableView';
import RelationlistController from '../relationlistcontroller';
import { dateFormatter } from '../utils';
......@@ -236,7 +235,6 @@ class ParticipantsTable {
m(TableView, {
tableHeight: '275px',
controller: this.ctrl,
keys: signupConfig.tableKeys,
tileContent: data => this.itemRow(data),
clickOnRows: false,
titles: [
......@@ -271,7 +269,6 @@ export default class viewEvent extends ItemView {
cloneEvent() {
const event = Object.assign({}, this.data);
console.log(event);
const eventInfoToDelete = [
'_id',
......@@ -284,7 +281,6 @@ export default class viewEvent extends ItemView {
'__proto__',
];
const now = new Date();
console.log(`${now.toISOString().slice(0, -5)}Z`);
if (event.time_end < `${now.toISOString().slice(0, -5)}Z`) {
eventInfoToDelete.push(...[
'time_advertising_end',
......@@ -299,7 +295,6 @@ export default class viewEvent extends ItemView {
delete event[key];
});
console.log(event);
this.controller.changeModus('new');
this.controller.data = event;
}
......
......@@ -4,7 +4,6 @@ import { ListSelect, DatalistController, Select } from 'amiv-web-ui-components';
// eslint-disable-next-line import/extensions
import { apiUrl } from 'networkConfig';
import { ResourceHandler } from '../auth';
import { loadingScreen } from '../layout';
import EditView from '../views/editView';
......@@ -88,7 +87,6 @@ export default class NewGroup extends EditView {
}
view() {
if (!this.form.schema) return m(loadingScreen);
return this.layout([
...this.form.renderSchema(['name', 'allow_self_enrollment', 'requires_storage']),
m('div', { style: { display: 'flex' } }, [
......@@ -98,7 +96,7 @@ export default class NewGroup extends EditView {
selection: this.form.data.moderator,
listTileAttrs: user => Object.assign({}, { title: `${user.firstname} ${user.lastname}` }),
selectedText: user => `${user.firstname} ${user.lastname}`,
onSelect: (data) => { console.log('data'); this.form.data.moderator = data; },
onSelect: (data) => { this.form.data.moderator = data; },
})),
]),
m(PermissionEditor, {
......
import m from 'mithril';
import { FileInput } from 'amiv-web-ui-components';
import { loadingScreen } from '../layout';
import EditView from '../views/editView';
export default class newJob extends EditView {
beforeSubmit() {
console.log(this.form.data);
// remove all unchanged files
if (this.form.data.pdf !== undefined &&
(this.form.data.pdf === null || 'upload_date' in this.form.data.pdf)) {
......@@ -26,7 +24,6 @@ export default class newJob extends EditView {
}
view() {
if (!this.form.schema) return m(loadingScreen);
return this.layout([
...this.form.renderSchema(['company']),
m(FileInput, this.form.bind({
......
......@@ -12,7 +12,7 @@ import {
} from 'polythene-mithril';
import { styler } from 'polythene-core-css';
import { icons } from './views/elements';
import { deleteSession, getUserRights } from './auth';
import { deleteSession, getUserRights, getSchema } from './auth';
import { colors } from './style';
const layoutStyle = [
......@@ -93,8 +93,40 @@ class Menupoint {
}
}
export class loadingScreen {
view() {
return m('div', {
style: {
height: '100%',
width: '100%',
display: 'flex',
'flex-direction': 'column',
'justify-content': 'center',
'align-items': 'center',
'animation-name': 'popup',
'animation-duration': '2000ms',
},
}, m('div', { style: { height: '5vh', 'font-size': '4em' } }, 'Loading...'), m('div', {
style: {
height: '20vh',
width: '20vh',
'animation-name': 'spin',
'animation-duration': '2500ms',
'animation-iteration-count': 'infinite',
'animation-timing-function': 'linear',
},
}, m('div', {
style: { height: '20vh', width: '20vh', display: 'inline-block' },
}, m(SVG, {
style: { width: 'inherit', height: 'inherit' },
content: m.trust(icons.amivWheel),
}))));
}
}
export class Layout {
view({ children }) {
if (!getSchema()) return m(loadingScreen);
const userRights = getUserRights();
return m('div', [
m('div.wrapper-main.smooth', [
......@@ -177,37 +209,6 @@ export class Layout {
}
}
export class loadingScreen {
view() {
return m('div', {
style: {
height: '100%',
width: '100%',
display: 'flex',
'flex-direction': 'column',
'justify-content': 'center',
'align-items': 'center',
'animation-name': 'popup',
'animation-duration': '2000ms',
},
}, m('div', { style: { height: '5vh', 'font-size': '4em' } }, 'Loading...'), m('div', {
style: {
height: '20vh',
width: '20vh',
'animation-name': 'spin',
'animation-duration': '2500ms',
'animation-iteration-count': 'infinite',
'animation-timing-function': 'linear',
},
}, m('div', {
style: { height: '20vh', width: '20vh', display: 'inline-block' },
}, m(SVG, {
style: { width: 'inherit', height: 'inherit' },
content: m.trust(icons.amivWheel),
}))));
}
}
export class Error404 {
view() {
return m('div', {
......
......@@ -71,8 +71,6 @@ export default class RelationlistController {
// update total number of pages
this.totalPages = Math.ceil(data._meta.total / 10);
console.log(data._items.map(item => item._id));
const itemsWithoutRelation = data._items.filter(item => !(this.secondaryKey in item));
const itemsWithRelation = data._items.filter(item => (this.secondaryKey in item));
......@@ -115,7 +113,6 @@ export default class RelationlistController {
// save totalPages as a constant to avoid race condition with pages added during this
// process
const { totalPages } = this;
console.log(totalPages);
if (totalPages === 1) {
resolve(firstPage);
......@@ -128,6 +125,7 @@ export default class RelationlistController {
// look if all pages were collected
const missingPages = Array.from(new Array(totalPages), (x, i) => i + 1).filter(i =>
!(i in pages));
// eslint-disable-next-line no-console
console.log('missingPages', missingPages);
if (missingPages.length === 0) {
// collect all the so-far loaded pages in order (sorted keys)
......
{
"apiUrl": "https://api-dev.amiv.ethz.ch/",
"events": {
"keyDescriptors": {
"title_de": "German Title",
"title_en": "English Title",
"location": "Location",
"show_website": "Event is shown on the website",
"priority": "Priority",
"time_end": "Ending time",
"time_register_end": "Deadline for registration",
"time_start": "Starting time",
"spots": "Spots available",
"allow_email_signup": "Event open for non-AMIV members",
"price": "Price",
"signup_count": "Signed-up participants",
"catchphrase_en": "Catchphrase in English. Announce and Website.",
"catchphrase_de": "Schlagwort auf Deutsch",
"description_de": "Beschreibung auf Deutsch",
"description_en": "Description in English",
"img_banner": "Banner as png",
"img_poster": "Poster as png",
"img_thumbnail": "Thumbnail as png",
"show_infoscreen": "Does the event show on the infoscreen?",
"img_infoscreen": "Infoscreen as png",
"time_advertising_end": "Advertisment ends on",
"time_advertising_start": "Advertisement starts on",
"selection_strategy": "TODO what is this?",
"show_announce": "Does it belong to announce?"
},
"tableKeys": [
"title_de",
"time_start",
"time_end",
"time_register_end",
"show_website",
"priority"
],
"notPatchableKeys": [
"signup_count",
"unaccepted_count"
],
"searchKeys": [
"title_de",
"title_en",
"location"
]
},
"users": {
"keyDescriptors": {
"legi": "Legi",
"firstname": "First Name",
"lastname": "Last Name",
"rfid": "RFID",
"phone": "Phone",
"nethz": "nethz",
"gender": "Gender",
"department": "Department",
"email": "Email"
},
"tableKeys": [
"firstname",
"lastname",
"nethz",
"legi",
"membership"
],
"searchKeys": [
"firstname",
"lastname",
"nethz",
"legi",
"email"
],
"notPatchableKeys": [
"password_set"
]
},
"joboffers":{
"keyDescriptors": {
"company": "Company",
"email": "Email",
"description_en": "Job description",
"description_de": "Job Beschreibung",
"logo": "Logo as png",
"pdf": "PDF provided by company",
"time_end": "Application deadline",
"title_de": "Stelle auf Deutsch",
"title_en": "Position title in English",
"show_website": "Is the job listed on the website?",
"_id":"Job ID."
},
"tableKeys": [
"title_de",
"time_end",
"show_website"
]
},
"groups": {
"keyDescriptors": {
"name": "Name"
},
"searchKeys": ["name"],
"patchableKeys": ["name"]
},
"groupmemberships": {
"patchableKeys": ["user", "group"]
},
"eventsignups": {
"patchableKeys": ["event"],
"tableKeys": [
"_created",
"user.lastname",
"user.firstname",
"email"
],
"searchKeys": []
},
"sessions": {
"searchKeys": []
},
"studydocuments": {
"searchKeys": [
"title",
"lecture",
"professor",
"author",
"uploader",
"department"
],
"notPatchableKeys": ["uploader"]
},
"blacklist": {
"searchKeys": [
"reason",
"price"
]
}
}
import m from 'mithril';
import { FileInput } from 'amiv-web-ui-components';
import { Button, List, ListTile, Snackbar } from 'polythene-mithril';
// eslint-disable-next-line import/extensions
import { apiUrl } from 'networkConfig';
import EditView from '../views/editView';
import { loadingScreen } from '../layout';
import { getSchema } from '../auth';
export default class editDoc extends EditView {
// constructor zu file upload
constructor(vnode) {
// remove the files list as it is impossible to validate
const docSchema = getSchema().definitions['Study Document'];
delete docSchema.properties.files;
super(vnode);
if (!('files' in this.form.data)) {
this.form.data.files = [{ name: 'add file' }];
}
}
oninit() {
// load schema
m.request(`${apiUrl}/docs/api-docs`).then((schema) => {
// remove the files list as it is impossible to validate
const docSchema = schema.definitions['Study Document'];
delete docSchema.properties.files;
this.form.setSchema(docSchema);
}).catch((error) => { console.log(error); });
}
beforeSubmit() {
// check if there are files uploaded
const files = [];
......@@ -51,7 +42,6 @@ export default class editDoc extends EditView {
}
view() {
if (!this.form.schema) return m(loadingScreen);
return this.layout([
m('h3', 'Add a New Studydocument'),
this.form._renderField('semester', {
......
import m from 'mithril';
import { TextInput } from 'amiv-web-ui-components';
import { loadingScreen } from '../layout';
import EditView from '../views/editView';
export default class UserEdit extends EditView {
......@@ -11,7 +10,6 @@ export default class UserEdit extends EditView {
view() {
if (!this.form.schema) return m(loadingScreen);
return this.layout([
...this.form.renderSchema(['lastname', 'firstname', 'email', 'nethz', 'legi']),
m(TextInput, this.form.bind({
......
......@@ -3,7 +3,6 @@ import { DatalistController } from 'amiv-web-ui-components';
import EditUser from './editUser';
import ViewUser from './viewUser';
import TableView from '../views/tableView';
import { users as config } from '../resourceConfig.json';
import ItemController from '../itemcontroller';
import { loadingScreen } from '../layout';
import { ResourceHandler } from '../auth';
......@@ -31,10 +30,11 @@ export class UserTable {
);
}
view() {
const tableKeys = ['firstname', 'lastname', 'nethz', 'legi', 'membership'];
return m(TableView, {
controller: this.ctrl,
keys: config.tableKeys,
titles: config.tableKeys.map(key => config.keyDescriptors[key] || key),
keys: tableKeys,
titles: tableKeys.map(key => this.handler.schema.properties[key].title || key),
filters: [[
{ name: 'not members', query: { membership: 'none' } },
{ name: 'regular members', query: { membership: 'regular' } },
......
import m from 'mithril';
import { IconButton, Toolbar, ToolbarTitle, Button } from 'polythene-mithril';
import { Form } from 'amiv-web-ui-components';
// eslint-disable-next-line import/extensions
import { apiUrl } from 'networkConfig';
import ItemView from './itemView';
import { icons } from './elements';
import { colors } from '../style';
// Mapper for resource vs schema-object names
const objectNameForResource = {
users: 'User',
groupmemberships: 'Group Membership',
groups: 'Group',
eventsignups: 'Event Signup',
events: 'Event',
studydocuments: 'Study Document',