Commit da0b678e authored by adietmue's avatar adietmue
Browse files

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

parent 69687bfa
const m = require('mithril'); 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 = { module.exports = {
oninit() { Courses.load(); }, oninit() { courses.get(); },
view() { view() {
return m('table', [ return m('table', [
...@@ -15,16 +21,32 @@ module.exports = { ...@@ -15,16 +21,32 @@ module.exports = {
m('th', 'Ending time'), m('th', 'Ending time'),
]), ]),
]), ]),
m('tbody', Courses.list.map(course => m('tbody', courses.list.map(course =>
m('tr', [ m('tr', [
m('td', course.lecture.title), m('td', course.lecture.title),
m('td', course.lecture.department), m('td', course.lecture.department),
m('td', course.assistant.name), m('td', course.assistant),
course.datetimes.map(timeslot => [ course.datetimes.map(timeslot => [
m('td', timeslot.start), m('td', timeslot.start),
m('td', timeslot.end), 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 // User Sidebar
const m = require('mithril'); const m = require('mithril');
const { UserCourses } = require('./api.js'); const { userCourses } = require('./api.js');
function courseView(course) { function courseView(course) {
return m('li', `${course.lecture.title}, ${course.assistant}`); return m('li', course);
} }
function asList(courses) { return courses.map(courseView); } function asList(courses) { return courses.map(courseView); }
module.exports = { module.exports = {
oninit() { UserCourses.load(); }, oninit() { userCourses.get(); },
view() { view() {
return [ return [
m('h1', 'Selected Courses'), m('h1', 'Selected Courses'),
UserCourses.selected.length ? [ userCourses.selected.length ? [
asList(UserCourses.selected), userCourses.selected.map(selId =>
m('button', { onclick() { UserCourses.reserve(); } }, 'reserve'), 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('p', 'No courses selected.'),
m('h1', 'Reserved Courses'), m('h1', 'Reserved Courses'),
UserCourses.reserved.length ? [ userCourses.reserved.length ? [
asList(UserCourses.reserved), asList(userCourses.reserved),
m('button', { onclick() { UserCourses.pay(); } }, 'Pay'), m('button', { onclick() { userCourses.pay(); } }, 'Pay'),
] : m('p', 'No courses reserved.'), ] : m('p', 'No courses reserved.'),
m('h1', 'Accepted Courses'), m('h1', 'Accepted Courses'),
UserCourses.accepted.length ? [ userCourses.accepted.length ? [
asList(UserCourses.accepted), asList(userCourses.accepted),
] : m('p', 'No courses accepted.'), ] : m('p', 'No courses accepted.'),
]; ];
}, },
......
...@@ -137,9 +137,18 @@ const Session = { ...@@ -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 // Request to the PVK backend
function request({ function request({
resource, method = 'GET', id = '', data = {}, query = {}, resource,
method = 'GET',
id = '',
data = {},
query = {},
headers = {},
}) { }) {
// Parse query such that the backend understands it // Parse query such that the backend understands it
const parsedQuery = {}; const parsedQuery = {};
...@@ -148,18 +157,24 @@ function request({ ...@@ -148,18 +157,24 @@ function request({
}); });
const queryString = m.buildQueryString(parsedQuery); const queryString = m.buildQueryString(parsedQuery);
const allHeaders = Object.assign(
{},
{ Authorization: `Token ${Session.data.token}` },
headers,
);
// Send the request // Send the request
return m.request({ return m.request({
method, method,
data, data,
url: `${pvkApiUrl}/${resource}/${id}?${queryString}`, url: `${pvkApiUrl}/${resource}/${id}?${queryString}`,
headers: { Authorization: `Token ${Session.data.token}` }, headers: allHeaders,
}).catch((err) => { }).catch((err) => {
// If the error is 401, the token is invalid -> auto log out // If the error is 401, the token is invalid -> auto log out
if (err._error.code === 401) { Session.clear(); } if (err._error.code === 401) { Session.clear(); }
// Actual error information is in the '_error' field, pass this on // Pass on Error
throw err._error; throw err;
}); });
} }
...@@ -168,22 +183,117 @@ function request({ ...@@ -168,22 +183,117 @@ function request({
class Resource { class Resource {
constructor(name, query = {}) { constructor(name, query = {}) {
this.name = name; this.name = name;
this.list = [];
this.query = query; 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() { isBusy() {
return request({ resource: this.name, query: this.query }).then((data) => { 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 // Fill own list
this.list = data._items; this._items = {};
data._items.forEach((item) => { this._items[item._id] = item; });
// Pass on data // Pass on data
return 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: { resources: {
selections: new Resource( selections: new Resource(
'selections', 'selections',
...@@ -195,15 +305,16 @@ const UserCourses = { ...@@ -195,15 +305,16 @@ const UserCourses = {
), ),
}, },
load() { get() {
this.resources.selections.load().then(() => { this.resources.selections.get();
// We are only interested in one the first (and single) element this.resources.signups.get();
this.resources.selections.list = this.resources.selections.list[0] || [];
});
this.resources.signups.load();
}, },
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() { get reserved() {
return this.resources.signups.list.filter(({ status }) => return this.resources.signups.list.filter(({ status }) =>
status === 'reserved'); status === 'reserved');
...@@ -213,18 +324,58 @@ const UserCourses = { ...@@ -213,18 +324,58 @@ const UserCourses = {
status === 'accepted'); status === 'accepted');
}, },
// TODO(Alex) selectCourse(courseId) {
select() {}, // 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() {}, reserve() {},
pay() {}, pay() {},
}; };
const Courses = new Resource('courses', { embedded: { lecture: 1 } }); const courses = new Resource('courses', { embedded: { lecture: 1 } });
const lectures = new Resource('lectures');
module.exports = { module.exports = {
Session, Session,
request, request,
UserCourses, userCourses,
Courses, courses,
lectures,
}; };
Supports Markdown
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