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

Commit da0b678e authored by Alexander Dietmüller's avatar Alexander Dietmüller
Browse files

Frontend: Api can now create/update/delete items. CourseList and Sidebar use this.

parent 69687bfa
const m = require('mithril');
const { Courses } = require('./api.js');
const { courses, userCourses } = require('./api.js');
function isSelected(course) {
return userCourses.selected.some(sel => sel === course._id);
}
function isBusy() { return userCourses.resources.selections.isBusy() };
module.exports = {
oninit() { Courses.load(); },
oninit() { courses.get(); },
view() {
return m('table', [
......@@ -15,16 +21,32 @@ module.exports = {
m('th', 'Ending time'),
]),
]),
m('tbody', Courses.list.map(course =>
m('tbody', courses.list.map(course =>
m('tr', [
m('td', course.lecture.title),
m('td', course.lecture.department),
m('td', course.assistant.name),
m('td', course.assistant),
course.datetimes.map(timeslot => [
m('td', timeslot.start),
m('td', timeslot.end),
]),
m('td', m('button', 'add course')),
m(
'td',
m(
'button',
{
onclick() { userCourses.selectCourse(course._id); },
disabled: isSelected(course) || isBusy(),
// ?
// true : false,
// return false;
// return !((UserCourses.selected.courses || []).some(sel =>
// sel === course._id));
// },
},
'add course',
),
),
]))),
]);
},
......
// User Sidebar
const m = require('mithril');
const { UserCourses } = require('./api.js');
const { userCourses } = require('./api.js');
function courseView(course) {
return m('li', `${course.lecture.title}, ${course.assistant}`);
return m('li', course);
}
function asList(courses) { return courses.map(courseView); }
module.exports = {
oninit() { UserCourses.load(); },
oninit() { userCourses.get(); },
view() {
return [
m('h1', 'Selected Courses'),
UserCourses.selected.length ? [
asList(UserCourses.selected),
m('button', { onclick() { UserCourses.reserve(); } }, 'reserve'),
userCourses.selected.length ? [
userCourses.selected.map(selId =>
m('li', [
m('span', selId),
m(
'button',
{
onclick() { userCourses.deselectCourse(selId); },
disabled: userCourses.resources.selections.isBusy(),
},
'X',
),
])),
m('button', { onclick() { userCourses.reserve(); } }, 'reserve'),
] : m('p', 'No courses selected.'),
m('h1', 'Reserved Courses'),
UserCourses.reserved.length ? [
asList(UserCourses.reserved),
m('button', { onclick() { UserCourses.pay(); } }, 'Pay'),
userCourses.reserved.length ? [
asList(userCourses.reserved),
m('button', { onclick() { userCourses.pay(); } }, 'Pay'),
] : m('p', 'No courses reserved.'),
m('h1', 'Accepted Courses'),
UserCourses.accepted.length ? [
asList(UserCourses.accepted),
userCourses.accepted.length ? [
asList(userCourses.accepted),
] : m('p', 'No courses accepted.'),
];
},
......
......@@ -137,9 +137,18 @@ const Session = {
};
// Helper to filter temp out of list
function withoutTemp(list, temp) { return list.filter(item => item !== temp); }
// Request to the PVK backend
function request({
resource, method = 'GET', id = '', data = {}, query = {},
resource,
method = 'GET',
id = '',
data = {},
query = {},
headers = {},
}) {
// Parse query such that the backend understands it
const parsedQuery = {};
......@@ -148,18 +157,24 @@ function request({
});
const queryString = m.buildQueryString(parsedQuery);
const allHeaders = Object.assign(
{},
{ Authorization: `Token ${Session.data.token}` },
headers,
);
// Send the request
return m.request({
method,
data,
url: `${pvkApiUrl}/${resource}/${id}?${queryString}`,
headers: { Authorization: `Token ${Session.data.token}` },
headers: allHeaders,
}).catch((err) => {
// If the error is 401, the token is invalid -> auto log out
if (err._error.code === 401) { Session.clear(); }
// Actual error information is in the '_error' field, pass this on
throw err._error;
// Pass on Error
throw err;
});
}
......@@ -168,22 +183,117 @@ function request({
class Resource {
constructor(name, query = {}) {
this.name = name;
this.list = [];
this.query = query;
// List for items confirmend by api
this._items = {};
// Temporary objects to react instantly to requests
this._items_new = [];
this._items_updated = {};
this._items_deleted = [];
}
load() {
return request({ resource: this.name, query: this.query }).then((data) => {
isBusy() {
return Object.keys(this._items_updated).length !== 0 ||
this._items_deleted.length !== 0;
}
get list() {
const currentItems = Object.keys(this._items)
.filter(key => this._items_deleted.indexOf(key) === -1)
.map(key => this._items_updated[key] || this._items[key]);
return [...currentItems, ...this._items_new];
}
get() {
return request({
resource: this.name,
query: this.query,
}).then((data) => {
// Fill own list
this.list = data._items;
this._items = {};
data._items.forEach((item) => { this._items[item._id] = item; });
// Pass on data
return data;
});
}
post(data) {
// Add data already to list of unprocessed items
this._items_new.push(data);
// Send request, remove temporary object as soon as response arrives
return request({
resource: this.name,
method: 'POST',
data,
}).then((confirmedData) => {
// Success!
this._items_new = withoutTemp(this._items_new, data);
this._items[confirmedData._id] = confirmedData;
return confirmedData;
}).catch((err) => {
this._items_new = withoutTemp(this._items_new, data);
throw err;
});
}
patchItem(id, changes) {
// Add changes to temporary storage
this._items_updated[id] = Object.assign(
{},
this._items[id],
changes,
);
// Send request, remove temporary object as soon as response arrives
return request({
resource: this.name,
id,
method: 'PATCH',
headers: { 'If-Match': this._items[id]._etag },
data: changes,
}).then((confirmedData) => {
// Remove item
this._items[id] = confirmedData;
delete this._items_updated[id];
}).catch((err) => {
// If 412: Data is outdated, reload!
if (err._error.code === 412) { this.get(); }
delete this._items_updated[id];
throw err;
});
}
deleteItem(id) {
// Set item id on deleted-list
this._items_deleted.push(id);
// Send request, remove temporary object as soon as response arrives
return request({
resource: this.name,
id,
method: 'DELETE',
headers: { 'If-Match': this._items[id]._etag },
}).then((result) => {
// Remove item
delete this._items[id];
this._items_deleted = withoutTemp(this._items_deleted, id);
return result;
}).catch((err) => {
// If 412: Data is outdated, reload!
if (err._error.code === 412) { this.get(); }
this._items_deleted = withoutTemp(this._items_deleted, id);
throw err;
});
}
}
const userCourses = {
const UserCourses = {
resources: {
selections: new Resource(
'selections',
......@@ -195,15 +305,16 @@ const UserCourses = {
),
},
load() {
this.resources.selections.load().then(() => {
// We are only interested in one the first (and single) element
this.resources.selections.list = this.resources.selections.list[0] || [];
});
this.resources.signups.load();
get() {
this.resources.selections.get();
this.resources.signups.get();
},
get selected() { return this.resources.selections.list; },
get selected() {
// We are only interested in the first (only) item
const sel = this.resources.selections.list[0];
return sel ? sel.courses : [];
},
get reserved() {
return this.resources.signups.list.filter(({ status }) =>
status === 'reserved');
......@@ -213,18 +324,58 @@ const UserCourses = {
status === 'accepted');
},
// TODO(Alex)
select() {},
selectCourse(courseId) {
// If user already has a selection and update it
const currentSelection = this.resources.selections.list[0] || [];
if (this.selected.length > 0) {
const selectionId = currentSelection._id;
const newSelection = [
...currentSelection.courses,
courseId,
];
return this.resources.selections.patchItem(
selectionId,
{ courses: newSelection },
);
// TODO: Provide error feedback to user in .catch
}
// Otherwise create a new selection
return this.resources.selections.post({
nethz: Session.user.nethz,
courses: [courseId],
});
},
deselectCourse(courseId) {
const selection = this.resources.selections.list[0] || [];
const newSelection = selection.courses.filter(item => item !== courseId);
const selectionId = selection._id;
if (newSelection.length === 0) {
this.resources.selections.deleteItem(selectionId)
.then((res) => { console.log(res); })
.catch((err) => { console.log(err); });
} else {
this.resources.selections.patchItem(
selectionId,
{ courses: newSelection },
);
}
// TODO: Provide error feedback to user in .catch
},
reserve() {},
pay() {},
};
const Courses = new Resource('courses', { embedded: { lecture: 1 } });
const courses = new Resource('courses', { embedded: { lecture: 1 } });
const lectures = new Resource('lectures');
module.exports = {
Session,
request,
UserCourses,
Courses,
userCourses,
courses,
lectures,
};
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment