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
Showing
with 1026 additions and 376 deletions
import m from 'mithril';
import { Converter } from 'showdown';
import { Card } from 'polythene-mithril';
import { Chip } from 'amiv-web-ui-components';
// eslint-disable-next-line import/extensions
import { apiUrl } from 'networkConfig';
import ItemView from '../views/itemView';
import { dateFormatter } from '../utils';
import { icons, Property } from '../views/elements';
export default class viewJob extends ItemView {
constructor(vnode) {
super(vnode);
this.markdown = new Converter();
}
view() {
const stdMargin = { margin: '5px' };
return this.layout([
m('div', { style: { height: '50px' } }, [
// company logo if existing
this.data.logo ? m('img', {
src: `${apiUrl}/${this.data.logo.file}`,
height: '50px',
style: { float: 'left' },
}) : '',
m('h3', {
style: { 'line-height': '50px', 'margin-top': '0px' },
}, this.data.company),
]),
m('div.maincontainer', [
m(Chip, { svg: this.data.show_website ? icons.checked : icons.clear }, 'website'),
]),
// below the title, most important details are listed
m('div', { style: { display: 'flex', margin: '5px 0px 0px 5px' } }, [
this.data.time_end ? m(Property, {
title: 'Offer Ends',
style: stdMargin,
}, `${dateFormatter(this.data.time_end)}`) : '',
m(Property, {
title: 'PDF',
style: stdMargin,
}, this.data.pdf
? m('a', { href: `${apiUrl}${this.data.pdf.file}`, target: '_blank' }, this.data.pdf.name)
: 'not available'),
]),
m('div.viewcontainer', [
m('div.viewcontainercolumn', m(Card, {
content: m('div.maincontainer', [
m('div.pe-card__title', this.data.title_de),
m('div', m.trust(this.markdown.makeHtml(this.data.description_de))),
]),
})),
m('div.viewcontainercolumn', m(Card, {
content: m('div.maincontainer', [
m('div.pe-card__title', this.data.title_en),
m('div', m.trust(this.markdown.makeHtml(this.data.description_en))),
]),
})),
]),
]);
}
}
import m from 'mithril';
import '@material/drawer';
import { List, ListTile, Icon, Toolbar, ToolbarTitle } from 'polythene-mithril';
import {
List,
ListTile,
Icon,
Toolbar,
Dialog,
SVG,
Button,
IconButton,
Snackbar,
} from 'polythene-mithril';
import { styler } from 'polythene-core-css';
import { icons } from './views/elements';
import { deleteSession, getUserRights, getSchema } from './auth';
import { colors } from './style';
const layoutStyle = [
{
body: {
padding: 0,
margin: 0,
},
body: { padding: 0, margin: 0, overflow: 'hidden' },
'.main-toolbar': {
backgroundColor: '#1f2d54',
color: '#fff',
height: '72px',
'grid-column': '1 / span 2',
'grid-row': 1,
},
......@@ -22,60 +27,145 @@ const layoutStyle = [
width: '100%',
display: 'grid',
'grid-template-columns': '200px auto',
'grid-template-rows': '72px auto',
'grid-template-rows': '64px auto',
},
'.wrapper-sidebar': {
'grid-column': 1,
'grid-row': 2,
'@media (min-width:1200px)': { 'grid-column': 1 },
'@media (max-width:1200px)': {
position: 'absolute',
top: '64px',
left: '-200px',
width: '200px',
'z-index': 100000,
},
height: '100%',
'grid-row': 2,
'overflow-y': 'auto',
background: '#cccccc',
color: 'white',
},
'.wrapper-content': {
height: 'calc(100vh - 72px)',
'grid-column': 2,
'@media (min-width:1200px)': { 'grid-column': 2 },
'@media (max-width:1200px)': { 'grid-column': '1 / span 2' },
height: 'calc(100vh - 64px)',
'grid-row': 2,
background: '#eee',
overflow: 'hidden',
},
'.menu-button': {
'@media (min-width:1200px)': { display: 'none' },
'@media (max-width:1200px)': { display: 'inline' },
},
'.content-hider': {
display: 'none',
position: 'absolute',
top: '64px',
left: '200px',
width: '100%',
height: '100%',
background: '#000000aa',
'z-index': 100000000,
},
},
];
styler.add('layout', layoutStyle);
function toggleDrawer() {
const drawer = document.querySelector('.wrapper-sidebar');
const shadow = document.querySelector('.content-hider');
if (drawer.style.left === '0px') {
drawer.style.left = '-200px';
shadow.style.display = 'none';
} else {
drawer.style.left = '0px';
shadow.style.display = 'block';
}
}
class Menupoint {
view({ attrs: { title, href, icon = null } }) {
return m(ListTile, {
url: {
href,
oncreate: m.route.link,
},
front: icon ? m(Icon, {
avatar: true,
svg: m.trust(icon),
}) : '',
url: { href, oncreate: m.route.link },
front: icon ? m(Icon, { svg: m.trust(icon) }) : '',
ink: true,
title,
});
}
}
export default 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 Layout {
view({ children }) {
if (!getSchema()) return m(loadingScreen);
const userRights = getUserRights();
return m('div', [
m('div.wrapper-main.smooth', [
m(Toolbar, { className: 'main-toolbar' }, [
m(ToolbarTitle, { text: 'AMIV Admintools' }),
m(Toolbar, {
className: 'main-toolbar',
tone: 'dark',
style: { backgroundColor: colors.amiv_blue, color: '#ffffff' },
}, [
m('div.menu-button', m(IconButton, {
className: 'menu-button',
icon: { svg: { content: m.trust(icons.menu) } },
events: { onclick: () => { toggleDrawer(); } },
style: { color: '#ffffff' },
})),
m('div', { style: { 'font-size': '18px', 'margin-left': '20px' } }, 'AMIV Admintools'),
m('a', {
href: 'https://gitlab.ethz.ch/amiv/amiv-admintool/issues/new?issuable_template=Bug',
target: '_blank',
style: {
color: '#888888',
'text-decoration': 'none',
'text-align': 'right',
'margin-right': '20px',
'margin-left': 'auto',
},
}, 'Is something not working? Report a bug!'),
m(Button, {
label: 'logout',
events: { onclick: deleteSession },
}),
]),
m(
'nav.mdc-drawer.mdc-drawer--permanent.mdc-typography.wrapper-sidebar',
'div.mdc-typography.wrapper-sidebar',
{ style: { width: '200px' } },
m(List, {
className: 'drawer-menu',
header: {
title: 'Menu',
},
header: { title: 'Menu' },
hoverable: true,
tiles: [
m(Menupoint, {
userRights.users.indexOf('POST') > -1 && m(Menupoint, {
href: '/users',
icon: icons.iconUsersSVG,
title: 'Users',
......@@ -87,25 +177,56 @@ export default class Layout {
}),
m(Menupoint, {
href: '/groups',
icon: icons.group,
title: 'Groups',
}),
userRights.joboffers.indexOf('POST') > -1 && m(Menupoint, {
href: '/joboffers',
icon: icons.iconJobsSVG,
title: 'Job offers',
}),
m(Menupoint, {
href: '/studydocuments',
icon: icons.studydoc,
title: 'Studydocs',
}),
m(Menupoint, {
href: '/blacklist',
icon: icons.blacklist,
title: 'Blacklist',
}),
m(Menupoint, {
href: '/announce',
title: 'Announce',
href: '/infoscreen',
icon: icons.iconEventSVG,
title: 'Infoscreen',
}),
],
}),
),
m('div.wrapper-content', children),
// shadow over content in case drawer is out
m('div.content-hider'),
]),
m(Snackbar),
// dialog element will show when Dialog.show() is called, this is only a placeholder
m(Dialog),
]);
}
}
/*return m('div.wrapper-sidebar.smooth', m('div.container-fluid', [
m('a[href=/]', { oncreate: m.route.link }, [
m('img.sidebar-logo[src="res/logo/main.svg"]'),
]),
m('ul.nav.nav-pills.nav-stacked.nav-sidebar', [
]),
]));*/
export class Error404 {
view() {
return m('div', {
style: {
height: '100%',
width: '100%',
display: 'flex',
'flex-direction': 'column',
'justify-content': 'center',
'align-items': 'center',
},
}, [
m('div', { style: { height: '5vh', 'font-size': '4em' } }, 'Error 404: Item Not Found!'),
]);
}
}
import m from 'mithril';
import Stream from 'mithril/stream';
import { ResourceHandler } from './auth';
import { debounce } from './utils';
// IS NOT IN USE
// IS NOT IN USE
// IS NOT IN USE
// IS NOT IN USE
// IS NOT IN USE
// IS NOT IN USE
// IS NOT IN USE
// IS NOT IN USE
// IS NOT IN USE
// IS NOT IN USE
export default class DatalistController {
constructor(resource, query = {}, searchKeys = false, onlineSearch = true) {
this.onlineSearch = onlineSearch;
if (onlineSearch) {
this.handler = new ResourceHandler(resource, searchKeys);
} else {
this.handler = new ResourceHandler(resource, false);
this.clientSearchKeys = searchKeys || [];
}
constructor(resource, query = {}, searchKeys = []) {
this.handler = new ResourceHandler(resource, searchKeys);
this.query = query || {};
this.search = null;
this.filter = null;
// state pointer that is counted up every time the table is refreshed so
// we can tell infinite scroll that the data-version has changed.
this.stateCounter = Stream(0);
this.refresh();
this.debouncedSearch = debounce((search) => {
this.setSearch(search);
this.refresh();
m.redraw();
}, 100);
// keep track of the total number of pages
this.totalPages = null;
}
refresh() {
......@@ -36,64 +50,66 @@ export default class DatalistController {
const query = Object.assign({}, this.query);
query.max_results = 10;
query.page = pageNum;
query.where = { ...this.filter, ...this.query.where };
// remove where again if it is empty
if (Object.keys(query.where).length === 0) delete query.where;
return new Promise((resolve, reject) => {
return new Promise((resolve) => {
this.handler.get(query).then((data) => {
// If onlineSearch is false, we filter the page-results at the client
// because the API would not understand the search pattern, e.g. for
// embedded keys like user.firstname
if (!this.onlineSearch && this.clientSearchKeys.length > 0 && this.search) {
const response = [];
// We go through all response items and will add them to response if
// they match the query.
data._items.forEach((item) => {
// Try every search Key seperately, such that any match with any
// key is sufficient
this.clientSearchKeys.some((key) => {
if (key.match(/.*\..*/)) {
// traverse the key, this is a key pointing to a sub-object
let intermediateObject = Object.assign({}, item);
key.split('.').forEach((subKey) => {
intermediateObject = intermediateObject[subKey];
});
if (intermediateObject.includes(this.search)) {
response.push(item);
// return true to end the search of this object, it is already
// matched
return true;
}
} else if (item[key] && item[key].includes(this.search)) {
response.push(item);
// return true to end the search of this object, it is already
// matched
return true;
}
return false;
});
});
resolve(response);
} else {
resolve(data._items);
// update total number of pages
this.totalPages = Math.ceil(data._meta.total / 50);
resolve(data._items);
});
});
}
/*
* Get all available pages
*/
getFullList() {
return new Promise((resolve) => {
// get first page to refresh total page count
this.getPageData(1).then((firstPage) => {
const pages = { 1: firstPage };
// save totalPages as a constant to avoid race condition with pages added during this
// process
const { totalPages } = this;
if (totalPages === 1) {
resolve(firstPage);
}
// now fetch all the missing pages
Array.from(new Array(totalPages - 1), (x, i) => i + 2).forEach((pageNum) => {
this.getPageData(pageNum).then((newPage) => {
pages[pageNum] = newPage;
// 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)
// and flatten them into 1 array
resolve([].concat(...Object.keys(pages).sort().map(key => pages[key])));
}
});
});
});
});
}
setSearch(search) {
if (this.onlineSearch) {
this.search = search;
this.query.search = search;
this.refresh();
} else if (this.clientSearchKeys.length > 0) {
this.search = search;
this.refresh();
}
this.query.search = search;
}
setFilter(filter) {
this.filter = filter;
this.refresh();
}
setQuery(query) {
this.query = query;
this.query.search = this.search;
this.query = Object.assign({}, query, { search: this.query.search });
this.refresh();
}
}
import { login } from './auth';
const m = require('mithril');
const FormState = {
username: '',
setUsername(v) { FormState.username = v; },
password: '',
setPassword(v) { FormState.password = v; },
};
export default class LoginScreen {
view() {
return m('div', { class: 'loginPanel smooth' }, [
m('div.col-sm-4'),
m('div.col-sm-4', [
m('img.login-logo', { src: 'res/logo/main.svg' }),
m('div.input-group', [
m('input.form-control', {
oninput: m.withAttr('value', FormState.setUsername),
placeholder: 'user',
}),
m('span.input-group-addon', '@student.ethz.ch'),
]),
m('br'),
m('input.form-control', {
oninput: m.withAttr('value', FormState.setPassword),
placeholder: 'password',
type: 'password',
}),
m('br'),
m('button.btn.btn-default', {
onclick() {
login(FormState.username, FormState.password).then(() => {
m.route.set('/users');
});
},
}, 'Submit'),
]),
m('div.col-sm-4'),
m('div', [
m('span', FormState.username),
m('span', FormState.password),
]),
]);
}
}
import m from 'mithril';
import EditView from './views/editView';
import SelectList from './views/selectList';
const m = require('mithril');
export class MembershipView extends EditView {
export default class MembershipView extends EditView {
constructor(vnode) {
super(vnode, 'groupmemberships', { user: 1, group: 1 });
}
......@@ -23,17 +21,3 @@ export class MembershipView extends EditView {
]);
}
}
export class NewMembership {
constructor() {
this.selectUser = new SelectList('users', ['firstname', 'lastname'], {
view(vnode) {
return m('span', `${vnode.attrs.firstname} ${vnode.attrs.lastname}`);
},
});
}
view() {
return m(this.selectUser);
}
}
{
"apiUrl": "https://api-dev.amiv.ethz.ch",
"ownUrl": "https://admin-dev.amiv.ethz.ch",
"hookUrl": "https://webhooks.amiv.ethz.ch/hook/websitepipeline",
"oAuthID": "AMIV Admintool Dev"
}
{
"apiUrl": "https://api-dev.amiv.ethz.ch",
"ownUrl": "http://localhost:9000",
"hookUrl": "http://0.0.0.0:5000/hook/websitepipeline",
"oAuthID": "Local Tool"
}
{
"apiUrl": "http://127.0.0.1:5000",
"ownUrl": "http://localhost:9000",
"hookUrl": "http://0.0.0.0:6000/hook/websitepipeline",
"oAuthID": "Local Tool"
}
{
"apiUrl": "https://api.amiv.ethz.ch",
"ownUrl": "https://admin.amiv.ethz.ch",
"hookUrl": "https://webhooks.amiv.ethz.ch/hook/websitepipeline",
"oAuthID": "AMIV Admintool"
}
{
"apiUrl": "https://api-staging.amiv.ethz.ch",
"ownUrl": "https://admin-staging.amiv.ethz.ch",
"hookUrl": "https://webhooks.amiv.ethz.ch/hook/websitepipeline",
"oAuthID": "AMIV Admintool Staging"
}
import m from 'mithril';
import Stream from 'mithril/stream';
import { ResourceHandler } from './auth';
import { debounce } from './utils';
// IS NOT IN USE
// IS NOT IN USE
// IS NOT IN USE
// IS NOT IN USE
// IS NOT IN USE
// IS NOT IN USE
// IS NOT IN USE
// IS NOT IN USE
// IS NOT IN USE
// IS NOT IN USE
export default class RelationlistController {
/*
* Controller for a list of data embedding a relationship.
* The secondary api endpoint is embedded into the list items of the primary endpoint results.
* Searches are applied to both resources, queries and filters need to be specified for each.
*
* @param {bool} includeWithoutRelation - Specifies what to do in case the relation is undefined.
* By default, such items are excluded, if true they will be included into the list.
*/
constructor({
primary,
secondary,
query = {},
searchKeys = [],
secondaryQuery = {},
secondarySearchKeys = [],
includeWithoutRelation = false,
}) {
this.handler = new ResourceHandler(primary, searchKeys);
this.handler2 = new ResourceHandler(secondary, secondarySearchKeys);
this.secondaryKey = secondary.slice(0, -1);
this.query = query || {};
this.query2 = secondaryQuery || {};
this.filter = null;
this.filter2 = null;
this.sort = null;
this.includeWithoutRelation = includeWithoutRelation;
// state pointer that is counted up every time the table is refreshed so
// we can tell infinite scroll that the data-version has changed.
this.stateCounter = Stream(0);
this.refresh();
this.debouncedSearch = debounce((search) => {
this.setSearch(search);
this.refresh();
m.redraw();
}, 100);
// keep track of the total number of pages
this.totalPages = null;
}
refresh() {
this.stateCounter(this.stateCounter() + 1);
}
infiniteScrollParams(item) {
return {
item,
pageData: pageNum => this.getPageData(pageNum),
pageKey: pageNum => `${pageNum}-${this.stateCounter()}`,
maxPages: this.totalPages ? this.totalPages : undefined,
};
}
getPageData(pageNum) {
// We first query the primary resource for a list of items and then query the secondary
// resource for the items specified by the relation in the primary resource
// We apply Queries for both resources seperately.
const query = Object.assign({}, this.query);
query.max_results = 50;
query.page = pageNum;
query.where = { ...this.filter, ...this.query.where };
query.sort = this.sort || query.sort;
return new Promise((resolve) => {
this.handler.get(query).then((data) => {
// update total number of pages
this.totalPages = Math.ceil(data._meta.total / 50);
const itemsWithoutRelation = data._items.filter(item => !(this.secondaryKey in item));
const itemsWithRelation = data._items.filter(item => (this.secondaryKey in item));
const query2 = Object.assign({}, this.query2);
query2.where = {
_id: { $in: itemsWithRelation.map(item => item[this.secondaryKey]) },
...this.filter2,
...this.query2.where,
};
this.handler2.get(query2).then((secondaryData) => {
// check which secondary items were filtered out
const secondaryIds = secondaryData._items.map(item => item._id);
// filter the primary list to only include those items that have a relation to
// the queried secondary IDs
const filteredPrimaries = itemsWithRelation.filter(
item => secondaryIds.includes(item[this.secondaryKey]),
);
// embed the secondary data
const embeddedList = filteredPrimaries.map((item) => {
const itemCopy = Object.assign({}, item);
itemCopy[this.secondaryKey] = secondaryData._items.find(
relItem => relItem._id === item[this.secondaryKey],
);
return itemCopy;
});
// now return the list of filteredPrimaries with the secondary data embedded
if (this.includeWithoutRelation) resolve([...embeddedList, ...itemsWithoutRelation]);
else resolve(embeddedList);
});
});
});
}
/*
* Get all available pages
*/
getFullList() {
return new Promise((resolve) => {
// get first page to refresh total page count
this.getPageData(1).then((firstPage) => {
const pages = { 1: firstPage };
// save totalPages as a constant to avoid race condition with pages added during this
// process
const { totalPages } = this;
if (totalPages === 1) {
resolve(firstPage);
}
// now fetch all the missing pages
Array.from(new Array(totalPages - 1), (x, i) => i + 2).forEach((pageNum) => {
this.getPageData(pageNum).then((newPage) => {
pages[pageNum] = newPage;
// 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)
// and flatten them into 1 array
resolve([].concat(...Object.keys(pages).sort().map(key => pages[key])));
}
});
});
});
});
}
setSearch(search) {
this.query.search = search;
this.query2.search = search;
}
setFilter(filter) {
this.filter = {};
this.filter2 = {};
// split up filters between resouce and relation
Object.keys(filter).forEach((key) => {
if (key.startsWith(`${this.secondaryKey}.`)) {
this.filter2[key.slice(this.secondaryKey.length + 1)] = filter[key];
} else {
this.filter[key] = filter[key];
}
});
this.refresh();
}
setQuery(query) {
this.query = Object.assign({}, query, { search: this.query.search });
this.refresh();
}
setSort(sort) {
this.sort = sort;
this.refresh();
}
}
import m from 'mithril';
import { FileInput } from 'amiv-web-ui-components';
import { Button, List, ListTile, Snackbar } from 'polythene-mithril';
import EditView from '../views/editView';
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' }];
}
}
beforeSubmit() {
// check if there are files uploaded
const files = [];
Object.keys(this.form.data).forEach((key) => {
if (key.startsWith('new_file_') && this.form.data[key]) {
files.push(this.form.data[key]);
delete this.form.data[key];
}
});
// in case that there are no files, eject an error
if (this.controller.modus === 'new' && files.length === 0) {
Snackbar.show({ title: 'You need to upload at least one file.' });
this.form.valid = false;
return;
}
// now post all together as FormData
const submitData = new FormData();
Object.keys(this.form.data).forEach((key) => {
if (key !== 'files') submitData.append(key, this.form.data[key]);
});
files.forEach((file) => { submitData.append('files', file); });
this.submit(submitData).then(() => this.controller.changeModus('view'));
}
view() {
return this.layout([
m('h3', 'Add a New Studydocument'),
this.form._renderField('semester', {
...this.form.schema.properties.semester,
style: { width: '100px' },
}),
...this.form.renderSchema(['type', 'lecture', 'title', 'course_year', 'professor', 'author']),
// file upload: work in progress, so far all files get deleted with a patch
m('div', [
'WARNING: Files added here will remove all files currently uploaded. If you want to add',
'/edit a file in this studydoc, reupload all other files as well.',
m(List, {
tiles: [...this.form.data.files.entries()].map(numAndFile => m(ListTile, {
content: [
m(FileInput, this.form.bind({
name: `new_file_${numAndFile[0]}`,
label: numAndFile[1].name,
})),
],
})),
}),
// additional file
m(Button, {
label: 'Additional File',
className: 'blue-button',
border: true,
events: { onclick: () => { this.form.data.files.push({ name: 'add file' }); } },
}),
]),
]);
}
}
import m from 'mithril';
import viewDoc from './viewDoc';
import editDoc from './editDoc';
import ItemController from '../itemcontroller';
import { loadingScreen } from '../layout';
export default class studydocItem {
constructor() {
this.controller = new ItemController('studydocuments');
}
view() {
if (!this.controller || (!this.controller.data && this.controller.modus !== 'new')) {
return m(loadingScreen);
}
if (this.controller.modus !== 'view') return m(editDoc, { controller: this.controller });
return m(viewDoc, { controller: this.controller });
}
}
import m from 'mithril';
import { DatalistController } from 'amiv-web-ui-components';
import TableView from '../views/tableView';
import { ResourceHandler } from '../auth';
/* Table of all studydocuments */
export default class StudydocTable {
constructor() {
this.handler = new ResourceHandler('studydocuments');
this.ctrl = new DatalistController((query, search) => this.handler.get({ search, ...query }));
}
getItemData(data) {
return [
m('div', { style: { width: 'calc(100% - 36em)' } }, data.title),
m('div', { style: { width: '8em' } }, data.author),
m('div', { style: { width: '4em' } }, data.course_year),
m('div', { style: { width: '4em' } }, data.semester),
m('div', { style: { width: '10em' } }, data.lecture),
m('div', { style: { width: '10em' } }, data.files.map((file) => {
const splittedFilenames = file.name.split('.');
return `.${splittedFilenames[splittedFilenames.length - 1]} `;
})),
];
}
view() {
return m(TableView, {
controller: this.ctrl,
keys: ['title', 'author', 'course_year', 'semester', 'lecture'],
tileContent: this.getItemData,
titles: [
{ text: 'Title', width: 'calc(100% - 36em)' },
{ text: 'Author', width: '8em' },
{ text: 'Year', width: '4em' },
{ text: 'Sem.', width: '4em' },
{ text: 'Lecture', width: '10em' },
{ text: 'Files', width: '10em' },
],
onAdd: () => { m.route.set('/newstudydocument'); },
});
}
}
import m from 'mithril';
import ItemView from '../views/itemView';
import { Property } from '../views/elements';
export default class viewDoc extends ItemView {
view() {
const stdMarg = { margin: '5px' };
return this.layout(m('div.maincontainer', [
m('h3', {
style: { 'margin-top': '0px', 'margin-bottom': '0px' },
}, this.data.title),
// below the title, most important details are listed
m('div', { style: { display: 'flex' } }, [
this.data.lecture && m(Property, {
title: 'Lecture',
style: stdMarg,
}, `${this.data.lecture} ${this.data.department.toUpperCase()}`),
this.data.semster && m(Property, {
title: 'Semester',
style: stdMarg,
}, this.data.semester),
this.data.department && !this.data.lecture && m(Property, {
title: 'Department',
style: stdMarg,
}, this.data.department.toUpperCase()),
this.data.professor && m(Property, {
title: 'Professor',
style: stdMarg,
}, this.data.professor),
this.data.author && m(Property, {
title: 'Author',
style: stdMarg,
}, this.data.author),
this.data.uploader && m(Property, {
title: 'Uploader',
style: stdMarg,
}, this.data.uploader),
]),
]));
}
}
import { ButtonCSS, addTypography } from 'polythene-css';
import { ButtonCSS, addTypography, CardCSS, ShadowCSS } from 'polythene-css';
import { styler } from 'polythene-core-css';
addTypography();
// https://material.io/tools/color/#!/?view.left=0&view.right=1
// &secondary.color=e8462b&primary.color=274284
// eslint-disable-next-line import/prefer-default-export
export const colors = {
amiv_blue: '#1F2D54',
amiv_red: '#e8462b',
green: '#4ef599',
blue: '#274284',
// light_blue: '#5378E1',
light_blue: '#5a6db4',
orange: 'orange',
};
ButtonCSS.addStyle('.blue-button', {
color_light_text: 'blue',
color_light_text: colors.blue,
});
ButtonCSS.addStyle('.blue-button-filled', {
color_light_background: colors.blue,
color_light_text: 'white',
});
ButtonCSS.addStyle('.red-row-button', {
color_light_text: 'white',
color_light_background: colors.amiv_red,
padding_h: 0,
font_size: 12,
margin_h: 0,
});
ButtonCSS.addStyle('.blue-row-button', {
color_light_text: 'white',
color_light_background: colors.light_blue,
padding_h: 0,
font_size: 12,
margin_h: 0,
});
CardCSS.addStyle('.pe-card', {
border_radius: '4',
});
ShadowCSS.addStyle('.pe-shadow', {
'shadow-top-z-1': 'initial',
'shadow-bottom-z-1': '0 0 1px 0 rgba(0, 0, 0, 0.37)',
});
// style for general containers
const style = [
{
'.maincontainer': {
padding: '0 5px',
},
'.viewcontainer': {
display: 'flex',
'flex-wrap': 'wrap',
padding: '0 5px',
},
'.viewcontainercolumn': {
width: '500px',
'flex-grow': 1,
margin: '5px',
},
h1: {
margin: '0 5px',
},
p: {
margin: '0',
},
a: {
color: 'rgba(0, 0, 0, 0.87)',
},
},
];
styler.add('containers', style);
import ItemView from './views/itemView';
import EditView from './views/editView';
import TableView from './views/tableView';
import { inputGroup, selectGroup, submitButton } from './views/elements';
import SelectList from './views/selectList';
import { users as config } from './config.json';
import DatalistController from './listcontroller';
const m = require('mithril');
class UserView extends ItemView {
constructor() {
super('users');
// a controller to handle the groupmemberships of this user
this.groupmemberships = new DatalistController('groupmemberships', {
where: { user: this.id },
embedded: { group: 1 },
});
// a controller to handle the eventsignups of this user
this.eventsignups = new DatalistController('eventsignups', {
where: { user: this.id },
embedded: { event: 1 },
});
// initially, don't display the choice field for a new group
// (this will be displayed once the user clicks on 'new')
this.groupchoice = false;
// a controller to handle the list of possible groups to join
this.groupcontroller = new DatalistController('groups', {}, ['name']);
// exclude the groups where the user is already a member
this.groupmemberships.handler.get({ where: { user: this.id } })
.then((data) => {
const groupIds = data._items.map(item => item.group);
this.groupcontroller.setQuery({
where: { _id: { $nin: groupIds } },
});
});
}
oninit() {
this.handler.getItem(this.id, this.embedded).then((item) => {
this.data = item;
m.redraw();
});
this.groupmemberships.refresh();
}
view() {
// do not render anything if there is no data yet
if (!this.data) return m.trust('');
let membershipBadge = m('span.label.label-important', 'No Member');
if (this.data.membership === 'regular') {
membershipBadge = m('span.label.label-success', 'Member');
} else if (this.data.membership === 'extraordinary') {
membershipBadge = m('span.label.label-success', 'Extraordinary Member');
} else if (this.data.membership === 'honorary') {
membershipBadge = m('span.label.label-warning', 'Honorary Member');
}
const detailKeys = [
'email', 'phone', 'nethz', 'legi', 'rfid', 'department', 'gender'];
// Selector that is only displayed if "new" is clicked in the
// groupmemberships. Selects a group to request membership for.
const groupSelect = m(SelectList, {
controller: this.groupcontroller,
listTileAttrs: data => Object.assign({}, { title: data.name }),
onSubmit: (group) => {
this.groupchoice = false;
this.groupmemberships.handler.post({
user: this.data._id,
group: group._id,
}).then(() => {
this.groupmemberships.refresh();
});
},
});
return m('div', [
m('h1', `${this.data.firstname} ${this.data.lastname}`),
membershipBadge,
m('table', detailKeys.map(key => m('tr', [
m('td.detail-descriptor', config.keyDescriptors[key]),
m('td', this.data[key] ? this.data[key] : ''),
]))),
m('h2', 'Memberships'), m('br'),
this.groupchoice ? groupSelect : '',
m(TableView, {
controller: this.groupmemberships,
keys: ['group.name', 'expiry'],
titles: ['groupname', 'expiry'],
onAdd: () => { this.groupchoice = true; },
}),
m('h2', 'Signups'), m('br'),
m(TableView, {
controller: this.eventsignups,
keys: ['event.title_de'],
titles: ['event'],
}),
]);
}
}
class UserEdit extends EditView {
constructor(vnode) {
super(vnode, 'users');
}
getForm() {
return m('form', [
m('div.row', [
m(inputGroup, this.bind({
classes: 'col-xs-6', title: 'Last Name', name: 'lastname',
})),
m(inputGroup, this.bind({
classes: 'col-xs-6', title: 'First Name', name: 'firstname',
})),
m(inputGroup, this.bind({ title: 'Email', name: 'email' })),
m(selectGroup, this.bind({
classes: 'col-xs-6',
title: 'Membership Status',
name: 'membership',
options: ['regular', 'extraordinary', 'honory'],
})),
m(selectGroup, this.bind({
classes: 'col-xs-6',
title: 'Gender',
name: 'gender',
options: ['male', 'female'],
})),
]),
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 NewUser extends UserEdit {
constructor(vnode) {
super(vnode);
this.data = {
gender: 'male',
membership: 'regular',
};
this.valid = false;
// if the creation is finished, UI should switch to new User
this.callback = (response) => { m.route.set(`/users/${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 UserModal {
constructor() {
this.edit = false;
}
view() {
if (this.edit) {
return m(UserEdit, { onfinish: () => { this.edit = false; m.redraw(); } });
}
// else
return m('div', [
m('div.btn.btn-default', { onclick: () => { this.edit = true; } }, 'Edit'),
m('br'),
m(UserView),
]);
}
}
export class UserTable {
constructor() {
this.ctrl = new DatalistController('users', {}, config.tableKeys);
}
view() {
return m(TableView, {
controller: this.ctrl,
keys: config.tableKeys,
titles: config.tableKeys.map(key => config.keyDescriptors[key] || key),
onAdd: () => { m.route.set('/newuser'); },
});
}
}
import m from 'mithril';
import { TextInput } from 'amiv-web-ui-components';
import EditView from '../views/editView';
export default class UserEdit extends EditView {
beforeSubmit() {
if ('rfid' in this.form.data && !this.form.data.rfid) delete this.form.data.rfid;
this.submit(this.form.data).then(() => this.controller.changeModus('view'));
}
view() {
return this.layout([
...this.form.renderSchema(['lastname', 'firstname', 'email', 'phone', 'nethz', 'legi']),
m(TextInput, this.form.bind({
type: 'password',
name: 'password',
label: 'New password',
floatingLabel: true,
})),
...this.form.renderSchema(['rfid', 'send_newsletter', 'membership', 'department']),
]);
}
}
import m from 'mithril';
import { DatalistController } from 'amiv-web-ui-components';
import EditUser from './editUser';
import ViewUser from './viewUser';
import TableView from '../views/tableView';
import ItemController from '../itemcontroller';
import { loadingScreen } from '../layout';
import { ResourceHandler } from '../auth';
export class UserItem {
constructor() {
this.controller = new ItemController('users');
}
view() {
if (!this.controller || (!this.controller.data && this.controller.modus !== 'new')) {
return m(loadingScreen);
}
if (this.controller.modus !== 'view') return m(EditUser, { controller: this.controller });
return m(ViewUser, { controller: this.controller });
}
}
export class UserTable {
constructor() {
this.handler = new ResourceHandler('users');
this.ctrl = new DatalistController(
(query, search) => this.handler.get({ search, ...query }),
{ sort: [['lastname', 1]] },
);
}
view() {
const tableKeys = ['firstname', 'lastname', 'nethz', 'legi', 'membership'];
return m(TableView, {
controller: this.ctrl,
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' } },
{ name: 'extraordinary members', query: { membership: 'extraordinary' } },
{ name: 'honorary member', query: { membership: 'honorary' } },
], [
{ name: 'ITET', query: { department: 'itet' } },
{ name: 'MAVT', query: { department: 'mavt' } },
]],
onAdd: () => { m.route.set('/newuser'); },
});
}
}
import m from 'mithril';
import { Card, Toolbar, ToolbarTitle, Button, Snackbar } from 'polythene-mithril';
import { ListSelect, DatalistController, Chip } from 'amiv-web-ui-components';
import ItemView from '../views/itemView';
import TableView from '../views/tableView';
import RelationlistController from '../relationlistcontroller';
import { ResourceHandler } from '../auth';
import { icons, Property } from '../views/elements';
import { colors } from '../style';
export default class UserView extends ItemView {
constructor(vnode) {
super(vnode);
// a controller to handle the groupmemberships of this user
this.groupmemberships = new RelationlistController({
primary: 'groupmemberships', secondary: 'groups', query: { where: { user: this.data._id } },
});
// a controller to handle the eventsignups of this user
this.eventsignups = new RelationlistController({
primary: 'eventsignups', secondary: 'events', query: { where: { user: this.data._id } },
});
// initially, don't display the choice field for a new group
// (this will be displayed once the user clicks on 'new')
this.groupchoice = false;
// a controller to handle the list of possible groups to join
this.groupHandler = new ResourceHandler('groups', ['name']);
this.groupController = new DatalistController(
(query, search) => this.groupHandler.get({ search, ...query }),
);
// exclude the groups where the user is already a member
this.groupmemberships.handler.get({ where: { user: this.data._id } })
.then((data) => {
const groupIds = data._items.map(item => item.group);
this.groupController.setQuery({
where: { _id: { $nin: groupIds } },
});
});
this.sessionsHandler = new ResourceHandler('sessions');
}
oninit() {
this.groupmemberships.refresh();
}
view() {
const stdMargin = { margin: '5px' };
let membership = m(Chip, {
svg: icons.clear,
svgBackground: colors.amiv_red,
style: stdMargin,
}, 'No Member');
if (this.data.membership === 'regular') {
membership = m(Chip, {
svg: icons.checked,
svgBackground: colors.green,
style: stdMargin,
}, 'Regular Member');
} else if (this.data.membership === 'extraordinary') {
membership = m(Chip, {
svg: icons.checked,
svgBackground: colors.green,
style: stdMargin,
}, 'Extraordinary Member');
} else if (this.data.membership === 'honorary') {
membership = m(Chip, {
svg: icons.star,
svgBackground: colors.orange,
style: stdMargin,
}, 'Honorary Member');
}
// Selector that is only displayed if "new" is clicked in the
// groupmemberships. Selects a group to request membership for.
const groupSelect = m(ListSelect, {
controller: this.groupController,
listTileAttrs: group => Object.assign({}, { title: group.name }),
selectedText: group => group.name,
onSubmit: (group) => {
this.groupchoice = false;
this.groupmemberships.handler.post({
user: this.data._id,
group: group._id,
}).then(() => {
this.groupmemberships.refresh();
m.redraw();
});
},
onCancel: () => { this.groupchoice = false; m.redraw(); },
});
const now = new Date();
return this.layout([
m('div.maincontainer', [
m('h1', `${this.data.firstname} ${this.data.lastname}`),
membership,
this.data.department && m(
Chip,
{ svg: icons.department, style: stdMargin },
this.data.department,
),
m(Chip, {
svg: this.data.send_newsletter ? icons.checked : icons.clear,
style: stdMargin,
}, 'newsletter'),
m('div', { style: { display: 'flex' } }, [
this.data.nethz && m(Property, { title: 'NETHZ', style: stdMargin }, this.data.nethz),
this.data.email && m(Property, { title: 'Email', style: stdMargin }, this.data.email),
m(Property, { title: 'Legi', style: stdMargin }, this.data.legi ? this.data.legi : '-'),
m(Property, { title: 'RFID', style: stdMargin }, this.data.rfid ? this.data.rfid : '-'),
this.data.phone && m(Property, { title: 'Phone', style: stdMargin }, this.data.phone),
]),
]),
m('div.viewcontainer', [
m('div.viewcontainercolumn', m(Card, {
style: { height: '350px' },
content: m('div', [
m(Toolbar, { compact: true }, [
m(ToolbarTitle, { text: 'Event Signups' }),
]),
m(TableView, {
tableHeight: '175px',
controller: this.eventsignups,
tileContent: item => m('div', item.event.title_en || item.event.title_de),
titles: ['Event'],
clickOnRows: (data) => { m.route.set(`/events/${data.event._id}`); },
filters: [[{
name: 'upcoming',
query: { 'event.time_start': { $gte: `${now.toISOString().slice(0, -5)}Z` } },
}, {
name: 'past',
query: { 'event.time_start': { $lt: `${now.toISOString().slice(0, -5)}Z` } },
}]],
// per default, enable the 'upcoming' filter
initFilterIdxs: [[0, 0]],
}),
]),
})),
m('div.viewcontainercolumn', m(Card, {
style: { height: '350px' },
content: m('div', [
this.groupchoice && groupSelect,
m(Toolbar, { compact: true }, [
m(ToolbarTitle, { text: 'Group Memberships' }),
m(Button, {
className: 'blue-button',
label: 'add',
events: { onclick: () => { this.groupchoice = true; } },
}),
]),
m(TableView, {
tableHeight: '225px',
controller: this.groupmemberships,
keys: ['group.name', 'expiry'],
titles: ['Group Name', 'Expires'],
clickOnRows: (data) => { m.route.set(`/groups/${data.group._id}`); },
}),
]),
})),
]),
], [
m(Button, {
label: 'log out all Sessions',
className: 'itemView-delete-button',
border: true,
events: {
onclick: () => {
this.sessionsHandler.get({
where: { user: this.data._id },
}).then((response) => {
if (response._items.length === 0) {
Snackbar.show({ title: 'No active sessions for this user.' });
} else {
response._items.forEach((session) => {
this.sessionsHandler.delete(session);
});
}
});
},
},
}),
]);
}
}