Skip to content

Commit

Permalink
chore: Release 1.9.0 (#6631)
Browse files Browse the repository at this point in the history
chore: Release 1.9.0
  • Loading branch information
iamareebjamal authored Nov 28, 2019
2 parents 997edf3 + f3bb95c commit 36e20e9
Show file tree
Hide file tree
Showing 18 changed files with 322 additions and 274 deletions.
13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
## Changelog

##### v1.8.0 (Unreleased):
##### v1.9.0 (2019-11-28):

- Fix billing info requirements from attendees
- Fix stripe connection issue in event wizard
- Check proper access permissions for event exporting API


##### v1.8.0 (2019-11-24):

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

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

Expand Down
9 changes: 7 additions & 2 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,11 +146,14 @@ def create_app():
from app.api.users import user_misc_routes
from app.api.orders import order_misc_routes
from app.api.role_invites import role_invites_misc_routes
from app.api.auth import ticket_blueprint, authorised_blueprint
from app.api.auth import authorised_blueprint
from app.api.admin_translations import admin_blueprint
from app.api.orders import alipay_blueprint
from app.api.settings import admin_misc_routes
from app.api.server_version import info_route
from app.api.custom.orders import ticket_blueprint
from app.api.custom.orders import order_blueprint
from app.api.custom.invoices import event_blueprint

app.register_blueprint(api_v1)
app.register_blueprint(event_copy)
Expand All @@ -164,12 +167,14 @@ def create_app():
app.register_blueprint(attendee_misc_routes)
app.register_blueprint(order_misc_routes)
app.register_blueprint(role_invites_misc_routes)
app.register_blueprint(ticket_blueprint)
app.register_blueprint(authorised_blueprint)
app.register_blueprint(admin_blueprint)
app.register_blueprint(alipay_blueprint)
app.register_blueprint(admin_misc_routes)
app.register_blueprint(info_route)
app.register_blueprint(ticket_blueprint)
app.register_blueprint(order_blueprint)
app.register_blueprint(event_blueprint)

add_engine_pidguard(db.engine)

Expand Down
131 changes: 2 additions & 129 deletions app/api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,40 +15,29 @@
get_jwt_identity)
from flask_limiter.util import get_remote_address
from healthcheck import EnvironmentDump
from flask_rest_jsonapi.exceptions import ObjectNotFound
from sqlalchemy.orm.exc import NoResultFound

from app import get_settings
from app import limiter
from app.api.helpers.db import save_to_db, get_count, safe_query
from app.api.helpers.db import save_to_db, get_count
from app.api.helpers.auth import AuthManager, blacklist_token
from app.api.helpers.jwt import jwt_authenticate
from app.api.helpers.errors import ForbiddenError, UnprocessableEntityError, NotFoundError, BadRequestError
from app.api.helpers.errors import UnprocessableEntityError, NotFoundError, BadRequestError
from app.api.helpers.files import make_frontend_url
from app.api.helpers.mail import send_email_to_attendees
from app.api.helpers.mail import send_email_with_action, \
send_email_confirmation
from app.api.helpers.notification import send_notification_with_action
from app.api.helpers.order import create_pdf_tickets_for_holder, calculate_order_amount
from app.api.helpers.storage import UPLOAD_PATHS
from app.api.helpers.storage import generate_hash
from app.api.helpers.third_party_auth import GoogleOAuth, FbOAuth, TwitterOAuth, InstagramOAuth
from app.api.helpers.ticketing import TicketingManager
from app.api.helpers.utilities import get_serializer, str_generator
from app.api.helpers.permission_manager import has_access
from app.models import db
from app.models.mail import PASSWORD_RESET, PASSWORD_CHANGE, \
PASSWORD_RESET_AND_VERIFY
from app.models.notification import PASSWORD_CHANGE as PASSWORD_CHANGE_NOTIF
from app.models.discount_code import DiscountCode
from app.models.order import Order
from app.models.user import User
from app.models.event_invoice import EventInvoice


logger = logging.getLogger(__name__)
authorised_blueprint = Blueprint('authorised_blueprint', __name__, url_prefix='/')
ticket_blueprint = Blueprint('ticket_blueprint', __name__, url_prefix='/v1')
auth_routes = Blueprint('auth', __name__, url_prefix='/v1/auth')


Expand Down Expand Up @@ -386,72 +375,6 @@ def return_file(file_name_prefix, file_path, identifier):
return response


@ticket_blueprint.route('/tickets/<string:order_identifier>')
@jwt_required
def ticket_attendee_authorized(order_identifier):
if current_user:
try:
order = Order.query.filter_by(identifier=order_identifier).first()
except NoResultFound:
return NotFoundError({'source': ''}, 'This ticket is not associated with any order').respond()
if current_user.can_download_tickets(order):
key = UPLOAD_PATHS['pdf']['tickets_all'].format(identifier=order_identifier)
file_path = '../generated/tickets/{}/{}/'.format(key, generate_hash(key)) + order_identifier + '.pdf'
try:
return return_file('ticket', file_path, order_identifier)
except FileNotFoundError:
create_pdf_tickets_for_holder(order)
return return_file('ticket', file_path, order_identifier)
else:
return ForbiddenError({'source': ''}, 'Unauthorized Access').respond()
else:
return ForbiddenError({'source': ''}, 'Authentication Required to access ticket').respond()


@ticket_blueprint.route('/orders/invoices/<string:order_identifier>')
@jwt_required
def order_invoices(order_identifier):
if current_user:
try:
order = Order.query.filter_by(identifier=order_identifier).first()
except NoResultFound:
return NotFoundError({'source': ''}, 'Order Invoice not found').respond()
if current_user.can_download_tickets(order):
key = UPLOAD_PATHS['pdf']['order'].format(identifier=order_identifier)
file_path = '../generated/invoices/{}/{}/'.format(key, generate_hash(key)) + order_identifier + '.pdf'
try:
return return_file('invoice', file_path, order_identifier)
except FileNotFoundError:
create_pdf_tickets_for_holder(order)
return return_file('invoice', file_path, order_identifier)
else:
return ForbiddenError({'source': ''}, 'Unauthorized Access').respond()
else:
return ForbiddenError({'source': ''}, 'Authentication Required to access Invoice').respond()


@ticket_blueprint.route('/events/invoices/<string:invoice_identifier>')
@jwt_required
def event_invoices(invoice_identifier):
if not current_user:
return ForbiddenError({'source': ''}, 'Authentication Required to access Invoice').respond()
try:
event_invoice = EventInvoice.query.filter_by(identifier=invoice_identifier).first()
event_id = event_invoice.event_id
except NoResultFound:
return NotFoundError({'source': ''}, 'Event Invoice not found').respond()
if not current_user.is_organizer(event_id) and not current_user.is_staff:
return ForbiddenError({'source': ''}, 'Unauthorized Access').respond()
key = UPLOAD_PATHS['pdf']['event_invoices'].format(identifier=invoice_identifier)
file_path = '../generated/invoices/{}/{}/'.format(key, generate_hash(key)) + invoice_identifier + '.pdf'
try:
return return_file('event-invoice', file_path, invoice_identifier)
except FileNotFoundError:
raise ObjectNotFound({'source': ''},
"The Event Invoice isn't available at the moment. \
Invoices are usually issued on the 1st of every month")


# Access for Environment details & Basic Auth Support
def requires_basic_auth(f):
@wraps(f)
Expand All @@ -470,53 +393,3 @@ def decorated(*args, **kwargs):
def environment_details():
envdump = EnvironmentDump(include_config=False)
return envdump.dump_environment()


@ticket_blueprint.route('/orders/resend-email', methods=['POST'])
@limiter.limit(
'5/minute', key_func=lambda: request.json['data']['user'], error_message='Limit for this action exceeded'
)
@limiter.limit(
'60/minute', key_func=get_remote_address, error_message='Limit for this action exceeded'
)
def resend_emails():
"""
Sends confirmation email for pending and completed orders on organizer request
:param order_identifier:
:return: JSON response if the email was succesfully sent
"""
order_identifier = request.json['data']['order']
order = safe_query(db, Order, 'identifier', order_identifier, 'identifier')
if (has_access('is_coorganizer', event_id=order.event_id)):
if order.status == 'completed' or order.status == 'placed':
# fetch tickets attachment
order_identifier = order.identifier
key = UPLOAD_PATHS['pdf']['tickets_all'].format(identifier=order_identifier)
ticket_path = 'generated/tickets/{}/{}/'.format(key, generate_hash(key)) + order_identifier + '.pdf'
key = UPLOAD_PATHS['pdf']['order'].format(identifier=order_identifier)
invoice_path = 'generated/invoices/{}/{}/'.format(key, generate_hash(key)) + order_identifier + '.pdf'

# send email.
send_email_to_attendees(order=order, purchaser_id=current_user.id, attachments=[ticket_path, invoice_path])
return jsonify(status=True, message="Verification emails for order : {} has been sent succesfully".
format(order_identifier))
else:
return UnprocessableEntityError({'source': 'data/order'},
"Only placed and completed orders have confirmation").respond()
else:
return ForbiddenError({'source': ''}, "Co-Organizer Access Required").respond()


@ticket_blueprint.route('/orders/calculate-amount', methods=['POST'])
@jwt_required
def calculate_amount():
data = request.get_json()
tickets = data['tickets']
discount_code = None
if 'discount-code' in data:
discount_code_id = data['discount-code']
discount_code = safe_query(db, DiscountCode, 'id', discount_code_id, 'id')
if not TicketingManager.match_discount_quantity(discount_code, tickets, None):
return UnprocessableEntityError({'source': 'discount-code'}, 'Discount Usage Exceeded').respond()

return jsonify(calculate_order_amount(tickets, discount_code))
59 changes: 59 additions & 0 deletions app/api/custom/invoices.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from flask import Blueprint
from flask_rest_jsonapi.exceptions import ObjectNotFound
from flask_jwt_extended import current_user, jwt_required
from sqlalchemy.orm.exc import NoResultFound


from app.api.auth import return_file
from app.api.helpers.errors import ForbiddenError, NotFoundError
from app.api.helpers.order import create_pdf_tickets_for_holder
from app.api.helpers.storage import UPLOAD_PATHS, generate_hash
from app.models.order import Order
from app.models.event_invoice import EventInvoice
from app.api.custom.orders import order_blueprint

event_blueprint = Blueprint('event_blueprint', __name__, url_prefix='/v1/events')


@event_blueprint.route('/invoices/<string:invoice_identifier>')
@jwt_required
def event_invoices(invoice_identifier):
if not current_user:
return ForbiddenError({'source': ''}, 'Authentication Required to access Invoice').respond()
try:
event_invoice = EventInvoice.query.filter_by(identifier=invoice_identifier).first()
event_id = event_invoice.event_id
except NoResultFound:
return NotFoundError({'source': ''}, 'Event Invoice not found').respond()
if not current_user.is_organizer(event_id) and not current_user.is_staff:
return ForbiddenError({'source': ''}, 'Unauthorized Access').respond()
key = UPLOAD_PATHS['pdf']['event_invoices'].format(identifier=invoice_identifier)
file_path = '../generated/invoices/{}/{}/'.format(key, generate_hash(key)) + invoice_identifier + '.pdf'
try:
return return_file('event-invoice', file_path, invoice_identifier)
except FileNotFoundError:
raise ObjectNotFound({'source': ''},
"The Event Invoice isn't available at the moment. \
Invoices are usually issued on the 1st of every month")


@order_blueprint.route('/invoices/<string:order_identifier>')
@jwt_required
def order_invoices(order_identifier):
if current_user:
try:
order = Order.query.filter_by(identifier=order_identifier).first()
except NoResultFound:
return NotFoundError({'source': ''}, 'Order Invoice not found').respond()
if current_user.can_download_tickets(order):
key = UPLOAD_PATHS['pdf']['order'].format(identifier=order_identifier)
file_path = '../generated/invoices/{}/{}/'.format(key, generate_hash(key)) + order_identifier + '.pdf'
try:
return return_file('invoice', file_path, order_identifier)
except FileNotFoundError:
create_pdf_tickets_for_holder(order)
return return_file('invoice', file_path, order_identifier)
else:
return ForbiddenError({'source': ''}, 'Unauthorized Access').respond()
else:
return ForbiddenError({'source': ''}, 'Authentication Required to access Invoice').respond()
95 changes: 95 additions & 0 deletions app/api/custom/orders.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
from flask import Blueprint, jsonify, request
from flask_jwt_extended import current_user, jwt_required
from flask_limiter.util import get_remote_address
from sqlalchemy.orm.exc import NoResultFound


from app import limiter
from app.models import db
from app.api.auth import return_file
from app.api.helpers.db import safe_query
from app.api.helpers.mail import send_email_to_attendees
from app.api.helpers.errors import ForbiddenError, UnprocessableEntityError, NotFoundError
from app.api.helpers.order import calculate_order_amount, create_pdf_tickets_for_holder
from app.api.helpers.storage import UPLOAD_PATHS
from app.api.helpers.storage import generate_hash
from app.api.helpers.ticketing import TicketingManager
from app.api.helpers.permission_manager import has_access
from app.models.discount_code import DiscountCode
from app.models.order import Order

order_blueprint = Blueprint('order_blueprint', __name__, url_prefix='/v1/orders')
ticket_blueprint = Blueprint('ticket_blueprint', __name__, url_prefix='/v1/tickets')


@ticket_blueprint.route('/<string:order_identifier>')
@order_blueprint.route('/<string:order_identifier>/tickets-pdf')
@jwt_required
def ticket_attendee_authorized(order_identifier):
if current_user:
try:
order = Order.query.filter_by(identifier=order_identifier).first()
except NoResultFound:
return NotFoundError({'source': ''}, 'This ticket is not associated with any order').respond()
if current_user.can_download_tickets(order):
key = UPLOAD_PATHS['pdf']['tickets_all'].format(identifier=order_identifier)
file_path = '../generated/tickets/{}/{}/'.format(key, generate_hash(key)) + order_identifier + '.pdf'
try:
return return_file('ticket', file_path, order_identifier)
except FileNotFoundError:
create_pdf_tickets_for_holder(order)
return return_file('ticket', file_path, order_identifier)
else:
return ForbiddenError({'source': ''}, 'Unauthorized Access').respond()
else:
return ForbiddenError({'source': ''}, 'Authentication Required to access ticket').respond()


@order_blueprint.route('/resend-email', methods=['POST'])
@limiter.limit(
'5/minute', key_func=lambda: request.json['data']['user'], error_message='Limit for this action exceeded'
)
@limiter.limit(
'60/minute', key_func=get_remote_address, error_message='Limit for this action exceeded'
)
def resend_emails():
"""
Sends confirmation email for pending and completed orders on organizer request
:param order_identifier:
:return: JSON response if the email was succesfully sent
"""
order_identifier = request.json['data']['order']
order = safe_query(db, Order, 'identifier', order_identifier, 'identifier')
if (has_access('is_coorganizer', event_id=order.event_id)):
if order.status == 'completed' or order.status == 'placed':
# fetch tickets attachment
order_identifier = order.identifier
key = UPLOAD_PATHS['pdf']['tickets_all'].format(identifier=order_identifier)
ticket_path = 'generated/tickets/{}/{}/'.format(key, generate_hash(key)) + order_identifier + '.pdf'
key = UPLOAD_PATHS['pdf']['order'].format(identifier=order_identifier)
invoice_path = 'generated/invoices/{}/{}/'.format(key, generate_hash(key)) + order_identifier + '.pdf'

# send email.
send_email_to_attendees(order=order, purchaser_id=current_user.id, attachments=[ticket_path, invoice_path])
return jsonify(status=True, message="Verification emails for order : {} has been sent succesfully".
format(order_identifier))
else:
return UnprocessableEntityError({'source': 'data/order'},
"Only placed and completed orders have confirmation").respond()
else:
return ForbiddenError({'source': ''}, "Co-Organizer Access Required").respond()


@order_blueprint.route('/calculate-amount', methods=['POST'])
@jwt_required
def calculate_amount():
data = request.get_json()
tickets = data['tickets']
discount_code = None
if 'discount-code' in data:
discount_code_id = data['discount-code']
discount_code = safe_query(db, DiscountCode, 'id', discount_code_id, 'id')
if not TicketingManager.match_discount_quantity(discount_code, tickets, None):
return UnprocessableEntityError({'source': 'discount-code'}, 'Discount Usage Exceeded').respond()

return jsonify(calculate_order_amount(tickets, discount_code))
Loading

0 comments on commit 36e20e9

Please sign in to comment.