Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • maspect/amiv-admintool
  • emustafa/amiv-admintool
  • dvruette/amiv-admintool
  • amiv/amiv-admintool
4 results
Show changes
Commits on Source (213)
Showing with 19322 additions and 556 deletions
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile
{
"name": "amiv-admintool",
"image": "mcr.microsoft.com/vscode/devcontainers/javascript-node:14",
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [9000],
// Uncomment the next line to run commands after the container is created.
"postCreateCommand": "npm install"
// Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root.
//"remoteUser": "node"
}
......@@ -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,
......
.ftpconfig
node_modules/
package-lock.json
.idea/
dist/
\ No newline at end of file
dist/
.DS_Store
stages:
- test
- build
- deploy
build_master_dev:
stage: build
image: docker:latest
services:
- docker:dind
eslint:
stage: test
image: node:latest
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
- npm install
script:
- docker build --build-arg NPM_BUILD_COMMAND=build-dev --pull -t "$CI_REGISTRY_IMAGE:dev" ./
- docker push "$CI_REGISTRY_IMAGE:dev"
only:
- master
- npm run lint
build_master_prod:
build_master:
stage: build
image: docker:latest
image: docker:stable
services:
- docker:dind
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
- echo "$CI_DOCKER_REGISTRY_TOKEN" | docker login -u "$CI_DOCKER_REGISTRY_USER" --password-stdin
script:
- docker build --pull -t "$CI_REGISTRY_IMAGE" ./
- docker push "$CI_REGISTRY_IMAGE"
- docker build --build-arg NPM_BUILD_COMMAND=build --pull -t "$CI_REGISTRY_IMAGE:prod" ./
- docker build --build-arg NPM_BUILD_COMMAND=build-staging --pull -t "$CI_REGISTRY_IMAGE:staging" ./
- docker build --build-arg NPM_BUILD_COMMAND=build-local --pull -t "$CI_REGISTRY_IMAGE:local" ./
- docker push "$CI_REGISTRY_IMAGE:prod"
- docker push "$CI_REGISTRY_IMAGE:staging"
- docker push "$CI_REGISTRY_IMAGE:local"
environment:
name: production
url: https://admin.amiv.ethz.ch
only:
- master
# On branches except master: verify that build works, do not push to registry
build:
build_dev:
stage: build
image: docker:latest
image: docker:stable
services:
- docker:dind
before_script:
- echo "$CI_DOCKER_REGISTRY_TOKEN_DEV" | docker login -u "$CI_DOCKER_REGISTRY_USER_DEV" --password-stdin
script:
- docker build --pull ./
except:
- master
deploy_prod:
stage: deploy
image: amiveth/service-update-helper
script:
- /update.py
only:
- master
deploy_dev:
stage: deploy
image: amiveth/service-update-helper
script:
- export CI_DEPLOY_SERVICE="$CI_DEPLOY_SERVICE_DEV"
- /update.py
only:
- master
- docker build --build-arg NPM_BUILD_COMMAND=build-dev --pull -t "$CI_REGISTRY_IMAGE_DEV" ./
- docker push "$CI_REGISTRY_IMAGE_DEV"
environment:
name: development
url: https://admin-dev.amiv.ethz.ch
deploy_prod:
deploy:
stage: deploy
image: amiveth/service-update-helper
image: amiveth/ansible-ci-helper
script:
- export CI_DEPLOY_SERVICE="$CI_DEPLOY_SERVICE_PROD"
- /update.py
only:
- master
- python /main.py
# Summary
> Hi! Thanks for contributing to the admintools by reporting a new Bug!
> In order to fix the bug soon, please help us to figure out what causes the
> malfunction!
> describe the issue here
# Steps to Reproduce
> What is the main menu point (left menu bar) where this issue occurs?
> Copy and Paste the url from the window where the issue occured
> Describe in your own words what steps you did before the issue occured
# Additional Debug Information
> Please add information on what browser and operating system you are using
> If possible, can you make a screenshot of the bug?
> Please open the "developer tools" of your browser and copy and paste any
> printouts in the "console"
/label ~bug
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/src/relationlistcontroller.js"
}
]
}
\ No newline at end of file
# First stage: Build project
FROM node as build
FROM node:14 as build
ARG NPM_BUILD_COMMAND=build
......@@ -12,26 +12,11 @@ RUN npm run $NPM_BUILD_COMMAND
# Second stage: Server to deliver files
FROM node:alpine
# Port 8080 can be used as non root
EXPOSE 8080
# Create user with home directory and no password
RUN adduser -Dh /admintool admintool
USER admintool
WORKDIR /admintool
# Install http server
RUN npm install --no-save http-server
FROM nginx:1.15-alpine
# Copy files from first stage
COPY --from=build /index.html /admintool/
COPY --from=build /dist /admintool/dist
# Serve index.html for every file which is not found on the server
# Hotfix for direct links
RUN ln index.html 404.html
COPY --from=build /index.html /var/www/
COPY --from=build /dist /var/www/dist
# Run server (-g will automatically serve the gzipped files if possible)
CMD ["/admintool/node_modules/.bin/http-server", "-g", "/admintool"]
# Copy nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Admintool
# Developer Installation
## w/ Docker
Start the dev Container in VS Code
run ```npm run start``` inside the container.
The development server is available under localhost:9000. It refreshes automatically as soon as you save changes in any `.js` file.
## w/o Docker
```
npm install
......@@ -12,6 +20,20 @@ And now, start developing:
npm run start
```
*Warning*: For installation on Ubuntu 16.04 (and possibly similar), you need to install nodejs from the repos source.
1. Remove nodejs if you already have it (ONLY IF YOU REALLY WANT!):
```
sudo apt remove nodejs
```
2. Add nodejs10 from repo (download): `curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash -`
3. Install: `sudo apt install -y nodejs`
4. Clean-up and install the packages for amiv-admintools
```
rm -rf node_modules/
npm install
npm run start
```
This will open up a local server outputting the current version of the admintools. It refreshes automatically as soon as you save changes in any `.js` file.
### File Structure:
......
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
root /var/www/;
index index.html;
try_files $uri /index.html =404;
}
source diff could not be displayed: it is too large. Options to address this: view the blob.
......@@ -7,7 +7,9 @@
"start": "webpack-dev-server --hot --inline",
"build": "webpack -p --config webpack.config.prod.js",
"build-dev": "webpack -p --config webpack.config.dev.js",
"lint": "eslint src/**"
"build-staging": "webpack -p --config webpack.config.staging.js",
"build-local": "webpack -p --config webpack.config.local.js",
"lint": "eslint src/**/*.js src/*.js"
},
"repository": {
"type": "git",
......@@ -18,30 +20,36 @@
"@material/drawer": "^0.30.0",
"@material/select": "^0.35.1",
"ajv": "^5.5.0",
"amiv-web-ui-components": "git+https://git@gitlab.ethz.ch/amiv/web-ui-components.git#360e65da63f4511db1f6ac56ce103be9254d1d9f",
"axios": "^0.17.1",
"client-oauth2": "^4.2.0",
"mithril": "^1.1.6",
"mithril-infinite": "^1.2.4",
"polythene-core-css": "^1.2.0",
"polythene-css": "^1.2.0",
"polythene-mithril": "^1.2.0",
"querystring": "^0.2.0"
"polythene-mithril": "1.2.0",
"querystring": "^0.2.0",
"showdown": "^1.9.0"
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-preset-env": "^1.6.1",
"compression-webpack-plugin": "^1.1.11",
"css-loader": "^0.28.11",
"eslint": "^4.10.0",
"eslint-config-airbnb-base": "^12.1.0",
"eslint-loader": "^1.9.0",
"eslint-plugin-import": "^2.9.0",
"file-loader": "^1.1.5",
"style-loader": "^0.19.0",
"webpack": "^3.8.1",
"webpack-dev-server": "^2.9.5"
"@babel/cli": "^7.2.3",
"@babel/core": "^7.2.2",
"@babel/plugin-proposal-object-rest-spread": "^7.2.0",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/preset-env": "^7.2.3",
"babel-eslint": "^10.0.1",
"babel-loader": "^8.0.4",
"compression-webpack-plugin": "^2.0.0",
"css-loader": "^2.1.0",
"eslint": "^5.16.0",
"eslint-config-airbnb-base": "^13.1.0",
"eslint-import-resolver-webpack": "^0.10.1",
"eslint-loader": "^3.0.0",
"eslint-plugin-import": "^2.14.0",
"file-loader": "^3.0.1",
"style-loader": "^0.23.1",
"webpack": "^4.28.3",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.14"
}
}
import tool from 'announcetool';
const m = require('mithril');
export default class AnnounceTool {
oncreate() {
if (tool.wasRenderedOnce()) {
// jQuery catches the first document.ready, but afterwards we have to
// trigger a render
tool.render();
}
}
view() {
return m('div', [
m('div#tableset', [
m('p#events'),
m('div#buttonrow', [
m('button#preview.btn.btn-default', 'Preview'),
m('button#reset.btn.btn-default', 'Reset'),
m('button#send.btn.btn-default', 'Send'),
]),
]),
m('br'),
m('hr'),
m('textarea#target'),
]);
}
}
import m from 'mithril';
import axios from 'axios';
import ClientOAuth2 from 'client-oauth2';
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: '',
// user admins are a very special case as the permissions on the resource can only
// be seen by requesting users and check whether you see their membership
isUserAdmin: false
userID: null,
schema: null,
rights: {
users: [],
joboffers: [],
studydocuments: [],
},
};
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' },
validateStatus: () => true,
});
// OAuth Handler
const oauth = new ClientOAuth2({
clientId: oAuthID,
......@@ -21,22 +36,17 @@ const oauth = new ClientOAuth2({
redirectUri: `${ownUrl}/oauthcallback`,
});
export function resetSession() {
function resetSession() {
APISession.authenticated = false;
APISession.token = '';
localStorage.remove('token');
window.location.replace(oauth.token.getUri());
}
const amivapi = axios.create({
baseURL: apiUrl,
headers: { 'Content-Type': 'application/json' },
});
function checkToken(token) {
// check if a token is still valid
return new Promise((resolve, reject) => {
amivapi.get('users', {
amivapi.get(`sessions/${token}`, {
headers: { 'Content-Type': 'application/json', Authorization: token },
}).then((response) => {
if (response.status === 200) resolve(response.data);
......@@ -53,19 +63,22 @@ 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((users) => {
checkToken(token).then((session) => {
APISession.token = token;
APISession.authenticated = true;
// if we see the membership of more than 1 person in the response, we
// have admin rights on users
if (users._items[0].membership && users._items[1].membership) {
APISession.isUserAdmin = true;
}
console.log(APISession);
resolve();
APISession.userID = session.user;
amivapi.get('/', {
headers: { 'Content-Type': 'application/json', Authorization: token },
}).then((response) => {
const rights = {};
response.data._links.child.forEach(({ href, methods }) => {
rights[href] = methods;
});
APISession.rights = rights;
resolve();
});
}).catch(resetSession);
} else resetSession();
}
......@@ -82,12 +95,58 @@ export function getSession() {
'Content-Type': 'application/json',
Authorization: APISession.token,
},
validateStatus: () => true,
});
resolve(authenticatedSession);
}).catch(resetSession);
});
}
export function deleteSession() {
return new Promise((resolve, reject) => {
getSession().then((api) => {
api.get(`sessions/${APISession.token}`).then((response) => {
if (response.status === 200) {
api.delete(
`sessions/${response.data._id}`,
{ headers: { 'If-Match': response.data._etag } },
).then((deleteResponse) => {
if (deleteResponse.status === 204) {
resetSession();
resolve(deleteResponse.data);
} else reject();
}).catch(reject);
} else reject();
}).catch(reject);
});
});
}
export function getCurrentUser() {
return APISession.userID;
}
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',
sessions: 'Session',
};
export class ResourceHandler {
/* Handler to get and manipulate resource items
*
......@@ -96,18 +155,32 @@ export class ResourceHandler {
* when search is set, any of these keys may match the search pattern
* E.g. for an event, this may be ['title_de', 'title_en', 'location']
*/
constructor(resource, searchKeys = false) {
constructor(resource, searchKeys = []) {
// backwards compatibiliity, should be removed in future
// eslint-disable-next-line no-param-reassign
if (!searchKeys) searchKeys = [];
this.resource = resource;
// special case for users
this.rights = [];
this.schema = JSON.parse(JSON.stringify(getSchema().components.schemas[
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.length === 0 ? possibleSearchKeys : searchKeys;
checkAuthenticated().then(() => {
// again special case for users
if (resource === 'users' && APISession.isUserAdmin) {
this.searchKeys = searchKeys || config[resource].searchKeys;
this.searchKeys = searchKeys.length === 0 ? possibleSearchKeys : searchKeys;
}
});
}
......@@ -125,20 +198,38 @@ export class ResourceHandler {
const fullQuery = {};
if ('search' in query && query.search && query.search.length > 0) {
if ('search' in query && query.search && query.search.length > 0
&& this.searchKeys.length !== 0) {
// translate search into where, we just look if any field contains search
// The search-string may match any of the keys in the object specified in the
// constructor
const searchQuery = {
$or: this.searchKeys.map((key) => {
const fieldQuery = {};
fieldQuery[key] = {
$regex: `${query.search}`,
$options: 'i',
};
return fieldQuery;
}),
};
let searchQuery;
if (query.search.split(' ').length > 1) {
searchQuery = {
$and: query.search.split(' ').map(searchPhrase => ({
$or: this.searchKeys.map((key) => {
const fieldQuery = {};
fieldQuery[key] = {
$regex: `${searchPhrase}`,
$options: 'i',
};
return fieldQuery;
}),
})),
};
} else {
searchQuery = {
$or: this.searchKeys.map((key) => {
const fieldQuery = {};
fieldQuery[key] = {
$regex: `${query.search}`,
$options: 'i',
};
return fieldQuery;
}),
};
}
// if there exists already a where-filter, AND them together
if ('where' in query) {
......@@ -155,7 +246,25 @@ export class ResourceHandler {
.forEach((key) => { fullQuery[key] = JSON.stringify(query[key]); });
// now we can acutally build the query string
return `?${m.buildQueryString(fullQuery)}`;
const maxResults = 50; // max_results for setting the page size of the return.
return `?${m.buildQueryString(fullQuery)}&max_results=${maxResults}`;
}
networkError(e) {
// eslint-disable-next-line no-console
console.log(e);
Snackbar.show({ title: 'Network error, try again.', style: { color: 'red' } });
}
// in future, we may communicate based on the data available
// therefore, require data already here
// eslint-disable-next-line no-unused-vars
error422(data) {
Snackbar.show({ title: 'Errors in object, please fix.' });
}
successful(title) {
Snackbar.show({ title, style: { color: 'green' } });
}
get(query) {
......@@ -165,13 +274,15 @@ export class ResourceHandler {
if (Object.keys(query).length > 0) url += this.buildQuerystring(query);
api.get(url).then((response) => {
if (response.status >= 400) {
resetSession();
Snackbar.show({ title: response.data, style: { color: 'red' } });
if (response.status === 401) resetSession();
reject();
} else {
this.rights = response.data._links.self.methods;
resolve(response.data);
}
}).catch((e) => {
console.log(e);
this.networkError(e);
reject(e);
});
});
......@@ -189,14 +300,17 @@ export class ResourceHandler {
})}`;
}
api.get(url).then((response) => {
if (response.status >= 400) {
resetSession();
if (response.status === 404) {
m.route.set('/404');
} else if (response.status >= 400) {
Snackbar.show({ title: response.data, style: { color: 'red' } });
if (response.status === 401) resetSession();
reject();
} else {
resolve(response.data);
}
}).catch((e) => {
console.log(e);
this.networkError(e);
reject(e);
});
});
......@@ -208,54 +322,58 @@ export class ResourceHandler {
getSession().then((api) => {
api.post(this.resource, item).then((response) => {
if (response.code === 201) {
this.successful('Creation successful.');
resolve({});
} else if (response.status === 422) {
this.error422(response.data);
reject(response.data);
} else if (response.status >= 400) {
resetSession();
Snackbar.show({ title: response.data, style: { color: 'red' } });
if (response.status === 401) resetSession();
reject();
} else {
resolve(response.data);
}
}).catch((e) => {
console.log(e);
this.networkError(e);
reject(e);
});
});
});
}
patch(item, formData = false) {
patch(item) {
const isFormData = item instanceof FormData;
let patchInfo = {};
if (isFormData) patchInfo = { id: item.get('_id'), etag: item.get('_etag') };
else patchInfo = { id: item._id, etag: item._etag };
return new Promise((resolve, reject) => {
getSession().then((api) => {
let submitData = item;
if (!isFormData) submitData = Object.assign({}, item);
// not all fields in the item can be patched. We filter out the fields
// we are allowed to send
let submitData;
if (formData) {
submitData = new FormData();
Object.keys(item).forEach((key) => {
if (!this.noPatchKeys.includes(key)) {
submitData.append(key, item[key]);
}
});
} else {
submitData = Object.assign({}, item);
this.noPatchKeys.forEach((key) => { delete submitData[key]; });
}
api.patch(`${this.resource}/${item._id}`, submitData, {
headers: { 'If-Match': item._etag },
this.noPatchKeys.forEach((key) => {
if (isFormData) submitData.delete(key);
else delete submitData[key];
});
api.patch(`${this.resource}/${patchInfo.id}`, submitData, {
headers: { 'If-Match': patchInfo.etag },
}).then((response) => {
if (response.status === 422) {
this.error422(response.data);
reject(response.data);
} else if (response.status >= 400) {
resetSession();
Snackbar.show({ title: response.data, style: { color: 'red' } });
if (response.status === 401) resetSession();
reject();
} else {
this.successful('Change successful.');
resolve(response.data);
}
}).catch((e) => {
console.log(e);
this.networkError(e);
reject(e);
});
});
......@@ -269,13 +387,15 @@ export class ResourceHandler {
headers: { 'If-Match': item._etag },
}).then((response) => {
if (response.status >= 400) {
resetSession();
Snackbar.show({ title: response.data, style: { color: 'red' } });
if (response.status === 401) resetSession();
reject();
} else {
this.successful('Delete successful.');
resolve();
}
}).catch((e) => {
console.log(e);
this.networkError(e);
reject(e);
});
});
......@@ -285,11 +405,13 @@ export class ResourceHandler {
export class OauthRedirect {
view() {
oauth.token.getToken(m.route.get()).then((response) => {
APISession.authenticated = true;
APISession.token = response.accessToken;
localStorage.set('token', response.accessToken);
m.route.set('/users');
oauth.token.getToken(m.route.get()).then((auth) => {
localStorage.set('token', auth.accessToken);
checkAuthenticated().then(() => {
// checkAuthenticated will check whetehr the token is valid
// and store all relevant session info for easy access
m.route.set('/');
}).catch(resetSession);
});
return 'redirecting...';
}
......
/* eslint-disable operator-assignment */
/* eslint-disable no-restricted-globals */
/* eslint-disable no-bitwise */
/* eslint-disable no-plusplus */
/* eslint-disable no-param-reassign */
/**
*
* Base64 encode / decode
* http://www.webtoolkit.info
*
* */
const Base64 = {
// private property
_keyStr: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=',
// public method for encoding
encode(input) {
let output = '';
let chr1; let chr2; let chr3; let enc1; let enc2; let enc3; let enc4;
let i = 0;
input = Base64._utf8_encode(input);
while (i < input.length) {
chr1 = input.charCodeAt(i++);
chr2 = input.charCodeAt(i++);
chr3 = input.charCodeAt(i++);
enc1 = chr1 >> 2;
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
enc4 = chr3 & 63;
if (isNaN(chr2)) {
enc3 = 64;
enc4 = 64;
} else if (isNaN(chr3)) {
enc4 = 64;
}
output = output
+ this._keyStr.charAt(enc1)
+ this._keyStr.charAt(enc2)
+ this._keyStr.charAt(enc3)
+ this._keyStr.charAt(enc4);
} // Whend
return output;
}, // End Function encode
// public method for decoding
decode(input) {
let output = '';
let chr1; let chr2; let chr3;
let enc1; let enc2; let enc3; let enc4;
let i = 0;
// eslint-disable-next-line no-useless-escape
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, '');
while (i < input.length) {
enc1 = this._keyStr.indexOf(input.charAt(i++));
enc2 = this._keyStr.indexOf(input.charAt(i++));
enc3 = this._keyStr.indexOf(input.charAt(i++));
enc4 = this._keyStr.indexOf(input.charAt(i++));
chr1 = (enc1 << 2) | (enc2 >> 4);
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
chr3 = ((enc3 & 3) << 6) | enc4;
output = output + String.fromCharCode(chr1);
// eslint-disable-next-line eqeqeq
if (enc3 != 64) {
output = output + String.fromCharCode(chr2);
}
// eslint-disable-next-line eqeqeq
if (enc4 != 64) {
output = output + String.fromCharCode(chr3);
}
} // Whend
output = Base64._utf8_decode(output);
return output;
}, // End Function decode
// private method for UTF-8 encoding
_utf8_encode(string) {
let utftext = '';
string = string.replace(/\r\n/g, '\n');
for (let n = 0; n < string.length; n++) {
const c = string.charCodeAt(n);
if (c < 128) {
utftext += String.fromCharCode(c);
} else if (c > 127 && c < 2048) {
utftext += String.fromCharCode((c >> 6) | 192);
utftext += String.fromCharCode((c & 63) | 128);
} else {
utftext += String.fromCharCode((c >> 12) | 224);
utftext += String.fromCharCode(((c >> 6) & 63) | 128);
utftext += String.fromCharCode((c & 63) | 128);
}
} // Next n
return utftext;
}, // End Function _utf8_encode
// private method for UTF-8 decoding
_utf8_decode(utftext) {
let string = '';
let i = 0;
let c; let c2; let c3;
c = 0;
c2 = 0;
while (i < utftext.length) {
c = utftext.charCodeAt(i);
if (c < 128) {
string += String.fromCharCode(c);
i++;
} else if (c > 191 && c < 224) {
c2 = utftext.charCodeAt(i + 1);
string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
i += 2;
} else {
c2 = utftext.charCodeAt(i + 1);
c3 = utftext.charCodeAt(i + 2);
string += String.fromCharCode(
((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63),
);
i += 3;
}
} // Whend
return string;
}, // End Function _utf8_decode
};
export default Base64;
import m from 'mithril';
import { TextField } from 'polythene-mithril';
import { ListSelect, DatalistController } from 'amiv-web-ui-components';
import { ResourceHandler } from '../auth';
import EditView from '../views/editView';
class NanoController {
constructor(resource) {
this.resource = resource;
this.handler = new ResourceHandler(resource);
this.data = {};
}
post(data) {
return new Promise((resolve, reject) => {
this.handler.post(data).then(() => {
this.cancel();
}).catch(reject);
});
}
cancel() {
m.route.set(`/${this.resource}`);
}
}
/**
* Table of all possible permissions to edit
*
* @class PermissionEditor (name)
*/
export default class NewBlacklist extends EditView {
constructor({ attrs }) {
super({ attrs: { controller: new NanoController('blacklist'), ...attrs } });
this.userHandler = new ResourceHandler('users', ['firstname', 'lastname', 'email', 'nethz']);
this.userController = new DatalistController((query, search) => this.userHandler.get(
{ search, ...query },
));
}
beforeSubmit() {
const { data } = this.form;
// exchange user object with string of id
this.submit({ ...data, user: data.user ? data.user._id : undefined }).then(() => {
this.controller.changeModus('view');
});
}
view() {
return this.layout([
m('div', { style: { display: 'flex' } }, [
m(TextField, { label: 'User: ', disabled: true, style: { width: '160px' } }),
m('div', { style: { 'flex-grow': 1 } }, m(ListSelect, {
controller: this.userController,
selection: this.form.data.user,
listTileAttrs: user => Object.assign({}, { title: `${user.firstname} ${user.lastname}` }),
selectedText: user => `${user.firstname} ${user.lastname}`,
onSelect: (data) => { this.form.data.user = data; },
})),
]),
...this.form.renderSchema(['reason', 'start_time', 'price']),
]);
}
}
import m from 'mithril';
import { Button } from 'polythene-mithril';
import TableView from '../views/tableView';
import { ResourceHandler } from '../auth';
import RelationlistController from '../relationlistcontroller';
import { dateFormatter } from '../utils';
export default class BlacklistTable {
constructor() {
this.handler = new ResourceHandler('blacklist');
this.ctrl = new RelationlistController({
primary: 'blacklist',
secondary: 'users',
query: { sort: [['start_time', -1]] },
});
}
getItemData(data) {
return [
m(
'div', { style: { width: '18em' } },
m(
'div', { style: { 'font-weight': 'bold' } },
`${data.user.firstname} ${data.user.lastname}`,
),
m('div', data.user.email),
),
m(
'div', { style: { width: 'calc(100%-18em)' } },
m('div', `From ${dateFormatter(data.start_time, false)}
${data.end_time ? ` to ${dateFormatter(data.end_time, false)}` : ''}`),
m('div', `Reason: ${data.reason}`),
data.price && m('div', `price: ${data.price}`),
),
m('div', { style: { 'flex-grow': '100' } }),
m('div', (!data.end_time && this.ctrl.handler.rights.includes('POST')) && m(Button, {
// Button to mark this entry as resolved
className: 'blue-row-button',
borders: false,
label: 'redeem',
events: {
onclick: () => {
const date = new Date(Date.now());
const patchdata = Object.assign({}, data);
delete patchdata.user;
patchdata.end_time = `${date.toISOString().slice(0, -5)}Z`;
this.ctrl.handler.patch(patchdata).then(() => {
this.ctrl.refresh();
m.redraw();
});
},
},
})),
];
}
view() {
return m(TableView, {
clickOnRows: false,
controller: this.ctrl,
keys: [null, null],
tileContent: data => this.getItemData(data),
titles: [
{ text: 'User', width: '18em' },
{ text: 'Detail', width: '9em' },
],
onAdd: (this.ctrl.handler.rights.includes('POST')) ? () => {
m.route.set('/newblacklistentry');
} : false,
});
}
}
import ItemView from './views/itemView';
import EditView from './views/editView';
import { inputGroup, selectGroup, submitButton } from './views/elements';
import TableView from './views/tableView';
import { events as config } from './config.json';
const m = require('mithril');
export class EventView extends ItemView {
constructor() {
super('events');
this.memberships = [];
}
view() {
// do not render anything if there is no data yet
if (!this.data) return m.trust('');
let comissionBadge = m('span.label.label-important', 'Who is resp of this event?');
if (this.data.membership === 'kultur') {
comissionBadge = m('span.label.label-success', 'Kulturi event');
} else if (this.data.membership === 'eestec') {
comissionBadge = m('span.label.label-important', 'EESTEC event');
} else if (this.data.membership === 'limes') {
comissionBadge = m('span.label.label-warning', 'LIMES event');
}
// TODO Question Lio171201:are we missing a "responsible" key?
const detailKeys = [
'title_de',
'rfid',
'location', 'time_start', 'time_end',
'show_website', 'catchphrase',
'time_register_start', 'price', 'allow_email_signup'];
return m('div', [
m('h1', `${this.data.title_de}`),
comissionBadge,
m('table', detailKeys.map(key => m('tr', [
m('td.detail-descriptor', config.keyDescriptors[key]),
m('td', this.data[key] ? this.data[key] : ''),
]))),
m('h2', 'Location'), m('br'),
m(TableView, {
resource: 'events',
keys: ['event.location'],
query: {
where: { user: this.id },
embedded: { group: 1 },
},
}),
m('h2', 'Signups'), m('br'),
m(TableView, {
resource: 'events',
keys: ['event.title_de'],
query: {
where: { user: this.id },
embedded: { event: 1 },
},
}),
]);
}
}
class EventEdit extends EditView {
constructor(vnode) {
super(vnode, 'events');
}
getForm() {
return m('form', [
m('div.row', [
m(inputGroup, this.bind({ title: 'Deutscher Titel', name: 'title_de' })),
m(inputGroup, this.bind({ title: 'English Title', name: 'title_en' })),
m(inputGroup, this.bind({ title: 'Location', name: 'location' })),
// m(inputGroup, this.bind({ title: 'Date-start', name: 'datetimepicker1' })),
// $('#datetimepicker1').datetimepicker();
m(selectGroup, this.bind({
classes: 'col-xs-6',
title: 'May non-AMIV members register?',
name: 'allow_email_signup',
options: [true, false],
})),
m(selectGroup, this.bind({
classes: 'col-xs-6',
title: 'Show on the website?',
name: 'show_website',
options: [true, false],
})),
m(selectGroup, this.bind({
classes: 'col-xs-6',
title: 'Piority from 1 to 10?',
name: 'priority',
// could be done with array.apply:
options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
})),
]),
m('span', JSON.stringify(this.data)),
m('span', JSON.stringify(this.errors)),
]);
}
view() {
// do not render anything if there is no data yet
if (!this.data) return m.trust('');
return m('form', [
this.getForm(),
m(submitButton, {
active: this.valid,
args: {
onclick: this.submit('PATCH', config.patchableKeys),
class: 'btn-warning',
},
text: 'Update',
}),
]);
}
}
export class NewEvent extends EventEdit {
constructor(vnode) {
super(vnode);
this.data = {
title_de: 'Unvollstaendiges Event',
priority: 7,
show_website: false,
};
this.valid = false;
// if the creation is finished, UI should switch to new Event
this.callback = (response) => { m.route.set(`/events/${response.data._id}`); };
}
view() {
return m('form', [
this.getForm(),
m(submitButton, {
active: this.valid,
args: {
onclick: this.submit('POST', config.patchableKeys),
class: 'btn-warning',
},
text: 'Create',
}),
]);
}
}
export class EventModal {
constructor() {
this.edit = false;
}
view() {
if (this.edit) {
return m(EventEdit, { onfinish: () => { this.edit = false; m.redraw(); } });
}
// else
return m('div', [
m('div.btn.btn-default', { onclick: () => { this.edit = true; } }, 'Edit'),
m('br'),
m(EventView),
]);
}
}
import m from 'mithril';
import { RaisedButton, RadioGroup, Slider, Switch } from 'polythene-mithril';
import { styler } from 'polythene-core-css';
import {
RadioGroup, Switch, Dialog, Button, Tabs, Icon, TextField,
} from 'polythene-mithril';
import { FileInput, ListSelect, DatalistController } from 'amiv-web-ui-components';
import { TabsCSS, ButtonCSS } from 'polythene-css';
// eslint-disable-next-line import/extensions
import { apiUrl } from 'networkConfig';
import { apiUrl, ownUrl } from 'networkConfig';
import { ResourceHandler } from '../auth';
import { colors } from '../style';
import { icons } from '../views/elements';
import EditView from '../views/editView';
import { fileInput } from '../views/elements';
import Base64 from '../base64';
const style = [
ButtonCSS.addStyle('.nav-button', {
color_light_border: 'rgba(0, 0, 0, 0.09)',
color_light_disabled_background: 'rgba(0, 0, 0, 0.09)',
color_light_disabled_border: 'transparent',
});
TabsCSS.addStyle('.edit-tabs', {
color_light: '#555555',
// no hover effect
color_light_hover: '#555555',
color_light_selected: colors.amiv_blue,
color_light_tab_indicator: colors.amiv_blue,
});
const styles = [
{
'.mywrapper': {
padding: '10px',
'.imgPlaceholder': {
background: '#999',
position: 'relative',
},
'.imgPlaceholder > div': {
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0,
'font-size': '16px',
display: 'flex',
'justify-content': 'center',
'align-items': 'center',
},
'.imgBackground': {
'background-size': 'contain',
'background-position': 'center',
'background-repeat': 'no-repeat',
},
},
];
styler.add('event-add', style);
styler.add('eventEdit', styles);
export default class newEvent extends EditView {
constructor(vnode) {
super(vnode);
this.currentpage = 1;
if (!this.data.priority) this.data.priority = 1;
// Create a usercontroller to handle the moderator field
this.userHandler = new ResourceHandler('users', ['firstname', 'lastname', 'email', 'nethz']);
this.userController = new DatalistController((query, search) => this.userHandler.get(
{ search, ...query },
));
// check whether the user has the right to create events or can only propose
this.rightSubmit = !m.route.get().startsWith('/proposeevent');
// proposition URL-link decoder
if (this.rightSubmit && m.route.param('proposition')) {
const data = JSON.parse(Base64.decode(m.route.param('proposition')));
this.form.data = data;
}
if (this.form.data.priority === 10) this.form.data.high_priority = true;
// read additional_fields to make it editable
if (this.data.additional_fields) {
const copy = JSON.parse(this.data.additional_fields);
this.data.add_fields_sbb = 'SBB_Abo' in copy.properties;
this.data.add_fields_food = 'Food' in copy.properties;
this.data.additional_fields = {};
if (this.form.data.additional_fields) {
const copy = JSON.parse(this.form.data.additional_fields);
this.form.data.add_fields_sbb = 'sbb_abo' in copy.properties;
this.form.data.add_fields_food = 'food' in copy.properties;
this.form.data.additional_fields = null;
let i = 0;
while (`text${i}` in copy.properties) {
this.form.data[`add_fields_text${i}`] = copy.properties[`text${i}`].title;
i += 1;
}
// TODO: find a better solution to keep track of the additional textfields
this.add_fields_text_index = i;
} else {
this.add_fields_text_index = 0;
}
// price can either not be set or set to null
// if it is 0 however, that would mean that there actually is a price that
// you can edit
this.hasprice = 'price' in this.data && this.data.price !== null;
this.hasregistration = 'time_advertising_start' in this.data;
this.hasprice = 'price' in this.form.data && this.form.data.price !== null;
this.hasregistration = 'spots' in this.form.data || 'time_registration_start' in this.form.data;
}
beforeSubmit() {
// Collect images seperate from everything else
// Here comes all the processing from the state of our input form into data send to the api.
// In particular, we have to:
// - remove images from the patch that should not get changed, add images in right format that
// should be changed
// - transfer states like add_fields_sbb etc. into actual additional_fields
// - dependent on user rights, either submit to API or create an event proposal link
const { data } = JSON.parse(JSON.stringify(this.form));
// Images that should be changed have new_{key} set, this needs to get uploaded to the API
// All the other images should be removed from the upload to not overwrite them.
const images = {};
['thumbnail', 'banner', 'infoscreen', 'poster'].forEach((key) => {
if (this.data[`new_${key}`]) {
images[`img_${key}`] = this.data[`new_${key}`];
delete this.data[`new_${key}`];
['thumbnail', 'infoscreen', 'poster'].forEach((key) => {
if (this.form.data[`new_${key}`]) {
images[`img_${key}`] = this.form.data[`new_${key}`];
delete data[`new_${key}`];
}
if (this.data[`img_${key}`]) {
delete this.data[`img_${key}`];
if (data[`img_${key}`] !== undefined && data[`img_${key}`] !== null) {
delete data[`img_${key}`];
}
});
// Merge Options for additional fields
// This is the sceleton schema:
const additionalFields = {
$schema: 'http://json-schema.org/draft-04/schema#',
additionalProperties: false,
......@@ -58,79 +131,186 @@ export default class newEvent extends EditView {
properties: {},
required: [],
};
if (this.data.add_fields_sbb) {
additionalFields.properties.SBB_Abo = {
if (data.add_fields_sbb) {
additionalFields.properties.sbb_abo = {
type: 'string',
enum: ['None', 'GA', 'Halbtax', 'Gleis 7'],
title: 'SBB Abonnement',
enum: ['None', 'GA', 'Halbtax', 'Gleis 7', 'HT + Gleis 7'],
};
additionalFields.required.push('SBB_Abo');
additionalFields.required.push('sbb_abo');
}
if (this.data.add_fields_food) {
additionalFields.properties.Food = {
if (data.add_fields_food) {
additionalFields.properties.food = {
type: 'string',
title: 'Food',
enum: ['Omnivor', 'Vegi', 'Vegan', 'Other'],
};
additionalFields.properties.specialFood = {
'Special Food Requirements': {
type: 'string',
},
additionalFields.properties.food_special = {
type: 'string',
title: 'Special Food Requirements',
};
additionalFields.required.push('food');
}
// There can be an arbitrary number of text fields added.
let i = 0;
while (`add_fields_text${i}` in data) {
const fieldName = `text${i}`;
additionalFields.properties[fieldName] = {
type: 'string',
minLength: 1,
title: data[`add_fields_text${i}`],
};
additionalFields.required.push('Food');
additionalFields.required.push(fieldName);
delete data[`add_fields_text${i}`];
i += 1;
}
if ('add_fields_sbb' in this.data) delete this.data.add_fields_sbb;
if ('add_fields_food' in this.data) delete this.data.add_fields_food;
// Remove our intermediate form states from the the data that is uploaded
if ('add_fields_sbb' in data) delete data.add_fields_sbb;
if ('add_fields_food' in data) delete data.add_fields_food;
// if the properties are empty, we null the whole field, otherwise we send a json string
// of the additional fields object
// If there are no additional_fields, the properties are empty, and we null the whole field,
// otherwise we send a json string of the additional fields object
if (Object.keys(additionalFields.properties).length > 0) {
this.data.additional_fields = JSON.stringify(additionalFields);
data.additional_fields = JSON.stringify(additionalFields);
} else {
this.data.additional_fields = null;
data.additional_fields = null;
}
// Translate state high_priority into a priority for the event
if (data.high_priority === true) data.priority = 10;
else data.priority = 1;
delete data.high_priority;
// if spots is not set, also remove 'allow_email_signup'
if (!('spots' in this.data) && 'allow_email_signup' in this.data
&& !this.data.allow_email_signup) {
delete this.data.allow_email_signup;
if (!('spots' in data) && 'allow_email_signup' in data
&& !data.allow_email_signup) {
delete data.allow_email_signup;
}
console.log(this.data);
if (Object.keys(images).length > 0) {
images._id = this.data._id;
images._etag = this.data._etag;
// first upload the images as formData, then the rest as JSON
this.controller.handler.patch(images, true).then(({ _etag }) => {
this.data._etag = _etag;
this.submit();
// Propose Event <=> Submit Changes dependent on the user rights
if (this.rightSubmit) {
// Submition tool
// Change moderator from user object to user id
if (data.moderator) data.moderator = data.moderator._id;
// first upload the data as JSON, then the images as form data
this.submit(data).then(({ _id, _etag }) => {
if (Object.keys(images).length > 0) {
// there are changed images to upload, they are added as an additional PATCH on this event
const imageForm = new FormData();
Object.keys(images).forEach(key => imageForm.append(key, images[key]));
imageForm.append('_id', _id);
imageForm.append('_etag', _etag);
this.controller.patch(imageForm).then(() => {
m.route.set(`/events/${_id}`);
})
.catch(() => {
this.controller.id = _id;
this.controller.handler.getItem(_id).then((item) => {
this.controller.data = item;
this.form.data = item;
this.controller.changeModus('edit');
window.history.replaceState({}, '', `/events/${_id}`);
});
});
} else {
m.route.set(`/events/${_id}`);
}
});
} else {
this.submit();
// Propose tool
Dialog.show({
title: 'Congratulations!',
body: [
m(
'div',
'You sucessfuly setup an event.',
'Please send this link to the respectiv board member for validation.',
),
m('input', {
type: 'text',
style: { width: '335px' },
value: `${ownUrl}/newevent?${m.buildQueryString({
proposition: Base64.encode(JSON.stringify(this.form.data)),
})}`,
id: 'textId',
}),
],
backdrop: true,
footerButtons: [
m(Button, {
label: 'Copy',
events: {
onclick: () => {
const copyText = document.getElementById('textId');
copyText.select();
document.execCommand('copy');
},
},
}),
],
});
}
}
view() {
const buttonRight = m(RaisedButton, {
label: 'next',
disabled: this.currentpage === 5,
events: {
onclick: () => {
this.currentpage = Math.min(this.currentpage + 1, 5);
},
},
// load image urls from the API data
['thumbnail', 'poster', 'infoscreen'].forEach((key) => {
const img = this.form.data[`img_${key}`];
if (typeof (img) === 'object' && img !== null && 'file' in img) {
// the data from the API has a weird format, we only need the url to display the image
this.form.data[`img_${key}`] = { url: `${apiUrl}${img.file}` };
}
});
const buttonLeft = m(RaisedButton, {
label: 'previous',
// Define the number of Tabs and their titles
const titles = ['Event Description', 'When and Where?', 'Signups', 'Internal Info'];
if (this.rightSubmit) titles.push('Images');
// Data fields of the event in the different tabs. Ordered the same way as the titles
const keysPages = [[
'title_en',
'catchphrase_en',
'description_en',
'title_de',
'catchphrase_de',
'description_de',
],
['time_start', 'time_end', 'location'],
['price', 'spots', 'time_register_start', 'time_register_end', 'time_deregister_end'],
['moderator', 'time_advertising_start', 'time_advertising_end'],
[],
];
// Look which Tabs have errors
const errorPages = keysPages.map(keysOfOnePage => keysOfOnePage.map((key) => {
if (this.form.errors && key in this.form.errors) return this.form.errors[key].length > 0;
return false;
}).includes(true));
// Navigation Buttons to go to next/previous page
const buttonRight = m(Button, {
label: m('div.pe-button__label', m(Icon, {
svg: { content: m.trust(icons.ArrowRight) },
style: { top: '-5px', float: 'right' },
}), 'next'),
disabled: this.currentpage === titles.length,
ink: false,
border: true,
className: 'nav-button',
events: { onclick: () => { this.currentpage = Math.min(this.currentpage + 1, 5); } },
});
const buttonLeft = m(Button, {
label: m('div.pe-button__label', m(Icon, {
svg: { content: m.trust(icons.ArrowLeft) },
style: { top: '-5px', float: 'left' },
}), 'previous'),
disabled: this.currentpage === 1,
events: {
onclick: () => {
this.currentpage = Math.max(1, this.currentpage - 1);
},
},
ink: false,
border: true,
className: 'nav-button',
events: { onclick: () => { this.currentpage = Math.max(1, this.currentpage - 1); } },
});
const radioButtonSelectionMode = m(RadioGroup, {
name: 'Selection Mode',
buttons: [
......@@ -145,152 +325,304 @@ export default class newEvent extends EditView {
],
onChange: (state) => {
this.selection_strategy = state.value;
this.data.selection_strategy = state.value;
console.log(this.data); // Temp proof of concept.
this.form.data.selection_strategy = state.value;
},
value: this.selection_strategy,
});
const title = [
'Event Description', 'When and Where?', 'Signups', 'Advertisement', 'Images',
][this.currentpage - 1];
// Processing for additional text fields users have to fill in for the signup.
const addFieldsText = [];
let i = 0;
while (`add_fields_text${i}` in this.form.data) {
addFieldsText.push(this.form._renderField(`add_fields_text${i}`, {
type: 'string',
label: `Label for Textfield ${i}`,
}));
const fieldIndex = i;
addFieldsText.push(m(Button, {
label: `Remove Textfield ${i}`,
className: 'red-row-button',
events: {
onclick: () => {
let index = fieldIndex;
while (`add_fields_text${index + 1}` in this.form.data) {
this.form.data[`add_fields_text${index}`] = this.form.data[
`add_fields_text${index + 1}`];
index += 1;
}
delete this.form.data[`add_fields_text${index}`];
this.add_fields_text_index = index;
},
},
}));
i += 1;
}
// checks currentPage and selects the fitting page
return this.layout([
m('h3', title),
buttonLeft,
m.trust('&nbsp;'),
buttonRight,
m('br'),
m('div', {
style: { display: (this.currentpage === 1) ? 'block' : 'none' },
}, this.renderPage({
title_en: { type: 'text', label: 'English Event Title' },
catchphrase_en: { type: 'text', label: 'English Catchphrase' },
description_en: {
type: 'text',
label: 'English Description',
multiLine: true,
rows: 5,
},
title_de: { type: 'text', label: 'German Event Title' },
catchphrase_de: { type: 'text', label: 'German Catchphrase' },
description_de: {
type: 'text',
label: 'German Description',
multiLine: true,
rows: 5,
},
// navigation bar
// all pages are displayed, current is highlighted,
// validation errors are shown per page by red icon-background
m(Tabs, {
className: 'edit-tabs',
// it would be too easy if we could set the background color in the theme class
style: { backgroundColor: colors.orange },
onChange: ({ index }) => { this.currentpage = index + 1; },
centered: true,
selectedTabIndex: this.currentpage - 1,
}, [...titles.entries()].map((numAndTitle) => {
const buttonAttrs = { label: numAndTitle[1] };
if (errorPages[numAndTitle[0]]) {
// in case of an error, put an error icon before the tab label
buttonAttrs.label = m('div', m(Icon, {
svg: { content: m.trust(icons.error) },
style: { top: '-2px', 'margin-right': '4px' },
}), numAndTitle[1]);
}
return buttonAttrs;
})),
m('div', {
style: { display: (this.currentpage === 2) ? 'block' : 'none' },
}, this.renderPage({
time_start: { type: 'datetime', label: 'Event Start Time' },
time_end: { type: 'datetime', label: 'Event End Time' },
location: { type: 'text', label: 'Location' },
})),
m('div', {
style: { display: (this.currentpage === 3) ? 'block' : 'none' },
}, [
m(Switch, {
label: 'people have to pay something to attend this event',
style: { 'margin-bottom': '5px' },
checked: this.hasprice,
onChange: ({ checked }) => {
this.hasprice = checked;
if (!checked) {
// if it originally had a price, set to null, otherwise delete
if (this.controller.data.price) this.data.price = null;
else delete this.data.price;
}
},
}),
...this.hasprice && this.renderPage({
price: { type: 'number', label: 'Price', min: 0, step: 0.01 },
}),
m('br'),
m(Switch, {
label: 'people have to register to attend this event',
checked: this.hasregistration,
onChange: ({ checked }) => {
this.hasregistration = checked;
if (!checked) {
delete this.data.spots;
delete this.data.time_register_start;
delete this.data.time_register_end;
delete this.data.add_fields_sbb;
delete this.data.add_fields_food;
delete this.data.allow_email_signup;
delete this.data.selection_strategy;
}
},
}),
...this.hasregistration && this.renderPage({
spots: {
type: 'number',
label: 'Number of Spots',
help: '0 for open event',
focusHelp: true,
min: 0,
},
time_register_start: { type: 'datetime', label: 'Start of Registration' },
time_register_end: { type: 'datetime', label: 'End of Registration' },
add_fields_food: { type: 'checkbox', label: 'Food limitations' },
add_fields_sbb: { type: 'checkbox', label: 'SBB Abbonement' },
}),
m('br'),
...this.hasregistration && this.renderPage({
allow_email_signup: { type: 'checkbox', label: 'Allow Email Signup' },
}),
this.hasregistration && radioButtonSelectionMode,
]),
m('div', {
style: { display: (this.currentpage === 4) ? 'block' : 'none' },
}, [
...this.renderPage({
time_advertising_start: {
type: 'datetime',
label: 'Start of Advertisement',
required: true,
},
time_advertising_end: {
type: 'datetime',
label: 'End of Advertisement',
required: true,
},
}),
m.trust('Priority<br>'),
m(Slider, {
min: 1,
max: 10,
stepSize: 1,
// value: this.data.priority || 1,
// onChange: ({ value }) => { this.data.priority = value; },
}),
...this.renderPage({
show_website: { type: 'checkbox', label: 'Advertise on Website' },
show_announce: { type: 'checkbox', label: 'Advertise in Announce' },
show_infoscreen: {
type: 'checkbox',
label: 'Advertise on Infoscreen',
},
}),
]),
m('div', {
style: { display: (this.currentpage === 5) ? 'block' : 'none' },
}, [
['thumbnail', 'banner', 'poster', 'infoscreen'].map(key => [
this.data[`img_${key}`] ? m('img', {
src: `${apiUrl}${this.data[`img_${key}`].file}`,
style: { 'max-height': '50px', 'max-width': '100px' },
}) : m('div', `currently no ${key} image set`),
m(fileInput, this.bind({
name: `new_${key}`,
label: `New ${key} Image`,
accept: 'image/png, image/jpeg',
})),
m('div.maincontainer', { style: { height: 'calc(100vh - 180px)', 'overflow-y': 'auto' } }, [
// page 1: title & description
m('div', {
style: { display: (this.currentpage === 1) ? 'block' : 'none' },
}, [
...this.form.renderSchema(['title_en', 'catchphrase_en']),
this.form._renderField('description_en', {
type: 'string',
label: 'English Description',
multiLine: true,
rows: 5,
}),
...this.form.renderSchema(['title_de', 'catchphrase_de']),
this.form._renderField('description_de', {
type: 'string',
label: 'German Description',
multiLine: true,
rows: 5,
}),
]),
// page 2: when & where
m('div', {
style: { display: (this.currentpage === 2) ? 'block' : 'none' },
}, this.form.renderSchema(['time_start', 'time_end', 'location'])),
// page 3: registration
m('div', {
style: { display: (this.currentpage === 3) ? 'block' : 'none' },
}, [
m(Switch, {
label: 'people have to pay something to attend this event',
style: { 'margin-bottom': '5px' },
checked: this.hasprice,
onChange: ({ checked }) => {
this.hasprice = checked;
if (!checked) {
// if it originally had a price, set to null, otherwise delete
if (this.controller.data.price) this.form.data.price = null;
else delete this.form.data.price;
}
},
}),
...(this.hasprice ? this.form.renderSchema(['price']) : []),
m('br'),
m(Switch, {
label: 'people have to register to attend this event',
style: { 'margin-bottom': '5px' },
checked: this.hasregistration,
onChange: ({ checked }) => {
this.hasregistration = checked;
if (!checked) {
// remove all the data connected to registration
delete this.form.data.spots;
delete this.form.data.time_register_start;
delete this.form.data.time_register_end;
delete this.form.data.time_deregister_end;
delete this.form.data.add_fields_sbb;
delete this.form.data.add_fields_food;
delete this.form.data.allow_email_signup;
delete this.form.data.selection_strategy;
}
},
}),
...(this.hasregistration ? this.form.renderSchema([
'spots', 'time_register_start', 'time_register_end', 'time_deregister_end']) : []),
this.hasregistration && this.form._renderField('add_fields_food', {
type: 'boolean',
label: 'Food Limitations',
}),
this.hasregistration && this.form._renderField('add_fields_sbb', {
type: 'boolean',
label: 'SBB Abonnement',
}),
this.hasregistration && m('br'),
...(this.hasregistration ? addFieldsText : []),
this.hasregistration && m('br'),
this.hasregistration && m(Button, {
label: 'Additional Textfield',
className: 'blue-button',
border: true,
events: {
onclick: () => {
this.form.data[`add_fields_text${this.add_fields_text_index}`] = '';
this.add_fields_text_index += 1;
},
},
}),
this.hasregistration && m('br'),
...(this.hasregistration ? this.form.renderSchema(['allow_email_signup']) : []),
m('br'),
...(this.hasregistration ? 'Only check the "Email Signup" box, '
+ 'if you expect non-ETH members to attend the event!' : []),
m('br'),
this.hasregistration && radioButtonSelectionMode,
m('br'),
m(Switch, {
label: 'people can register via an external link',
checked: this.hasexternalregistration,
onChange: ({ checked }) => {
this.hasexternalregistration = checked;
if (!checked) {
delete this.form.data.external_registration;
}
},
}),
this.hasexternalregistration && this.form._renderField('external_registration', {
type: 'string',
label: 'External Registration Link',
}),
]),
// PAGE 4: Internal Info
m('div', {
style: { display: (this.currentpage === 4) ? 'block' : 'none' },
}, [
m('div', { style: { display: 'flex', 'margin-top': '5px' } }, [
m(TextField, {
label: 'Moderator: ',
disabled: true,
style: { width: '200px' },
help: 'Can edit the event and see signups.',
}),
m('div', { style: { 'flex-grow': 1 } }, m(ListSelect, {
controller: this.userController,
selection: this.form.data.moderator,
listTileAttrs: user => Object.assign(
{},
{ title: `${user.firstname} ${user.lastname}` },
),
selectedText: user => `${user.firstname} ${user.lastname}`,
onSelect: (data) => { this.form.data.moderator = data; },
})),
]),
...this.form.renderSchema(['time_advertising_start', 'time_advertising_end']),
...this.form.renderSchema(['type']),
...this.form.renderSchema(['show_website', 'show_announce', 'show_infoscreen']),
// pritority update
this.form._renderField('high_priority', {
type: 'boolean',
label: 'Set high Priority',
}),
m('div', 'Please send an email to info@amiv.ch in order to show your event on'
+ 'the infoscreen until the new infoscreen tool is ready.'),
]),
// page 5: images
m('div', {
style: { display: (this.currentpage === 5) ? 'block' : 'none' },
}, [
m('div', 'Formats for the files: Thumbnail: 1:1, Poster: Any DIN-A, Infoscreen: 16:9'),
// All images and placeholders are placed next to each other in the following div:
m('div', { style: { width: '90%', display: 'flex' } }, [
// POSTER
m('div', { style: { width: '30%' } }, [
m('div', 'Poster'),
// imgPlaceholder has exactly a 1:1 aspect ratio
m('div.imgPlaceholder', { style: { width: '100%', 'padding-bottom': '141%' } }, [
// inside, we display the image. if it has a wrong aspect ratio, grey areas
// from the imgPlaceholder will be visible behind the image
this.form.data.img_poster ? m('div.imgBackground', {
style: { 'background-image': `url(${this.form.data.img_poster.url})` },
// Placeholder in case that there is no image
}) : m('div', 'No Poster'),
]),
m(Button, {
className: 'red-row-button',
borders: false,
label: 'remove',
events: { onclick: () => { this.form.data.img_poster = null; } },
}),
]),
// INFOSCREEN
m('div', { style: { width: '50%', 'margin-left': '5%' } }, [
m('div', 'Infoscreen'),
// imgPlaceholder has exactly a 16:9 aspect ratio
m('div.imgPlaceholder', { style: { width: '100%', 'padding-bottom': '56.25%' } }, [
// inside, we display the image. if it has a wrong aspect ratio, grey areas
// from the imgPlaceholder will be visible behind the image
this.form.data.img_infoscreen ? m('div.imgBackground', {
style: { 'background-image': `url(${this.form.data.img_infoscreen.url})` },
// Placeholder in case that there is no image
}) : m('div', 'No Infoscreen Image'),
]),
m(Button, {
className: 'red-row-button',
borders: false,
label: 'remove',
events: { onclick: () => { this.form.data.img_infoscreen = null; } },
}),
]),
// THUMBNAIL
m('div', { style: { width: '10%', 'margin-left': '5%' } }, [
m('div', 'Thumbnail'),
// imgPlaceholder has exactly a 16:9 aspect ratio
m('div.imgPlaceholder', { style: { width: '100%', 'padding-bottom': '100%' } }, [
// inside, we display the image. if it has a wrong aspect ratio, grey areas
// from the imgPlaceholder will be visible behind the image
this.form.data.img_thumbnail ? m('div.imgBackground', {
style: { 'background-image': `url(${this.form.data.img_thumbnail.url})` },
// Placeholder in case that there is no image
}) : m('div', 'No Thumbnail'),
]),
m(Button, {
className: 'red-row-button',
borders: false,
label: 'remove',
events: { onclick: () => { this.form.data.img_thumbnail = null; } },
}),
]),
]),
// old stuff, goes through all images
['thumbnail', 'poster', 'infoscreen'].map(key => [
// input to upload a new image
m(FileInput, this.form.bind({
name: `new_${key}`,
label: `New ${key} Image`,
accept: 'image/png, image/jpeg',
onChange: ({ value }) => {
// if a new image file is selected, we display it using a data encoded url
const reader = new FileReader();
reader.onload = ({ target: { result } }) => {
this.form.data[`img_${key}`] = { url: result };
m.redraw();
};
reader.readAsDataURL(value);
this.form.data[`new_${key}`] = value;
},
})),
]),
]),
// bottom back & forth Buttons
m('div', {
style: {
display: 'flex',
'justify-content': 'space-between',
padding: '35px',
'padding-top': '20px',
},
}, [buttonLeft, buttonRight]),
]),
]);
], this.rightSubmit ? 'submit' : 'propose', false);
}
}