Skip to content

Commit 36e20e9

Browse files
chore: Release 1.9.0 (#6631)
chore: Release 1.9.0
2 parents 997edf3 + f3bb95c commit 36e20e9

File tree

18 files changed

+322
-274
lines changed

18 files changed

+322
-274
lines changed

CHANGELOG.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
11
## Changelog
22

3-
##### v1.8.0 (Unreleased):
3+
##### v1.9.0 (2019-11-28):
4+
5+
- Fix billing info requirements from attendees
6+
- Fix stripe connection issue in event wizard
7+
- Check proper access permissions for event exporting API
8+
9+
10+
##### v1.8.0 (2019-11-24):
411

512
- Run `python manage.py fix_digit_identifier` to correct all digit identifiers
13+
- Handelled invalid price value in paid tickets
14+
- Check if event identifier does not contain of all digits
15+
- Fix check for `is_email_overridden` for speaker form
16+
- Improve test timings
617

718
##### v1.7.0 (2019-10-19):
819

app/__init__.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,11 +146,14 @@ def create_app():
146146
from app.api.users import user_misc_routes
147147
from app.api.orders import order_misc_routes
148148
from app.api.role_invites import role_invites_misc_routes
149-
from app.api.auth import ticket_blueprint, authorised_blueprint
149+
from app.api.auth import authorised_blueprint
150150
from app.api.admin_translations import admin_blueprint
151151
from app.api.orders import alipay_blueprint
152152
from app.api.settings import admin_misc_routes
153153
from app.api.server_version import info_route
154+
from app.api.custom.orders import ticket_blueprint
155+
from app.api.custom.orders import order_blueprint
156+
from app.api.custom.invoices import event_blueprint
154157

155158
app.register_blueprint(api_v1)
156159
app.register_blueprint(event_copy)
@@ -164,12 +167,14 @@ def create_app():
164167
app.register_blueprint(attendee_misc_routes)
165168
app.register_blueprint(order_misc_routes)
166169
app.register_blueprint(role_invites_misc_routes)
167-
app.register_blueprint(ticket_blueprint)
168170
app.register_blueprint(authorised_blueprint)
169171
app.register_blueprint(admin_blueprint)
170172
app.register_blueprint(alipay_blueprint)
171173
app.register_blueprint(admin_misc_routes)
172174
app.register_blueprint(info_route)
175+
app.register_blueprint(ticket_blueprint)
176+
app.register_blueprint(order_blueprint)
177+
app.register_blueprint(event_blueprint)
173178

174179
add_engine_pidguard(db.engine)
175180

app/api/auth.py

Lines changed: 2 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -15,40 +15,29 @@
1515
get_jwt_identity)
1616
from flask_limiter.util import get_remote_address
1717
from healthcheck import EnvironmentDump
18-
from flask_rest_jsonapi.exceptions import ObjectNotFound
1918
from sqlalchemy.orm.exc import NoResultFound
2019

2120
from app import get_settings
2221
from app import limiter
23-
from app.api.helpers.db import save_to_db, get_count, safe_query
22+
from app.api.helpers.db import save_to_db, get_count
2423
from app.api.helpers.auth import AuthManager, blacklist_token
2524
from app.api.helpers.jwt import jwt_authenticate
26-
from app.api.helpers.errors import ForbiddenError, UnprocessableEntityError, NotFoundError, BadRequestError
25+
from app.api.helpers.errors import UnprocessableEntityError, NotFoundError, BadRequestError
2726
from app.api.helpers.files import make_frontend_url
28-
from app.api.helpers.mail import send_email_to_attendees
2927
from app.api.helpers.mail import send_email_with_action, \
3028
send_email_confirmation
3129
from app.api.helpers.notification import send_notification_with_action
32-
from app.api.helpers.order import create_pdf_tickets_for_holder, calculate_order_amount
33-
from app.api.helpers.storage import UPLOAD_PATHS
34-
from app.api.helpers.storage import generate_hash
3530
from app.api.helpers.third_party_auth import GoogleOAuth, FbOAuth, TwitterOAuth, InstagramOAuth
36-
from app.api.helpers.ticketing import TicketingManager
3731
from app.api.helpers.utilities import get_serializer, str_generator
38-
from app.api.helpers.permission_manager import has_access
3932
from app.models import db
4033
from app.models.mail import PASSWORD_RESET, PASSWORD_CHANGE, \
4134
PASSWORD_RESET_AND_VERIFY
4235
from app.models.notification import PASSWORD_CHANGE as PASSWORD_CHANGE_NOTIF
43-
from app.models.discount_code import DiscountCode
44-
from app.models.order import Order
4536
from app.models.user import User
46-
from app.models.event_invoice import EventInvoice
4737

4838

4939
logger = logging.getLogger(__name__)
5040
authorised_blueprint = Blueprint('authorised_blueprint', __name__, url_prefix='/')
51-
ticket_blueprint = Blueprint('ticket_blueprint', __name__, url_prefix='/v1')
5241
auth_routes = Blueprint('auth', __name__, url_prefix='/v1/auth')
5342

5443

@@ -386,72 +375,6 @@ def return_file(file_name_prefix, file_path, identifier):
386375
return response
387376

388377

389-
@ticket_blueprint.route('/tickets/<string:order_identifier>')
390-
@jwt_required
391-
def ticket_attendee_authorized(order_identifier):
392-
if current_user:
393-
try:
394-
order = Order.query.filter_by(identifier=order_identifier).first()
395-
except NoResultFound:
396-
return NotFoundError({'source': ''}, 'This ticket is not associated with any order').respond()
397-
if current_user.can_download_tickets(order):
398-
key = UPLOAD_PATHS['pdf']['tickets_all'].format(identifier=order_identifier)
399-
file_path = '../generated/tickets/{}/{}/'.format(key, generate_hash(key)) + order_identifier + '.pdf'
400-
try:
401-
return return_file('ticket', file_path, order_identifier)
402-
except FileNotFoundError:
403-
create_pdf_tickets_for_holder(order)
404-
return return_file('ticket', file_path, order_identifier)
405-
else:
406-
return ForbiddenError({'source': ''}, 'Unauthorized Access').respond()
407-
else:
408-
return ForbiddenError({'source': ''}, 'Authentication Required to access ticket').respond()
409-
410-
411-
@ticket_blueprint.route('/orders/invoices/<string:order_identifier>')
412-
@jwt_required
413-
def order_invoices(order_identifier):
414-
if current_user:
415-
try:
416-
order = Order.query.filter_by(identifier=order_identifier).first()
417-
except NoResultFound:
418-
return NotFoundError({'source': ''}, 'Order Invoice not found').respond()
419-
if current_user.can_download_tickets(order):
420-
key = UPLOAD_PATHS['pdf']['order'].format(identifier=order_identifier)
421-
file_path = '../generated/invoices/{}/{}/'.format(key, generate_hash(key)) + order_identifier + '.pdf'
422-
try:
423-
return return_file('invoice', file_path, order_identifier)
424-
except FileNotFoundError:
425-
create_pdf_tickets_for_holder(order)
426-
return return_file('invoice', file_path, order_identifier)
427-
else:
428-
return ForbiddenError({'source': ''}, 'Unauthorized Access').respond()
429-
else:
430-
return ForbiddenError({'source': ''}, 'Authentication Required to access Invoice').respond()
431-
432-
433-
@ticket_blueprint.route('/events/invoices/<string:invoice_identifier>')
434-
@jwt_required
435-
def event_invoices(invoice_identifier):
436-
if not current_user:
437-
return ForbiddenError({'source': ''}, 'Authentication Required to access Invoice').respond()
438-
try:
439-
event_invoice = EventInvoice.query.filter_by(identifier=invoice_identifier).first()
440-
event_id = event_invoice.event_id
441-
except NoResultFound:
442-
return NotFoundError({'source': ''}, 'Event Invoice not found').respond()
443-
if not current_user.is_organizer(event_id) and not current_user.is_staff:
444-
return ForbiddenError({'source': ''}, 'Unauthorized Access').respond()
445-
key = UPLOAD_PATHS['pdf']['event_invoices'].format(identifier=invoice_identifier)
446-
file_path = '../generated/invoices/{}/{}/'.format(key, generate_hash(key)) + invoice_identifier + '.pdf'
447-
try:
448-
return return_file('event-invoice', file_path, invoice_identifier)
449-
except FileNotFoundError:
450-
raise ObjectNotFound({'source': ''},
451-
"The Event Invoice isn't available at the moment. \
452-
Invoices are usually issued on the 1st of every month")
453-
454-
455378
# Access for Environment details & Basic Auth Support
456379
def requires_basic_auth(f):
457380
@wraps(f)
@@ -470,53 +393,3 @@ def decorated(*args, **kwargs):
470393
def environment_details():
471394
envdump = EnvironmentDump(include_config=False)
472395
return envdump.dump_environment()
473-
474-
475-
@ticket_blueprint.route('/orders/resend-email', methods=['POST'])
476-
@limiter.limit(
477-
'5/minute', key_func=lambda: request.json['data']['user'], error_message='Limit for this action exceeded'
478-
)
479-
@limiter.limit(
480-
'60/minute', key_func=get_remote_address, error_message='Limit for this action exceeded'
481-
)
482-
def resend_emails():
483-
"""
484-
Sends confirmation email for pending and completed orders on organizer request
485-
:param order_identifier:
486-
:return: JSON response if the email was succesfully sent
487-
"""
488-
order_identifier = request.json['data']['order']
489-
order = safe_query(db, Order, 'identifier', order_identifier, 'identifier')
490-
if (has_access('is_coorganizer', event_id=order.event_id)):
491-
if order.status == 'completed' or order.status == 'placed':
492-
# fetch tickets attachment
493-
order_identifier = order.identifier
494-
key = UPLOAD_PATHS['pdf']['tickets_all'].format(identifier=order_identifier)
495-
ticket_path = 'generated/tickets/{}/{}/'.format(key, generate_hash(key)) + order_identifier + '.pdf'
496-
key = UPLOAD_PATHS['pdf']['order'].format(identifier=order_identifier)
497-
invoice_path = 'generated/invoices/{}/{}/'.format(key, generate_hash(key)) + order_identifier + '.pdf'
498-
499-
# send email.
500-
send_email_to_attendees(order=order, purchaser_id=current_user.id, attachments=[ticket_path, invoice_path])
501-
return jsonify(status=True, message="Verification emails for order : {} has been sent succesfully".
502-
format(order_identifier))
503-
else:
504-
return UnprocessableEntityError({'source': 'data/order'},
505-
"Only placed and completed orders have confirmation").respond()
506-
else:
507-
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/invoices.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from flask import Blueprint
2+
from flask_rest_jsonapi.exceptions import ObjectNotFound
3+
from flask_jwt_extended import current_user, jwt_required
4+
from sqlalchemy.orm.exc import NoResultFound
5+
6+
7+
from app.api.auth import return_file
8+
from app.api.helpers.errors import ForbiddenError, NotFoundError
9+
from app.api.helpers.order import create_pdf_tickets_for_holder
10+
from app.api.helpers.storage import UPLOAD_PATHS, generate_hash
11+
from app.models.order import Order
12+
from app.models.event_invoice import EventInvoice
13+
from app.api.custom.orders import order_blueprint
14+
15+
event_blueprint = Blueprint('event_blueprint', __name__, url_prefix='/v1/events')
16+
17+
18+
@event_blueprint.route('/invoices/<string:invoice_identifier>')
19+
@jwt_required
20+
def event_invoices(invoice_identifier):
21+
if not current_user:
22+
return ForbiddenError({'source': ''}, 'Authentication Required to access Invoice').respond()
23+
try:
24+
event_invoice = EventInvoice.query.filter_by(identifier=invoice_identifier).first()
25+
event_id = event_invoice.event_id
26+
except NoResultFound:
27+
return NotFoundError({'source': ''}, 'Event Invoice not found').respond()
28+
if not current_user.is_organizer(event_id) and not current_user.is_staff:
29+
return ForbiddenError({'source': ''}, 'Unauthorized Access').respond()
30+
key = UPLOAD_PATHS['pdf']['event_invoices'].format(identifier=invoice_identifier)
31+
file_path = '../generated/invoices/{}/{}/'.format(key, generate_hash(key)) + invoice_identifier + '.pdf'
32+
try:
33+
return return_file('event-invoice', file_path, invoice_identifier)
34+
except FileNotFoundError:
35+
raise ObjectNotFound({'source': ''},
36+
"The Event Invoice isn't available at the moment. \
37+
Invoices are usually issued on the 1st of every month")
38+
39+
40+
@order_blueprint.route('/invoices/<string:order_identifier>')
41+
@jwt_required
42+
def order_invoices(order_identifier):
43+
if current_user:
44+
try:
45+
order = Order.query.filter_by(identifier=order_identifier).first()
46+
except NoResultFound:
47+
return NotFoundError({'source': ''}, 'Order Invoice not found').respond()
48+
if current_user.can_download_tickets(order):
49+
key = UPLOAD_PATHS['pdf']['order'].format(identifier=order_identifier)
50+
file_path = '../generated/invoices/{}/{}/'.format(key, generate_hash(key)) + order_identifier + '.pdf'
51+
try:
52+
return return_file('invoice', file_path, order_identifier)
53+
except FileNotFoundError:
54+
create_pdf_tickets_for_holder(order)
55+
return return_file('invoice', file_path, order_identifier)
56+
else:
57+
return ForbiddenError({'source': ''}, 'Unauthorized Access').respond()
58+
else:
59+
return ForbiddenError({'source': ''}, 'Authentication Required to access Invoice').respond()

app/api/custom/orders.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
from flask import Blueprint, jsonify, request
2+
from flask_jwt_extended import current_user, jwt_required
3+
from flask_limiter.util import get_remote_address
4+
from sqlalchemy.orm.exc import NoResultFound
5+
6+
7+
from app import limiter
8+
from app.models import db
9+
from app.api.auth import return_file
10+
from app.api.helpers.db import safe_query
11+
from app.api.helpers.mail import send_email_to_attendees
12+
from app.api.helpers.errors import ForbiddenError, UnprocessableEntityError, NotFoundError
13+
from app.api.helpers.order import calculate_order_amount, create_pdf_tickets_for_holder
14+
from app.api.helpers.storage import UPLOAD_PATHS
15+
from app.api.helpers.storage import generate_hash
16+
from app.api.helpers.ticketing import TicketingManager
17+
from app.api.helpers.permission_manager import has_access
18+
from app.models.discount_code import DiscountCode
19+
from app.models.order import Order
20+
21+
order_blueprint = Blueprint('order_blueprint', __name__, url_prefix='/v1/orders')
22+
ticket_blueprint = Blueprint('ticket_blueprint', __name__, url_prefix='/v1/tickets')
23+
24+
25+
@ticket_blueprint.route('/<string:order_identifier>')
26+
@order_blueprint.route('/<string:order_identifier>/tickets-pdf')
27+
@jwt_required
28+
def ticket_attendee_authorized(order_identifier):
29+
if current_user:
30+
try:
31+
order = Order.query.filter_by(identifier=order_identifier).first()
32+
except NoResultFound:
33+
return NotFoundError({'source': ''}, 'This ticket is not associated with any order').respond()
34+
if current_user.can_download_tickets(order):
35+
key = UPLOAD_PATHS['pdf']['tickets_all'].format(identifier=order_identifier)
36+
file_path = '../generated/tickets/{}/{}/'.format(key, generate_hash(key)) + order_identifier + '.pdf'
37+
try:
38+
return return_file('ticket', file_path, order_identifier)
39+
except FileNotFoundError:
40+
create_pdf_tickets_for_holder(order)
41+
return return_file('ticket', file_path, order_identifier)
42+
else:
43+
return ForbiddenError({'source': ''}, 'Unauthorized Access').respond()
44+
else:
45+
return ForbiddenError({'source': ''}, 'Authentication Required to access ticket').respond()
46+
47+
48+
@order_blueprint.route('/resend-email', methods=['POST'])
49+
@limiter.limit(
50+
'5/minute', key_func=lambda: request.json['data']['user'], error_message='Limit for this action exceeded'
51+
)
52+
@limiter.limit(
53+
'60/minute', key_func=get_remote_address, error_message='Limit for this action exceeded'
54+
)
55+
def resend_emails():
56+
"""
57+
Sends confirmation email for pending and completed orders on organizer request
58+
:param order_identifier:
59+
:return: JSON response if the email was succesfully sent
60+
"""
61+
order_identifier = request.json['data']['order']
62+
order = safe_query(db, Order, 'identifier', order_identifier, 'identifier')
63+
if (has_access('is_coorganizer', event_id=order.event_id)):
64+
if order.status == 'completed' or order.status == 'placed':
65+
# fetch tickets attachment
66+
order_identifier = order.identifier
67+
key = UPLOAD_PATHS['pdf']['tickets_all'].format(identifier=order_identifier)
68+
ticket_path = 'generated/tickets/{}/{}/'.format(key, generate_hash(key)) + order_identifier + '.pdf'
69+
key = UPLOAD_PATHS['pdf']['order'].format(identifier=order_identifier)
70+
invoice_path = 'generated/invoices/{}/{}/'.format(key, generate_hash(key)) + order_identifier + '.pdf'
71+
72+
# send email.
73+
send_email_to_attendees(order=order, purchaser_id=current_user.id, attachments=[ticket_path, invoice_path])
74+
return jsonify(status=True, message="Verification emails for order : {} has been sent succesfully".
75+
format(order_identifier))
76+
else:
77+
return UnprocessableEntityError({'source': 'data/order'},
78+
"Only placed and completed orders have confirmation").respond()
79+
else:
80+
return ForbiddenError({'source': ''}, "Co-Organizer Access Required").respond()
81+
82+
83+
@order_blueprint.route('/calculate-amount', methods=['POST'])
84+
@jwt_required
85+
def calculate_amount():
86+
data = request.get_json()
87+
tickets = data['tickets']
88+
discount_code = None
89+
if 'discount-code' in data:
90+
discount_code_id = data['discount-code']
91+
discount_code = safe_query(db, DiscountCode, 'id', discount_code_id, 'id')
92+
if not TicketingManager.match_discount_quantity(discount_code, tickets, None):
93+
return UnprocessableEntityError({'source': 'discount-code'}, 'Discount Usage Exceeded').respond()
94+
95+
return jsonify(calculate_order_amount(tickets, discount_code))

0 commit comments

Comments
 (0)