Skip to content

Commit 43ac5c6

Browse files
Merge pull request #6400 from fossasia/development
chore: Release v1.6.0
2 parents f74582e + a85bde9 commit 43ac5c6

File tree

139 files changed

+27826
-26122
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

139 files changed

+27826
-26122
lines changed

.travis.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ before-install:
1919
- npm -g install npm@latest
2020

2121
install:
22-
- npm install -g [email protected]
22+
- npm install -g aglio
23+
- npm install -g [email protected]
2324
- pip3 install -r requirements/tests.txt
2425

2526
env:
@@ -30,6 +31,7 @@ before_script:
3031
- export DATABASE_URL=postgresql://postgres@localhost:5432/test
3132
- export TEST_DATABASE_URL=postgresql://postgres@localhost:5432/test
3233
- bash scripts/test_multiple_heads.sh
34+
- aglio --input docs/api/api_blueprint_source.apib --compile --output docs/api/api_blueprint.apib
3335
- dredd
3436

3537
script:
@@ -38,3 +40,8 @@ script:
3840
after_success:
3941
- 'bash <(curl -s https://codecov.io/bash)'
4042
- bash scripts/push_api_docs.sh
43+
44+
branches:
45+
only:
46+
- master
47+
- development

app/__init__.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from app.api.helpers.cache import cache
3131
from werkzeug.middleware.profiler import ProfilerMiddleware
3232
from app.views import BlueprintsManager
33-
from app.api.helpers.auth import AuthManager
33+
from app.api.helpers.auth import AuthManager, is_token_blacklisted
3434
from app.api.helpers.scheduled_jobs import send_after_event_mail, send_event_fee_notification, \
3535
send_event_fee_notification_followup, change_session_state_on_event_completion, \
3636
expire_pending_tickets, send_monthly_event_invoice, event_invoices_mark_due
@@ -104,9 +104,16 @@ def create_app():
104104
# set up jwt
105105
app.config['JWT_HEADER_TYPE'] = 'JWT'
106106
app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(days=1)
107+
app.config['JWT_REFRESH_TOKEN_EXPIRES'] = timedelta(days=365)
107108
app.config['JWT_ERROR_MESSAGE_KEY'] = 'error'
109+
app.config['JWT_TOKEN_LOCATION'] = ['cookies', 'headers']
110+
app.config['JWT_REFRESH_COOKIE_PATH'] = '/v1/auth/token/refresh'
111+
app.config['JWT_SESSION_COOKIE'] = False
112+
app.config['JWT_BLACKLIST_ENABLED'] = True
113+
app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = ['refresh']
108114
_jwt = JWTManager(app)
109115
_jwt.user_loader_callback_loader(jwt_user_loader)
116+
_jwt.token_in_blacklist_loader(is_token_blacklisted)
110117

111118
# setup celery
112119
app.config['CELERY_BROKER_URL'] = app.config['REDIS_URL']
@@ -240,7 +247,7 @@ def update_sent_state(sender=None, headers=None, **kwargs):
240247

241248
scheduler.add_job(send_after_event_mail, 'cron', hour=5, minute=30)
242249
scheduler.add_job(send_event_fee_notification, 'cron', day=1)
243-
scheduler.add_job(send_event_fee_notification_followup, 'cron', day=15)
250+
scheduler.add_job(send_event_fee_notification_followup, 'cron', day=1, month='1-12')
244251
scheduler.add_job(change_session_state_on_event_completion, 'cron', hour=5, minute=30)
245252
scheduler.add_job(expire_pending_tickets, 'cron', minute=45)
246253
scheduler.add_job(send_monthly_event_invoice, 'cron', day=1, month='1-12')

app/api/__init__.py

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
AttendeeRelationshipRequired, AttendeeListPost
1717
from app.api.bootstrap import api
1818
from app.api.custom_forms import CustomFormList, CustomFormListPost, CustomFormDetail, CustomFormRelationshipRequired
19+
from app.api.custom_form_options import CustomFormOptionList, CustomFormOptionDetail, CustomFormOptionRelationship
1920
from app.api.custom_placeholders import CustomPlaceholderList, CustomPlaceholderDetail, CustomPlaceholderRelationship
2021
from app.api.custom_system_roles import CustomSystemRoleList, CustomSystemRoleDetail, CustomSystemRoleRelationship
2122
from app.api.discount_codes import DiscountCodeList, DiscountCodeDetail, DiscountCodeRelationshipOptional, \
@@ -89,11 +90,12 @@
8990
# users
9091
api.route(UserList, 'user_list', '/users', '/events/<int:event_id>/organizers')
9192
api.route(UserDetail, 'user_detail', '/users/<int:id>', '/notifications/<int:notification_id>/user',
92-
'/event-invoices/<int:event_invoice_id>/user', '/speakers/<int:speaker_id>/user',
93+
'/event-invoices/<int:event_invoice_id>/user', '/event-invoices/<event_invoice_identifier>/user',
9394
'/access-codes/<int:access_code_id>/marketer', '/email-notifications/<int:email_notification_id>/user',
9495
'/discount-codes/<int:discount_code_id>/marketer', '/sessions/<int:session_id>/creator',
9596
'/attendees/<int:attendee_id>/user', '/feedbacks/<int:feedback_id>/user', '/events/<int:event_id>/owner',
96-
'/alternate-emails/<int:user_email_id>/user', '/favourite-events/<int:user_favourite_event_id>/user')
97+
'/alternate-emails/<int:user_email_id>/user', '/favourite-events/<int:user_favourite_event_id>/user',
98+
'/speakers/<int:speaker_id>/user')
9799
api.route(UserRelationship, 'user_notification', '/users/<int:id>/relationships/notifications')
98100
api.route(UserRelationship, 'user_feedback', '/users/<int:id>/relationships/feedbacks')
99101
api.route(UserRelationship, 'user_event_invoices', '/users/<int:id>/relationships/event-invoices')
@@ -261,14 +263,14 @@
261263
'/sponsors/<int:sponsor_id>/event', '/tracks/<int:track_id>/event',
262264
'/speakers-calls/<int:speakers_call_id>/event', '/session-types/<int:session_type_id>/event',
263265
'/event-copyrights/<int:copyright_id>/event', '/tax/<int:tax_id>/event',
264-
'/event-invoices/<int:event_invoice_id>/event', '/discount-codes/<int:discount_code_id>/event',
266+
'/event-invoices/<int:event_invoice_id>/event', '/event-invoices/<event_invoice_identifier>/event',
265267
'/sessions/<int:session_id>/event', '/ticket-tags/<int:ticket_tag_id>/event',
266268
'/role-invites/<int:role_invite_id>/event', '/speakers/<int:speaker_id>/event',
267269
'/access-codes/<int:access_code_id>/event', '/email-notifications/<int:email_notification_id>/event',
268270
'/attendees/<int:attendee_id>/event', '/custom-forms/<int:custom_form_id>/event',
269271
'/orders/<order_identifier>/event', '/faqs/<int:faq_id>/event', '/faq-types/<int:faq_type_id>/event',
270272
'/feedbacks/<int:feedback_id>/event', '/stripe-authorizations/<int:stripe_authorization_id>/event',
271-
'/user-favourite-events/<int:user_favourite_event_id>/event')
273+
'/user-favourite-events/<int:user_favourite_event_id>/event', '/discount-codes/<int:discount_code_id>/event')
272274
api.route(EventRelationship, 'event_ticket', '/events/<int:id>/relationships/tickets',
273275
'/events/<identifier>/relationships/tickets')
274276
api.route(EventRelationship, 'event_ticket_tag', '/events/<int:id>/relationships/ticket-tags',
@@ -465,15 +467,20 @@
465467
# event invoices
466468
api.route(EventInvoiceList, 'event_invoice_list', '/event-invoices', '/events/<int:event_id>/event-invoices',
467469
'/events/<event_identifier>/event-invoices', '/users/<int:user_id>/event-invoices')
468-
api.route(EventInvoiceDetail, 'event_invoice_detail', '/event-invoices/<int:id>')
470+
api.route(EventInvoiceDetail, 'event_invoice_detail', '/event-invoices/<int:id>',
471+
'/event-invoices/<event_invoice_identifier>')
469472
api.route(EventInvoiceRelationshipRequired, 'event_invoice_user',
470-
'/event-invoices/<int:id>/relationships/user')
473+
'/event-invoices/<int:id>/relationships/user',
474+
'/event-invoices/<event_invoice_identifier>/relationships/user')
471475
api.route(EventInvoiceRelationshipRequired, 'event_invoice_event',
472-
'/event-invoices/<int:id>/relationships/event')
476+
'/event-invoices/<int:id>/relationships/event',
477+
'/event-invoices/<event_invoice_identifier>/relationships/event')
473478
api.route(EventInvoiceRelationshipRequired, 'event_invoice_order',
474-
'/event-invoices/<int:id>/relationships/order')
479+
'/event-invoices/<int:id>/relationships/order',
480+
'/event-invoices/<event_invoice_identifier>/relationships/order')
475481
api.route(EventInvoiceRelationshipOptional, 'event_invoice_discount_code',
476-
'/event-invoices/<int:id>/relationships/discount-code')
482+
'/event-invoices/<int:id>/relationships/discount-code',
483+
'/event-invoices/<event_invoice_identifier>/relationships/discount-code')
477484

478485
# discount codes
479486
api.route(DiscountCodeListPost, 'discount_code_list_post', '/discount-codes')
@@ -483,6 +490,7 @@
483490
api.route(DiscountCodeDetail, 'discount_code_detail', '/discount-codes/<int:id>',
484491
'/events/<int:event_id>/discount-code', '/event-invoices/<int:event_invoice_id>/discount-code',
485492
'/events/<int:discount_event_id>/discount-codes/<code>',
493+
'/event-invoices/<event_invoice_identifier>/discount-code',
486494
'/events/<discount_event_identifier>/discount-codes/<code>')
487495
api.route(DiscountCodeRelationshipRequired, 'discount_code_event',
488496
'/discount-codes/<int:id>/relationships/event')
@@ -568,10 +576,17 @@
568576
api.route(CustomFormListPost, 'custom_form_list_post', '/custom-forms')
569577
api.route(CustomFormList, 'custom_form_list', '/events/<int:event_id>/custom-forms',
570578
'/events/<event_identifier>/custom-forms')
571-
api.route(CustomFormDetail, 'custom_form_detail', '/custom-forms/<int:id>')
579+
api.route(CustomFormDetail, 'custom_form_detail', '/custom-forms/<int:id>',
580+
'/custom-form-options/<int:custom_form_option_id>/custom-form')
572581
api.route(CustomFormRelationshipRequired, 'custom_form_event',
573582
'/custom-forms/<int:id>/relationships/event')
574583

584+
# custom form options
585+
api.route(CustomFormOptionList, 'custom_form_option_list', '/custom-forms/<int:custom_form_id>/custom-form-options')
586+
api.route(CustomFormOptionDetail, 'custom_form_option_detail', '/custom-form-options/<int:id>')
587+
api.route(CustomFormOptionRelationship, 'custom_form_option_form',
588+
'/custom-form-options/<int:id>/relationships/custom-form')
589+
575590
# FAQ
576591
api.route(FaqListPost, 'faq_list_post', '/faqs')
577592
api.route(FaqList, 'faq_list', '/events/<int:event_id>/faqs', '/events/<event_identifier>/faqs',

app/api/attendees.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -179,11 +179,6 @@ def before_update_object(self, obj, data, kwargs):
179179
raise UnprocessableEntity(
180180
{'pointer': '/data/relationships/ticket'}, "Invalid Ticket"
181181
)
182-
if not user.is_verified and ticket.price == 0:
183-
raise UnprocessableEntity(
184-
{'pointer': '/data/relationships/ticket'},
185-
"Unverified user cannot buy free tickets"
186-
)
187182

188183
if 'device_name_checkin' in data:
189184
if 'checkin_times' not in data or data['checkin_times'] is None:

app/api/auth.py

Lines changed: 84 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import base64
2-
import base64
32
import logging
43
import random
54
import string
5+
from datetime import timedelta
66
from functools import wraps
77

88
import requests
99
from flask import request, jsonify, make_response, Blueprint, send_file
10-
from flask_jwt_extended import jwt_required, current_user, create_access_token
10+
from flask_jwt_extended import (
11+
jwt_required, jwt_refresh_token_required,
12+
fresh_jwt_required, unset_jwt_cookies,
13+
current_user, create_access_token,
14+
create_refresh_token, set_refresh_cookies,
15+
get_jwt_identity)
1116
from flask_limiter.util import get_remote_address
1217
from healthcheck import EnvironmentDump
1318
from flask_rest_jsonapi.exceptions import ObjectNotFound
@@ -16,24 +21,26 @@
1621
from app import get_settings
1722
from app import limiter
1823
from app.api.helpers.db import save_to_db, get_count, safe_query
19-
from app.api.helpers.auth import AuthManager
24+
from app.api.helpers.auth import AuthManager, blacklist_token
2025
from app.api.helpers.jwt import jwt_authenticate
2126
from app.api.helpers.errors import ForbiddenError, UnprocessableEntityError, NotFoundError, BadRequestError
2227
from app.api.helpers.files import make_frontend_url
2328
from app.api.helpers.mail import send_email_to_attendees
2429
from app.api.helpers.mail import send_email_with_action, \
2530
send_email_confirmation
2631
from app.api.helpers.notification import send_notification_with_action
27-
from app.api.helpers.order import create_pdf_tickets_for_holder
32+
from app.api.helpers.order import create_pdf_tickets_for_holder, calculate_order_amount
2833
from app.api.helpers.storage import UPLOAD_PATHS
2934
from app.api.helpers.storage import generate_hash
3035
from app.api.helpers.third_party_auth import GoogleOAuth, FbOAuth, TwitterOAuth, InstagramOAuth
36+
from app.api.helpers.ticketing import TicketingManager
3137
from app.api.helpers.utilities import get_serializer, str_generator
3238
from app.api.helpers.permission_manager import has_access
3339
from app.models import db
3440
from app.models.mail import PASSWORD_RESET, PASSWORD_CHANGE, \
3541
PASSWORD_RESET_AND_VERIFY
3642
from app.models.notification import PASSWORD_CHANGE as PASSWORD_CHANGE_NOTIF
43+
from app.models.discount_code import DiscountCode
3744
from app.models.order import Order
3845
from app.models.user import User
3946
from app.models.event_invoice import EventInvoice
@@ -45,9 +52,7 @@
4552
auth_routes = Blueprint('auth', __name__, url_prefix='/v1/auth')
4653

4754

48-
@authorised_blueprint.route('/auth/session', methods=['POST'])
49-
@auth_routes.route('/login', methods=['POST'])
50-
def login():
55+
def authenticate(allow_refresh_token=False, existing_identity=None):
5156
data = request.get_json()
5257
username = data.get('email', data.get('username'))
5358
password = data.get('password')
@@ -57,13 +62,64 @@ def login():
5762
return jsonify(error='username or password missing'), 400
5863

5964
identity = jwt_authenticate(username, password)
60-
61-
if identity:
62-
access_token = create_access_token(identity.id, fresh=True)
63-
return jsonify(access_token=access_token)
64-
else:
65+
if not identity or (existing_identity and identity != existing_identity):
66+
# For fresh login, credentials should match existing user
6567
return jsonify(error='Invalid Credentials'), 401
6668

69+
remember_me = data.get('remember-me')
70+
include_in_response = data.get('include-in-response')
71+
add_refresh_token = allow_refresh_token and remember_me
72+
73+
expiry_time = timedelta(minutes=90) if add_refresh_token else None
74+
access_token = create_access_token(identity.id, fresh=True, expires_delta=expiry_time)
75+
response_data = {'access_token': access_token}
76+
77+
if add_refresh_token:
78+
refresh_token = create_refresh_token(identity.id)
79+
if include_in_response:
80+
response_data['refresh_token'] = refresh_token
81+
82+
response = jsonify(response_data)
83+
84+
if add_refresh_token and not include_in_response:
85+
set_refresh_cookies(response, refresh_token)
86+
87+
return response
88+
89+
90+
@authorised_blueprint.route('/auth/session', methods=['POST'])
91+
@auth_routes.route('/login', methods=['POST'])
92+
def login():
93+
return authenticate(allow_refresh_token=True)
94+
95+
96+
@auth_routes.route('/fresh-login', methods=['POST'])
97+
@jwt_required
98+
def fresh_login():
99+
return authenticate(existing_identity=current_user)
100+
101+
102+
@auth_routes.route('/token/refresh', methods=['POST'])
103+
@jwt_refresh_token_required
104+
def refresh_token():
105+
current_user = get_jwt_identity()
106+
new_token = create_access_token(identity=current_user, fresh=False)
107+
return jsonify({'access_token': new_token})
108+
109+
110+
@auth_routes.route('/logout', methods=['POST'])
111+
def logout():
112+
response = jsonify({'success': True})
113+
unset_jwt_cookies(response)
114+
return response
115+
116+
117+
@auth_routes.route('/blacklist', methods=['POST'])
118+
@jwt_required
119+
def blacklist_token_rquest():
120+
blacklist_token(current_user)
121+
return jsonify({'success': True})
122+
67123

68124
@auth_routes.route('/oauth/<provider>', methods=['GET'])
69125
def redirect_uri(provider):
@@ -290,7 +346,7 @@ def reset_password_patch():
290346

291347

292348
@auth_routes.route('/change-password', methods=['POST'])
293-
@jwt_required
349+
@fresh_jwt_required
294350
def change_password():
295351
old_password = request.json['data']['old-password']
296352
new_password = request.json['data']['new-password']
@@ -449,3 +505,18 @@ def resend_emails():
449505
"Only placed and completed orders have confirmation").respond()
450506
else:
451507
return ForbiddenError({'source': ''}, "Co-Organizer Access Required").respond()
508+
509+
510+
@ticket_blueprint.route('/orders/calculate-amount', methods=['POST'])
511+
@jwt_required
512+
def calculate_amount():
513+
data = request.get_json()
514+
tickets = data['tickets']
515+
discount_code = None
516+
if 'discount-code' in data:
517+
discount_code_id = data['discount-code']
518+
discount_code = safe_query(db, DiscountCode, 'id', discount_code_id, 'id')
519+
if not TicketingManager.match_discount_quantity(discount_code, tickets, None):
520+
return UnprocessableEntityError({'source': 'discount-code'}, 'Discount Usage Exceeded').respond()
521+
522+
return jsonify(calculate_order_amount(tickets, discount_code))

app/api/custom_form_options.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from flask_rest_jsonapi import ResourceDetail, ResourceList, ResourceRelationship
2+
from app.models import db
3+
from app.api.schema.custom_form_options import CustomFormOptionSchema
4+
from app.models.custom_form_option import CustomFormOptions
5+
6+
7+
class CustomFormOptionList(ResourceList):
8+
"""
9+
Create and List Custom Form Options
10+
"""
11+
12+
def query(self, view_kwargs):
13+
query_ = self.session.query(CustomFormOptions)
14+
if view_kwargs.get('custom_form_id'):
15+
query_ = self.session.query(CustomFormOptions).filter(
16+
getattr(CustomFormOptions, 'custom_form_id') == view_kwargs['custom_form_id'])
17+
return query_
18+
19+
schema = CustomFormOptionSchema
20+
data_layer = {'session': db.session,
21+
'model': CustomFormOptions,
22+
'methods': {
23+
'query': query
24+
}}
25+
26+
27+
class CustomFormOptionDetail(ResourceDetail):
28+
"""
29+
CustomForm Resource
30+
"""
31+
32+
schema = CustomFormOptionSchema
33+
data_layer = {'session': db.session,
34+
'model': CustomFormOptions
35+
}
36+
37+
38+
class CustomFormOptionRelationship(ResourceRelationship):
39+
"""
40+
CustomForm Resource
41+
"""
42+
43+
schema = CustomFormOptionSchema
44+
data_layer = {'session': db.session,
45+
'model': CustomFormOptions
46+
}

app/api/discount_codes.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,14 @@ def before_get_object(self, view_kwargs):
276276
else:
277277
view_kwargs['id'] = None
278278

279+
if view_kwargs.get('event_invoice_identifier') and has_access('is_admin'):
280+
event_invoice = safe_query(self, EventInvoice, 'identifier', view_kwargs['event_invoice_identifier'],
281+
'event_invoice_identifier')
282+
if event_invoice.discount_code_id:
283+
view_kwargs['id'] = event_invoice.discount_code_id
284+
else:
285+
view_kwargs['id'] = None
286+
279287
if view_kwargs.get('id'):
280288
try:
281289
discount = self.session.query(

0 commit comments

Comments
 (0)