Commit 76ee384b authored by Sandro Lutz's avatar Sandro Lutz
Browse files

Add PaginationController

parent 0b2f184b
...@@ -61,14 +61,18 @@ import { Button, Checkbox, Dropdown, TextField } from '../components'; ...@@ -61,14 +61,18 @@ import { Button, Checkbox, Dropdown, TextField } from '../components';
* *
* Default behavior of buttons is to trigger `onchange`. * Default behavior of buttons is to trigger `onchange`.
*/ */
export default class FilterViewComponent {
constructor() {
this.values = {};
}
export default class FilterViewComponent {
oninit(vnode) { oninit(vnode) {
this.onchange = vnode.attrs.onchange; this.onchange = vnode.attrs.onchange;
this.notify(); if (vnode.attrs.values) {
this.values = vnode.attrs.values;
} else {
this.values = {};
vnode.attrs.fields.forEach(field => {
this.values[field.key] = field.default || '';
});
}
} }
notify() { notify() {
......
...@@ -95,10 +95,11 @@ ...@@ -95,10 +95,11 @@
"events.free": "Gratis", "events.free": "Gratis",
"events.small_fee": "Kleine Teilnahmegebühr", "events.small_fee": "Kleine Teilnahmegebühr",
"events.not_found": "Event nichr gefunden", "events.not_found": "Event nichr gefunden",
"events.no_selection": "Kein Event ausgewählt",
"events.signed_up": "Du hast dich für diesen Event angemeldet.", "events.signed_up": "Du hast dich für diesen Event angemeldet.",
"events.no_registration": "Keine Anmeldung erforderlich", "events.no_registration": "Keine Anmeldung erforderlich",
"events.registration_over": "Das Registrierungs-Fenster ist geschlossen.", "events.registration_over": "Das Anmeldefenster ist geschlossen.",
"events.registrations_starts_at": "Die Registrierungs-Periode startet am %{time}", "events.registration_starts_at": "Das Anmeldefenster öffnet am %{time}",
"events.%n_spots_available": [ "events.%n_spots_available": [
[-1, -1, "Plätze verfügbar"], [-1, -1, "Plätze verfügbar"],
[0, 0, "Keine Plätze verfügbar"], [0, 0, "Keine Plätze verfügbar"],
...@@ -109,6 +110,8 @@ ...@@ -109,6 +110,8 @@
"events.amiv_members_only": "Dieser Event ist nur für AMIV Mitglieder.", "events.amiv_members_only": "Dieser Event ist nur für AMIV Mitglieder.",
"events.signup": "anmelden", "events.signup": "anmelden",
"events.delete_signup": "Anmeldung löschen", "events.delete_signup": "Anmeldung löschen",
"events.loading": "Laden...",
"events.load_more": "Mehr Events laden",
"companies.contact_information": "Kontakt-Informationen", "companies.contact_information": "Kontakt-Informationen",
"companies.email": "Email", "companies.email": "Email",
"companies.phone": "Telefon", "companies.phone": "Telefon",
......
...@@ -95,10 +95,11 @@ ...@@ -95,10 +95,11 @@
"events.free": "Free", "events.free": "Free",
"events.small_fee": "Small fee", "events.small_fee": "Small fee",
"events.not_found": "Event not found", "events.not_found": "Event not found",
"events.no_selection": "No event selected",
"events.signed_up": "You signed up for this event.", "events.signed_up": "You signed up for this event.",
"events.no_registration": "No registration required", "events.no_registration": "No registration required",
"events.registration_over": "The registration period is over.", "events.registration_over": "The registration period is over.",
"events.registrations_starts_at": "The registration starts at %{time}", "events.registration_starts_at": "The registration starts at %{time}",
"events.%n_spots_available": [ "events.%n_spots_available": [
[-1, -1, "Spots available"], [-1, -1, "Spots available"],
[0, 0, "No spots available"], [0, 0, "No spots available"],
...@@ -109,6 +110,8 @@ ...@@ -109,6 +110,8 @@
"events.amiv_members_only": "This event is for AMIV members only.", "events.amiv_members_only": "This event is for AMIV members only.",
"events.signup": "signup", "events.signup": "signup",
"events.delete_signup": "delete signup", "events.delete_signup": "delete signup",
"events.loading": "Loading...",
"events.load_more": "Load more events",
"companies.contact_information": "Contact information", "companies.contact_information": "Contact information",
"companies.email": "Email", "companies.email": "Email",
"companies.phone": "Phone", "companies.phone": "Phone",
......
import m from 'mithril'; import m from 'mithril';
import Stream from 'mithril/stream';
import { apiUrl } from 'config'; import { apiUrl } from 'config';
import { getToken, getUserId, isLoggedIn } from './auth'; import { getToken, getUserId, isLoggedIn } from './auth';
import { currentLanguage } from './language'; import { currentLanguage } from './language';
import PaginationController from './pagination';
/** /**
* Event class * Event class
*/ */
export class Event { export class Event {
/**
* Constructor
*
* @param {object} event object loaded from the API
*/
constructor(event) { constructor(event) {
// Expose all properties of `event` // Expose all properties of `event`
Object.keys(event).forEach(key => { Object.keys(event).forEach(key => {
...@@ -17,6 +22,7 @@ export class Event { ...@@ -17,6 +22,7 @@ export class Event {
/** /**
* Load the signup data of the authenticated user. * Load the signup data of the authenticated user.
*
* @return {Promise} * @return {Promise}
*/ */
async loadSignup() { async loadSignup() {
...@@ -47,6 +53,7 @@ export class Event { ...@@ -47,6 +53,7 @@ export class Event {
/** /**
* Checks if the signup data has been loaded. * Checks if the signup data has been loaded.
*
* @return {Boolean} * @return {Boolean}
*/ */
get hasSignupDataLoaded() { get hasSignupDataLoaded() {
...@@ -55,6 +62,7 @@ export class Event { ...@@ -55,6 +62,7 @@ export class Event {
/** /**
* Get signup data of the authenticated user. * Get signup data of the authenticated user.
*
* @return {object} signup data * @return {object} signup data
*/ */
get signupData() { get signupData() {
...@@ -63,6 +71,7 @@ export class Event { ...@@ -63,6 +71,7 @@ export class Event {
/** /**
* Sign off the authenticated user from this event. * Sign off the authenticated user from this event.
*
* @return {Promise} * @return {Promise}
*/ */
async signoff() { async signoff() {
...@@ -81,6 +90,7 @@ export class Event { ...@@ -81,6 +90,7 @@ export class Event {
/** /**
* Sign up the authenticated user for this event. * Sign up the authenticated user for this event.
*
* @param {*} additionalFields * @param {*} additionalFields
* @param {string} email email address (required if not logged in!) * @param {string} email email address (required if not logged in!)
* @return {Promise} * @return {Promise}
...@@ -138,125 +148,131 @@ export class Event { ...@@ -138,125 +148,131 @@ export class Event {
} }
} }
export class EventController { /**
constructor(query = {}) { * EventListController class (inherited from `PaginationController`)
this.query = query; *
// state pointer that is counted up every time the table is refreshed so * Used to handle a list of a specific type of event (e.g. all past events)
// we can tell infinite scroll that the data-version has changed. */
this._stateCounter = Stream(0); export class EventListController extends PaginationController {
constructor(query = {}, additionalQuery = {}) {
super('events', query, additionalQuery);
} }
get stateCounter() { async _loadData(query) {
return this._stateCounter(); const items = await super._loadData(query);
return items.map(event => {
const otherLanguage = currentLanguage() === 'en' ? 'de' : 'en';
const newEvent = Object.assign({}, event);
newEvent.title = newEvent[`title_${currentLanguage()}`] || newEvent[`title_${otherLanguage}`];
newEvent.description =
newEvent[`description_${currentLanguage()}`] || newEvent[`description_${otherLanguage}`];
return new Event(newEvent);
});
} }
}
refresh() { /**
this._stateCounter(this.stateCounter + 1); * EventController class
} *
* Managing multiple type of event lists and handling of the currently selected event.
*/
export class EventController {
/**
* Constructor
*
* @param {object} query initial query
* @param {boolean} upcomingSkipRegistrationOpen if `true`, skip all events with open registration in upcoming event list
*/
constructor(query = {}, upcomingSkipRegistrationOpen = false) {
this.query = query;
this._pastEvents = new EventListController(query, () => {
const date = `${new Date().toISOString().split('.')[0]}Z`;
return {
where: {
time_advertising_end: { $lt: date },
$and: [
{ $or: [{ time_start: null }, { time_start: { $lt: date } }] },
{ $or: [{ time_end: null }, { time_end: { $lt: date } }] },
],
},
};
});
infiniteScrollParams(item, before) { let upcomingAdditionalQuery;
const date = `${new Date().toISOString().split('.')[0]}Z`; if (upcomingSkipRegistrationOpen) {
return { upcomingAdditionalQuery = () => {
item, const date = `${new Date().toISOString().split('.')[0]}Z`;
before, return {
pageData: pageNum =>
this.getPageData(pageNum, {
where: { where: {
time_advertising_end: { $lt: date }, show_website: true,
$and: [ time_start: { $gt: date },
{ $or: [{ time_start: null }, { time_start: { $lt: date } }] }, time_advertising_start: { $lt: date },
{ $or: [{ time_end: null }, { time_end: { $lt: date } }] }, $or: [{ time_register_end: { $lt: date } }, { time_register_start: { $gt: date } }],
],
}, },
}), };
pageKey: pageNum => `${pageNum}-${this.stateCounter}`, };
}; } else {
} upcomingAdditionalQuery = () => {
const date = `${new Date().toISOString().split('.')[0]}Z`;
/** return {
* Get page data according to saved query where: {
* @param {number} pageNum show_website: true,
*/ time_start: { $gt: date },
async getPageData(pageNum, additionalQuery = {}) { time_advertising_start: { $lt: date },
const date = `${new Date().toISOString().split('.')[0]}Z`; },
// for some reason this is called before the object is instantiated. };
const query = JSON.parse(JSON.stringify(Object.assign({}, this.query, additionalQuery))); };
query.where = query.where || {}; }
query.where.show_website = true; this._upcomingEvents = new EventListController(query, upcomingAdditionalQuery);
query.where.time_advertising_start = { $lt: date };
query.max_results = query.max_results || 10;
query.page = pageNum;
return EventController._getData(query); this._openRegistrationEvents = new EventListController(query, () => {
const date = `${new Date().toISOString().split('.')[0]}Z`;
return {
where: {
show_website: true,
time_register_start: { $lt: date },
time_register_end: { $gt: date },
},
};
});
} }
/** /** Set a new query used by all EventListController to load events */
* Get all events with their registration open async setQuery(query) {
*/ const newQuery = JSON.stringify(query || {});
async getWithOpenRegistration() { const oldQuery = JSON.stringify(this.query);
const date = `${new Date().toISOString().split('.')[0]}Z`;
// for some reason this is called before the object is instantiated.
const query = JSON.parse(JSON.stringify(this.query || {}));
query.where = query.where || {};
query.where.show_website = true;
query.where.time_register_start = { $lt: date };
query.where.time_register_end = { $gt: date };
return EventController._getData(query); if (newQuery === oldQuery) return false;
}
/** this.query = JSON.parse(newQuery);
* Get all upcoming events this.openRegistrationEvents.setQuery(this.query);
* @param {Boolean} skipRegistrationOpen skip events which have their registration open this.upcomingEvents.setQuery(this.query);
*/ this.pastEvents.setQuery(this.query);
async getUpcoming(skipRegistrationOpen = false) { await this.refresh();
const date = `${new Date().toISOString().split('.')[0]}Z`; return true;
// for some reason this is called before the object is instantiated. }
const query = JSON.parse(JSON.stringify(this.query || {}));
query.where = query.where || {};
query.where.show_website = true;
if (!skipRegistrationOpen) {
query.where.time_start = { $gt: date };
} else {
query.where.time_start = { $gt: date };
query.where.$or = [
{ time_register_end: { $lt: date } },
{ time_register_start: { $gt: date } },
];
}
query.where.time_advertising_start = { $lt: date };
return EventController._getData(query); /** Refresh all event data */
async refresh() {
await this.openRegistrationEvents.loadAll();
await this.upcomingEvents.loadAll();
await this.pastEvents.loadPageData(1);
} }
static async _getData(query) { /** Get EventListController for all events with open registration window */
// Parse query such that the backend understands it get openRegistrationEvents() {
const parsedQuery = {}; return this._openRegistrationEvents;
Object.keys(query).forEach(key => { }
parsedQuery[key] = key === 'sort' ? query[key] : JSON.stringify(query[key]);
});
const queryString = m.buildQueryString(parsedQuery);
const response = await m.request({ /** Get EventListController for all upcoming events */
method: 'GET', get upcomingEvents() {
url: `${apiUrl}/events?${queryString}`, return this._upcomingEvents;
headers: {
Authorization: getToken(),
},
});
return response._items.map(event => {
const otherLanguage = currentLanguage() === 'en' ? 'de' : 'en';
const newEvent = Object.assign({}, event);
newEvent.title = newEvent[`title_${currentLanguage()}`] || newEvent[`title_${otherLanguage}`];
newEvent.description =
newEvent[`description_${currentLanguage()}`] || newEvent[`description_${otherLanguage}`];
return new Event(newEvent);
});
} }
setQuery(query) { /** Get EventListController for all past events */
this.query = JSON.parse(JSON.stringify(query || {})); get pastEvents() {
this.refresh(); return this._pastEvents;
} }
/** /**
...@@ -272,12 +288,12 @@ export class EventController { ...@@ -272,12 +288,12 @@ export class EventController {
Authorization: getToken(), Authorization: getToken(),
}, },
}); });
event.title = event[`title_${currentLanguage()}`] || event[`title_${otherLanguage}`];
event.description =
event[`description_${currentLanguage()}`] || event[`description_${otherLanguage}`];
if (!event.show_website) { if (!event.show_website) {
throw new Error('Event not found'); throw new Error('Event not found');
} }
event.title = event[`title_${currentLanguage()}`] || event[`title_${otherLanguage}`];
event.description =
event[`description_${currentLanguage()}`] || event[`description_${otherLanguage}`];
this._selectedEvent = new Event(event); this._selectedEvent = new Event(event);
return this._selectedEvent; return this._selectedEvent;
} }
......
import m from 'mithril';
import { apiUrl } from 'config';
import { getToken } from './auth';
/**
* PaginationController class
*
* This is a generic class for different API resources.
*/
export default class PaginationController {
/**
* Constructor
*
* @param {*} resource resource name (e.g. `events`)
* @param {*} query initial query
* @param {*} additionalQuery additional query to be added to every query
* @public
*/
constructor(resource, query = {}, additionalQuery = {}) {
this.resource = resource;
this.query = query;
this.additionalQuery = additionalQuery;
this._lastLoadedPage = 0;
this._totalPages = 1;
}
/**
* Get the total number of pages available to load
*
* @return {int}
* @public
*/
get totalPages() {
return this._totalPages;
}
/**
* Get last loaded page number
*
* @return {int}
* @public
*/
get lastLoadedPage() {
return this._lastLoadedPage;
}
/**
* Set a new query to load the configured resource
*
* @public
*/
setQuery(query) {
this.query = JSON.parse(JSON.stringify(query || {}));
this._pages = [];
this._lastLoadedPage = 0;
this._totalPages = 1;
}
/**
* Load all pages of this resource query
*
* @return {Promise}
* @public
*/
async loadAll() {
let currentPage = 1;
const promiseList = [];
while (currentPage <= this._totalPages) {
promiseList.push(this.loadPageData(currentPage));
currentPage += 1;
}
await Promise.all(promiseList);
}
/**
* Map over all loaded pages (missing pages are skipped!)
*
* @param {function} callback function called with every loaded page
* @public
*/
map(callback) {
return this._pages.map(page => callback(page.items));
}
/**
* Get data of a specific page
*
* @param {int} pageNum page number to load the data from
* @return {Promise}
* @public
*/
async getPageData(pageNum) {
if (
this._pages[pageNum] &&
new Date() < new Date(this._pages[pageNum].datetime.getTime() + 60000)
) {
return this._pages[pageNum].items;
}
await this.loadPageData(pageNum);
return this._pages[pageNum].items;
}
/**
* Load data of a specific page (This function does not return any data!)
*
* @param {int} pageNum page number to load the data from
* @return {Promise}
* @public
*/
async loadPageData(pageNum) {
let additionalQuery;
if (typeof this.additionalQuery === 'function') {
additionalQuery = this.additionalQuery();
} else {
({ additionalQuery } = this);
}
const date = `${new Date().toISOString().split('.')[0]}Z`;
const query = JSON.parse(
JSON.stringify(
Object.assign({}, this.query, additionalQuery, {
where: Object.assign({}, this.query.where, additionalQuery.where),
})
)
);
if (
this.query.where &&
this.query.where.$or &&
additionalQuery.where &&
additionalQuery.where.$or
) {
query.where.$and = query.where.$and || [];
query.where.$and.push(
JSON.parse(JSON.stringify({ $or: this.query.where.$or })),
JSON.parse(JSON.stringify({ $or: additionalQuery.where.$or }))
);
delete query.where.$or;
}
if (this.query.where && this.query.where.$and) {
query.where.$and = query.where.$and.concat(JSON.parse(JSON.stringify(this.query.where.$and)));
}
if (additionalQuery.where && additionalQuery.where.$and) {
query.where.$and = query.where.$and.concat(
JSON.parse(JSON.stringify(additionalQuery.where.$and))
);