From 43e8aa10181c8fb68aabee4e6edfe7d0f7ffdca2 Mon Sep 17 00:00:00 2001 From: Hermann <hermann.blum@mavt.ethz.ch> Date: Sat, 11 May 2019 13:25:27 +0200 Subject: [PATCH] Simplify Config and parse more settings from the openapi schema --- .eslintrc.js | 2 +- src/auth.js | 45 ++++++++--- src/blacklist/editBlacklist.js | 4 +- src/events/editEvent.js | 6 -- src/events/viewEvent.js | 5 -- src/groups/editGroup.js | 4 +- src/jobs/editJob.js | 3 - src/layout.js | 65 +++++++-------- src/relationlistcontroller.js | 4 +- src/resourceConfig.json | 139 --------------------------------- src/studydocs/editDoc.js | 18 +---- src/users/editUser.js | 2 - src/users/userTool.js | 6 +- src/views/editView.js | 25 +----- 14 files changed, 83 insertions(+), 245 deletions(-) delete mode 100644 src/resourceConfig.json diff --git a/.eslintrc.js b/.eslintrc.js index f561085..697167a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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, diff --git a/src/auth.js b/src/auth.js index 1327234..fe3d61e 100644 --- a/src/auth.js +++ b/src/auth.js @@ -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' } }); } diff --git a/src/blacklist/editBlacklist.js b/src/blacklist/editBlacklist.js index ee71bb4..129f8c2 100644 --- a/src/blacklist/editBlacklist.js +++ b/src/blacklist/editBlacklist.js @@ -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']), diff --git a/src/events/editEvent.js b/src/events/editEvent.js index fdcb17e..6ea3748 100644 --- a/src/events/editEvent.js +++ b/src/events/editEvent.js @@ -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'); diff --git a/src/events/viewEvent.js b/src/events/viewEvent.js index 60c3d8f..c2f35f3 100644 --- a/src/events/viewEvent.js +++ b/src/events/viewEvent.js @@ -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; } diff --git a/src/groups/editGroup.js b/src/groups/editGroup.js index b354ea0..dee6678 100644 --- a/src/groups/editGroup.js +++ b/src/groups/editGroup.js @@ -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, { diff --git a/src/jobs/editJob.js b/src/jobs/editJob.js index daae250..bc3352c 100644 --- a/src/jobs/editJob.js +++ b/src/jobs/editJob.js @@ -1,12 +1,10 @@ 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({ diff --git a/src/layout.js b/src/layout.js index 36c4ce0..02b069f 100644 --- a/src/layout.js +++ b/src/layout.js @@ -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', { diff --git a/src/relationlistcontroller.js b/src/relationlistcontroller.js index ced9580..2439245 100644 --- a/src/relationlistcontroller.js +++ b/src/relationlistcontroller.js @@ -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) diff --git a/src/resourceConfig.json b/src/resourceConfig.json deleted file mode 100644 index de3a0db..0000000 --- a/src/resourceConfig.json +++ /dev/null @@ -1,139 +0,0 @@ -{ - "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" - ] - } -} diff --git a/src/studydocs/editDoc.js b/src/studydocs/editDoc.js index f7bd2f3..12b53ac 100644 --- a/src/studydocs/editDoc.js +++ b/src/studydocs/editDoc.js @@ -1,31 +1,22 @@ 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', { diff --git a/src/users/editUser.js b/src/users/editUser.js index 5d90b29..8099bae 100644 --- a/src/users/editUser.js +++ b/src/users/editUser.js @@ -1,6 +1,5 @@ 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({ diff --git a/src/users/userTool.js b/src/users/userTool.js index 08a4d56..2bf11ee 100644 --- a/src/users/userTool.js +++ b/src/users/userTool.js @@ -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' } }, diff --git a/src/views/editView.js b/src/views/editView.js index 6a8b15a..3c71fe9 100644 --- a/src/views/editView.js +++ b/src/views/editView.js @@ -1,24 +1,10 @@ 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', - joboffers: 'Job Offer', - blacklist: 'Blacklist', -}; - export default class EditView extends ItemView { /** * Extension of ItemView to edit a data item @@ -38,13 +24,7 @@ export default class EditView extends ItemView { const validInitially = this.controller.modus === 'edit'; // start a form to collect the submit data this.form = new Form({}, validInitially, 4, Object.assign({}, this.controller.data)); - } - - oninit() { - // load schema - m.request(`${apiUrl}/docs/api-docs`).then((schema) => { - this.form.setSchema(schema.definitions[objectNameForResource[this.resource]]); - }).catch((error) => { console.log(error); }); + this.form.setSchema(this.handler.schema); } /** @@ -66,7 +46,6 @@ export default class EditView extends ItemView { request.then((response) => { resolve(response); }).catch((error) => { - console.log(error); // Process the API error if ('_issues' in error) { // there are problems with some fields, display them @@ -74,10 +53,12 @@ export default class EditView extends ItemView { this.form.errors[field] = [error._issues[field]]; this.form.valid = false; }); + // eslint-disable-next-line no-console console.log(this.form.errors); m.redraw(); reject(error); } else { + // eslint-disable-next-line no-console console.log(error); } }); -- GitLab