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..c0eec976 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). @@ -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 service create \ --name mongodb -p 27017:27017 --network backend\ -e MONGODB_DATABASE=amivapi \ @@ -102,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 database locally MONGO_PORT = 27017 MONGO_DBNAME = 'amivapi' MONGO_USERNAME = 'amivapi' @@ -152,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 @@ -169,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 @@ -176,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: +If you have installed AMIV API locally, you can use the CLI to start it. -```sh -# Start development server -amivapi run dev +First, change `MONGO_HOST = 'mongodb'` in `config.py` to `'MONGO_HOST = 'localhost'`. +Then, in CLI (with the environment active): -# Start production server (requires the `bjoern` package) -amivapi run prod - -# 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 ``` @@ -204,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 80180317..98da250d 100644 --- a/amivapi/events/email_links.py +++ b/amivapi/events/email_links.py @@ -7,16 +7,18 @@ Needed when external users want to sign up for public events or users want to sign off via links. """ +from datetime import datetime 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, make_response,\ + render_template from itsdangerous import BadSignature, URLSafeSerializer 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): @@ -66,12 +68,58 @@ def on_delete_signup(token): try: s = URLSafeSerializer(get_token_secret()) signup_id = ObjectId(s.loads(token)) + + except BadSignature: + return "Unknown token" + + # Verify if user confirmed + # definitive = request.args.get('DEFINITIVE_DELETE') + # Get first name for personal greeting + 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." + 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"] + 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"] + 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, + 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/', methods=['POST']) +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) diff --git a/amivapi/events/model.py b/amivapi/events/model.py index 65cd8d2a..267e0f5e 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 new file mode 100644 index 00000000..b0b61142 --- /dev/null +++ b/amivapi/events/templates/unregister_event.html @@ -0,0 +1,47 @@ + + + + + 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}} 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, + 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 59dc669b..9724606e 100644 --- a/amivapi/settings.py +++ b/amivapi/settings.py @@ -62,6 +62,9 @@ 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' diff --git a/amivapi/tests/events/test_emails.py b/amivapi/tests/events/test_emails.py index 5a79215e..1546d048 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=200) # Check that signup was deleted self.api.get('/eventsignups/%s' % signup['_id'], status_code=404)