From d8dae95022fddc9ac39b5a825ecee8474f8b7895 Mon Sep 17 00:00:00 2001 From: Lionel Trebuchon Date: Sat, 23 Nov 2019 01:33:08 +0100 Subject: [PATCH 01/13] Enhance documentation, take note about unregistering from events mailing list --- .gitignore | 3 +++ README.md | 9 +++++++-- amivapi/events/email_links.py | 7 ++++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 91512f95..d23eb761 100644 --- a/.gitignore +++ b/.gitignore @@ -66,3 +66,6 @@ config.yaml amivapi/config.yaml apikeys.yaml amivapi_storage + +# Pycharm +.idea/* \ No newline at end of file diff --git a/README.md b/README.md index bb0c533c..e5ac7466 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ feel free to fork and modify. If you only want to use AMIV API, check out the online documentation (There's a link in the github description above). -If you are an administrator and wish to get the AMIV API running, keep reading! +If you are an administrator and wish to get the AMIV API r`unning, keep reading! If you are a developer looking to work on AMIV API, it's best to look at the code directly. You can start with [bootstrap.py](amivapi/bootstrap.py), @@ -72,8 +72,13 @@ The following command runs a MongoDB service available on the default port password `amivapi`. ```sh +# Initialize "swarm", a scheduling and clustering tool, that will enable us to create a network overlay +docker swarm init + # Create a network so that the api service can later be connected to the db -docker network create --driver overlay backend +docker network create --driver overlay backend # Overlay so that it is representative of the real-life AMIV structure + +# docker service create \ --name mongodb -p 27017:27017 --network backend\ -e MONGODB_DATABASE=amivapi \ diff --git a/amivapi/events/email_links.py b/amivapi/events/email_links.py index 80180317..aa5f4ad0 100644 --- a/amivapi/events/email_links.py +++ b/amivapi/events/email_links.py @@ -10,7 +10,7 @@ from bson import ObjectId from eve.methods.delete import deleteitem_internal from eve.methods.patch import patch_internal -from flask import Blueprint, current_app, redirect +from flask import Blueprint, current_app, redirect, request from itsdangerous import BadSignature, URLSafeSerializer from amivapi.events.queue import update_waiting_list @@ -72,6 +72,11 @@ def on_delete_signup(token): deleteitem_internal('eventsignups', concurrency_check=False, **{current_app.config['ID_FIELD']: signup_id}) + # Verify if user confirmed + # definitive = request.args.get('definitive') + # if definitive: + + # Removal redirect_url = current_app.config.get('SIGNUP_DELETED_REDIRECT') if redirect_url: return redirect(redirect_url) From 4d84d0698801f52e65c5990381e0233ce0e2d8e8 Mon Sep 17 00:00:00 2001 From: Lionel Trebuchon Date: Sat, 23 Nov 2019 17:54:43 +0100 Subject: [PATCH 02/13] Enhance README.md with how to run API locally (Python) --- README.md | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index e5ac7466..f11654ef 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ You only need to install Docker, nothing else is required. ### Manual Installation for Development -For development, we recommend to clone the repository and install AMIV API +For development, we recommend to **fork** the repository and install AMIV API manually. First of all, we advise using a [virtual environment](https://docs.python.org/3/tutorial/venv.html). @@ -107,7 +107,7 @@ Now it's time to configure AMIV API. Create a file `config.py` ROOT_PASSWORD = 'root' # MongoDB Configuration -MONGO_HOST = 'mongodb' +MONGO_HOST = 'mongodb' # or 'localhost' if you run the API from Python MONGO_PORT = 27017 MONGO_DBNAME = 'amivapi' MONGO_USERNAME = 'amivapi' @@ -157,7 +157,7 @@ Configuration files can be used easily for services using docker config create amivapi_config config.py ``` -Now start the API service (make sure to put it in the same network as MongoDB +Now start the API service (make sure to put it in the same network (here `backend`) as MongoDB if you are running a MongoDB service locally). ```sh @@ -174,6 +174,13 @@ docker service create \ --name amivapi-cron --network backend \ --config source=amivapi_config,target=/api/config.py \ amiveth/amivapi amivapi cron --continuous + +# To attach your command line to the docker instance, use +docker attach amivapi{...} # Use tab completion to find the name of the service + +# As we run docker as a service, it restarts by itself even if you use docker kill +# To stop the service, use +docker service rm amivapi ``` (If you want to mount the config somewhere else, you can use the environment @@ -181,23 +188,17 @@ variable `AMIVAPI_CONFIG` to specify the config path in the container.) ### Run locally -If you have installed AMIV API locally, you can use the CLI to start it: - -```sh -# Start development server -amivapi run dev +If you have installed AMIV API locally, you can use the CLI to start it. -# Start production server (requires the `bjoern` package) -amivapi run prod +First, change `MONGO_HOST = 'mongodb'` in `config.py` to `'MONGO_HOST = 'localhost'`. +Then, in CLI: -# Execute scheduled tasks periodically -amivapi cron --continuous - -# Specify config if its not `config.py` in the current directory -amivapi --config run dev - -# Get help, works for sub-commands as well -amivapi --help +```sh +amivapi run dev # Start development server +amivapi run prod # Start production server (requires the `bjoern` package) +amivapi cron --continuous # Execute scheduled tasks periodically +amivapi --config run dev # Specify config if its not `config.py` in the current directory +amivapi --help # Get help, works for sub-commands as well amivapi run --help ``` From cb1ec323b5c43c638aa1d450d86058fc89ca7ef1 Mon Sep 17 00:00:00 2001 From: Lionel Trebuchon Date: Sat, 23 Nov 2019 18:22:50 +0100 Subject: [PATCH 03/13] WIP on adding one security to unregister from event --- amivapi/events/email_links.py | 39 +++++++---- .../events/templates/unregister_event.html | 67 +++++++++++++++++++ amivapi/settings.py | 4 +- amivapi/utils.py | 2 + 4 files changed, 98 insertions(+), 14 deletions(-) create mode 100644 amivapi/events/templates/unregister_event.html diff --git a/amivapi/events/email_links.py b/amivapi/events/email_links.py index aa5f4ad0..6687183e 100644 --- a/amivapi/events/email_links.py +++ b/amivapi/events/email_links.py @@ -10,7 +10,7 @@ from bson import ObjectId from eve.methods.delete import deleteitem_internal from eve.methods.patch import patch_internal -from flask import Blueprint, current_app, redirect, request +from flask import Blueprint, current_app, redirect, request, make_response, render_template, g from itsdangerous import BadSignature, URLSafeSerializer from amivapi.events.queue import update_waiting_list @@ -66,19 +66,32 @@ def on_delete_signup(token): try: s = URLSafeSerializer(get_token_secret()) signup_id = ObjectId(s.loads(token)) + # event_name = s.loads(token) except BadSignature: return "Unknown token" - deleteitem_internal('eventsignups', concurrency_check=False, - **{current_app.config['ID_FIELD']: signup_id}) - # Verify if user confirmed - # definitive = request.args.get('definitive') - # if definitive: - - # Removal - redirect_url = current_app.config.get('SIGNUP_DELETED_REDIRECT') - if redirect_url: - return redirect(redirect_url) - else: - return current_app.config['SIGNUP_DELETED_TEXT'] + # definitive = request.args.get('DEFINITIVE_DELETE') + # Get first name for personal greeting + query = {'_id': ObjectId(g.current_user)} + projection = {'firstname': 1} # Firstame is a required field for users + data = current_app.data.driver.db['users'].find_one(query, projection) + user = data['firstname'] + unregister_page = True + if unregister_page: + # Serve the unregister_event page + response = make_response(render_template("unregister_event.html", + user=user, + event=event_name, + error_msg=error_msg)) + response.set_cookie('token', token) + return response + else: # Legacy + # Removal + deleteitem_internal('eventsignups', concurrency_check=False, + **{current_app.config['ID_FIELD']: signup_id}) + redirect_url = current_app.config.get('SIGNUP_DELETED_REDIRECT') + if redirect_url: + return redirect(redirect_url) + else: + return current_app.config['SIGNUP_DELETED_TEXT'] diff --git a/amivapi/events/templates/unregister_event.html b/amivapi/events/templates/unregister_event.html new file mode 100644 index 00000000..22829f4d --- /dev/null +++ b/amivapi/events/templates/unregister_event.html @@ -0,0 +1,67 @@ + + + + + AMIV Unregister from Event + + + + {% if error_msg %} + +

{{ error_msg }}

+ +
+ {% endif %} + + + +
+

+ {% if user %} + Hi {{user}}! + {% else %} + Hello! + {% endif %} +

+

+ {% if user and event %} + We will irrevocably unregister you ({{user}}) from the event {{event}}. + Is that ok? + {% else %} + You clicked on a opt-out link of the AMIV at ETHZ student organization. We cannot process your request, + because we either do {not} know the event you wish to unregister from, or your user name, or both. + {% endif %} +

+ +
+ {% if user %} + + + {% else %} +
+ + +
+
+ + +
+
+
+ + +
+ + {% endif %} +
+
+ + + diff --git a/amivapi/settings.py b/amivapi/settings.py index 59dc669b..67935427 100644 --- a/amivapi/settings.py +++ b/amivapi/settings.py @@ -62,11 +62,13 @@ REMOTE_MAILING_LIST_ADDRESS = None REMOTE_MAILING_LIST_KEYFILE = None REMOTE_MAILING_LIST_DIR = './' # Use home directory on remote by default +# Signups via email (@email_blueprint.route('/delete_signup/') in email_links.py) +# DEFINITIVE_DELETE = '' # SMTP server defaults API_MAIL = 'api@amiv.ethz.ch' API_MAIL_SUBJECT = "[AMIV] {subject}" -SMTP_HOST = 'localhost' +SMTP_HOST = 'localhost' # None SMTP_PORT = 587 SMTP_TIMEOUT = 10 diff --git a/amivapi/utils.py b/amivapi/utils.py index c6253f5b..bc55e567 100644 --- a/amivapi/utils.py +++ b/amivapi/utils.py @@ -96,6 +96,8 @@ def mail(to, subject, text): 'receivers': to, 'text': text }) + elif app.config.get('DEBUG', False): + print('{}{}{}{}'.format(subject, sender, to, text)) elif config.SMTP_SERVER and config.SMTP_PORT: msg = MIMEText(text) msg['Subject'] = subject From 1bf724cb654afa58f415097e9d0f458464e4014f Mon Sep 17 00:00:00 2001 From: Lionel Trebuchon Date: Sat, 23 Nov 2019 20:13:12 +0100 Subject: [PATCH 04/13] Transmit token to unregister page --- amivapi/events/email_links.py | 62 ++++++++++++------- .../events/templates/unregister_event.html | 26 +------- 2 files changed, 43 insertions(+), 45 deletions(-) diff --git a/amivapi/events/email_links.py b/amivapi/events/email_links.py index 6687183e..a3740031 100644 --- a/amivapi/events/email_links.py +++ b/amivapi/events/email_links.py @@ -7,6 +7,7 @@ Needed when external users want to sign up for public events or users want to sign off via links. """ +import sys from bson import ObjectId from eve.methods.delete import deleteitem_internal from eve.methods.patch import patch_internal @@ -66,32 +67,49 @@ def on_delete_signup(token): try: s = URLSafeSerializer(get_token_secret()) signup_id = ObjectId(s.loads(token)) - # event_name = s.loads(token) + except BadSignature: return "Unknown token" # Verify if user confirmed # definitive = request.args.get('DEFINITIVE_DELETE') # Get first name for personal greeting - query = {'_id': ObjectId(g.current_user)} - projection = {'firstname': 1} # Firstame is a required field for users - data = current_app.data.driver.db['users'].find_one(query, projection) - user = data['firstname'] - unregister_page = True - if unregister_page: + error_msg = '' + query = {'_id': signup_id} + data_signup = current_app.data.driver.db['eventsignups'].find_one(query) + if data_signup is None: + error_msg = "This event might not exist anymore, or the link is broken, or we made a mistake." + user = data_signup['user'] + if user is None: + user = data_signup['email'] + else: + query = {'_id': user} + data_user = current_app.data.driver.db['users'].find_one(query) + user = data_user["firstname"] + # Serve the unregister_event page - response = make_response(render_template("unregister_event.html", - user=user, - event=event_name, - error_msg=error_msg)) - response.set_cookie('token', token) - return response - else: # Legacy - # Removal - deleteitem_internal('eventsignups', concurrency_check=False, - **{current_app.config['ID_FIELD']: signup_id}) - redirect_url = current_app.config.get('SIGNUP_DELETED_REDIRECT') - if redirect_url: - return redirect(redirect_url) - else: - return current_app.config['SIGNUP_DELETED_TEXT'] + response = make_response(render_template("unregister_event.html", + user=user, + event=data_signup["title_en"], + error_msg=error_msg, + token=token)) + response.set_cookie('token', token) + return response + + +@email_blueprint.route('/delete_confirmed/') +def on_delete_confirmed(token): + try: + s = URLSafeSerializer(get_token_secret()) + signup_id = ObjectId(s.loads(token)) + + except BadSignature: + return "Unknown token" + + deleteitem_internal('eventsignups', concurrency_check=False, + **{current_app.config['ID_FIELD']: signup_id}) + redirect_url = current_app.config.get('SIGNUP_DELETED_REDIRECT') + if redirect_url: + return redirect(redirect_url) + else: + return current_app.config['SIGNUP_DELETED_TEXT'] diff --git a/amivapi/events/templates/unregister_event.html b/amivapi/events/templates/unregister_event.html index 22829f4d..a7a85deb 100644 --- a/amivapi/events/templates/unregister_event.html +++ b/amivapi/events/templates/unregister_event.html @@ -37,30 +37,10 @@

{% endif %}

-
- {% if user %} - - - {% else %} -
- - -
-
- - -
-
-
- - -
- - {% endif %} + +
+ From 12e674fadfa46f7a44726211e4b7715078fa996d Mon Sep 17 00:00:00 2001 From: Lionel Trebuchon Date: Sat, 23 Nov 2019 22:01:53 +0100 Subject: [PATCH 05/13] Finalize unregister function, force POST request and includes date information --- amivapi/events/email_links.py | 18 +++++++++++++----- amivapi/events/emails.py | 15 ++++++++------- amivapi/events/templates/unregister_event.html | 4 ++-- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/amivapi/events/email_links.py b/amivapi/events/email_links.py index a3740031..5c786622 100644 --- a/amivapi/events/email_links.py +++ b/amivapi/events/email_links.py @@ -7,7 +7,7 @@ Needed when external users want to sign up for public events or users want to sign off via links. """ -import sys +from datetime import datetime from bson import ObjectId from eve.methods.delete import deleteitem_internal from eve.methods.patch import patch_internal @@ -17,7 +17,7 @@ from amivapi.events.queue import update_waiting_list from amivapi.events.utils import get_token_secret -email_blueprint = Blueprint('emails', __name__) +email_blueprint = Blueprint('emails', __name__, template_folder='templates') def add_confirmed_before_insert(items): @@ -86,18 +86,26 @@ def on_delete_signup(token): query = {'_id': user} data_user = current_app.data.driver.db['users'].find_one(query) user = data_user["firstname"] - + event = data_signup['event'] + query = {'_id': event} + data_event = current_app.data.driver.db['events'].find_one(query) + event_name = data_event["title_en"] + if event_name is None: + event_name = data_event["title_en"] + print(data_event["time_start"]) + event_date = datetime.strftime(data_event["time_start"], '%Y-%m-%d %H:%M') # Serve the unregister_event page response = make_response(render_template("unregister_event.html", user=user, - event=data_signup["title_en"], + event=event_name, + event_date=event_date, error_msg=error_msg, token=token)) response.set_cookie('token', token) return response -@email_blueprint.route('/delete_confirmed/') +@email_blueprint.route('/delete_confirmed/', methods = ['POST']) def on_delete_confirmed(token): try: s = URLSafeSerializer(get_token_secret()) diff --git a/amivapi/events/emails.py b/amivapi/events/emails.py index 3630cb03..e42fe154 100644 --- a/amivapi/events/emails.py +++ b/amivapi/events/emails.py @@ -36,13 +36,14 @@ def notify_signup_accepted(event, signup): deletion_link = url_for('emails.on_delete_signup', token=token, _external=True) - mail([email], - 'Eventsignup accepted', - current_app.config['ACCEPT_EMAIL_TEXT'].format( - name=name, - title=event.get('title_en') or event.get('title_de'), - link=deletion_link, - deadline=event['time_register_end'].strftime('%H.%M %d.%m.%Y'))) + print(deletion_link) + # mail([email], + # 'Eventsignup accepted', + # current_app.config['ACCEPT_EMAIL_TEXT'].format( + # name=name, + # title=event.get('title_en') or event.get('title_de'), + # link=deletion_link, + # deadline=event['time_register_end'].strftime('%H.%M %d.%m.%Y'))) def send_confirmmail_to_unregistered_users(items): diff --git a/amivapi/events/templates/unregister_event.html b/amivapi/events/templates/unregister_event.html index a7a85deb..2bb32d32 100644 --- a/amivapi/events/templates/unregister_event.html +++ b/amivapi/events/templates/unregister_event.html @@ -29,7 +29,7 @@

{% if user and event %} - We will irrevocably unregister you ({{user}}) from the event {{event}}. + We will irrevocably unregister you ({{user}}) from the event {{event}} on {{event_date}}. Is that ok? {% else %} You clicked on a opt-out link of the AMIV at ETHZ student organization. We cannot process your request, @@ -37,7 +37,7 @@

{% endif %}

-
+
From f52e0bc16fb177b1fa61b445c40cfd922f04a42f Mon Sep 17 00:00:00 2001 From: Lionel Trebuchon Date: Sat, 23 Nov 2019 23:06:50 +0100 Subject: [PATCH 06/13] Remove typos and printouts --- README.md | 8 ++++---- amivapi/settings.py | 2 +- amivapi/utils.py | 2 -- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index f11654ef..abffa755 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ feel free to fork and modify. If you only want to use AMIV API, check out the online documentation (There's a link in the github description above). -If you are an administrator and wish to get the AMIV API r`unning, keep reading! +If you are an administrator and wish to get the AMIV API running, keep reading! If you are a developer looking to work on AMIV API, it's best to look at the code directly. You can start with [bootstrap.py](amivapi/bootstrap.py), @@ -36,7 +36,7 @@ You only need to install Docker, nothing else is required. ### Manual Installation for Development -For development, we recommend to **fork** the repository and install AMIV API +For development, we recommend to fork the repository and install AMIV API manually. First of all, we advise using a [virtual environment](https://docs.python.org/3/tutorial/venv.html). @@ -76,7 +76,7 @@ password `amivapi`. docker swarm init # Create a network so that the api service can later be connected to the db -docker network create --driver overlay backend # Overlay so that it is representative of the real-life AMIV structure +docker network create --driver overlay backend # docker service create \ @@ -176,7 +176,7 @@ docker service create \ amiveth/amivapi amivapi cron --continuous # To attach your command line to the docker instance, use -docker attach amivapi{...} # Use tab completion to find the name of the service +docker attach amivapi... # Use tab completion to find the name of the service # As we run docker as a service, it restarts by itself even if you use docker kill # To stop the service, use diff --git a/amivapi/settings.py b/amivapi/settings.py index 67935427..657ca71c 100644 --- a/amivapi/settings.py +++ b/amivapi/settings.py @@ -68,7 +68,7 @@ # SMTP server defaults API_MAIL = 'api@amiv.ethz.ch' API_MAIL_SUBJECT = "[AMIV] {subject}" -SMTP_HOST = 'localhost' # None +SMTP_HOST = 'localhost' # None in case you want to accept that no mails get sent (local testing) SMTP_PORT = 587 SMTP_TIMEOUT = 10 diff --git a/amivapi/utils.py b/amivapi/utils.py index bc55e567..c6253f5b 100644 --- a/amivapi/utils.py +++ b/amivapi/utils.py @@ -96,8 +96,6 @@ def mail(to, subject, text): 'receivers': to, 'text': text }) - elif app.config.get('DEBUG', False): - print('{}{}{}{}'.format(subject, sender, to, text)) elif config.SMTP_SERVER and config.SMTP_PORT: msg = MIMEText(text) msg['Subject'] = subject From f48523bb4d145c8bfc95d25bc3a661220638b4cd Mon Sep 17 00:00:00 2001 From: Lionel Trebuchon Date: Sun, 24 Nov 2019 12:38:29 +0100 Subject: [PATCH 07/13] Reinstore email in notify_signup_accepted --- amivapi/events/emails.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/amivapi/events/emails.py b/amivapi/events/emails.py index e42fe154..3630cb03 100644 --- a/amivapi/events/emails.py +++ b/amivapi/events/emails.py @@ -36,14 +36,13 @@ def notify_signup_accepted(event, signup): deletion_link = url_for('emails.on_delete_signup', token=token, _external=True) - print(deletion_link) - # mail([email], - # 'Eventsignup accepted', - # current_app.config['ACCEPT_EMAIL_TEXT'].format( - # name=name, - # title=event.get('title_en') or event.get('title_de'), - # link=deletion_link, - # deadline=event['time_register_end'].strftime('%H.%M %d.%m.%Y'))) + mail([email], + 'Eventsignup accepted', + current_app.config['ACCEPT_EMAIL_TEXT'].format( + name=name, + title=event.get('title_en') or event.get('title_de'), + link=deletion_link, + deadline=event['time_register_end'].strftime('%H.%M %d.%m.%Y'))) def send_confirmmail_to_unregistered_users(items): From 188548b08e8d33b3461d35109f23d3a7949c87f9 Mon Sep 17 00:00:00 2001 From: Lionel Trebuchon Date: Sun, 24 Nov 2019 13:15:32 +0100 Subject: [PATCH 08/13] Take care of case where event has no date --- amivapi/events/email_links.py | 6 ++++-- amivapi/events/model.py | 2 -- amivapi/events/templates/unregister_event.html | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/amivapi/events/email_links.py b/amivapi/events/email_links.py index 5c786622..78b51132 100644 --- a/amivapi/events/email_links.py +++ b/amivapi/events/email_links.py @@ -92,8 +92,10 @@ def on_delete_signup(token): event_name = data_event["title_en"] if event_name is None: event_name = data_event["title_en"] - print(data_event["time_start"]) - event_date = datetime.strftime(data_event["time_start"], '%Y-%m-%d %H:%M') + if data_event["time_start"] is None: + event_date = "a yet undefined day." + else: + event_date = datetime.strftime(data_event["time_start"], '%Y-%m-%d %H:%M') # Serve the unregister_event page response = make_response(render_template("unregister_event.html", user=user, diff --git a/amivapi/events/model.py b/amivapi/events/model.py index b02ac554..3b3cc8c1 100644 --- a/amivapi/events/model.py +++ b/amivapi/events/model.py @@ -257,7 +257,6 @@ 'description': 'Start time of the event itself. If you define ' 'a start time, an end time is required, too.', 'example': '2018-10-17T18:00:00Z', - 'type': 'datetime', 'nullable': True, 'default': None, @@ -269,7 +268,6 @@ 'description': 'End time of the event itself. If you define ' 'an end time, a start time is required, too.', 'example': '2018-10-17T22:00:00Z', - 'type': 'datetime', 'nullable': True, 'default': None, diff --git a/amivapi/events/templates/unregister_event.html b/amivapi/events/templates/unregister_event.html index 2bb32d32..6a60dc31 100644 --- a/amivapi/events/templates/unregister_event.html +++ b/amivapi/events/templates/unregister_event.html @@ -29,7 +29,7 @@

{% if user and event %} - We will irrevocably unregister you ({{user}}) from the event {{event}} on {{event_date}}. + We will irrevocably unregister you ({{user}}) from the event {{event}} on {{event_date}}. Is that ok? {% else %} You clicked on a opt-out link of the AMIV at ETHZ student organization. We cannot process your request, From 23cbd432176d14d69b6c77664cd622ebfc9bc9be Mon Sep 17 00:00:00 2001 From: Sandro Lutz Date: Wed, 27 Nov 2019 01:04:43 +0100 Subject: [PATCH 09/13] Fix newsletter subscription when upgrading users to members via LDAP. When users are upgraded to members via LDAP, subscribe them to the newsletter. Newsletter settings of already imported members are not touched. Closes #400 --- amivapi/ldap.py | 2 +- amivapi/tests/test_ldap.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/amivapi/ldap.py b/amivapi/ldap.py index 550ab154..06174ba4 100644 --- a/amivapi/ldap.py +++ b/amivapi/ldap.py @@ -195,9 +195,9 @@ def _create_or_update_user(ldap_data): # Membership will not be downgraded and email not be overwritten # Newletter settings will also not be adjusted ldap_data.pop('email', None) - ldap_data.pop('send_newsletter', None) if db_data.get('membership') != u"none": ldap_data.pop('membership', None) + ldap_data.pop('send_newsletter', None) user = patch_internal('users', ldap_data, diff --git a/amivapi/tests/test_ldap.py b/amivapi/tests/test_ldap.py index e2ff3ce8..b87656d0 100644 --- a/amivapi/tests/test_ldap.py +++ b/amivapi/tests/test_ldap.py @@ -214,6 +214,26 @@ def test_update_user(self): else: self.assertEqual(result[field], db_value) + def test_upgrade_membership(self): + # Insert non-member and upgrade by ldap later + user = self.api.post('/users', data={ + 'nethz': 'pablo', + 'email': 'pablo@ethz.ch', # this will be auto-generated + 'firstname': 'P', + 'lastname': 'Ablo', + 'department': 'itet', + 'membership': 'none', + 'gender': 'female', + 'legi': '01234567', + 'send_newsletter': False, + }, status_code=201).json + self.assertFalse(user['send_newsletter']) + + with self.app.test_request_context(): + result = ldap._create_or_update_user(self.fake_filtered_data()) + + self.assertTrue(result['send_newsletter']) + def test_search(self): """Test that ldap is correctly queried.""" test_query = "äüáíðáßðöó" From 1476ecb5b749eaa47154971413c87598cbdc6737 Mon Sep 17 00:00:00 2001 From: Sandro Lutz Date: Wed, 27 Nov 2019 01:59:09 +0100 Subject: [PATCH 10/13] Update position field of eventsignups. - Add documentation for position field. - Add position field to post response. Fixes #405 Fixes #410 --- amivapi/events/__init__.py | 2 ++ amivapi/events/model.py | 6 ++++++ amivapi/events/projections.py | 5 +++++ amivapi/tests/events/test_projections.py | 6 ++++-- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/amivapi/events/__init__.py b/amivapi/events/__init__.py index 2c591f4e..6e9e6ce9 100644 --- a/amivapi/events/__init__.py +++ b/amivapi/events/__init__.py @@ -21,6 +21,7 @@ add_email_to_signup_collection, add_position_to_signup, add_position_to_signup_collection, + add_position_to_signup_on_inserted, add_signup_count_to_event, add_signup_count_to_event_collection ) @@ -49,6 +50,7 @@ def init_app(app): # Show user's position in the signup list app.on_fetched_resource_eventsignups += add_position_to_signup_collection app.on_fetched_item_eventsignups += add_position_to_signup + app.on_inserted_eventsignups += add_position_to_signup_on_inserted # Show signup count in events app.on_fetched_resource_events += add_signup_count_to_event_collection diff --git a/amivapi/events/model.py b/amivapi/events/model.py index 3b3cc8c1..267e0f5e 100644 --- a/amivapi/events/model.py +++ b/amivapi/events/model.py @@ -598,6 +598,12 @@ 'type': 'boolean', 'admin_only': True }, + 'position': { + 'description': "Position of the signup within all signups of " + "the event.", + 'readonly': True, + 'type': 'integer', + } } } } diff --git a/amivapi/events/projections.py b/amivapi/events/projections.py index 24ae738d..1b11880b 100644 --- a/amivapi/events/projections.py +++ b/amivapi/events/projections.py @@ -37,6 +37,11 @@ def add_position_to_signup_collection(response): add_position_to_signup(item) +def add_position_to_signup_on_inserted(items): + for item in items: + add_position_to_signup(item) + + def add_signup_count_to_event(item): """After an event is fetched from the database we add the current signup count""" diff --git a/amivapi/tests/events/test_projections.py b/amivapi/tests/events/test_projections.py index a4fc2e27..7be098bd 100644 --- a/amivapi/tests/events/test_projections.py +++ b/amivapi/tests/events/test_projections.py @@ -63,8 +63,8 @@ def test_signup_count_projected(self): self.assertEqual(event['signup_count'], 100) self.assertEqual(event['unaccepted_count'], 1) - def test_waitinglist_position_projection(self): - """Test that waiting list position is correctly inserted into a + def test_signuplist_position_projection(self): + """Test that signup list position is correctly inserted into a signup information""" with freeze_time("2016-01-01 00:00:00") as frozen_time: # Create a new event @@ -93,6 +93,8 @@ def test_waitinglist_position_projection(self): 'event': str(event['_id']), 'user': str(late_user['_id']) }, status_code=201).json + # Check that position is part of the received entity + self.assertEqual(signup['position'], 4) # GET the late user's signup to check his position signup_info = self.api.get( From 35d3f164171403172e1478ab1655ffb8271dd2f2 Mon Sep 17 00:00:00 2001 From: Sandro Lutz Date: Wed, 27 Nov 2019 17:17:25 +0100 Subject: [PATCH 11/13] Allow admins to overwrite FCFS decision for POST requests. Even if FCFS in enabled, admins should be allowed to force a signup to be accepted. This worked as intended for PATCH requests already, and now also works for POST. Fixes #381 --- amivapi/events/queue.py | 6 ++-- amivapi/tests/events/test_queue.py | 45 +++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/amivapi/events/queue.py b/amivapi/events/queue.py index 3653cd8c..01b10f40 100644 --- a/amivapi/events/queue.py +++ b/amivapi/events/queue.py @@ -4,7 +4,7 @@ # you to buy us beer if we meet and you like the software. """Logic to implement different signup queues.""" -from flask import current_app +from flask import current_app, g from pymongo import ASCENDING from amivapi.events.emails import notify_signup_accepted @@ -65,7 +65,9 @@ def update_waiting_list(event_id): def add_accepted_before_insert(signups): """Add the accepted field before inserting signups.""" for signup in signups: - signup['accepted'] = False + # Admins may provide a value for `accepted`. + # If not provided or not admin set it to false`. + signup['accepted'] = g.resource_admin and signup.get('accepted', False) def update_waiting_list_after_insert(signups): diff --git a/amivapi/tests/events/test_queue.py b/amivapi/tests/events/test_queue.py index 45ab4efd..25ffa0c4 100644 --- a/amivapi/tests/events/test_queue.py +++ b/amivapi/tests/events/test_queue.py @@ -4,7 +4,41 @@ # you to buy us beer if we meet and you like the software. """Test that people are correctly added and removed from the waiting list""" -from amivapi.tests.utils import WebTestNoAuth +from amivapi.tests.utils import WebTestNoAuth, WebTest + + +class EventsignupQueuePermissionTest(WebTest): + def test_fcfs_users_cannot_provide_accepted(self): + """Test that with fcfs admins can provide accepted + field while normal users cannot""" + event = self.new_object('events', spots=1, + selection_strategy='fcfs') + + user1 = self.new_object('users') + user2 = self.new_object('users') + + user1_signup = self.api.post('/eventsignups', data={ + 'user': str(user1['_id']), + 'event': str(event['_id']) + }, token=self.get_user_token(user1['_id']), status_code=201).json + + self.assertTrue(user1_signup['accepted']) + + # Check that a normal user cannot provide the accepted field + self.api.post('/eventsignups', data={ + 'user': str(user2['_id']), + 'event': str(event['_id']), + 'accepted': True + }, token=self.get_user_token(user2['_id']), status_code=422) + + # Check that admins can always provide the accepted field + user2_signup = self.api.post('/eventsignups', data={ + 'user': str(user2['_id']), + 'event': str(event['_id']), + 'accepted': True + }, token=self.get_root_token(), status_code=201).json + + self.assertTrue(user2_signup['accepted']) class EventsignupQueueTest(WebTestNoAuth): @@ -55,6 +89,15 @@ def test_fcfs_users_get_auto_accepted(self): status_code=200).json self.assertTrue(user2_signup['accepted']) + # post accepted signup as admin + user1_signup = self.api.post('/eventsignups', data={ + 'user': str(user1['_id']), + 'event': str(event['_id']), + 'accepted': True + }, status_code=201).json + + self.assertTrue(user1_signup['accepted']) + def test_fcfs_users_get_auto_accepted_unlimited_spots(self): """Test that with fcfs the users get automatically accepted on signup for events with unlimited spaces""" From f10dc1dfb42259c33c96da59a54648486d645abc Mon Sep 17 00:00:00 2001 From: Lionel Trebuchon Date: Sun, 1 Dec 2019 16:52:39 +0100 Subject: [PATCH 12/13] Trial to trigger the registration confirmation per email --- README.md | 4 ++-- amivapi/events/email_links.py | 10 ++++++---- amivapi/events/templates/unregister_event.html | 2 +- amivapi/settings.py | 5 +++-- amivapi/tests/events/test_emails.py | 6 +++++- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index abffa755..bc22b349 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ variable `AMIVAPI_CONFIG` to specify the config path in the container.) If you have installed AMIV API locally, you can use the CLI to start it. First, change `MONGO_HOST = 'mongodb'` in `config.py` to `'MONGO_HOST = 'localhost'`. -Then, in CLI: +Then, in CLI (with the environment active): ```sh amivapi run dev # Start development server @@ -210,7 +210,7 @@ amivapi run --help If you have docker installed you can simply run the tests in a Docker instance: ```sh -./run_tests.sh +./run_tests.sh # potentially try with sudo ``` By default, this will start a container with mongodb, and run diff --git a/amivapi/events/email_links.py b/amivapi/events/email_links.py index 78b51132..98da250d 100644 --- a/amivapi/events/email_links.py +++ b/amivapi/events/email_links.py @@ -11,7 +11,8 @@ from bson import ObjectId from eve.methods.delete import deleteitem_internal from eve.methods.patch import patch_internal -from flask import Blueprint, current_app, redirect, request, make_response, render_template, g +from flask import Blueprint, current_app, redirect, make_response,\ + render_template from itsdangerous import BadSignature, URLSafeSerializer from amivapi.events.queue import update_waiting_list @@ -78,7 +79,7 @@ def on_delete_signup(token): query = {'_id': signup_id} data_signup = current_app.data.driver.db['eventsignups'].find_one(query) if data_signup is None: - error_msg = "This event might not exist anymore, or the link is broken, or we made a mistake." + error_msg = "This event might not exist anymore or the link is broken." user = data_signup['user'] if user is None: user = data_signup['email'] @@ -95,7 +96,8 @@ def on_delete_signup(token): if data_event["time_start"] is None: event_date = "a yet undefined day." else: - event_date = datetime.strftime(data_event["time_start"], '%Y-%m-%d %H:%M') + event_date = datetime.strftime(data_event["time_start"], + '%Y-%m-%d %H:%M') # Serve the unregister_event page response = make_response(render_template("unregister_event.html", user=user, @@ -107,7 +109,7 @@ def on_delete_signup(token): return response -@email_blueprint.route('/delete_confirmed/', methods = ['POST']) +@email_blueprint.route('/delete_confirmed/', methods=['POST']) def on_delete_confirmed(token): try: s = URLSafeSerializer(get_token_secret()) diff --git a/amivapi/events/templates/unregister_event.html b/amivapi/events/templates/unregister_event.html index 6a60dc31..b0b61142 100644 --- a/amivapi/events/templates/unregister_event.html +++ b/amivapi/events/templates/unregister_event.html @@ -33,7 +33,7 @@

Is that ok? {% else %} You clicked on a opt-out link of the AMIV at ETHZ student organization. We cannot process your request, - because we either do {not} know the event you wish to unregister from, or your user name, or both. + because we either do not know the event you wish to unregister from, or your user name, or both. {% endif %}

diff --git a/amivapi/settings.py b/amivapi/settings.py index 657ca71c..9724606e 100644 --- a/amivapi/settings.py +++ b/amivapi/settings.py @@ -62,13 +62,14 @@ REMOTE_MAILING_LIST_ADDRESS = None REMOTE_MAILING_LIST_KEYFILE = None REMOTE_MAILING_LIST_DIR = './' # Use home directory on remote by default -# Signups via email (@email_blueprint.route('/delete_signup/') in email_links.py) +# Signups via email (@email_blueprint.route('/delete_signup/') +# in email_links.py) # DEFINITIVE_DELETE = '' # SMTP server defaults API_MAIL = 'api@amiv.ethz.ch' API_MAIL_SUBJECT = "[AMIV] {subject}" -SMTP_HOST = 'localhost' # None in case you want to accept that no mails get sent (local testing) +SMTP_HOST = 'localhost' SMTP_PORT = 587 SMTP_TIMEOUT = 10 diff --git a/amivapi/tests/events/test_emails.py b/amivapi/tests/events/test_emails.py index 5a79215e..a635e296 100644 --- a/amivapi/tests/events/test_emails.py +++ b/amivapi/tests/events/test_emails.py @@ -56,7 +56,9 @@ def test_email_signup_delete(self): # With redirect set self.app.config['SIGNUP_DELETED_REDIRECT'] = "somewhere" - self.api.get('/delete_signup/%s' % token, status_code=302) + self.api.get('/eventsignups/%s' % signup['_id'], status_code=200) + self.api.get('/delete_signup/%s' % token, status_code=200) + self.api.post('/delete_confirmed/%s' % token, status_code=302) # Check that signup was deleted self.api.get('/eventsignups/%s' % signup['_id'], status_code=404) @@ -72,7 +74,9 @@ def test_email_signup_delete(self): # Without redirect set self.app.config.pop('SIGNUP_DELETED_REDIRECT') + self.api.get('/eventsignups/%s' % signup['_id'], status_code=200) self.api.get('/delete_signup/%s' % token, status_code=200) + self.api.post('/delete_confirmed/%s' % token, status_code=302) # Check that signup was deleted self.api.get('/eventsignups/%s' % signup['_id'], status_code=404) From 8dbe85f9084393f1eee283acf2cb6ca1008a934e Mon Sep 17 00:00:00 2001 From: Lionel Trebuchon Date: Sun, 1 Dec 2019 18:12:33 +0100 Subject: [PATCH 13/13] Rewrite test_emails --- README.md | 2 +- amivapi/tests/events/test_emails.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bc22b349..c0eec976 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ Now it's time to configure AMIV API. Create a file `config.py` ROOT_PASSWORD = 'root' # MongoDB Configuration -MONGO_HOST = 'mongodb' # or 'localhost' if you run the API from Python +MONGO_HOST = 'mongodb' # or 'localhost' if you run the database locally MONGO_PORT = 27017 MONGO_DBNAME = 'amivapi' MONGO_USERNAME = 'amivapi' diff --git a/amivapi/tests/events/test_emails.py b/amivapi/tests/events/test_emails.py index a635e296..1546d048 100644 --- a/amivapi/tests/events/test_emails.py +++ b/amivapi/tests/events/test_emails.py @@ -76,7 +76,7 @@ def test_email_signup_delete(self): self.app.config.pop('SIGNUP_DELETED_REDIRECT') self.api.get('/eventsignups/%s' % signup['_id'], status_code=200) self.api.get('/delete_signup/%s' % token, status_code=200) - self.api.post('/delete_confirmed/%s' % token, status_code=302) + self.api.post('/delete_confirmed/%s' % token, status_code=200) # Check that signup was deleted self.api.get('/eventsignups/%s' % signup['_id'], status_code=404)