From bde88a4532069732b4a60f13e93418bec7b54e77 Mon Sep 17 00:00:00 2001 From: Areeb Jamal Date: Sun, 24 Nov 2019 06:09:46 +0530 Subject: [PATCH 01/11] chore: Update changelog for 1.8.0 --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20a87769b8..071eb134dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,12 @@ ## Changelog -##### v1.8.0 (Unreleased): +##### 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): From 4f32edf69a0528906c9cb1ae654d896bea6c4379 Mon Sep 17 00:00:00 2001 From: Kush Trivedi Date: Mon, 25 Nov 2019 12:34:42 +0530 Subject: [PATCH 02/11] feat: Addition and Updation of billing related fields (#6447) --- app/api/orders.py | 6 +-- app/models/event.py | 2 +- app/models/order.py | 2 +- .../rev-2019-09-15-08:29:32-d1c2b8711223_.py | 40 +++++++++++++++++++ 4 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 migrations/versions/rev-2019-09-15-08:29:32-d1c2b8711223_.py diff --git a/app/api/orders.py b/app/api/orders.py index 94199a0a7f..da91f4c46f 100644 --- a/app/api/orders.py +++ b/app/api/orders.py @@ -71,7 +71,7 @@ def is_payment_valid(order, mode): def check_billing_info(data): if data.get('amount') and data.get('amount') > 0 and not data.get('is_billing_enabled'): raise UnprocessableEntity({'pointer': '/data/attributes/is_billing_enabled'}, - "Billing information is mandatory for paid orders") + "Billing information is mandatory for this order") if data.get('is_billing_enabled') and not (data.get('company') and data.get('address') and data.get('city') and data.get('zipcode') and data.get('country')): raise UnprocessableEntity({'pointer': '/data/attributes/is_billing_enabled'}, @@ -111,8 +111,6 @@ def before_create_object(self, data, view_kwargs): :param view_kwargs: :return: """ - if data.get('amount') > 0 and not data.get('is_billing_enabled'): - data['is_billing_enabled'] = True free_ticket_quantity = 0 @@ -319,7 +317,7 @@ def before_update_object(self, order, data, view_kwargs): :param view_kwargs: :return: """ - if data.get('amount') or data.get('is_billing_enabled'): + if data.get('amount') and (data.get('is_billing_enabled') or order.event.is_billing_info_mandatory): check_billing_info(data) if (not has_access('is_coorganizer', event_id=order.event_id)) and (not current_user.id == order.user_id): raise ForbiddenException({'pointer': ''}, "Access Forbidden") diff --git a/app/models/event.py b/app/models/event.py index d30e5bea32..0a2492b9c7 100644 --- a/app/models/event.py +++ b/app/models/event.py @@ -97,7 +97,7 @@ class Event(SoftDeletionModel): payment_currency = db.Column(db.String) paypal_email = db.Column(db.String) is_tax_enabled = db.Column(db.Boolean, default=False) - is_billing_info_mandatory = db.Column(db.Boolean, default=False) + is_billing_info_mandatory = db.Column(db.Boolean, default=False, nullable=False) can_pay_by_paypal = db.Column(db.Boolean, default=False, nullable=False) can_pay_by_stripe = db.Column(db.Boolean, default=False, nullable=False) can_pay_by_cheque = db.Column(db.Boolean, default=False, nullable=False) diff --git a/app/models/order.py b/app/models/order.py index c6e3b634e5..07982c3592 100644 --- a/app/models/order.py +++ b/app/models/order.py @@ -55,7 +55,7 @@ class Order(SoftDeletionModel): transaction_id = db.Column(db.String) paid_via = db.Column(db.String) payment_mode = db.Column(db.String) - is_billing_enabled = db.Column(db.Boolean) + is_billing_enabled = db.Column(db.Boolean, nullable=False, default=False) brand = db.Column(db.String) exp_month = db.Column(db.Integer) exp_year = db.Column(db.Integer) diff --git a/migrations/versions/rev-2019-09-15-08:29:32-d1c2b8711223_.py b/migrations/versions/rev-2019-09-15-08:29:32-d1c2b8711223_.py new file mode 100644 index 0000000000..1111a33712 --- /dev/null +++ b/migrations/versions/rev-2019-09-15-08:29:32-d1c2b8711223_.py @@ -0,0 +1,40 @@ +"""empty message + +Revision ID: d1c2b8711223 +Revises: 9effd270a6ae +Create Date: 2019-09-15 08:29:32.373041 + +""" + +from alembic import op +import sqlalchemy as sa +import sqlalchemy_utils + + +# revision identifiers, used by Alembic. +revision = 'd1c2b8711223' +down_revision = '9effd270a6ae' + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('events', 'is_billing_info_mandatory', + existing_type=sa.BOOLEAN(), + server_default='False', + nullable=False) + op.alter_column('orders', 'is_billing_enabled', + existing_type=sa.BOOLEAN(), + server_default='False', + nullable=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('orders', 'is_billing_enabled', + existing_type=sa.BOOLEAN(), + nullable=True) + op.alter_column('events', 'is_billing_info_mandatory', + existing_type=sa.BOOLEAN(), + nullable=True) + # ### end Alembic commands ### From 04ec741536c6974627b51edcc280ab078db84f44 Mon Sep 17 00:00:00 2001 From: Prateek Jain Date: Mon, 25 Nov 2019 22:09:24 +0530 Subject: [PATCH 03/11] chore(refactor): Refactor code for custom orders route (#6610) --- app/__init__.py | 3 +- app/api/auth.py | 131 +----------------------------------- app/api/custom/orders.py | 139 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 143 insertions(+), 130 deletions(-) create mode 100644 app/api/custom/orders.py diff --git a/app/__init__.py b/app/__init__.py index 492415ac37..5c3a68781f 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -146,11 +146,12 @@ 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 app.register_blueprint(api_v1) app.register_blueprint(event_copy) diff --git a/app/api/auth.py b/app/api/auth.py index 6003e5c45c..0ece9c9326 100644 --- a/app/api/auth.py +++ b/app/api/auth.py @@ -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') @@ -386,72 +375,6 @@ def return_file(file_name_prefix, file_path, identifier): return response -@ticket_blueprint.route('/tickets/') -@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/') -@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/') -@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) @@ -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)) diff --git a/app/api/custom/orders.py b/app/api/custom/orders.py new file mode 100644 index 0000000000..16ee2af52f --- /dev/null +++ b/app/api/custom/orders.py @@ -0,0 +1,139 @@ +from flask import Blueprint, jsonify, request +from flask_rest_jsonapi.exceptions import ObjectNotFound +from flask_jwt_extended import current_user, jwt_required +from sqlalchemy.orm.exc import NoResultFound +from flask_limiter.util import get_remote_address + + +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, NotFoundError, UnprocessableEntityError +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.ticketing import TicketingManager +from app.api.helpers.permission_manager import has_access +from app.models.event_invoice import EventInvoice +from app.models.discount_code import DiscountCode +from app.models.order import Order + +ticket_blueprint = Blueprint('ticket_blueprint', __name__, url_prefix='/v1') + + +@ticket_blueprint.route('/tickets/') +@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/') +@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/') +@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") + + +@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)) From d264c1a5e9e05cdb7730ba40fa3b33007901b572 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 26 Nov 2019 03:06:17 +0530 Subject: [PATCH 04/11] =?UTF-8?q?chore(deps):=20update=20flask-caching=20r?= =?UTF-8?q?equirement=20from=20~=3D1.4=20to=20~=3D1=E2=80=A6=20(#6617)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates the requirements on [flask-caching](https://github.com/sh4nks/flask-caching) to permit the latest version. - [Release notes](https://github.com/sh4nks/flask-caching/releases) - [Changelog](https://github.com/sh4nks/flask-caching/blob/master/CHANGES) - [Commits](https://github.com/sh4nks/flask-caching/compare/v1.4.0...v1.8.0) Signed-off-by: dependabot-preview[bot] --- requirements/common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/common.txt b/requirements/common.txt index eed89e4ec6..a6b53b6589 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -29,7 +29,7 @@ unicode-slugify~=0.1 bleach~=3.1 stripe~=2.40.0 xhtml2pdf~=0.2 -flask-caching~=1.4 +flask-caching~=1.8 forex-python~=1.5 pycryptodome~=3.9.4 oauth2~=1.9 From dcb8b4e8b0f159df82a34be2f7a74dba99d6f3eb Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 27 Nov 2019 03:03:39 +0530 Subject: [PATCH 05/11] =?UTF-8?q?chore(deps):=20update=20icalendar=20requi?= =?UTF-8?q?rement=20from=20~=3D4.0.3=20to=20~=3D4.0=E2=80=A6=20(#6625)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates the requirements on [icalendar](https://github.com/collective/icalendar) to permit the latest version. - [Release notes](https://github.com/collective/icalendar/releases) - [Changelog](https://github.com/collective/icalendar/blob/master/CHANGES.rst) - [Commits](https://github.com/collective/icalendar/compare/4.0.3...4.0.4) Signed-off-by: dependabot-preview[bot] --- requirements/common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/common.txt b/requirements/common.txt index a6b53b6589..a0ff12f1c3 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -10,7 +10,7 @@ flask-jwt-extended~=3.24.1 flask-celeryext~=0.3 omise~=0.8.1 requests-oauthlib~=1.3 -icalendar~=4.0.3 +icalendar~=4.0.4 requests[security]~=2.22 psycopg2-binary~=2.8.4 SQLAlchemy-Utils~=0.35.0 From e9b879a7ef4c3bb75e8929d5c6c02d068f21e83d Mon Sep 17 00:00:00 2001 From: Suneet Srivastava Date: Wed, 27 Nov 2019 04:20:04 +0530 Subject: [PATCH 06/11] fix: Remove upcoming events from after event email text (#6624) --- app/api/helpers/mail.py | 4 ++-- app/api/helpers/query.py | 1 + app/api/helpers/scheduled_jobs.py | 14 ++++---------- app/api/helpers/system_mails.py | 3 +-- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/app/api/helpers/mail.py b/app/api/helpers/mail.py index ea5fe54e13..8cd2d681f1 100644 --- a/app/api/helpers/mail.py +++ b/app/api/helpers/mail.py @@ -204,7 +204,7 @@ def send_user_email_role_invite(email, role_name, event_name, link): ) -def send_email_after_event(email, event_name, upcoming_events): +def send_email_after_event(email, event_name, frontend_url): """email for role invite""" send_email( to=email, @@ -215,7 +215,7 @@ def send_email_after_event(email, event_name, upcoming_events): html=MAILS[AFTER_EVENT]['message'].format( email=email, event_name=event_name, - upcoming_events=upcoming_events + url=frontend_url ) ) diff --git a/app/api/helpers/query.py b/app/api/helpers/query.py index b7b1521c9f..52324d4d8f 100644 --- a/app/api/helpers/query.py +++ b/app/api/helpers/query.py @@ -38,6 +38,7 @@ def event_query(self, query_, view_kwargs, event_id='event_id', event_identifier return query_ +# TODO: Unused function. Remove def get_upcoming_events(): return Event.query.filter(Event.starts_at >= datetime.datetime.now()).filter( Event.ends_at >= datetime.datetime.now()).filter_by(deleted_at=None) diff --git a/app/api/helpers/scheduled_jobs.py b/app/api/helpers/scheduled_jobs.py index c52da33165..9ed45d5ac8 100644 --- a/app/api/helpers/scheduled_jobs.py +++ b/app/api/helpers/scheduled_jobs.py @@ -29,13 +29,6 @@ def send_after_event_mail(): from app import current_app as app with app.app_context(): events = Event.query.filter_by(state='published', deleted_at=None).all() - upcoming_events = get_upcoming_events() - upcoming_event_links = "
    " - for upcoming_event in upcoming_events: - frontend_url = get_settings()['frontend_url'] - upcoming_event_links += "
  • {}
  • " \ - .format(frontend_url, upcoming_event.id, upcoming_event.name) - upcoming_event_links += "
" for event in events: organizers = get_user_event_roles_by_role_name(event.id, 'organizer') speakers = Speaker.query.filter_by(event_id=event.id, deleted_at=None).all() @@ -44,16 +37,17 @@ def send_after_event_mail(): time_difference = current_time - event.ends_at time_difference_minutes = (time_difference.days * 24 * 60) + \ (time_difference.seconds / 60) + frontend_url = get_settings()['frontend_url'] if current_time > event.ends_at and time_difference_minutes < 1440: for speaker in speakers: if not speaker.is_email_overridden: - send_email_after_event(speaker.user.email, event.name, upcoming_event_links) + send_email_after_event(speaker.user.email, event.name, frontend_url) send_notif_after_event(speaker.user, event.name) for organizer in organizers: - send_email_after_event(organizer.user.email, event.name, upcoming_event_links) + send_email_after_event(organizer.user.email, event.name, frontend_url) send_notif_after_event(organizer.user, event.name) if owner: - send_email_after_event(owner.user.email, event.name, upcoming_event_links) + send_email_after_event(owner.user.email, event.name, frontend_url) send_notif_after_event(owner.user, event.name) diff --git a/app/api/helpers/system_mails.py b/app/api/helpers/system_mails.py index 459173933a..1d2a824e42 100644 --- a/app/api/helpers/system_mails.py +++ b/app/api/helpers/system_mails.py @@ -63,8 +63,7 @@ 'message': ( u"Hi {email},
" + u"Thank You for participating in our event. We hope you enjoyed it. " - u"Please check the list of more upcoming events.
" + - u"Here are the upcoming events: {upcoming_events}. Get ready!! " + u"Please check out other upcoming events around you on {url}
" ), 'sent_at': '1 day after the event' }, From 9d8a84871ea320500b32875041b080a653b3526e Mon Sep 17 00:00:00 2001 From: Prateek Jain Date: Wed, 27 Nov 2019 23:03:53 +0530 Subject: [PATCH 07/11] fix: Check user permission before exporting (#6581) --- app/api/exports.py | 174 ++++++++++++--------------------- app/api/helpers/permissions.py | 33 ++++++- 2 files changed, 91 insertions(+), 116 deletions(-) diff --git a/app/api/exports.py b/app/api/exports.py index ec7cc1dc82..74b0c63b6b 100644 --- a/app/api/exports.py +++ b/app/api/exports.py @@ -2,12 +2,11 @@ from flask import send_file, make_response, jsonify, url_for, \ current_app, request, Blueprint -from flask_jwt_extended import jwt_required, current_user +from flask_jwt_extended import current_user from app.api.helpers.export_helpers import export_event_json, create_export_job +from app.api.helpers.permissions import is_coorganizer, to_event_id from app.api.helpers.utilities import TASK_RESULTS -from app.models import db -from app.models.event import Event export_routes = Blueprint('exports', __name__, url_prefix='/v1') @@ -19,9 +18,10 @@ } -@export_routes.route('/events//export/json', methods=['POST']) -@jwt_required -def export_event(event_identifier): +@export_routes.route('/events//export/json', methods=['POST'], endpoint='export_event') +@to_event_id +@is_coorganizer +def export_event(event_id): from .helpers.tasks import export_event_task settings = EXPORT_SETTING @@ -30,11 +30,6 @@ def export_event(event_identifier): settings['document'] = request.json.get('document', False) settings['audio'] = request.json.get('audio', False) - if not event_identifier.isdigit(): - event = db.session.query(Event).filter_by(identifier=event_identifier).first() - event_id = event.id - else: - event_id = event_identifier # queue task task = export_event_task.delay( current_user.email, event_id, settings) @@ -53,8 +48,9 @@ def export_event(event_identifier): ) -@export_routes.route('/events//exports/') -@jwt_required +@export_routes.route('/events//exports/', endpoint='export_download') +@to_event_id +@is_coorganizer def export_download(event_id, path): if not path.startswith('/'): path = '/' + path @@ -65,15 +61,10 @@ def export_download(event_id, path): return response -@export_routes.route('/events//export/xcal', methods=['GET']) -@jwt_required -def export_event_xcal(event_identifier): - - if not event_identifier.isdigit(): - event = db.session.query(Event).filter_by(identifier=event_identifier).first() - event_id = str(event.id) - else: - event_id = event_identifier +@export_routes.route('/events//export/xcal', methods=['GET'], endpoint='export_event_xcal') +@to_event_id +@is_coorganizer +def export_event_xcal(event_id): from .helpers.tasks import export_xcal_task @@ -94,15 +85,10 @@ def event_export_task_base(event_id, settings): return path -@export_routes.route('/events//export/ical', methods=['GET']) -@jwt_required -def export_event_ical(event_identifier): - if not event_identifier.isdigit(): - event = db.session.query(Event).filter_by(identifier=event_identifier).first() - event_id = str(event.id) - else: - event_id = event_identifier - +@export_routes.route('/events//export/ical', methods=['GET'], endpoint='export_event_ical') +@to_event_id +@is_coorganizer +def export_event_ical(event_id): from .helpers.tasks import export_ical_task task = export_ical_task.delay(event_id) @@ -114,15 +100,11 @@ def export_event_ical(event_identifier): ) -@export_routes.route('/events//export/pentabarf', methods=['GET']) -@jwt_required -def export_event_pentabarf(event_identifier): - if not event_identifier.isdigit(): - event = db.session.query(Event).filter_by(identifier=event_identifier).first() - event_id = str(event.id) - else: - event_id = event_identifier - +@export_routes.route('/events//export/pentabarf', methods=['GET'], + endpoint='export_event_pentabarf') +@to_event_id +@is_coorganizer +def export_event_pentabarf(event_id): from .helpers.tasks import export_pentabarf_task task = export_pentabarf_task.delay(event_id) @@ -134,15 +116,11 @@ def export_event_pentabarf(event_identifier): ) -@export_routes.route('/events//export/orders/csv', methods=['GET']) -@jwt_required -def export_orders_csv(event_identifier): - if not event_identifier.isdigit(): - event = db.session.query(Event).filter_by(identifier=event_identifier).first() - event_id = str(event.id) - else: - event_id = event_identifier - +@export_routes.route('/events//export/orders/csv', methods=['GET'], + endpoint='export_orders_csv') +@to_event_id +@is_coorganizer +def export_orders_csv(event_id): from .helpers.tasks import export_order_csv_task task = export_order_csv_task.delay(event_id) @@ -154,15 +132,11 @@ def export_orders_csv(event_identifier): ) -@export_routes.route('/events//export/orders/pdf', methods=['GET']) -@jwt_required -def export_orders_pdf(event_identifier): - if not event_identifier.isdigit(): - event = db.session.query(Event).filter_by(identifier=event_identifier).first() - event_id = str(event.id) - else: - event_id = event_identifier - +@export_routes.route('/events//export/orders/pdf', methods=['GET'], + endpoint='export_orders_pdf') +@to_event_id +@is_coorganizer +def export_orders_pdf(event_id): from .helpers.tasks import export_order_pdf_task task = export_order_pdf_task.delay(event_id) @@ -174,15 +148,11 @@ def export_orders_pdf(event_identifier): ) -@export_routes.route('/events//export/attendees/csv', methods=['GET']) -@jwt_required -def export_attendees_csv(event_identifier): - if not event_identifier.isdigit(): - event = db.session.query(Event).filter_by(identifier=event_identifier).first() - event_id = str(event.id) - else: - event_id = event_identifier - +@export_routes.route('/events//export/attendees/csv', methods=['GET'], + endpoint='export_attendees_csv') +@to_event_id +@is_coorganizer +def export_attendees_csv(event_id): from .helpers.tasks import export_attendees_csv_task task = export_attendees_csv_task.delay(event_id) @@ -194,15 +164,11 @@ def export_attendees_csv(event_identifier): ) -@export_routes.route('/events//export/attendees/pdf', methods=['GET']) -@jwt_required -def export_attendees_pdf(event_identifier): - if not event_identifier.isdigit(): - event = db.session.query(Event).filter_by(identifier=event_identifier).first() - event_id = str(event.id) - else: - event_id = event_identifier - +@export_routes.route('/events//export/attendees/pdf', methods=['GET'], + endpoint='export_attendees_pdf') +@to_event_id +@is_coorganizer +def export_attendees_pdf(event_id): from .helpers.tasks import export_attendees_pdf_task task = export_attendees_pdf_task.delay(event_id) @@ -214,15 +180,11 @@ def export_attendees_pdf(event_identifier): ) -@export_routes.route('/events//export/sessions/csv', methods=['GET']) -@jwt_required -def export_sessions_csv(event_identifier): - if not event_identifier.isdigit(): - event = db.session.query(Event).filter_by(identifier=event_identifier).first() - event_id = str(event.id) - else: - event_id = event_identifier - +@export_routes.route('/events//export/sessions/csv', methods=['GET'], + endpoint='export_sessions_csv') +@to_event_id +@is_coorganizer +def export_sessions_csv(event_id): from .helpers.tasks import export_sessions_csv_task task = export_sessions_csv_task.delay(event_id) @@ -234,15 +196,11 @@ def export_sessions_csv(event_identifier): ) -@export_routes.route('/events//export/speakers/csv', methods=['GET']) -@jwt_required -def export_speakers_csv(event_identifier): - if not event_identifier.isdigit(): - event = db.session.query(Event).filter_by(identifier=event_identifier).first() - event_id = str(event.id) - else: - event_id = event_identifier - +@export_routes.route('/events//export/speakers/csv', methods=['GET'], + endpoint='export_speakers_csv') +@to_event_id +@is_coorganizer +def export_speakers_csv(event_id): from .helpers.tasks import export_speakers_csv_task task = export_speakers_csv_task.delay(event_id) @@ -254,15 +212,11 @@ def export_speakers_csv(event_identifier): ) -@export_routes.route('/events//export/sessions/pdf', methods=['GET']) -@jwt_required -def export_sessions_pdf(event_identifier): - if not event_identifier.isdigit(): - event = db.session.query(Event).filter_by(identifier=event_identifier).first() - event_id = str(event.id) - else: - event_id = event_identifier - +@export_routes.route('/events//export/sessions/pdf', methods=['GET'], + endpoint='export_sessions_pdf') +@to_event_id +@is_coorganizer +def export_sessions_pdf(event_id): from .helpers.tasks import export_sessions_pdf_task task = export_sessions_pdf_task.delay(event_id) @@ -274,15 +228,11 @@ def export_sessions_pdf(event_identifier): ) -@export_routes.route('/events//export/speakers/pdf', methods=['GET']) -@jwt_required -def export_speakers_pdf(event_identifier): - if not event_identifier.isdigit(): - event = db.session.query(Event).filter_by(identifier=event_identifier).first() - event_id = str(event.id) - else: - event_id = event_identifier - +@export_routes.route('/events//export/speakers/pdf', methods=['GET'], + endpoint='export_speakers_pdf') +@to_event_id +@is_coorganizer +def export_speakers_pdf(event_id): from .helpers.tasks import export_speakers_pdf_task task = export_speakers_pdf_task.delay(event_id) diff --git a/app/api/helpers/permissions.py b/app/api/helpers/permissions.py index 1b74b06015..3551c30671 100644 --- a/app/api/helpers/permissions.py +++ b/app/api/helpers/permissions.py @@ -1,11 +1,12 @@ from functools import wraps -from flask import current_app as app from flask_jwt_extended import verify_jwt_in_request, current_user from app.api.helpers.db import save_to_db from app.api.helpers.errors import ForbiddenError from flask import request from datetime import datetime +from app.models import db +from app.models.event import Event def second_order_decorator(inner_dec): @@ -145,6 +146,30 @@ def decorated_function(*args, **kwargs): return decorated_function +@second_order_decorator(jwt_required) +def to_event_id(func): + """ + Change event_identifier to event_id in kwargs + :param f: + :return: + """ + + @wraps(func) + def decorated_function(*args, **kwargs): + + if 'event_identifier' in kwargs: + if not kwargs['event_identifier'].isdigit(): + event = db.session.query(Event).filter_by(identifier=kwargs['event_identifier']).first() + kwargs['event_id'] = event.id + else: + kwargs['event_id'] = kwargs['event_identifier'] + + return func(*args, **kwargs) + + return decorated_function + + + @second_order_decorator(jwt_required) def is_coorganizer(f): """ @@ -157,9 +182,9 @@ def is_coorganizer(f): def decorated_function(*args, **kwargs): user = current_user - if user.is_staff: - return f(*args, **kwargs) - if 'event_id' in kwargs and user.has_event_access(kwargs['event_id']): + if user.is_staff or ('event_id' in kwargs and user.has_event_access(kwargs['event_id'])): + if 'event_identifier' in kwargs: + kwargs.pop('event_identifier', None) return f(*args, **kwargs) return ForbiddenError({'source': ''}, 'Co-organizer access is required.').respond() From 7f452c2a80897d655a8ff82383cc18400be1b85d Mon Sep 17 00:00:00 2001 From: Suneet Srivastava Date: Thu, 28 Nov 2019 02:33:29 +0530 Subject: [PATCH 08/11] fix: Removed decorator from stripe-authorizations (#6627) --- app/api/stripe_authorization.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/api/stripe_authorization.py b/app/api/stripe_authorization.py index 4f84633ff1..e769c47aa7 100644 --- a/app/api/stripe_authorization.py +++ b/app/api/stripe_authorization.py @@ -70,8 +70,6 @@ def after_create_object(self, stripe_authorization, data, view_kwargs): save_to_db(event) schema = StripeAuthorizationSchema - decorators = (api.has_permission('is_coorganizer', fetch="event_id", - fetch_as="event_id", model=StripeAuthorization),) methods = ['POST'] data_layer = {'session': db.session, 'model': StripeAuthorization, From 2afb5c7a721725e798571177ea25acc1a165ca10 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 28 Nov 2019 10:54:02 +0530 Subject: [PATCH 09/11] chore(deps): update stripe requirement from ~=2.40.0 to ~=2.41.0 (#6629) Updates the requirements on [stripe](https://github.com/stripe/stripe-python) to permit the latest version. - [Release notes](https://github.com/stripe/stripe-python/releases) - [Changelog](https://github.com/stripe/stripe-python/blob/master/CHANGELOG.md) - [Commits](https://github.com/stripe/stripe-python/compare/v2.40.0...v2.41.0) Signed-off-by: dependabot-preview[bot] --- requirements/common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/common.txt b/requirements/common.txt index a0ff12f1c3..a9d31258ab 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -27,7 +27,7 @@ SQLAlchemy-Continuum~=1.3.9 arrow~=0.15.4 unicode-slugify~=0.1 bleach~=3.1 -stripe~=2.40.0 +stripe~=2.41.0 xhtml2pdf~=0.2 flask-caching~=1.8 forex-python~=1.5 From 1521c3247b8113c629ea3f93ba9798578554453a Mon Sep 17 00:00:00 2001 From: Prateek Jain Date: Thu, 28 Nov 2019 18:39:16 +0530 Subject: [PATCH 10/11] chore(refactor): Make separate blueprint for events and orders (#6615) --- app/__init__.py | 6 +++- app/api/custom/invoices.py | 59 ++++++++++++++++++++++++++++++++++++ app/api/custom/orders.py | 62 ++++++-------------------------------- 3 files changed, 73 insertions(+), 54 deletions(-) create mode 100644 app/api/custom/invoices.py diff --git a/app/__init__.py b/app/__init__.py index 5c3a68781f..4df584942c 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -152,6 +152,8 @@ def create_app(): 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) @@ -165,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) diff --git a/app/api/custom/invoices.py b/app/api/custom/invoices.py new file mode 100644 index 0000000000..f370d6da32 --- /dev/null +++ b/app/api/custom/invoices.py @@ -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/') +@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/') +@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() diff --git a/app/api/custom/orders.py b/app/api/custom/orders.py index 16ee2af52f..b841f5ce87 100644 --- a/app/api/custom/orders.py +++ b/app/api/custom/orders.py @@ -1,8 +1,7 @@ from flask import Blueprint, jsonify, request -from flask_rest_jsonapi.exceptions import ObjectNotFound from flask_jwt_extended import current_user, jwt_required -from sqlalchemy.orm.exc import NoResultFound from flask_limiter.util import get_remote_address +from sqlalchemy.orm.exc import NoResultFound from app import limiter @@ -10,20 +9,21 @@ 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, NotFoundError, UnprocessableEntityError -from app.api.helpers.order import create_pdf_tickets_for_holder, calculate_order_amount +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.event_invoice import EventInvoice from app.models.discount_code import DiscountCode from app.models.order import Order -ticket_blueprint = Blueprint('ticket_blueprint', __name__, url_prefix='/v1') +order_blueprint = Blueprint('order_blueprint', __name__, url_prefix='/v1/orders') +ticket_blueprint = Blueprint('ticket_blueprint', __name__, url_prefix='/v1/tickets') -@ticket_blueprint.route('/tickets/') +@ticket_blueprint.route('/') +@order_blueprint.route('//tickets-pdf') @jwt_required def ticket_attendee_authorized(order_identifier): if current_user: @@ -45,51 +45,7 @@ def ticket_attendee_authorized(order_identifier): return ForbiddenError({'source': ''}, 'Authentication Required to access ticket').respond() -@ticket_blueprint.route('/orders/invoices/') -@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/') -@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") - - -@ticket_blueprint.route('/orders/resend-email', methods=['POST']) +@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' ) @@ -124,7 +80,7 @@ def resend_emails(): return ForbiddenError({'source': ''}, "Co-Organizer Access Required").respond() -@ticket_blueprint.route('/orders/calculate-amount', methods=['POST']) +@order_blueprint.route('/calculate-amount', methods=['POST']) @jwt_required def calculate_amount(): data = request.get_json() From f3bb95c02c73be32bea0d84163e1c6acff39b326 Mon Sep 17 00:00:00 2001 From: Areeb Jamal Date: Thu, 28 Nov 2019 21:53:12 +0530 Subject: [PATCH 11/11] chore: Prepare for release 1.9.0 (#6630) --- CHANGELOG.md | 7 +++++++ app/api/server_version.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 071eb134dc..adf819468f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ ## Changelog +##### 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 diff --git a/app/api/server_version.py b/app/api/server_version.py index 33908ef728..e58449295d 100644 --- a/app/api/server_version.py +++ b/app/api/server_version.py @@ -1,6 +1,6 @@ from flask import jsonify, Blueprint -SERVER_VERSION = '1.8.0' +SERVER_VERSION = '1.9.0' info_route = Blueprint('info', __name__) _build = {'version': SERVER_VERSION}