Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Release 1.9.0 #6631

Merged
merged 12 commits into from
Nov 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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