From b0487a0f36c8ed1c5b3c65de6078c9124d31f826 Mon Sep 17 00:00:00 2001
From: Sandro Lutz <code@temparus.ch>
Date: Sun, 4 Mar 2018 12:22:13 +0100
Subject: [PATCH] Merge remote-tracking branch 'origin/master' into
 studydocs-autocomplete

---
 src/models/events.js             | 115 +++++++++++++--------
 src/views/eventDetails.js        | 167 ++++++++++++++++++-------------
 src/views/form/jsonSchemaForm.js |  14 ++-
 src/views/form/selectGroup.js    |  18 +++-
 4 files changed, 197 insertions(+), 117 deletions(-)

diff --git a/src/models/events.js b/src/models/events.js
index 5c4fadbe..c779be86 100644
--- a/src/models/events.js
+++ b/src/models/events.js
@@ -1,7 +1,6 @@
 import m from 'mithril';
 import { apiUrl } from './config';
 import { getToken, getUserId, isLoggedIn } from './auth';
-import { log } from './log';
 
 const lang = 'de';
 const date = `${new Date().toISOString().split('.')[0]}Z`;
@@ -15,23 +14,23 @@ export function getList() {
   return this.list;
 }
 
-export function getCurrent() {
-  return this.current;
+export function getSelectedEvent() {
+  return this.selectedEvent;
 }
 
-export function getCurrentSignup() {
-  return this.currentSignup;
+export function getSignupForSelectedEvent() {
+  return this.selectedEventSignup;
 }
 
-export function currentSignupHasLoaded() {
-  return this.currentSignupLoaded;
+export function signupForSelectedEventHasLoaded() {
+  return this.selectedEventSignupLoaded;
 }
 
-export function checkCurrentSignup() {
+export function loadSignupForSelectedEvent() {
   const queryString = m.buildQueryString({
     where: JSON.stringify({
       user: getUserId(),
-      event: this.getCurrent()._id,
+      event: this.getSelectedEvent()._id,
     }),
   });
 
@@ -42,14 +41,58 @@ export function checkCurrentSignup() {
       Authorization: `Token ${getToken()}`,
     } : {},
   }).then((result) => {
-    [this.currentSignup] = result._items;
-    this.currentSignupLoaded = true;
+    [this.selectedEventSignup] = result._items;
+    this.selectedEventSignupLoaded = true;
   });
 }
 
-export function signupCurrent(additionalFields, email = '') {
+export function _signupUserForSelectedEvent(additionalFieldsString) {
+  if (typeof this.selectedEventSignup !== 'undefined') {
+    return m.request({
+      method: 'PATCH',
+      url: `${apiUrl}/eventsignups/${this.selectedEventSignup._id}`,
+      data: {
+        additional_fields: additionalFieldsString,
+      },
+      headers: getToken() ? {
+        Authorization: `Token ${getToken()}`,
+        'If-Match': this.selectedEventSignup._etag,
+      } : { 'If-Match': this.selectedEventSignup._etag },
+    }).then(() => { this.loadSignupForSelectedEvent(); });
+  }
+
+  return m.request({
+    method: 'POST',
+    url: `${apiUrl}/eventsignups`,
+    data: {
+      event: this.selectedEvent._id,
+      additional_fields: additionalFieldsString,
+      user: getUserId(),
+    },
+    headers: getToken() ? {
+      Authorization: `Token ${getToken()}`,
+    } : {},
+  }).then(() => { this.loadSignupForSelectedEvent(); });
+}
+
+export function _signupEmailForSelectedEvent(additionalFieldsString, email) {
+  return m.request({
+    method: 'POST',
+    url: `${apiUrl}/eventsignups`,
+    data: {
+      event: this.selectedEvent._id,
+      additional_fields: additionalFieldsString,
+      email,
+    },
+    headers: getToken() ? {
+      Authorization: `Token ${getToken()}`,
+    } : {},
+  }).then(() => { this.loadSignupForSelectedEvent(); });
+}
+
+export function signupForSelectedEvent(additionalFields, email = '') {
   let additionalFieldsString;
-  if (this.current.additional_fields === undefined ||
+  if (this.selectedEvent.additional_fields === undefined ||
     additionalFields === null || typeof additionalFields !== 'object') {
     additionalFieldsString = undefined;
   } else {
@@ -57,33 +100,23 @@ export function signupCurrent(additionalFields, email = '') {
   }
 
   if (isLoggedIn()) {
-    log(`UserId: ${getUserId()}`);
-    m.request({
-      method: 'POST',
-      url: `${apiUrl}/eventsignups`,
-      data: {
-        event: this.current._id,
-        additional_fields: additionalFieldsString,
-        user: getUserId(),
-      },
-      headers: getToken() ? {
-        Authorization: `Token ${getToken()}`,
-      } : {},
-    }).then(() => { this.checkCurrentSignup(); });
-  } else if (this.current.allow_email_signup) {
-    log(`Email: ${email}`);
+    return this._signupUserForSelectedEvent(additionalFieldsString);
+  } else if (this.selectedEvent.allow_email_signup) {
+    return this._signupEmailForSelectedEvent(additionalFieldsString, email);
+  }
+  return Promise.reject(new Error('Signup not allowed'));
+}
+
+export function signoffForSelectedEvent() {
+  if (isLoggedIn() && typeof this.selectedEventSignup !== 'undefined') {
     m.request({
-      method: 'POST',
-      url: `${apiUrl}/eventsignups`,
-      data: {
-        event: this.current._id,
-        additional_fields: additionalFieldsString,
-        email,
-      },
+      method: 'DELETE',
+      url: `${apiUrl}/eventsignups/${this.selectedEventSignup._id}`,
       headers: getToken() ? {
         Authorization: `Token ${getToken()}`,
-      } : {},
-    }).then(() => { this.checkCurrentSignup(); });
+        'If-Match': this.selectedEventSignup._etag,
+      } : { 'If-Match': this.selectedEventSignup._etag },
+    }).then(() => { this.loadSignupForSelectedEvent(); });
   }
 }
 
@@ -113,9 +146,9 @@ export function load(query = {}) {
   });
 }
 
-export function loadCurrent(eventId) {
-  this.current = this.getList().find(item => item._id === eventId);
-  if (typeof this.current === 'undefined') {
+export function selectEvent(eventId) {
+  this.selectedEvent = this.getList().find(item => item._id === eventId);
+  if (typeof this.selectedEvent === 'undefined') {
     this.load({
       where: {
         time_advertising_start: { $lte: date },
@@ -124,7 +157,7 @@ export function loadCurrent(eventId) {
       },
       sort: ['-priority', 'time_advertising_start'],
     }).then(() => {
-      this.current = this.getList().find(item => item._id === eventId);
+      this.selectedEvent = this.getList().find(item => item._id === eventId);
     });
   }
 }
diff --git a/src/views/eventDetails.js b/src/views/eventDetails.js
index 3408450e..82bddd7d 100644
--- a/src/views/eventDetails.js
+++ b/src/views/eventDetails.js
@@ -10,112 +10,145 @@ import JSONSchemaForm from './form/jsonSchemaForm';
 class EventSignupForm extends JSONSchemaForm {
   oninit(vnode) {
     super.oninit(vnode);
+    this.email = '';
     this.emailErrors = [];
     this.emailValid = false;
     if (isLoggedIn()) {
-      events.checkCurrentSignup();
+      events.loadSignupForSelectedEvent()
+        .then(() => {
+          if (typeof events.getSignupForSelectedEvent() !== 'undefined') {
+            this.data = JSON.parse(events.getSignupForSelectedEvent().additional_fields) || {};
+          }
+        });
     }
   }
 
-  submit() {
-    if (isLoggedIn()) {
-      events.signupCurrent(super.getValue());
-    } else {
-      events.signupCurrent(super.getValue(), this.email);
-    }
+  signup() {
+    events.signupForSelectedEvent(super.getValue(), this.email)
+      .then(() => log('Successfully signed up for the event!'))
+      .catch(() => log('Could not sign up of the event!'));
+  }
+
+  signoff() {
+    events.signoffForSelectedEvent();
+    this.validate();
   }
 
   view() {
     // do not render anything if there is no data yet
-    if (typeof events.getCurrent() === 'undefined') return m();
+    if (typeof events.getSelectedEvent() === 'undefined') return m('');
 
     if (isLoggedIn()) {
       // do not render form if there is no signup data of the current user
-      if (!events.currentSignupHasLoaded()) return m('span', 'Loading...');
-      if (typeof events.getCurrentSignup() === 'undefined') {
-        const elements = this.renderFormElements();
-        elements.push(m(button, {
-          active: super.isValid(),
-          args: {
-            onclick: () => this.submit(),
-          },
-          text: 'Signup',
-        }));
-        return m('form', elements);
+      if (!events.signupForSelectedEventHasLoaded()) return m('span', 'Loading...');
+
+      const elements = this.renderFormElements();
+      elements.push(this._renderSignupButton());
+      if (typeof events.getSignupForSelectedEvent() !== 'undefined') {
+        elements.unshift(m('div', 'You have already signed up. Update your data below.'));
+        elements.push(this._renderSignoffButton());
       }
-      return m('div', 'You have already signed up for this event.');
-    } else if (events.getCurrent().allow_email_signup) {
+      return m('form', elements);
+    } else if (events.getSelectedEvent().allow_email_signup) {
       const elements = this.renderFormElements();
-      elements.push(m(inputGroup, {
-        name: 'email',
-        title: 'Email',
-        args: {
-          type: 'text',
-        },
-        oninput: (e) => {
-          // bind changed data
-          this.email = e.target.value;
-
-          // validate if email address has the right structure
-          if (EmailValidator.validate(this.email)) {
-            this.emailValid = true;
-            this.emailErrors = [];
-          } else {
-            this.emailValid = false;
-            this.emailErrors = ['Not a valid email address'];
-          }
-        },
-        getErrors: () => this.emailErrors,
-        value: this.email,
-      }));
-      elements.push(m(button, {
-        active: this.emailValid && super.isValid(),
-        args: {
-          onclick: () => this.submit(),
-        },
-        text: 'Signup',
-      }));
+      elements.push(this._renderEmailField());
+      elements.push(this._renderSignupButton());
       return m('form', elements);
     }
     return m('div', 'This event is for AMIV members only.');
   }
+
+  isValid() {
+    if (!isLoggedIn()) {
+      return super.isValid() && this.emailValid;
+    }
+    return super.isValid();
+  }
+
+  _renderEmailField() {
+    return m(inputGroup, {
+      name: 'email',
+      title: 'Email',
+      args: {
+        type: 'text',
+      },
+      oninput: (e) => {
+        // bind changed data
+        this.email = e.target.value;
+
+        // validate if email address has the right structure
+        if (EmailValidator.validate(this.email)) {
+          this.emailValid = true;
+          this.emailErrors = [];
+        } else {
+          this.emailValid = false;
+          this.emailErrors = ['Not a valid email address'];
+        }
+      },
+      getErrors: () => this.emailErrors,
+      value: this.email,
+    });
+  }
+
+  _renderSignupButton() {
+    return m(button, {
+      name: 'signup',
+      title: 'Signup',
+      active: super.isValid(),
+      args: {
+        onclick: () => this.signup(),
+      },
+    });
+  }
+
+  _renderSignoffButton() {
+    return m(button, {
+      name: 'signoff',
+      title: 'Delete signup',
+      active: true,
+      args: {
+        onclick: () => this.signoff(),
+      },
+    });
+  }
 }
 
 export default class EventDetails {
   static oninit(vnode) {
-    events.loadCurrent(vnode.attrs.eventId);
+    events.selectEvent(vnode.attrs.eventId);
   }
 
   static view() {
-    if (typeof events.getCurrent() === 'undefined') {
-      return m();
+    if (typeof events.getSelectedEvent() === 'undefined') {
+      return m('');
     }
-    log(events.getCurrent());
+
     let eventSignupForm;
     const now = new Date();
-    const registerStart = new Date(events.getCurrent().time_register_start);
-    const registerEnd = new Date(events.getCurrent().time_register_end);
-    log(`Now: ${now}`);
-    log(`Start: ${registerStart}`);
-    log(`End: ${registerEnd}`);
+    const registerStart = new Date(events.getSelectedEvent().time_register_start);
+    const registerEnd = new Date(events.getSelectedEvent().time_register_end);
     if (registerStart <= now) {
       if (registerEnd >= now) {
         eventSignupForm = m(EventSignupForm, {
-          schema: events.getCurrent().additional_fields === undefined ?
-            undefined : JSON.parse(events.getCurrent().additional_fields),
+          schema: events.getSelectedEvent().additional_fields === undefined ?
+            undefined : JSON.parse(events.getSelectedEvent().additional_fields),
         });
       } else {
-        eventSignupForm = m('div', 'The registration period is over.');
+        let participantNotice = '';
+        if (events.getSignupForSelectedEvent() !== 'undefined') {
+          participantNotice = m('You signed up for this event.');
+        }
+        eventSignupForm = m('div', ['The registration period is over.', participantNotice]);
       }
     } else {
       eventSignupForm = m('div', `The registration starts at ${registerStart}`);
     }
     return m('div', [
-      m('h1', events.getCurrent().title_de),
-      m('span', events.getCurrent().time_start),
-      m('span', events.getCurrent().signup_count),
-      m('span', events.getCurrent().spots),
-      m('p', events.getCurrent().description_de),
+      m('h1', events.getSelectedEvent().title_de),
+      m('span', events.getSelectedEvent().time_start),
+      m('span', events.getSelectedEvent().signup_count),
+      m('span', events.getSelectedEvent().spots),
+      m('p', events.getSelectedEvent().description_de),
       eventSignupForm,
     ]);
   }
diff --git a/src/views/form/jsonSchemaForm.js b/src/views/form/jsonSchemaForm.js
index 8dc30b93..a83c8900 100644
--- a/src/views/form/jsonSchemaForm.js
+++ b/src/views/form/jsonSchemaForm.js
@@ -77,6 +77,12 @@ export default class JSONSchemaForm {
     return this.schema === undefined || this.valid;
   }
 
+  validate() {
+    // validate the new data against the schema
+    const validate = this.ajv.getSchema('schema');
+    this.valid = validate(this.data);
+  }
+
   getValue() {
     return this.data;
   }
@@ -104,9 +110,9 @@ export default class JSONSchemaForm {
       return m(selectGroup, this.bind({
         name: key,
         title: item.description,
+        type: item.items.enum.length > 8 ? 'select' : 'buttons',
+        options: item.items.enum,
         args: {
-          options: item.items.enum,
-          type: item.items.enum.length > 8 ? 'select' : 'buttons',
           multipleSelect: true,
         },
       }));
@@ -123,9 +129,9 @@ export default class JSONSchemaForm {
       return m(selectGroup, this.bind({
         name: key,
         title: item.description,
+        options: item.enum,
+        type: 'select',
         args: {
-          options: item.enum,
-          type: 'select',
           multipleSelect: false,
         },
       }));
diff --git a/src/views/form/selectGroup.js b/src/views/form/selectGroup.js
index e28bf259..45a8c8f5 100644
--- a/src/views/form/selectGroup.js
+++ b/src/views/form/selectGroup.js
@@ -16,12 +16,19 @@ export default class SelectGroup {
     args.onchange = vnode.attrs.onchange;
     args.oninput = vnode.attrs.oninput;
 
+    const options = vnode.attrs.options.map((option) => {
+      if (typeof option === 'object') {
+        return option;
+      }
+      return { value: option, text: option };
+    });
+
     switch (vnode.attrs.type) {
       case 'buttons': {
         if (args.multipleSelect) {
           return m('div', { class: vnode.attrs.classes }, [
             m(`label[for=${vnode.attrs.name}]`, vnode.attrs.title),
-            m('div', vnode.attrs.options.map(option =>
+            m('div', options.map(option =>
               m(inputGroup, {
                 name: vnode.attrs.name,
                 title: option.text,
@@ -47,10 +54,11 @@ export default class SelectGroup {
           ]);
         }
         return m('div', { class: vnode.attrs.classes }, [
-          m('div', vnode.attrs.options.map(option =>
+          m('div', options.map(option =>
             m(inputGroup, {
               name: vnode.attrs.name,
-              title: option,
+              title: option.text,
+              value: option.value,
               onchange: vnode.attrs.onchange,
               args: { type: 'radio' },
             }))),
@@ -89,7 +97,7 @@ export default class SelectGroup {
                   vnode.attrs.oninput({ target: { name: e.target.name, value } });
                 },
               },
-              vnode.attrs.options.map(option => m('option', { value: option.value }, option.text)),
+              options.map(option => m('option', { value: option.value }, option.text)),
             ),
           ]);
         }
@@ -98,7 +106,7 @@ export default class SelectGroup {
           m(
             `select[name=${vnode.attrs.name}][id=${vnode.attrs.name}]`,
             args,
-            vnode.attrs.options.map(option => m('option', { value: option.value }, option.text)),
+            options.map(option => m('option', { value: option.value }, option.text)),
           ),
         ]);
       }
-- 
GitLab