From 832c755e53568515c945d13982be7fb87b159044 Mon Sep 17 00:00:00 2001 From: Prateek Jain Date: Fri, 12 Jul 2019 15:35:13 +0530 Subject: [PATCH 01/30] feat: restricts unverified user for buying free tickets (#6140) * Restrict unverified user from making free orders. * Check ticket-type rather than payment-mode * No need to revert the old PR * Check if order contains all free tickets. * Required changes --- app/api/attendees.py | 4 ++-- app/api/orders.py | 23 +++++++++++++++++------ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/app/api/attendees.py b/app/api/attendees.py index d42f18c3a2..7629b975ab 100644 --- a/app/api/attendees.py +++ b/app/api/attendees.py @@ -1,7 +1,7 @@ from datetime import datetime from flask import Blueprint, request, jsonify, abort, make_response -from flask_jwt import current_identity, jwt_required +from flask_jwt import current_identity from flask_rest_jsonapi import ResourceDetail, ResourceList, ResourceRelationship from flask_rest_jsonapi.exceptions import ObjectNotFound from sqlalchemy.orm.exc import NoResultFound @@ -174,7 +174,7 @@ def before_update_object(self, obj, data, kwargs): user = safe_query(self, User, 'id', current_identity.id, 'user_id') ticket = db.session.query(Ticket).filter_by( id=int(data['ticket']), deleted_at=None - ).first() + ).first() if ticket is None: raise UnprocessableEntity( {'pointer': '/data/relationships/ticket'}, "Invalid Ticket" diff --git a/app/api/orders.py b/app/api/orders.py index 638a8253d9..783bcea7c8 100644 --- a/app/api/orders.py +++ b/app/api/orders.py @@ -1,8 +1,8 @@ -from datetime import datetime -from flask import request, jsonify, Blueprint, url_for, redirect -import omise import logging +from datetime import datetime +import omise +from flask import request, jsonify, Blueprint, url_for, redirect from flask_jwt import current_identity as current_user from flask_rest_jsonapi import ResourceDetail, ResourceList, ResourceRelationship from marshmallow_jsonapi import fields @@ -12,7 +12,6 @@ from app.api.bootstrap import api from app.api.data_layers.ChargesLayer import ChargesLayer from app.api.helpers.db import save_to_db, safe_query, safe_query_without_soft_deleted_entries -from app.api.helpers.storage import generate_hash, UPLOAD_PATHS from app.api.helpers.errors import BadRequestError from app.api.helpers.exceptions import ForbiddenException, UnprocessableEntity, ConflictException from app.api.helpers.files import make_frontend_url @@ -22,10 +21,12 @@ send_notif_ticket_cancel from app.api.helpers.order import delete_related_attendees_for_order, set_expiry_for_order, \ create_pdf_tickets_for_holder, create_onsite_attendees_for_order +from app.api.helpers.payment import AliPayPaymentsManager, OmisePaymentsManager from app.api.helpers.payment import PayPalPaymentsManager from app.api.helpers.permission_manager import has_access from app.api.helpers.permissions import jwt_required from app.api.helpers.query import event_query +from app.api.helpers.storage import generate_hash, UPLOAD_PATHS from app.api.helpers.ticketing import TicketingManager from app.api.helpers.utilities import dasherize, require_relationship from app.api.schema.orders import OrderSchema @@ -34,8 +35,6 @@ from app.models.order import Order, OrderTicket, get_updatable_fields from app.models.ticket_holder import TicketHolder from app.models.user import User -from app.api.helpers.payment import AliPayPaymentsManager, OmisePaymentsManager - order_misc_routes = Blueprint('order_misc', __name__, url_prefix='/v1') alipay_blueprint = Blueprint('alipay_blueprint', __name__, url_prefix='/v1/alipay') @@ -74,6 +73,9 @@ def before_create_object(self, data, view_kwargs): :param view_kwargs: :return: """ + + free_ticket_quantity = 0 + for ticket_holder in data['ticket_holders']: # Ensuring that the attendee exists and doesn't have an associated order. try: @@ -86,6 +88,15 @@ def before_create_object(self, data, view_kwargs): raise ConflictException({'pointer': '/data/relationships/attendees'}, "Attendee with id {} does not exists".format(str(ticket_holder))) + if ticket_holder_object.ticket.type == 'free': + free_ticket_quantity += 1 + + if not current_user.is_verified and free_ticket_quantity == len(data['ticket_holders']): + raise UnprocessableEntity( + {'pointer': '/data/relationships/order'}, + "Unverified user cannot place free orders" + ) + if data.get('cancel_note'): del data['cancel_note'] From f3525254eeae7df0987d240bbb4b2666d967e506 Mon Sep 17 00:00:00 2001 From: Uddeshya Singh Date: Fri, 12 Jul 2019 15:35:36 +0530 Subject: [PATCH 02/30] fix: order deletion triggers emails (#6184) --- app/api/orders.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/api/orders.py b/app/api/orders.py index 783bcea7c8..2c9737f7f9 100644 --- a/app/api/orders.py +++ b/app/api/orders.py @@ -334,15 +334,14 @@ def after_update_object(self, order, data, view_kwargs): # create pdf tickets. create_pdf_tickets_for_holder(order) - if order.status == 'cancelled': + if order.status == 'cancelled' and order.deleted_at is None: send_order_cancel_email(order) send_notif_ticket_cancel(order) # delete the attendees so that the tickets are unlocked. delete_related_attendees_for_order(order) - elif order.status == 'completed' or order.status == 'placed': - + elif (order.status == 'completed' or order.status == 'placed') and order.deleted_at is None: # Send email to attendees with invoices and tickets attached order_identifier = order.identifier From 59291ff08493bdd93f5f78bc036092d74d9d9d62 Mon Sep 17 00:00:00 2001 From: Abhinav Khare Date: Fri, 12 Jul 2019 21:18:57 +0530 Subject: [PATCH 03/30] chore: back up current docker compose --- docker-compose-dev.yaml | 76 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 docker-compose-dev.yaml diff --git a/docker-compose-dev.yaml b/docker-compose-dev.yaml new file mode 100644 index 0000000000..a7f0ebf4e3 --- /dev/null +++ b/docker-compose-dev.yaml @@ -0,0 +1,76 @@ +version: '3.5' + +x-environment-vars: &environment-vars + DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB} + ELASTICSEARCH_HOST: elastic:9200 + REDIS_URL: redis://redis:6379/0 + ADMIN_EMAIL: "@{ADMIN_EMAIL}" + ADMIN_PASSWORD: "@{ADMIN_PASSWORD}" + +x-defaults: &defaults + build: . + restart: unless-stopped + environment: + <<: *environment-vars + depends_on: + - postgres + - redis + - elastic + links: + - postgres:postgres + - redis:redis + - elastic:elastic + volumes: + - ./static:/data/app/static + +services: + + postgres: + image: postgres:10-alpine + container_name: opev-postgres + restart: unless-stopped + volumes: + - pg:/var/lib/postgresql/data + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + + redis: + image: redis:3-alpine + container_name: opev-redis + restart: unless-stopped + command: redis-server + volumes: + - rd:/var/lib/redis/data + + elastic: + image: elasticsearch:5.6.12-alpine + container_name: opev-elastic-search + restart: unless-stopped + environment: + - discovery.type=single-node + volumes: + - es:/usr/share/elasticsearch/data + ports: + - 9200:9200 + - 9300:9300 + + web: + <<: *defaults + container_name: opev-web + ports: + - 8080:8080 + + celery: + <<: *defaults + container_name: opev-celery + environment: + <<: *environment-vars + DEPLOYMENT: celery + C_FORCE_ROOT: "true" + +volumes: + pg: + rd: + es: From dcc365e84bac7c11bbbed80b66f4ebf3dd4591bb Mon Sep 17 00:00:00 2001 From: Saicharan Reddy Date: Fri, 12 Jul 2019 23:18:48 +0530 Subject: [PATCH 04/30] feat: Serving event invoices through a protected route (#6145) Refactored code to reduce nesting --- app/api/auth.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/app/api/auth.py b/app/api/auth.py index 127ea10231..8533e1ae64 100644 --- a/app/api/auth.py +++ b/app/api/auth.py @@ -10,6 +10,7 @@ from flask_jwt import current_identity as current_user, jwt_required 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 @@ -32,6 +33,8 @@ from app.models.notification import PASSWORD_CHANGE as PASSWORD_CHANGE_NOTIF 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='/') @@ -295,9 +298,9 @@ def change_password(): }) -def return_file(file_name_prefix, file_path, order_identifier): +def return_file(file_name_prefix, file_path, identifier): response = make_response(send_file(file_path)) - response.headers['Content-Disposition'] = 'attachment; filename=%s-%s.pdf' % (file_name_prefix, order_identifier) + response.headers['Content-Disposition'] = 'attachment; filename=%s-%s.pdf' % (file_name_prefix, identifier) return response @@ -345,6 +348,28 @@ def order_invoices(order_identifier): 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) From 336ec2d4b54fda3b9fe1ca4af06c217c514dc474 Mon Sep 17 00:00:00 2001 From: Uddeshya Singh Date: Sat, 13 Jul 2019 04:01:56 +0530 Subject: [PATCH 05/30] feat: add a resend email route for organizers (#6163) * add the resend email route * resend emails using ticket route * handle errors with UnprocessableEntry * modify order to user key --- app/api/auth.py | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/app/api/auth.py b/app/api/auth.py index 8533e1ae64..08f61f0d01 100644 --- a/app/api/auth.py +++ b/app/api/auth.py @@ -15,10 +15,11 @@ 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.auth import AuthManager -from app.api.helpers.db import save_to_db, get_count from app.api.helpers.errors import ForbiddenError, 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 @@ -27,6 +28,7 @@ from app.api.helpers.storage import generate_hash from app.api.helpers.third_party_auth import GoogleOAuth, FbOAuth, TwitterOAuth, InstagramOAuth 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 @@ -388,3 +390,38 @@ 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() From 8a01b6261b4496986aceb270707d6bd886fcfc4d Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 16 Jul 2019 03:51:52 +0530 Subject: [PATCH 06/30] chore(deps): update sqlalchemy-utils requirement (#6194) Updates the requirements on [sqlalchemy-utils](https://github.com/kvesteri/sqlalchemy-utils) to permit the latest version. - [Release notes](https://github.com/kvesteri/sqlalchemy-utils/releases) - [Changelog](https://github.com/kvesteri/sqlalchemy-utils/blob/master/CHANGES.rst) - [Commits](https://github.com/kvesteri/sqlalchemy-utils/compare/0.34.0...0.34.1) 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 9221d2a73a..077bc0ca94 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -13,7 +13,7 @@ requests-oauthlib~=0.8 icalendar~=3.12 requests[security]~=2.22 psycopg2-binary~=2.8.3 -SQLAlchemy-Utils~=0.34.0 +SQLAlchemy-Utils~=0.34.1 itsdangerous~=0.24 humanize~=0.5.1 celery~=4.3 From 4ea611e1b1784cfcd7a9d682dad76c5c51ce0a8f Mon Sep 17 00:00:00 2001 From: Shreyansh Dwivedi Date: Tue, 16 Jul 2019 05:44:55 +0530 Subject: [PATCH 07/30] fix: allows only owner and organizer to delete role-invites (#6190) --- app/api/role_invites.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/api/role_invites.py b/app/api/role_invites.py index b1982f5ade..6ccaaddd3d 100644 --- a/app/api/role_invites.py +++ b/app/api/role_invites.py @@ -135,6 +135,11 @@ def before_update_object(self, role_invite, data, view_kwargs): if not has_access('is_organizer', event_id=role_invite.event_id) and (len(list(data.keys())) > 1 or 'status' not in data): raise UnprocessableEntity({'source': ''}, "You can only change your status") + if data.get('deleted_at'): + if role_invite.role_name == 'owner' and not has_access('is_owner', event_id=role_invite.event_id): + raise ForbiddenException({'source': ''}, 'Owner access is required.') + if role_invite.role_name != 'owner' and not has_access('is_organizer', event_id=role_invite.event_id): + raise ForbiddenException({'source': ''}, 'Organizer access is required.') decorators = (api.has_permission('is_organizer', methods="DELETE", fetch="event_id", fetch_as="event_id", model=RoleInvite),) From a258111f3aad25e59969c9ec34af61cc4f8af734 Mon Sep 17 00:00:00 2001 From: Saicharan Reddy Date: Tue, 16 Jul 2019 13:13:23 +0530 Subject: [PATCH 08/30] feat: Add required fields for merge of invoices & order models (#6196) --- app/api/schema/orders.py | 2 ++ app/models/order.py | 9 ++++++++- migrations/versions/5b5cec07a402_.py | 30 ++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 migrations/versions/5b5cec07a402_.py diff --git a/app/api/schema/orders.py b/app/api/schema/orders.py index 9a38910672..2a354fd2e7 100644 --- a/app/api/schema/orders.py +++ b/app/api/schema/orders.py @@ -78,6 +78,8 @@ def initial_values(self, data): cancel_note = fields.Str(allow_none=True) order_notes = fields.Str(allow_none=True) tickets_pdf_url = fields.Url(dump_only=True) + is_event_invoice = fields.Bool(allow_none=False) + invoice_pdf_url = fields.Url(dump_only=True) # only used in the case of an on site attendee. on_site_tickets = fields.List(cls_or_instance=fields.Nested(OnSiteTicketSchema), load_only=True, allow_none=True) diff --git a/app/models/order.py b/app/models/order.py index 686469fd74..558f7ef7ef 100644 --- a/app/models/order.py +++ b/app/models/order.py @@ -66,6 +66,8 @@ class Order(SoftDeletionModel): cancel_note = db.Column(db.String, nullable=True) order_notes = db.Column(db.String) tickets_pdf_url = db.Column(db.String) + is_event_invoice = db.Column(db.Boolean) + invoice_pdf_url = db.Column(db.String) discount_code_id = db.Column( db.Integer, db.ForeignKey('discount_codes.id', ondelete='SET NULL'), nullable=True, default=None) @@ -97,7 +99,9 @@ def __init__(self, payment_mode=None, deleted_at=None, order_notes=None, - tickets_pdf_url=None): + tickets_pdf_url=None, + invoice_pdf_url=None, + is_event_invoice=False): self.identifier = get_new_order_identifier() self.quantity = quantity self.amount = amount @@ -120,6 +124,9 @@ def __init__(self, self.deleted_at = deleted_at self.order_notes = order_notes self.tickets_pdf_url = tickets_pdf_url + self.is_event_invoice = is_event_invoice + self.invoice_pdf_url = invoice_pdf_url + def __repr__(self): return '' % self.id diff --git a/migrations/versions/5b5cec07a402_.py b/migrations/versions/5b5cec07a402_.py new file mode 100644 index 0000000000..7fb1153f6b --- /dev/null +++ b/migrations/versions/5b5cec07a402_.py @@ -0,0 +1,30 @@ +"""empty message + +Revision ID: 5b5cec07a402 +Revises: ba651e8ce9a3 +Create Date: 2019-07-16 10:07:05.795451 + +""" + +from alembic import op +import sqlalchemy as sa +import sqlalchemy_utils + + +# revision identifiers, used by Alembic. +revision = '5b5cec07a402' +down_revision = 'ba651e8ce9a3' + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('orders', sa.Column('invoice_pdf_url', sa.String(), nullable=True)) + op.add_column('orders', sa.Column('is_event_invoice', sa.Boolean(), server_default='false', nullable=False)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('orders', 'is_event_invoice') + op.drop_column('orders', 'invoice_pdf_url') + # ### end Alembic commands ### From 367836d0ee2b0debab303e563c17c0b069cd4324 Mon Sep 17 00:00:00 2001 From: Areeb Jamal Date: Tue, 16 Jul 2019 13:20:25 +0530 Subject: [PATCH 09/30] =?UTF-8?q?Revert=20"feat:=20Add=20required=20fields?= =?UTF-8?q?=20for=20merge=20of=20invoices=20&=20order=E2=80=A6=20(#6198)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit a258111f3aad25e59969c9ec34af61cc4f8af734. --- app/api/schema/orders.py | 2 -- app/models/order.py | 9 +-------- migrations/versions/5b5cec07a402_.py | 30 ---------------------------- 3 files changed, 1 insertion(+), 40 deletions(-) delete mode 100644 migrations/versions/5b5cec07a402_.py diff --git a/app/api/schema/orders.py b/app/api/schema/orders.py index 2a354fd2e7..9a38910672 100644 --- a/app/api/schema/orders.py +++ b/app/api/schema/orders.py @@ -78,8 +78,6 @@ def initial_values(self, data): cancel_note = fields.Str(allow_none=True) order_notes = fields.Str(allow_none=True) tickets_pdf_url = fields.Url(dump_only=True) - is_event_invoice = fields.Bool(allow_none=False) - invoice_pdf_url = fields.Url(dump_only=True) # only used in the case of an on site attendee. on_site_tickets = fields.List(cls_or_instance=fields.Nested(OnSiteTicketSchema), load_only=True, allow_none=True) diff --git a/app/models/order.py b/app/models/order.py index 558f7ef7ef..686469fd74 100644 --- a/app/models/order.py +++ b/app/models/order.py @@ -66,8 +66,6 @@ class Order(SoftDeletionModel): cancel_note = db.Column(db.String, nullable=True) order_notes = db.Column(db.String) tickets_pdf_url = db.Column(db.String) - is_event_invoice = db.Column(db.Boolean) - invoice_pdf_url = db.Column(db.String) discount_code_id = db.Column( db.Integer, db.ForeignKey('discount_codes.id', ondelete='SET NULL'), nullable=True, default=None) @@ -99,9 +97,7 @@ def __init__(self, payment_mode=None, deleted_at=None, order_notes=None, - tickets_pdf_url=None, - invoice_pdf_url=None, - is_event_invoice=False): + tickets_pdf_url=None): self.identifier = get_new_order_identifier() self.quantity = quantity self.amount = amount @@ -124,9 +120,6 @@ def __init__(self, self.deleted_at = deleted_at self.order_notes = order_notes self.tickets_pdf_url = tickets_pdf_url - self.is_event_invoice = is_event_invoice - self.invoice_pdf_url = invoice_pdf_url - def __repr__(self): return '' % self.id diff --git a/migrations/versions/5b5cec07a402_.py b/migrations/versions/5b5cec07a402_.py deleted file mode 100644 index 7fb1153f6b..0000000000 --- a/migrations/versions/5b5cec07a402_.py +++ /dev/null @@ -1,30 +0,0 @@ -"""empty message - -Revision ID: 5b5cec07a402 -Revises: ba651e8ce9a3 -Create Date: 2019-07-16 10:07:05.795451 - -""" - -from alembic import op -import sqlalchemy as sa -import sqlalchemy_utils - - -# revision identifiers, used by Alembic. -revision = '5b5cec07a402' -down_revision = 'ba651e8ce9a3' - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('orders', sa.Column('invoice_pdf_url', sa.String(), nullable=True)) - op.add_column('orders', sa.Column('is_event_invoice', sa.Boolean(), server_default='false', nullable=False)) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('orders', 'is_event_invoice') - op.drop_column('orders', 'invoice_pdf_url') - # ### end Alembic commands ### From 7b54d2a0d96c56ece277ce17bdf02fa42d56837b Mon Sep 17 00:00:00 2001 From: Kush Trivedi Date: Tue, 16 Jul 2019 21:55:24 +0530 Subject: [PATCH 10/30] fix: Making Event Name mandatory for publishing of event (#6203) --- app/api/events.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/api/events.py b/app/api/events.py index ca07989aa4..2bbc55567b 100644 --- a/app/api/events.py +++ b/app/api/events.py @@ -79,6 +79,10 @@ def validate_event(user, modules, data): raise ConflictException({'pointer': '/data/attributes/location-name'}, "Online Event does not have any locaton") + if not data.get('name', None) and data.get('state', None) == 'published': + raise ConflictException({'pointer': '/data/attributes/location-name'}, + "Event Name is required to publish the event") + if data.get('searchable_location_name') and data.get('is_event_online'): raise ConflictException({'pointer': '/data/attributes/searchable-location-name'}, "Online Event does not have any locaton") From 23c83c1d0d3be47ddc175b02195baf3541313fcf Mon Sep 17 00:00:00 2001 From: Uddeshya Singh Date: Wed, 17 Jul 2019 12:41:06 +0530 Subject: [PATCH 11/30] fix: handle padding error for verification token (#6206) * handle padding error * catch specific base64 error --- app/api/auth.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/api/auth.py b/app/api/auth.py index 08f61f0d01..4c794f6504 100644 --- a/app/api/auth.py +++ b/app/api/auth.py @@ -174,7 +174,10 @@ def login_user(provider): @auth_routes.route('/verify-email', methods=['POST']) def verify_email(): - token = base64.b64decode(request.json['data']['token']) + try: + token = base64.b64decode(request.json['data']['token']) + except base64.binascii.Error: + return BadRequestError({'source': ''}, 'Invalid Token').respond() s = get_serializer() try: From 7d6399d8bec141a57676673e0791598742330c46 Mon Sep 17 00:00:00 2001 From: Saicharan Reddy Date: Thu, 18 Jul 2019 04:13:54 +0530 Subject: [PATCH 12/30] feat: Added order foreign keys & relationships in API (#6199) --- app/api/__init__.py | 3 +++ app/api/schema/event_invoices.py | 7 +++++++ app/api/schema/orders.py | 8 ++++++++ app/models/event_invoice.py | 4 ++++ app/models/order.py | 1 + migrations/versions/facee76912bc_.py | 30 ++++++++++++++++++++++++++++ 6 files changed, 53 insertions(+) create mode 100644 migrations/versions/facee76912bc_.py diff --git a/app/api/__init__.py b/app/api/__init__.py index eb3698dcf6..afba682ea1 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -462,6 +462,8 @@ '/event-invoices//relationships/user') api.route(EventInvoiceRelationshipRequired, 'event_invoice_event', '/event-invoices//relationships/event') +api.route(EventInvoiceRelationshipRequired, 'event_invoice_order', + '/event-invoices//relationships/order') api.route(EventInvoiceRelationshipOptional, 'event_invoice_discount_code', '/event-invoices//relationships/discount-code') @@ -600,6 +602,7 @@ api.route(OrderRelationship, 'order_event', '/orders//relationships/event') api.route(OrderRelationship, 'order_marketer', '/orders//relationships/marketer') api.route(OrderRelationship, 'order_discount', '/orders//relationships/discount-code') +api.route(OrderRelationship, 'order_event_invoice', '/orders//relationships/event-invoice/') # Event Statistics API api.route(EventStatisticsGeneralDetail, 'event_statistics_general_detail', '/events//general-statistics', diff --git a/app/api/schema/event_invoices.py b/app/api/schema/event_invoices.py index d5bfa25b1b..105e465414 100644 --- a/app/api/schema/event_invoices.py +++ b/app/api/schema/event_invoices.py @@ -49,6 +49,13 @@ class Meta: related_view_kwargs={'event_invoice_id': ''}, schema='UserSchemaPublic', type_='user') + order = Relationship(attribute='order', + self_view='v1.event_invoice_order', + self_view_kwargs={'id': ''}, + related_view='v1.order_detail', + related_view_kwargs={'id': ''}, + schema='OrderSchema', + type_='order') event = Relationship(attribute='event', self_view='v1.event_invoice_event', self_view_kwargs={'id': ''}, diff --git a/app/api/schema/orders.py b/app/api/schema/orders.py index 9a38910672..a1d03e0b31 100644 --- a/app/api/schema/orders.py +++ b/app/api/schema/orders.py @@ -116,6 +116,14 @@ def initial_values(self, data): schema='EventSchemaPublic', type_="event") + event_invoice = Relationship(attribute='invoice', + self_view='v1.order_invoice', + self_view_kwargs={'order_identifier': ''}, + related_view='v1.event_invoice_detail', + related_view_kwargs={'id': ''}, + schema='EventInvoiceSchema', + type_="event_invoice") + marketer = Relationship(attribute='marketer', self_view='v1.order_marketer', self_view_kwargs={'order_identifier': ''}, diff --git a/app/models/event_invoice.py b/app/models/event_invoice.py index c7f9173045..6d5af532c7 100644 --- a/app/models/event_invoice.py +++ b/app/models/event_invoice.py @@ -33,6 +33,7 @@ class EventInvoice(SoftDeletionModel): user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='SET NULL')) event_id = db.Column(db.Integer, db.ForeignKey('events.id', ondelete='SET NULL')) + order_id = db.Column(db.Integer, db.ForeignKey('orders.id', ondelete='SET NULL')) created_at = db.Column(db.DateTime(timezone=True)) completed_at = db.Column(db.DateTime(timezone=True), nullable=True, default=None) @@ -49,6 +50,9 @@ class EventInvoice(SoftDeletionModel): invoice_pdf_url = db.Column(db.String) event = db.relationship('Event', backref='invoices') + + order = db.relationship('Order', backref='event_invoices', foreign_keys=[order_id]) + user = db.relationship('User', backref='invoices') discount_code_id = db.Column(db.Integer, db.ForeignKey('discount_codes.id', ondelete='SET NULL'), diff --git a/app/models/order.py b/app/models/order.py index 686469fd74..c6e3b634e5 100644 --- a/app/models/order.py +++ b/app/models/order.py @@ -73,6 +73,7 @@ class Order(SoftDeletionModel): event = db.relationship('Event', backref='orders') user = db.relationship('User', backref='orders', foreign_keys=[user_id]) + invoices = db.relationship("EventInvoice", backref='invoice_order') marketer = db.relationship('User', backref='marketed_orders', foreign_keys=[marketer_id]) tickets = db.relationship("Ticket", secondary='orders_tickets', backref='order') order_tickets = db.relationship("OrderTicket", backref='order') diff --git a/migrations/versions/facee76912bc_.py b/migrations/versions/facee76912bc_.py new file mode 100644 index 0000000000..43276ab118 --- /dev/null +++ b/migrations/versions/facee76912bc_.py @@ -0,0 +1,30 @@ +"""empty message + +Revision ID: facee76912bc +Revises: ba651e8ce9a3 +Create Date: 2019-07-17 09:43:48.137732 + +""" + +from alembic import op +import sqlalchemy as sa +import sqlalchemy_utils + + +# revision identifiers, used by Alembic. +revision = 'facee76912bc' +down_revision = 'ba651e8ce9a3' + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('event_invoices', sa.Column('order_id', sa.Integer(), nullable=True)) + op.create_foreign_key(None, 'event_invoices', 'orders', ['order_id'], ['id'], ondelete='SET NULL') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'event_invoices', type_='foreignkey') + op.drop_column('event_invoices', 'order_id') + # ### end Alembic commands ### From 5802c422d23e41d28ccd18ae1a78bbb06722a502 Mon Sep 17 00:00:00 2001 From: Shreyansh Dwivedi Date: Sat, 20 Jul 2019 01:31:25 +0530 Subject: [PATCH 13/30] feat: add field for email exception in speaker schema (#6209) --- app/api/helpers/scheduled_jobs.py | 5 +++-- app/api/schema/speakers.py | 1 + app/api/sessions.py | 5 +++-- app/api/speakers.py | 15 +++++++++++++++ app/models/speaker.py | 3 +++ migrations/versions/96bca587b3ca_.py | 28 ++++++++++++++++++++++++++++ 6 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 migrations/versions/96bca587b3ca_.py diff --git a/app/api/helpers/scheduled_jobs.py b/app/api/helpers/scheduled_jobs.py index c48eefc738..146549e6c9 100644 --- a/app/api/helpers/scheduled_jobs.py +++ b/app/api/helpers/scheduled_jobs.py @@ -46,8 +46,9 @@ def send_after_event_mail(): (time_difference.seconds / 60) if current_time > event.ends_at and time_difference_minutes < 1440: for speaker in speakers: - send_email_after_event(speaker.user.email, event.name, upcoming_event_links) - send_notif_after_event(speaker.user, event.name) + if not speaker.is_email_overridden: + send_email_after_event(speaker.user.email, event.name, upcoming_event_links) + 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_notif_after_event(organizer.user, event.name) diff --git a/app/api/schema/speakers.py b/app/api/schema/speakers.py index bb7ca4ec3c..2bd0c05fde 100644 --- a/app/api/schema/speakers.py +++ b/app/api/schema/speakers.py @@ -39,6 +39,7 @@ class Meta: linkedin = fields.Url(allow_none=True) organisation = fields.Str(allow_none=True) is_featured = fields.Boolean(default=False) + is_email_overridden = fields.Boolean(default=False) position = fields.Str(allow_none=True) country = fields.Str(allow_none=True) city = fields.Str(allow_none=True) diff --git a/app/api/sessions.py b/app/api/sessions.py index ef1f8cb4b1..2b102337e0 100644 --- a/app/api/sessions.py +++ b/app/api/sessions.py @@ -161,8 +161,9 @@ def after_update_object(self, session, data, view_kwargs): frontend_url = get_settings()['frontend_url'] link = "{}/events/{}/sessions/{}" \ .format(frontend_url, event.identifier, session.id) - send_email_session_accept_reject(speaker.email, session, link) - send_notif_session_accept_reject(speaker, session.title, session.state, link, session.id) + if not speaker.is_email_overridden: + send_email_session_accept_reject(speaker.email, session, link) + send_notif_session_accept_reject(speaker, session.title, session.state, link, session.id) # Email for owner if session.event.get_owner(): diff --git a/app/api/speakers.py b/app/api/speakers.py index ac1e2b130f..ca2822d875 100644 --- a/app/api/speakers.py +++ b/app/api/speakers.py @@ -1,4 +1,5 @@ from flask import request +from flask_login import current_user from flask_rest_jsonapi import ResourceDetail, ResourceList, ResourceRelationship from flask_rest_jsonapi.exceptions import ObjectNotFound @@ -45,6 +46,13 @@ def before_post(self, args, kwargs, data=None): deleted_at=None)) > 0: raise ForbiddenException({'pointer': ''}, 'Speaker with this Email ID already exists') + if data.get('is_email_overriden') and not has_access('is_organizer', event_id=data['event']): + raise ForbiddenException({'pointer': 'data/attributes/is_email_overriden'}, + 'Organizer access required to override email') + elif data.get('is_email_overriden') and has_access('is_organizer', event_id=data['event']) and \ + not data.get('email'): + data['email'] = current_user.email + if 'sessions' in data: session_ids = data['sessions'] for session_id in session_ids: @@ -126,6 +134,13 @@ def before_update_object(self, speaker, data, view_kwargs): if data.get('photo_url') and data['photo_url'] != speaker.photo_url: start_image_resizing_tasks(speaker, data['photo_url']) + if data.get('is_email_overriden') and not has_access('is_organizer', event_id=speaker.event_id): + raise ForbiddenException({'pointer': 'data/attributes/is_email_overriden'}, + 'Organizer access required to override email') + elif data.get('is_email_overriden') and has_access('is_organizer', event_id=speaker.event_id) and \ + not data.get('email'): + data['email'] = current_user.email + def after_patch(self, result): """ method to create session speaker link diff --git a/app/models/speaker.py b/app/models/speaker.py index ca577abd36..1bba4bd138 100644 --- a/app/models/speaker.py +++ b/app/models/speaker.py @@ -24,6 +24,7 @@ class Speaker(SoftDeletionModel): linkedin = db.Column(db.String) organisation = db.Column(db.String) is_featured = db.Column(db.Boolean, default=False) + is_email_overridden = db.Column(db.Boolean, default=False) position = db.Column(db.String) country = db.Column(db.String) city = db.Column(db.String) @@ -51,6 +52,7 @@ def __init__(self, linkedin=None, organisation=None, is_featured=False, + is_email_overridden=False, position=None, country=None, city=None, @@ -76,6 +78,7 @@ def __init__(self, self.github = github self.linkedin = linkedin self.is_featured = is_featured + self.is_email_overridden = is_email_overridden self.organisation = organisation self.position = position self.country = country diff --git a/migrations/versions/96bca587b3ca_.py b/migrations/versions/96bca587b3ca_.py new file mode 100644 index 0000000000..e17fd3d97c --- /dev/null +++ b/migrations/versions/96bca587b3ca_.py @@ -0,0 +1,28 @@ +"""add column is_email_overriden to speaker table + +Revision ID: 96bca587b3ca +Revises: facee76912bc +Create Date: 2019-07-18 04:50:05.175917 + +""" + +from alembic import op +import sqlalchemy as sa +import sqlalchemy_utils + + +# revision identifiers, used by Alembic. +revision = '96bca587b3ca' +down_revision = 'facee76912bc' + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('speaker', sa.Column('is_email_overridden', sa.Boolean(), server_default='False', nullable=False)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('speaker', 'is_email_overridden') + # ### end Alembic commands ### From 42f14d57ac6139a71c6c6c9dbad0ce452efab57b Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sat, 20 Jul 2019 14:23:09 +0530 Subject: [PATCH 14/30] chore(deps): update apscheduler requirement from ~=3.6.0 to ~=3.6.1 (#6217) Updates the requirements on [apscheduler](https://github.com/agronholm/apscheduler) to permit the latest version. - [Release notes](https://github.com/agronholm/apscheduler/releases) - [Changelog](https://github.com/agronholm/apscheduler/blob/master/docs/versionhistory.rst) - [Commits](https://github.com/agronholm/apscheduler/compare/v3.6.0...v3.6.1) 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 077bc0ca94..341d731417 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -18,7 +18,7 @@ itsdangerous~=0.24 humanize~=0.5.1 celery~=4.3 redis~=3.2 -apscheduler~=3.6.0 +apscheduler~=3.6.1 pillow~=6.1.0 amqp~=2.5 gunicorn~=19.9 From ff408e557f6ff12e37e72a5201d110d589ce50e3 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sat, 20 Jul 2019 18:36:05 +0200 Subject: [PATCH 15/30] chore: Remove sudo tag from travis config (#6219) __sudo: required__ no longer is. The __sudo__ command is now always available in Travis CI and there is no way to turn it off. --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 665d7c0a20..36da10af61 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,5 @@ language: python -sudo: required - services: - docker From 027a2c980a66c3094e20b304a0e00fbf9b7d0e3a Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sat, 20 Jul 2019 21:05:01 +0200 Subject: [PATCH 16/30] fix: state logic error in sessions.py (#6220) > if data['state'] is not 'draft' or not 'pending': The second half of this if statement is __always__ False because __python -c "print(not 'pending')"__ # --> False The correct ways to write this line are: ```python if data['state'] != 'draft' or data['state'] != 'pending': # or better yet... if data['state'] not in ('draft', 'pending'): ``` --- app/api/schema/sessions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/schema/sessions.py b/app/api/schema/sessions.py index 76fe7ba1af..3d35bb0506 100644 --- a/app/api/schema/sessions.py +++ b/app/api/schema/sessions.py @@ -54,7 +54,7 @@ def validate_date(self, data, original_data): {'pointer': '/data/attributes/starts-at'}, "starts-at should be after current date-time") if 'state' in data: - if data['state'] is not 'draft' or not 'pending': + if data['state'] not in ('draft', 'pending'): if not has_access('is_coorganizer', event_id=data['event']): return ForbiddenException({'source': ''}, 'Co-organizer access is required.') From 5a3ea1a5e712b420b5585458494c3259dedf8c47 Mon Sep 17 00:00:00 2001 From: Areeb Jamal Date: Sun, 21 Jul 2019 02:24:49 +0530 Subject: [PATCH 17/30] chore: Migrate to flask_jwt_extended (#6216) --- app/__init__.py | 13 +++--- app/api/admin_statistics_api/events.py | 6 +-- app/api/attendees.py | 10 ++-- app/api/auth.py | 31 +++++++++++-- app/api/discount_codes.py | 5 +- app/api/events.py | 8 ++-- app/api/exports.py | 30 ++++++------ app/api/feedbacks.py | 2 +- app/api/helpers/export_helpers.py | 10 ++-- app/api/helpers/import_helpers.py | 4 +- app/api/helpers/jwt.py | 37 +++++++-------- app/api/helpers/permission_manager.py | 46 +++++++++---------- app/api/helpers/permissions.py | 28 +++++------ app/api/helpers/ticketing.py | 2 - app/api/import_jobs.py | 4 +- app/api/imports.py | 8 ++-- app/api/orders.py | 2 +- app/api/sessions.py | 4 +- app/api/settings.py | 4 +- app/api/tickets.py | 4 +- app/api/uploads.py | 6 +-- app/api/user_favourite_events.py | 6 +-- app/api/users.py | 2 +- requirements/common.txt | 2 +- tests/all/integration/api/helpers/test_jwt.py | 4 +- .../api/helpers/test_permission_manager.py | 4 +- 26 files changed, 152 insertions(+), 130 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index e7a1f3f519..8df1bcca8a 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -10,7 +10,7 @@ from flask_migrate import Migrate, MigrateCommand from flask_script import Manager from flask_login import current_user -from flask_jwt import JWT +from flask_jwt_extended import JWTManager from flask_limiter import Limiter from datetime import timedelta from flask_cors import CORS @@ -26,7 +26,7 @@ import stripe from app.settings import get_settings from app.models import db -from app.api.helpers.jwt import jwt_authenticate, jwt_identity +from app.api.helpers.jwt import jwt_user_loader from app.api.helpers.cache import cache from werkzeug.middleware.profiler import ProfilerMiddleware from app.views import BlueprintsManager @@ -102,10 +102,11 @@ def create_app(): app.logger.setLevel(logging.ERROR) # set up jwt - app.config['JWT_AUTH_USERNAME_KEY'] = 'email' - app.config['JWT_EXPIRATION_DELTA'] = timedelta(seconds=24 * 60 * 60) - app.config['JWT_AUTH_URL_RULE'] = '/auth/session' - _jwt = JWT(app, jwt_authenticate, jwt_identity) + app.config['JWT_HEADER_TYPE'] = 'JWT' + app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(days=1) + app.config['JWT_ERROR_MESSAGE_KEY'] = 'error' + _jwt = JWTManager(app) + _jwt.user_loader_callback_loader(jwt_user_loader) # setup celery app.config['CELERY_BROKER_URL'] = app.config['REDIS_URL'] diff --git a/app/api/admin_statistics_api/events.py b/app/api/admin_statistics_api/events.py index 53f832dd8b..fe2dae28a2 100644 --- a/app/api/admin_statistics_api/events.py +++ b/app/api/admin_statistics_api/events.py @@ -1,6 +1,6 @@ from flask_rest_jsonapi import ResourceDetail from flask import jsonify, Blueprint -from flask_jwt import jwt_required +from flask_jwt_extended import jwt_required from sqlalchemy.sql import text from app.api.bootstrap import api @@ -12,7 +12,7 @@ @event_statistics.route('/event-topics', methods=['GET']) -@jwt_required() +@jwt_required def event_topic_count(): result_set = db.engine.execute(text( "SELECT event_topics.name AS name, event_topics.id AS id, " + @@ -25,7 +25,7 @@ def event_topic_count(): @event_statistics.route('/event-types', methods=['GET']) -@jwt_required() +@jwt_required def event_types_count(): result_set = db.engine.execute(text( "SELECT event_types.name AS name, event_types.id AS id, " + diff --git a/app/api/attendees.py b/app/api/attendees.py index 7629b975ab..e5017e7786 100644 --- a/app/api/attendees.py +++ b/app/api/attendees.py @@ -1,7 +1,7 @@ from datetime import datetime from flask import Blueprint, request, jsonify, abort, make_response -from flask_jwt import current_identity +from flask_jwt_extended import current_user from flask_rest_jsonapi import ResourceDetail, ResourceList, ResourceRelationship from flask_rest_jsonapi.exceptions import ObjectNotFound from sqlalchemy.orm.exc import NoResultFound @@ -146,7 +146,7 @@ def before_get_object(self, view_kwargs): :return: """ attendee = safe_query(self, TicketHolder, 'id', view_kwargs['id'], 'attendee_id') - if not has_access('is_registrar_or_user_itself', user_id=current_identity.id, event_id=attendee.event_id): + if not has_access('is_registrar_or_user_itself', user_id=current_user.id, event_id=attendee.event_id): raise ForbiddenException({'source': 'User'}, 'You are not authorized to access this.') def before_delete_object(self, obj, kwargs): @@ -171,7 +171,7 @@ def before_update_object(self, obj, data, kwargs): # raise ForbiddenException({'source': 'User'}, 'You are not authorized to access this.') if 'ticket' in data: - user = safe_query(self, User, 'id', current_identity.id, 'user_id') + user = safe_query(self, User, 'id', current_user.id, 'user_id') ticket = db.session.query(Ticket).filter_by( id=int(data['ticket']), deleted_at=None ).first() @@ -278,7 +278,7 @@ def send_receipt(): except NoResultFound: raise ObjectNotFound({'parameter': '{identifier}'}, "Order not found") - if (order.user_id != current_identity.id) and (not has_access('is_registrar', event_id=order.event_id)): + if (order.user_id != current_user.id) and (not has_access('is_registrar', event_id=order.event_id)): abort( make_response(jsonify(error="You need to be the event organizer or order buyer to send receipts."), 403) ) @@ -287,7 +287,7 @@ def send_receipt(): make_response(jsonify(error="Cannot send receipt for an incomplete order"), 409) ) else: - send_email_to_attendees(order, current_identity.id) + send_email_to_attendees(order, current_user.id) return jsonify(message="receipt sent to attendees") else: abort( diff --git a/app/api/auth.py b/app/api/auth.py index 4c794f6504..a4cf7fa4ce 100644 --- a/app/api/auth.py +++ b/app/api/auth.py @@ -7,7 +7,7 @@ import requests from flask import request, jsonify, make_response, Blueprint, send_file -from flask_jwt import current_identity as current_user, jwt_required +from flask_jwt_extended import jwt_required, current_user, create_access_token from flask_limiter.util import get_remote_address from healthcheck import EnvironmentDump from flask_rest_jsonapi.exceptions import ObjectNotFound @@ -17,6 +17,7 @@ from app import limiter from app.api.helpers.db import save_to_db, get_count, safe_query from app.api.helpers.auth import AuthManager +from app.api.helpers.jwt import jwt_authenticate from app.api.helpers.errors import ForbiddenError, UnprocessableEntityError, NotFoundError, BadRequestError from app.api.helpers.files import make_frontend_url from app.api.helpers.mail import send_email_to_attendees @@ -44,6 +45,26 @@ auth_routes = Blueprint('auth', __name__, url_prefix='/v1/auth') +@authorised_blueprint.route('/auth/session', methods=['POST']) +@auth_routes.route('/login', methods=['POST']) +def login(): + data = request.get_json() + username = data.get('email', data.get('username')) + password = data.get('password') + criterion = [username, password] + + if not all(criterion): + return jsonify(error='username or password missing'), 400 + + identity = jwt_authenticate(username, password) + + if identity: + access_token = create_access_token(identity.id, fresh=True) + return jsonify(access_token=access_token) + else: + return jsonify(error='Invalid Credentials'), 401 + + @auth_routes.route('/oauth/', methods=['GET']) def redirect_uri(provider): if provider == 'facebook': @@ -269,7 +290,7 @@ def reset_password_patch(): @auth_routes.route('/change-password', methods=['POST']) -@jwt_required() +@jwt_required def change_password(): old_password = request.json['data']['old-password'] new_password = request.json['data']['new-password'] @@ -310,7 +331,7 @@ def return_file(file_name_prefix, file_path, identifier): @ticket_blueprint.route('/tickets/') -@jwt_required() +@jwt_required def ticket_attendee_authorized(order_identifier): if current_user: try: @@ -332,7 +353,7 @@ def ticket_attendee_authorized(order_identifier): @ticket_blueprint.route('/orders/invoices/') -@jwt_required() +@jwt_required def order_invoices(order_identifier): if current_user: try: @@ -354,7 +375,7 @@ def order_invoices(order_identifier): @ticket_blueprint.route('/events/invoices/') -@jwt_required() +@jwt_required def event_invoices(invoice_identifier): if not current_user: return ForbiddenError({'source': ''}, 'Authentication Required to access Invoice').respond() diff --git a/app/api/discount_codes.py b/app/api/discount_codes.py index 10d8869fed..4e7d9e5b0a 100644 --- a/app/api/discount_codes.py +++ b/app/api/discount_codes.py @@ -1,5 +1,6 @@ from datetime import datetime +from flask_jwt_extended import current_user from flask_rest_jsonapi import ResourceDetail, ResourceList, ResourceRelationship from flask_rest_jsonapi.exceptions import ObjectNotFound from sqlalchemy.orm.exc import NoResultFound @@ -7,7 +8,7 @@ from app.api.helpers.db import safe_query from app.api.helpers.exceptions import ConflictException, ForbiddenException, UnprocessableEntity, MethodNotAllowed from app.api.helpers.permission_manager import has_access -from app.api.helpers.permissions import jwt_required, current_identity +from app.api.helpers.permissions import jwt_required from app.api.helpers.utilities import require_relationship from app.api.schema.discount_codes import DiscountCodeSchemaEvent, DiscountCodeSchemaPublic, DiscountCodeSchemaTicket from app.models import db @@ -52,7 +53,7 @@ def before_post(self, args, kwargs, data): elif data['used_for'] == 'event' and not has_access('is_admin') and 'events' in data: raise UnprocessableEntity({'source': ''}, "Please verify your permission or check your relationship") - data['user_id'] = current_identity.id + data['user_id'] = current_user.id def before_create_object(self, data, view_kwargs): if data['used_for'] == 'event': diff --git a/app/api/events.py b/app/api/events.py index 2bbc55567b..13f5312f52 100644 --- a/app/api/events.py +++ b/app/api/events.py @@ -1,5 +1,5 @@ from flask import request, current_app -from flask_jwt import current_identity, _jwt_required +from flask_jwt_extended import verify_jwt_in_request, current_user from flask_rest_jsonapi import ResourceDetail, ResourceList, ResourceRelationship from flask_rest_jsonapi.exceptions import ObjectNotFound from marshmallow_jsonapi import fields @@ -109,9 +109,9 @@ def query(self, view_kwargs): """ query_ = self.session.query(Event).filter_by(state='published') if 'Authorization' in request.headers: - _jwt_required(current_app.config['JWT_DEFAULT_REALM']) + verify_jwt_in_request() query2 = self.session.query(Event) - query2 = query2.join(Event.roles).filter_by(user_id=current_identity.id).join(UsersEventsRoles.role). \ + query2 = query2.join(Event.roles).filter_by(user_id=current_user.id).join(UsersEventsRoles.role). \ filter(or_(Role.name == COORGANIZER, Role.name == ORGANIZER, Role.name == OWNER)) query_ = query_.union(query2) @@ -459,7 +459,7 @@ def before_patch(self, args, kwargs, data=None): :param data: :return: """ - user = User.query.filter_by(id=current_identity.id).one() + user = User.query.filter_by(id=current_user.id).one() modules = Module.query.first() validate_event(user, modules, data) diff --git a/app/api/exports.py b/app/api/exports.py index 2949293202..ec7cc1dc82 100644 --- a/app/api/exports.py +++ b/app/api/exports.py @@ -2,7 +2,7 @@ from flask import send_file, make_response, jsonify, url_for, \ current_app, request, Blueprint -from flask_jwt import jwt_required, current_identity +from flask_jwt_extended import jwt_required, current_user from app.api.helpers.export_helpers import export_event_json, create_export_job from app.api.helpers.utilities import TASK_RESULTS @@ -20,7 +20,7 @@ @export_routes.route('/events//export/json', methods=['POST']) -@jwt_required() +@jwt_required def export_event(event_identifier): from .helpers.tasks import export_event_task @@ -37,7 +37,7 @@ def export_event(event_identifier): event_id = event_identifier # queue task task = export_event_task.delay( - current_identity.email, event_id, settings) + current_user.email, event_id, settings) # create Job create_export_job(task.id, event_id) @@ -54,7 +54,7 @@ def export_event(event_identifier): @export_routes.route('/events//exports/') -@jwt_required() +@jwt_required def export_download(event_id, path): if not path.startswith('/'): path = '/' + path @@ -66,7 +66,7 @@ def export_download(event_id, path): @export_routes.route('/events//export/xcal', methods=['GET']) -@jwt_required() +@jwt_required def export_event_xcal(event_identifier): if not event_identifier.isdigit(): @@ -95,7 +95,7 @@ def event_export_task_base(event_id, settings): @export_routes.route('/events//export/ical', methods=['GET']) -@jwt_required() +@jwt_required def export_event_ical(event_identifier): if not event_identifier.isdigit(): event = db.session.query(Event).filter_by(identifier=event_identifier).first() @@ -115,7 +115,7 @@ def export_event_ical(event_identifier): @export_routes.route('/events//export/pentabarf', methods=['GET']) -@jwt_required() +@jwt_required def export_event_pentabarf(event_identifier): if not event_identifier.isdigit(): event = db.session.query(Event).filter_by(identifier=event_identifier).first() @@ -135,7 +135,7 @@ def export_event_pentabarf(event_identifier): @export_routes.route('/events//export/orders/csv', methods=['GET']) -@jwt_required() +@jwt_required def export_orders_csv(event_identifier): if not event_identifier.isdigit(): event = db.session.query(Event).filter_by(identifier=event_identifier).first() @@ -155,7 +155,7 @@ def export_orders_csv(event_identifier): @export_routes.route('/events//export/orders/pdf', methods=['GET']) -@jwt_required() +@jwt_required def export_orders_pdf(event_identifier): if not event_identifier.isdigit(): event = db.session.query(Event).filter_by(identifier=event_identifier).first() @@ -175,7 +175,7 @@ def export_orders_pdf(event_identifier): @export_routes.route('/events//export/attendees/csv', methods=['GET']) -@jwt_required() +@jwt_required def export_attendees_csv(event_identifier): if not event_identifier.isdigit(): event = db.session.query(Event).filter_by(identifier=event_identifier).first() @@ -195,7 +195,7 @@ def export_attendees_csv(event_identifier): @export_routes.route('/events//export/attendees/pdf', methods=['GET']) -@jwt_required() +@jwt_required def export_attendees_pdf(event_identifier): if not event_identifier.isdigit(): event = db.session.query(Event).filter_by(identifier=event_identifier).first() @@ -215,7 +215,7 @@ def export_attendees_pdf(event_identifier): @export_routes.route('/events//export/sessions/csv', methods=['GET']) -@jwt_required() +@jwt_required def export_sessions_csv(event_identifier): if not event_identifier.isdigit(): event = db.session.query(Event).filter_by(identifier=event_identifier).first() @@ -235,7 +235,7 @@ def export_sessions_csv(event_identifier): @export_routes.route('/events//export/speakers/csv', methods=['GET']) -@jwt_required() +@jwt_required def export_speakers_csv(event_identifier): if not event_identifier.isdigit(): event = db.session.query(Event).filter_by(identifier=event_identifier).first() @@ -255,7 +255,7 @@ def export_speakers_csv(event_identifier): @export_routes.route('/events//export/sessions/pdf', methods=['GET']) -@jwt_required() +@jwt_required def export_sessions_pdf(event_identifier): if not event_identifier.isdigit(): event = db.session.query(Event).filter_by(identifier=event_identifier).first() @@ -275,7 +275,7 @@ def export_sessions_pdf(event_identifier): @export_routes.route('/events//export/speakers/pdf', methods=['GET']) -@jwt_required() +@jwt_required def export_speakers_pdf(event_identifier): if not event_identifier.isdigit(): event = db.session.query(Event).filter_by(identifier=event_identifier).first() diff --git a/app/api/feedbacks.py b/app/api/feedbacks.py index 85c0949b95..14d8f304a9 100644 --- a/app/api/feedbacks.py +++ b/app/api/feedbacks.py @@ -1,6 +1,6 @@ +from flask_jwt_extended import current_user from flask_rest_jsonapi import ResourceDetail, ResourceList, ResourceRelationship from flask_rest_jsonapi.exceptions import ObjectNotFound -from flask_jwt import current_identity as current_user from app.api.bootstrap import api from app.api.helpers.db import safe_query diff --git a/app/api/helpers/export_helpers.py b/app/api/helpers/export_helpers.py index 7876666c19..30ec3de0e9 100644 --- a/app/api/helpers/export_helpers.py +++ b/app/api/helpers/export_helpers.py @@ -8,8 +8,8 @@ import requests from flask import current_app as app from flask import request, url_for -from flask_jwt import current_identity -from flask_login import current_user +from flask_jwt_extended import current_user +from flask_login import current_user as current_logged_user from app.api.helpers.db import save_to_db from app.api.helpers.storage import upload, UPLOAD_PATHS, UploadedFile @@ -231,10 +231,10 @@ def export_event_json(event_id, settings): def get_current_user(): - if current_identity: - return current_identity - else: + if current_user: return current_user + else: + return current_logged_user # HELPERS diff --git a/app/api/helpers/import_helpers.py b/app/api/helpers/import_helpers.py index ded716d3a5..d8554650c3 100644 --- a/app/api/helpers/import_helpers.py +++ b/app/api/helpers/import_helpers.py @@ -8,7 +8,7 @@ import requests from flask import current_app as app from flask import request -from flask_jwt import current_identity +from flask_jwt_extended import current_user from werkzeug import secure_filename from app.api.helpers.db import save_to_db @@ -149,7 +149,7 @@ def _delete_fields(srv, data): def create_import_job(task): """create import record in db""" ij = ImportJob(task=task, - user=current_identity) + user=current_user) save_to_db(ij, 'Import job saved') diff --git a/app/api/helpers/jwt.py b/app/api/helpers/jwt.py index e70bba1049..239f52879f 100644 --- a/app/api/helpers/jwt.py +++ b/app/api/helpers/jwt.py @@ -1,8 +1,12 @@ import base64 import json -from flask_jwt import _default_request_handler +from flask import _app_ctx_stack as ctx_stack +from flask_jwt_extended.view_decorators import _decode_jwt_from_request, _load_user +from flask_jwt_extended.config import config +from flask_jwt_extended.exceptions import JWTExtendedException, UserLoadError from flask_scrypt import check_password_hash +from jwt.exceptions import PyJWTError from app.models.user import User @@ -24,13 +28,8 @@ def jwt_authenticate(email, password): return None -def jwt_identity(payload): - """ - Jwt helper function - :param payload: - :return: - """ - return User.query.get(payload['identity']) +def jwt_user_loader(identity): + return User.query.filter_by(id=identity, deleted_at=None).first() def get_identity(): @@ -38,13 +37,15 @@ def get_identity(): To be used only if identity for expired tokens is required, otherwise use current_identity from flask_jwt :return: """ - token_second_segment = _default_request_handler().split('.')[1] - missing_padding = len(token_second_segment) % 4 - - # ensures the string is correctly padded to be a multiple of 4 - if missing_padding != 0: - token_second_segment += '=' * (4 - missing_padding) - - payload = json.loads(str(base64.b64decode(token_second_segment), 'utf-8')) - user = jwt_identity(payload) - return user + token = None + try: + token = _decode_jwt_from_request('access') + except (JWTExtendedException, PyJWTError): + token = getattr(ctx_stack.top, 'expired_jwt', None) + + if token: + try: + _load_user(token[config.identity_claim_key]) + return getattr(ctx_stack.top, 'jwt_user', None) + except UserLoadError: + pass diff --git a/app/api/helpers/permission_manager.py b/app/api/helpers/permission_manager.py index 924a2e3a5a..05a9d19637 100644 --- a/app/api/helpers/permission_manager.py +++ b/app/api/helpers/permission_manager.py @@ -1,5 +1,5 @@ from flask import current_app as app -from flask_jwt import _jwt_required, current_identity +from flask_jwt_extended import verify_jwt_in_request, current_user from sqlalchemy.orm.exc import NoResultFound from flask import request @@ -24,7 +24,7 @@ def is_super_admin(view, view_args, view_kwargs, *args, **kwargs): Do not use this if the resource is also accessible by a normal admin, use the is_admin decorator instead. :return: """ - user = current_identity + user = current_user if not user.is_super_admin: return ForbiddenError({'source': ''}, 'Super admin access is required').respond() return view(*view_args, **view_kwargs) @@ -32,7 +32,7 @@ def is_super_admin(view, view_args, view_kwargs, *args, **kwargs): @jwt_required def is_admin(view, view_args, view_kwargs, *args, **kwargs): - user = current_identity + user = current_user if not user.is_admin and not user.is_super_admin: return ForbiddenError({'source': ''}, 'Admin access is required').respond() @@ -41,7 +41,7 @@ def is_admin(view, view_args, view_kwargs, *args, **kwargs): @jwt_required def is_owner(view, view_args, view_kwargs, *args, **kwargs): - user = current_identity + user = current_user if user.is_staff: return view(*view_args, **view_kwargs) @@ -54,7 +54,7 @@ def is_owner(view, view_args, view_kwargs, *args, **kwargs): @jwt_required def is_organizer(view, view_args, view_kwargs, *args, **kwargs): - user = current_identity + user = current_user if user.is_staff: return view(*view_args, **view_kwargs) @@ -67,7 +67,7 @@ def is_organizer(view, view_args, view_kwargs, *args, **kwargs): @jwt_required def is_coorganizer(view, view_args, view_kwargs, *args, **kwargs): - user = current_identity + user = current_user if user.is_staff: return view(*view_args, **view_kwargs) @@ -80,7 +80,7 @@ def is_coorganizer(view, view_args, view_kwargs, *args, **kwargs): @jwt_required def is_coorganizer_but_not_admin(view, view_args, view_kwargs, *args, **kwargs): - user = current_identity + user = current_user if user.has_event_access(kwargs['event_id']): return view(*view_args, **view_kwargs) @@ -104,11 +104,11 @@ def is_coorganizer_endpoint_related_to_event(view, view_args, view_kwargs, *args user = get_identity() if user.is_staff: - _jwt_required(app.config['JWT_DEFAULT_REALM']) + verify_jwt_in_request() return view(*view_args, **view_kwargs) if user.has_event_access(kwargs['event_id']): - _jwt_required(app.config['JWT_DEFAULT_REALM']) + verify_jwt_in_request() return view(*view_args, **view_kwargs) return ForbiddenError({'source': ''}, 'Co-organizer access is required.').respond() @@ -120,7 +120,7 @@ def is_user_itself(view, view_args, view_kwargs, *args, **kwargs): Allows admin and super admin access to any resource irrespective of id. Otherwise the user can only access his/her resource. """ - user = current_identity + user = current_user if not user.is_admin and not user.is_super_admin and user.id != kwargs['user_id']: return ForbiddenError({'source': ''}, 'Access Forbidden').respond() return view(*view_args, **view_kwargs) @@ -132,7 +132,7 @@ def is_coorganizer_or_user_itself(view, view_args, view_kwargs, *args, **kwargs) Allows admin and super admin access to any resource irrespective of id. Otherwise the user can only access his/her resource. """ - user = current_identity + user = current_user if user.is_admin or user.is_super_admin or ('user_id' in kwargs and user.id == kwargs['user_id']): return view(*view_args, **view_kwargs) @@ -152,7 +152,7 @@ def is_speaker_for_session(view, view_args, view_kwargs, *args, **kwargs): Allows admin and super admin access to any resource irrespective of id. Otherwise the user can only access his/her resource. """ - user = current_identity + user = current_user if user.is_admin or user.is_super_admin: return view(*view_args, **view_kwargs) @@ -184,7 +184,7 @@ def is_speaker_itself_or_admin(view, view_args, view_kwargs, *args, **kwargs): Allows admin and super admin access to any resource irrespective of id. Otherwise the user can only access his/her resource. """ - user = current_identity + user = current_user if user.is_admin or user.is_super_admin: return view(*view_args, **view_kwargs) @@ -206,7 +206,7 @@ def is_session_self_submitted(view, view_args, view_kwargs, *args, **kwargs): Allows admin and super admin access to any resource irrespective of id. Otherwise the user can only access his/her resource. """ - user = current_identity + user = current_user if user.is_admin or user.is_super_admin: return view(*view_args, **view_kwargs) @@ -232,7 +232,7 @@ def is_registrar(view, view_args, view_kwargs, *args, **kwargs): """ Allows Organizer, Co-organizer and registrar to access the event resources.x` """ - user = current_identity + user = current_user event_id = kwargs['event_id'] if user.is_staff: @@ -248,7 +248,7 @@ def is_registrar_or_user_itself(view, view_args, view_kwargs, *args, **kwargs): Allows admin and super admin access to any resource irrespective of id. Otherwise the user can only access his/her resource. """ - user = current_identity + user = current_user if user.is_admin or user.is_super_admin or user.id == kwargs['user_id']: return view(*view_args, **view_kwargs) @@ -267,7 +267,7 @@ def is_track_organizer(view, view_args, view_kwargs, *args, **kwargs): """ Allows Organizer, Co-organizer and Track Organizer to access the resource(s). """ - user = current_identity + user = current_user event_id = kwargs['event_id'] if user.is_staff: @@ -282,7 +282,7 @@ def is_moderator(view, view_args, view_kwargs, *args, **kwargs): """ Allows Organizer, Co-organizer and Moderator to access the resource(s). """ - user = current_identity + user = current_user event_id = kwargs['event_id'] if user.is_staff: return view(*view_args, **view_kwargs) @@ -293,15 +293,15 @@ def is_moderator(view, view_args, view_kwargs, *args, **kwargs): @jwt_required def user_event(view, view_args, view_kwargs, *args, **kwargs): - user = current_identity + user = current_user view_kwargs['user_id'] = user.id return view(*view_args, **view_kwargs) def accessible_role_based_events(view, view_args, view_kwargs, *args, **kwargs): if 'POST' in request.method or 'withRole' in request.args: - _jwt_required(app.config['JWT_DEFAULT_REALM']) - user = current_identity + verify_jwt_in_request() + user = current_user if 'GET' in request.method and user.is_staff: return view(*view_args, **view_kwargs) @@ -312,8 +312,8 @@ def accessible_role_based_events(view, view_args, view_kwargs, *args, **kwargs): def create_event(view, view_args, view_kwargs, *args, **kwargs): if 'POST' in request.method or 'withRole' in request.args: - _jwt_required(app.config['JWT_DEFAULT_REALM']) - user = current_identity + verify_jwt_in_request() + user = current_user if user.can_create_event is False: return ForbiddenError({'source': ''}, 'Please verify your email').respond() diff --git a/app/api/helpers/permissions.py b/app/api/helpers/permissions.py index 91d4ce8003..1b74b06015 100644 --- a/app/api/helpers/permissions.py +++ b/app/api/helpers/permissions.py @@ -1,6 +1,6 @@ from functools import wraps from flask import current_app as app -from flask_jwt import _jwt_required, current_identity +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 @@ -39,9 +39,9 @@ def jwt_required(fn, realm=None): """ @wraps(fn) def decorator(*args, **kwargs): - _jwt_required(realm or app.config['JWT_DEFAULT_REALM']) - current_identity.last_accessed_at = datetime.utcnow() - save_to_db(current_identity) + verify_jwt_in_request() + current_user.last_accessed_at = datetime.utcnow() + save_to_db(current_user) return fn(*args, **kwargs) return decorator @@ -58,7 +58,7 @@ def is_super_admin(f): @wraps(f) def decorated_function(*args, **kwargs): - user = current_identity + user = current_user if not user.is_super_admin: return ForbiddenError({'source': ''}, 'Super admin access is required').respond() return f(*args, **kwargs) @@ -76,7 +76,7 @@ def is_admin(f): @wraps(f) def decorated_function(*args, **kwargs): - user = current_identity + user = current_user if not user.is_admin and not user.is_super_admin: return ForbiddenError({'source': ''}, 'Admin access is required').respond() return f(*args, **kwargs) @@ -95,7 +95,7 @@ def is_user_itself(f): @wraps(f) def decorated_function(*args, **kwargs): - user = current_identity + user = current_user if not user.is_admin and not user.is_super_admin and user.id != kwargs['id']: return ForbiddenError({'source': ''}, 'Access Forbidden').respond() return f(*args, **kwargs) @@ -113,7 +113,7 @@ def is_owner(f): @wraps(f) def decorated_function(*args, **kwargs): - user = current_identity + user = current_user if user.is_staff: return f(*args, **kwargs) @@ -134,7 +134,7 @@ def is_organizer(f): @wraps(f) def decorated_function(*args, **kwargs): - user = current_identity + user = current_user if user.is_staff: return f(*args, **kwargs) @@ -155,7 +155,7 @@ def is_coorganizer(f): @wraps(f) def decorated_function(*args, **kwargs): - user = current_identity + user = current_user if user.is_staff: return f(*args, **kwargs) @@ -176,7 +176,7 @@ def is_registrar(f): @wraps(f) def decorated_function(*args, **kwargs): - user = current_identity + user = current_user if user.is_staff: return f(*args, **kwargs) @@ -199,7 +199,7 @@ def is_track_organizer(f): @wraps(f) def decorated_function(*args, **kwargs): - user = current_identity + user = current_user if user.is_staff: return f(*args, **kwargs) @@ -222,7 +222,7 @@ def is_moderator(f): @wraps(f) def decorated_function(*args, **kwargs): - user = current_identity + user = current_user if user.is_staff: return f(*args, **kwargs) @@ -247,7 +247,7 @@ def accessible_events(f): @wraps(f) def decorated_function(*args, **kwargs): - user = current_identity + user = current_user if 'POST' in request.method: kwargs['user_id'] = user.id else: diff --git a/app/api/helpers/ticketing.py b/app/api/helpers/ticketing.py index 1ad2112da6..14a02aeb61 100644 --- a/app/api/helpers/ticketing.py +++ b/app/api/helpers/ticketing.py @@ -1,7 +1,5 @@ from datetime import datetime -from flask_jwt import current_identity as current_user - from app.api.helpers.db import save_to_db, get_count from app.api.helpers.exceptions import ConflictException from app.api.helpers.files import make_frontend_url diff --git a/app/api/import_jobs.py b/app/api/import_jobs.py index cb7465f214..05e2b83be3 100644 --- a/app/api/import_jobs.py +++ b/app/api/import_jobs.py @@ -4,7 +4,7 @@ from app.models import db from app.models.import_job import ImportJob from app.api.helpers.permissions import jwt_required -from flask_jwt import current_identity +from flask_jwt_extended import current_user class ImportJobList(ResourceList): @@ -13,7 +13,7 @@ class ImportJobList(ResourceList): """ def query(self, kwargs): query_ = self.session.query(ImportJob) - query_ = query_.filter_by(user_id=current_identity.id) + query_ = query_.filter_by(user_id=current_user.id) return query_ decorators = (jwt_required,) diff --git a/app/api/imports.py b/app/api/imports.py index d45f9d34c2..31d8031814 100644 --- a/app/api/imports.py +++ b/app/api/imports.py @@ -1,5 +1,5 @@ from flask import jsonify, url_for, current_app, Blueprint, abort -from flask_jwt import jwt_required, current_identity +from flask_jwt_extended import jwt_required, current_user from app.api.helpers.files import make_frontend_url from app.api.helpers.import_helpers import get_file_from_request, import_event_json, create_import_job @@ -9,7 +9,7 @@ @import_routes.route('/events/import/', methods=['POST']) -@jwt_required() +@jwt_required def import_event(source_type): if source_type == 'json': file_path = get_file_from_request(['zip']) @@ -17,8 +17,8 @@ def import_event(source_type): file_path = None abort(404) from .helpers.tasks import import_event_task - task = import_event_task.delay(email=current_identity.email, file=file_path, - source_type=source_type, creator_id=current_identity.id) + task = import_event_task.delay(email=current_user.email, file=file_path, + source_type=source_type, creator_id=current_user.id) # create import job create_import_job(task.id) diff --git a/app/api/orders.py b/app/api/orders.py index 2c9737f7f9..4737f9a7c4 100644 --- a/app/api/orders.py +++ b/app/api/orders.py @@ -3,7 +3,7 @@ import omise from flask import request, jsonify, Blueprint, url_for, redirect -from flask_jwt import current_identity as current_user +from flask_jwt_extended import current_user from flask_rest_jsonapi import ResourceDetail, ResourceList, ResourceRelationship from marshmallow_jsonapi import fields from marshmallow_jsonapi.flask import Schema diff --git a/app/api/sessions.py b/app/api/sessions.py index 2b102337e0..d3abb83444 100644 --- a/app/api/sessions.py +++ b/app/api/sessions.py @@ -1,3 +1,4 @@ +from flask_jwt_extended import current_user from flask_rest_jsonapi import ResourceDetail, ResourceList, ResourceRelationship from app.api.bootstrap import api @@ -6,7 +7,6 @@ from app.api.helpers.exceptions import ForbiddenException from app.api.helpers.mail import send_email_new_session, send_email_session_accept_reject from app.api.helpers.notification import send_notif_new_session_organizer, send_notif_session_accept_reject -from app.api.helpers.permissions import current_identity from app.api.helpers.permission_manager import has_access from app.api.helpers.query import event_query from app.api.helpers.utilities import require_relationship @@ -36,7 +36,7 @@ def before_post(self, args, kwargs, data): :return: """ require_relationship(['event', 'track'], data) - data['creator_id'] = current_identity.id + data['creator_id'] = current_user.id if get_count(db.session.query(Event).filter_by(id=int(data['event']), is_sessions_speakers_enabled=False)) > 0: raise ForbiddenException({'pointer': ''}, "Sessions are disabled for this Event") diff --git a/app/api/settings.py b/app/api/settings.py index 20ed8838d5..b33c166d44 100644 --- a/app/api/settings.py +++ b/app/api/settings.py @@ -1,6 +1,6 @@ from flask import current_app as app from flask import jsonify, request, Blueprint, make_response -from flask_jwt import current_identity as current_user, _jwt_required +from flask_jwt_extended import verify_jwt_in_request, current_user from flask_rest_jsonapi import ResourceDetail from app.api.bootstrap import api @@ -39,7 +39,7 @@ def before_get(self, args, kwargs): kwargs['id'] = 1 if 'Authorization' in request.headers: - _jwt_required(app.config['JWT_DEFAULT_REALM']) + verify_jwt_in_request() if current_user.is_admin or current_user.is_super_admin: self.schema = SettingSchemaAdmin diff --git a/app/api/tickets.py b/app/api/tickets.py index b6c5cebd71..8faac8e15e 100644 --- a/app/api/tickets.py +++ b/app/api/tickets.py @@ -1,7 +1,7 @@ from flask import request, current_app from flask_rest_jsonapi import ResourceDetail, ResourceList, ResourceRelationship from flask_rest_jsonapi.exceptions import ObjectNotFound -from flask_jwt import current_identity as current_user, _jwt_required +from flask_jwt_extended import current_user, verify_jwt_in_request from sqlalchemy.orm.exc import NoResultFound from app.api.bootstrap import api @@ -92,7 +92,7 @@ def query(self, view_kwargs): """ if 'Authorization' in request.headers: - _jwt_required(current_app.config['JWT_DEFAULT_REALM']) + verify_jwt_in_request() if current_user.is_super_admin or current_user.is_admin: query_ = self.session.query(Ticket) elif view_kwargs.get('event_id') and has_access('is_organizer', event_id=view_kwargs['event_id']): diff --git a/app/api/uploads.py b/app/api/uploads.py index 67232fc7ef..3900dbefba 100644 --- a/app/api/uploads.py +++ b/app/api/uploads.py @@ -1,6 +1,6 @@ from flask import Blueprint from flask import make_response, request, jsonify, abort -from flask_jwt import jwt_required +from flask_jwt_extended import jwt_required from app.api.helpers.files import uploaded_image, uploaded_file from app.api.helpers.storage import UPLOAD_PATHS, upload_local, upload import uuid @@ -9,7 +9,7 @@ @upload_routes.route('/image', methods=['POST']) -@jwt_required() +@jwt_required def upload_image(): image = request.json['data'] extension = '.{}'.format(image.split(";")[0].split("/")[1]) @@ -29,7 +29,7 @@ def upload_image(): @upload_routes.route('/files', methods=['POST']) -@jwt_required() +@jwt_required def upload_file(): force_local = request.args.get('force_local', 'false') if 'file' in request.files: diff --git a/app/api/user_favourite_events.py b/app/api/user_favourite_events.py index 36051bc57a..46c5fd6e38 100644 --- a/app/api/user_favourite_events.py +++ b/app/api/user_favourite_events.py @@ -1,5 +1,5 @@ from flask import request, current_app as app -from flask_jwt import current_identity as current_user, _jwt_required, jwt_required +from flask_jwt_extended import current_user, jwt_required, verify_jwt_in_request from flask_rest_jsonapi import ResourceDetail, ResourceList, ResourceRelationship from flask_rest_jsonapi.exceptions import ObjectNotFound from sqlalchemy.orm.exc import NoResultFound @@ -30,7 +30,7 @@ def before_post(self, args, kwargs, data): require_relationship(['event'], data) if 'Authorization' in request.headers: - _jwt_required(app.config['JWT_DEFAULT_REALM']) + verify_jwt_in_request() else: raise ForbiddenException({'source': ''}, 'Only Authorized Users can favourite an event') @@ -81,7 +81,7 @@ class UserFavouriteEventDetail(ResourceDetail): """ User Favourite Events detail by id """ - @jwt_required() + @jwt_required def before_get_object(self, view_kwargs): if view_kwargs.get('id') is not None: diff --git a/app/api/users.py b/app/api/users.py index f86f4e7c85..c6f6e11d64 100644 --- a/app/api/users.py +++ b/app/api/users.py @@ -1,7 +1,7 @@ import base64 from flask import Blueprint, request, jsonify, abort, make_response -from flask_jwt import current_identity as current_user +from flask_jwt_extended import current_user from flask_rest_jsonapi import ResourceDetail, ResourceList, ResourceRelationship from sqlalchemy.orm.exc import NoResultFound import urllib.error diff --git a/requirements/common.txt b/requirements/common.txt index 341d731417..56f85ced4d 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -6,7 +6,7 @@ Flask-SQLAlchemy~=2.1 Flask-Migrate~=2.5 Flask-Login~=0.4 Flask-Scrypt~=0.1.3 -Flask-JWT~=0.3.2 +flask-jwt-extended~=3.20.0 flask-celeryext~=0.3 omise~=0.8.1 requests-oauthlib~=0.8 diff --git a/tests/all/integration/api/helpers/test_jwt.py b/tests/all/integration/api/helpers/test_jwt.py index a0fbb1f56b..5459e29fc4 100644 --- a/tests/all/integration/api/helpers/test_jwt.py +++ b/tests/all/integration/api/helpers/test_jwt.py @@ -1,6 +1,6 @@ import unittest -from flask_jwt import _default_jwt_encode_handler +from flask_jwt_extended import create_access_token from app import current_app as app from app.api.helpers.jwt import jwt_authenticate, get_identity @@ -43,7 +43,7 @@ def test_get_identity(self): save_to_db(event) # Authenticate User - self.auth = {'Authorization': "JWT " + str(_default_jwt_encode_handler(user), 'utf-8')} + self.auth = {'Authorization': "JWT " + create_access_token(user.id, fresh=True)} with app.test_request_context(headers=self.auth): self.assertEquals(get_identity().id, user.id) diff --git a/tests/all/integration/api/helpers/test_permission_manager.py b/tests/all/integration/api/helpers/test_permission_manager.py index f2fdb76672..10c8f4c6b7 100644 --- a/tests/all/integration/api/helpers/test_permission_manager.py +++ b/tests/all/integration/api/helpers/test_permission_manager.py @@ -1,7 +1,7 @@ import unittest from flask import Response -from flask_jwt import _default_jwt_encode_handler +from flask_jwt_extended import create_access_token from app import current_app as app from app.api.helpers.db import get_or_create, save_to_db @@ -26,7 +26,7 @@ def setUp(self): save_to_db(event) # Authenticate User - self.auth = {'Authorization': "JWT " + str(_default_jwt_encode_handler(user), 'utf-8')} + self.auth = {'Authorization': "JWT " + create_access_token(user.id, fresh=True)} def test_has_access(self): """Method to test whether user has access to different roles""" From 1f916039961b6163867ef9cd749355276e7a7014 Mon Sep 17 00:00:00 2001 From: Prateek Jain Date: Sun, 21 Jul 2019 13:50:04 +0530 Subject: [PATCH 18/30] fix: Change discount-code route to use event ID (#6208) --- app/api/__init__.py | 147 ++++++++++++++++++------------------ app/api/access_codes.py | 5 +- app/api/discount_codes.py | 4 +- docs/api/api_blueprint.apib | 6 +- tests/hook_main.py | 9 ++- 5 files changed, 91 insertions(+), 80 deletions(-) diff --git a/app/api/__init__.py b/app/api/__init__.py index afba682ea1..f084ec8c09 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -1,90 +1,90 @@ +from app.api.access_codes import AccessCodeList, AccessCodeListPost, AccessCodeDetail, AccessCodeRelationshipRequired, \ + AccessCodeRelationshipOptional +from app.api.activities import ActivityList, ActivityDetail +from app.api.admin_sales.discounted import AdminSalesDiscountedList +from app.api.admin_sales.events import AdminSalesByEventsList +from app.api.admin_sales.fees import AdminSalesFeesList +from app.api.admin_sales.invoices import AdminSalesInvoicesList +from app.api.admin_sales.locations import AdminSalesByLocationList +from app.api.admin_sales.marketer import AdminSalesByMarketerList +from app.api.admin_sales.organizer import AdminSalesByOrganizersList +from app.api.admin_statistics_api.events import AdminStatisticsEventDetail +from app.api.admin_statistics_api.mails import AdminStatisticsMailDetail +from app.api.admin_statistics_api.sessions import AdminStatisticsSessionDetail +from app.api.admin_statistics_api.users import AdminStatisticsUserDetail +from app.api.attendees import AttendeeList, AttendeeDetail, AttendeeRelationshipOptional, \ + AttendeeRelationshipRequired, AttendeeListPost from app.api.bootstrap import api -from app.api.event_orga import EventOrgaDetail -from app.api.stripe_authorization import StripeAuthorizationDetail, StripeAuthorizationRelationship, \ - StripeAuthorizationListPost -from app.api.ticket_fees import TicketFeeList, TicketFeeDetail -from app.api.users import UserList, UserDetail, UserRelationship -from app.api.user_emails import UserEmailListAdmin, UserEmailListPost, UserEmailList, UserEmailDetail, \ - UserEmailRelationship -from app.api.user_favourite_events import UserFavouriteEventListPost, UserFavouriteEventList, \ - UserFavouriteEventDetail, UserFavouriteEventRelationship -from app.api.notifications import NotificationList, NotificationListAdmin, NotificationDetail,\ - NotificationRelationship, NotificationActionDetail, NotificationActionRelationship, NotificationActionList +from app.api.custom_forms import CustomFormList, CustomFormListPost, CustomFormDetail, CustomFormRelationshipRequired +from app.api.custom_placeholders import CustomPlaceholderList, CustomPlaceholderDetail, CustomPlaceholderRelationship +from app.api.custom_system_roles import CustomSystemRoleList, CustomSystemRoleDetail, CustomSystemRoleRelationship +from app.api.discount_codes import DiscountCodeList, DiscountCodeDetail, DiscountCodeRelationshipOptional, \ + DiscountCodeRelationshipRequired, DiscountCodeListPost from app.api.email_notifications import EmailNotificationList, EmailNotificationListAdmin, EmailNotificationDetail, \ EmailNotificationRelationshipOptional, EmailNotificationRelationshipRequired -from app.api.tickets import TicketList, TicketListPost, TicketDetail, TicketRelationshipRequired, \ - TicketRelationshipOptional -from app.api.events import EventList, EventDetail, EventRelationship, EventCopyResource -from app.api.event_types import EventTypeList, EventTypeDetail, EventTypeRelationship +from app.api.event_copyright import EventCopyrightListPost, EventCopyrightDetail, EventCopyrightRelationshipRequired +from app.api.event_image_sizes import EventImageSizeDetail +from app.api.event_invoices import EventInvoiceList, EventInvoiceDetail, \ + EventInvoiceRelationshipRequired, EventInvoiceRelationshipOptional from app.api.event_locations import EventLocationList -from app.api.event_topics import EventTopicList, EventTopicDetail, EventTopicRelationship +from app.api.event_orga import EventOrgaDetail +from app.api.event_statistics import EventStatisticsGeneralDetail from app.api.event_sub_topics import EventSubTopicList, EventSubTopicListPost, EventSubTopicDetail, \ EventSubTopicRelationshipRequired, EventSubTopicRelationshipOptional +from app.api.event_topics import EventTopicList, EventTopicDetail, EventTopicRelationship +from app.api.event_types import EventTypeList, EventTypeDetail, EventTypeRelationship +from app.api.events import EventList, EventDetail, EventRelationship, EventCopyResource +from app.api.events_role_permission import EventsRolePermissionList, EventsRolePermissionDetail, \ + EventsRolePermissionRelationship +from app.api.faq_types import FaqTypeList, FaqTypeListPost, FaqTypeDetail, FaqTypeRelationshipOptional, \ + FaqTypeRelationshipRequired +from app.api.faqs import FaqListPost, FaqList, FaqDetail, FaqRelationshipRequired, FaqRelationshipOptional +from app.api.feedbacks import FeedbackListPost, FeedbackList, FeedbackDetail, \ + FeedbackRelationship +from app.api.full_text_search.events import EventSearchResultList +from app.api.import_jobs import ImportJobList, ImportJobDetail +from app.api.mails import MailList, MailDetail +from app.api.message_settings import MessageSettingsList, MessageSettingsDetail from app.api.microlocations import MicrolocationList, MicrolocationListPost, MicrolocationDetail, \ MicrolocationRelationshipRequired, MicrolocationRelationshipOptional +from app.api.modules import ModuleDetail +from app.api.notifications import NotificationList, NotificationListAdmin, NotificationDetail, \ + NotificationRelationship, NotificationActionDetail, NotificationActionRelationship, NotificationActionList +from app.api.order_statistics.events import OrderStatisticsEventDetail +from app.api.order_statistics.tickets import OrderStatisticsTicketDetail +from app.api.orders import OrdersList, OrderDetail, OrderRelationship, ChargeList, OrdersListPost +from app.api.pages import PageList, PageDetail +from app.api.panel_permissions import PanelPermissionList, PanelPermissionDetail, \ + PanelPermissionRelationship +from app.api.role_invites import RoleInviteListPost, RoleInviteList, RoleInviteDetail, RoleInviteRelationship +from app.api.roles import RoleList, RoleDetail +from app.api.service import ServiceList, ServiceDetail +from app.api.session_types import SessionTypeList, SessionTypeListPost, SessionTypeDetail, \ + SessionTypeRelationshipRequired, SessionTypeRelationshipOptional from app.api.sessions import SessionList, SessionListPost, SessionDetail, SessionRelationshipRequired, \ SessionRelationshipOptional +from app.api.settings import SettingDetail +from app.api.social_links import SocialLinkList, SocialLinkListPost, SocialLinkDetail, SocialLinkRelationship +from app.api.speaker_image_sizes import SpeakerImageSizeDetail from app.api.speakers import SpeakerList, SpeakerListPost, SpeakerDetail, SpeakerRelationshipRequired, \ SpeakerRelationshipOptional -from app.api.service import ServiceList, ServiceDetail -from app.api.social_links import SocialLinkList, SocialLinkListPost, SocialLinkDetail, SocialLinkRelationship -from app.api.sponsors import SponsorList, SponsorListPost, SponsorDetail, SponsorRelationship -from app.api.tracks import TrackList, TrackListPost, TrackDetail, TrackRelationshipOptional, TrackRelationshipRequired from app.api.speakers_calls import SpeakersCallList, SpeakersCallDetail, SpeakersCallRelationship -from app.api.event_invoices import EventInvoiceList, EventInvoiceDetail, \ - EventInvoiceRelationshipRequired, EventInvoiceRelationshipOptional -from app.api.role_invites import RoleInviteListPost, RoleInviteList, RoleInviteDetail, RoleInviteRelationship -from app.api.event_image_sizes import EventImageSizeDetail -from app.api.speaker_image_sizes import SpeakerImageSizeDetail -from app.api.roles import RoleList, RoleDetail -from app.api.custom_system_roles import CustomSystemRoleList, CustomSystemRoleDetail, CustomSystemRoleRelationship -from app.api.session_types import SessionTypeList, SessionTypeListPost, SessionTypeDetail, \ - SessionTypeRelationshipRequired, SessionTypeRelationshipOptional -from app.api.event_copyright import EventCopyrightListPost, EventCopyrightDetail, EventCopyrightRelationshipRequired -from app.api.pages import PageList, PageDetail -from app.api.user_permission import UserPermissionList, UserPermissionDetail -from app.api.events_role_permission import EventsRolePermissionList, EventsRolePermissionDetail, \ - EventsRolePermissionRelationship -from app.api.panel_permissions import PanelPermissionList, PanelPermissionDetail, \ - PanelPermissionRelationship -from app.api.message_settings import MessageSettingsList, MessageSettingsDetail +from app.api.sponsors import SponsorList, SponsorListPost, SponsorDetail, SponsorRelationship +from app.api.stripe_authorization import StripeAuthorizationDetail, StripeAuthorizationRelationship, \ + StripeAuthorizationListPost from app.api.tax import TaxList, TaxDetail, TaxRelationship -from app.api.settings import SettingDetail -from app.api.discount_codes import DiscountCodeList, DiscountCodeDetail, DiscountCodeRelationshipOptional, \ - DiscountCodeRelationshipRequired, DiscountCodeListPost +from app.api.ticket_fees import TicketFeeList, TicketFeeDetail from app.api.ticket_tags import TicketTagList, TicketTagListPost, TicketTagDetail, TicketTagRelationshipOptional, \ TicketTagRelationshipRequired -from app.api.attendees import AttendeeList, AttendeeDetail, AttendeeRelationshipOptional, \ - AttendeeRelationshipRequired, AttendeeListPost -from app.api.access_codes import AccessCodeList, AccessCodeListPost, AccessCodeDetail, AccessCodeRelationshipRequired, \ - AccessCodeRelationshipOptional -from app.api.custom_forms import CustomFormList, CustomFormListPost, CustomFormDetail, CustomFormRelationshipRequired -from app.api.faqs import FaqListPost, FaqList, FaqDetail, FaqRelationshipRequired, FaqRelationshipOptional -from app.api.feedbacks import FeedbackListPost, FeedbackList, FeedbackDetail, \ - FeedbackRelationship -from app.api.modules import ModuleDetail -from app.api.custom_placeholders import CustomPlaceholderList, CustomPlaceholderDetail, CustomPlaceholderRelationship -from app.api.activities import ActivityList, ActivityDetail -from app.api.orders import OrdersList, OrderDetail, OrderRelationship, ChargeList, OrdersListPost -from app.api.event_statistics import EventStatisticsGeneralDetail -from app.api.mails import MailList, MailDetail -from app.api.admin_statistics_api.sessions import AdminStatisticsSessionDetail -from app.api.admin_statistics_api.events import AdminStatisticsEventDetail -from app.api.admin_statistics_api.users import AdminStatisticsUserDetail -from app.api.admin_statistics_api.mails import AdminStatisticsMailDetail -from app.api.order_statistics.events import OrderStatisticsEventDetail -from app.api.order_statistics.tickets import OrderStatisticsTicketDetail -from app.api.faq_types import FaqTypeList, FaqTypeListPost, FaqTypeDetail, FaqTypeRelationshipOptional, \ - FaqTypeRelationshipRequired -from app.api.admin_sales.events import AdminSalesByEventsList -from app.api.admin_sales.organizer import AdminSalesByOrganizersList -from app.api.admin_sales.locations import AdminSalesByLocationList -from app.api.admin_sales.marketer import AdminSalesByMarketerList -from app.api.admin_sales.discounted import AdminSalesDiscountedList -from app.api.admin_sales.fees import AdminSalesFeesList -from app.api.admin_sales.invoices import AdminSalesInvoicesList -from app.api.full_text_search.events import EventSearchResultList -from app.api.import_jobs import ImportJobList, ImportJobDetail +from app.api.tickets import TicketList, TicketListPost, TicketDetail, TicketRelationshipRequired, \ + TicketRelationshipOptional +from app.api.tracks import TrackList, TrackListPost, TrackDetail, TrackRelationshipOptional, TrackRelationshipRequired +from app.api.user_emails import UserEmailListAdmin, UserEmailListPost, UserEmailList, UserEmailDetail, \ + UserEmailRelationship +from app.api.user_favourite_events import UserFavouriteEventListPost, UserFavouriteEventList, \ + UserFavouriteEventDetail, UserFavouriteEventRelationship +from app.api.user_permission import UserPermissionList, UserPermissionDetail +from app.api.users import UserList, UserDetail, UserRelationship # users api.route(UserList, 'user_list', '/users') @@ -474,7 +474,7 @@ '/tickets//discount-codes') api.route(DiscountCodeDetail, 'discount_code_detail', '/discount-codes/', '/events//discount-code', '/event-invoices//discount-code', - '/discount-codes/') + '/event//discount-code/') api.route(DiscountCodeRelationshipRequired, 'discount_code_event', '/discount-codes//relationships/event') api.route(DiscountCodeRelationshipOptional, 'discount_code_events', @@ -541,7 +541,8 @@ api.route(AccessCodeList, 'access_code_list', '/events//access-codes', '/events//access-codes', '/users//access-codes', '/tickets//access-codes') -api.route(AccessCodeDetail, 'access_code_detail', '/access-codes/', '/access-codes/') +api.route(AccessCodeDetail, 'access_code_detail', '/access-codes/', + '/event//access-code/') api.route(AccessCodeRelationshipRequired, 'access_code_event', '/access-codes//relationships/event') api.route(AccessCodeRelationshipOptional, 'access_code_user', diff --git a/app/api/access_codes.py b/app/api/access_codes.py index a7832ddfa9..fc46704146 100644 --- a/app/api/access_codes.py +++ b/app/api/access_codes.py @@ -111,8 +111,9 @@ def before_get(self, args, kwargs): :return: """ # Any registered user can fetch access code details using the code. - if kwargs.get('code'): - access = db.session.query(AccessCode).filter_by(code=kwargs.get('code')).first() + if kwargs.get('code') and kwargs.get('access_event_id'): + access = db.session.query(AccessCode).filter_by(code=kwargs.get('code'), + event_id=kwargs.get('access_event_id')).first() if access: kwargs['id'] = access.id else: diff --git a/app/api/discount_codes.py b/app/api/discount_codes.py index 4e7d9e5b0a..01eb42e7e0 100644 --- a/app/api/discount_codes.py +++ b/app/api/discount_codes.py @@ -209,7 +209,9 @@ def before_get(self, args, kwargs): if kwargs.get('code'): # filter on deleted_at is required to catch the id of a # discount code which has not been deleted. - discount = db.session.query(DiscountCode).filter_by(code=kwargs.get('code'), deleted_at=None).first() + discount = db.session.query(DiscountCode).filter_by(code=kwargs.get('code'), + event_id=kwargs.get('discount_event_id'), + deleted_at=None).first() if discount: kwargs['id'] = discount.id discount_tz = discount.valid_from.tzinfo diff --git a/docs/api/api_blueprint.apib b/docs/api/api_blueprint.apib index 5b4ae19c5b..65871837c5 100644 --- a/docs/api/api_blueprint.apib +++ b/docs/api/api_blueprint.apib @@ -17815,9 +17815,10 @@ Delete a single discount code (Check permission to delete). } } -## Get Discount Code Detail using the code [/v1/discount-codes/{code}] +## Get Discount Code Detail using the code [/v1/event/{event_id}/discount-code/{code}] + Parameters + code: DC101 (string) - code associated with a discount code. (DC101 is an example of code) + + event_id: 1 (integer) - ID of the Event in the form of an integer ### Get Discount Code Detail [GET] Get a single discount code using a code. @@ -18428,9 +18429,10 @@ Delete a single Access code. } } -## Access Code Detail using the Code [/v1/access-codes/{code}] +## Access Code Detail using the Code [/v1/event/{event_id}/access-code/{code}] + Parameters + code: AC101 (string) - code of the access-code. + + event_id: 1 (integer) - ID of the Event in the form of an integer. ### Access Code Detail [GET] Get a single access code using the code. diff --git a/tests/hook_main.py b/tests/hook_main.py index 0acbb518d4..55ac61d907 100644 --- a/tests/hook_main.py +++ b/tests/hook_main.py @@ -2804,7 +2804,7 @@ def discount_delete(transaction): @hooks.before("Discount Codes > Get Discount Code Detail using the code > Get Discount Code Detail") def discount_code_get_detail_using_code(transaction): """ - GET /discount-codes/DC101 + GET event/1/discount-codes/DC101 :param transaction: :return: """ @@ -2815,6 +2815,7 @@ def discount_code_get_detail_using_code(transaction): discount_code = DiscountCodeFactory(event_id=1) discount_code.code = 'DC101' + discount_code.event_id = 1 db.session.add(discount_code) db.session.commit() @@ -2940,11 +2941,15 @@ def access_code_delete(transaction): @hooks.before("Access Codes > Access Code Detail using the Code > Access Code Detail") def access_code_get_detail_using_code(transaction): """ - GET /access-codes/AC101 + GET event/1/access-code/AC101 :param transaction: :return: """ with stash['app'].app_context(): + event = EventFactoryBasic() + db.session.add(event) + db.session.commit() + access_code = AccessCodeFactory() access_code.code = 'AC101' db.session.add(access_code) From 4467fe3cca8f336dc632feb926bbf32a42e21943 Mon Sep 17 00:00:00 2001 From: Saicharan Reddy Date: Mon, 22 Jul 2019 13:03:35 +0530 Subject: [PATCH 19/30] refactor: Rewrite empty_string & add more test cases (#6222) Shifted test to unit dir Added tabs --- app/api/helpers/utilities.py | 7 +------ .../integration/api/helpers/test_utilities.py | 12 ------------ tests/all/unit/api/helpers/test_utilities.py | 16 +++++++++++++++- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/app/api/helpers/utilities.py b/app/api/helpers/utilities.py index 6d4ad07b41..24e8e44417 100644 --- a/app/api/helpers/utilities.py +++ b/app/api/helpers/utilities.py @@ -26,12 +26,7 @@ def require_relationship(resource_list, data): def string_empty(value): - is_not_str_type = type(value) is not str - if sys.version_info[0] < 3: - is_not_str_type = is_not_str_type and type(value) is not unicode - if type(value) is not value and is_not_str_type: - return False - return not (value and value.strip() and value != u'' and value != u' ') + return isinstance(value, str) and not value.strip() def strip_tags(html): diff --git a/tests/all/integration/api/helpers/test_utilities.py b/tests/all/integration/api/helpers/test_utilities.py index 073ea44f85..b71fe0cb7f 100644 --- a/tests/all/integration/api/helpers/test_utilities.py +++ b/tests/all/integration/api/helpers/test_utilities.py @@ -29,18 +29,6 @@ def test_require_relationship(self): data = ['event'] require_relationship(['sponsor', 'event'], data) - def test_string_empty(self): - """Method to test whether an empty string is correctly identified.""" - - with app.test_request_context(): - self.assertTrue(string_empty('')) - self.assertTrue(string_empty(' ')) - self.assertFalse(string_empty('some value')) - self.assertFalse(string_empty(' some value ')) - self.assertFalse(string_empty(str)) - self.assertFalse(string_empty(int)) - self.assertFalse(string_empty(None)) - def test_monthdelta(self): """Method to test difference in months result""" diff --git a/tests/all/unit/api/helpers/test_utilities.py b/tests/all/unit/api/helpers/test_utilities.py index 946294b281..3b5d95cfc5 100644 --- a/tests/all/unit/api/helpers/test_utilities.py +++ b/tests/all/unit/api/helpers/test_utilities.py @@ -1,6 +1,6 @@ import unittest from app.api.helpers.utilities import get_filename_from_cd - +from app.api.helpers.utilities import string_empty class TestUtilitiesHelperValidation(unittest.TestCase): def test_get_filename_from_cd(self): @@ -17,6 +17,20 @@ def test_get_filename_from_cd(self): self.assertEqual(expected_response_first, response_first) self.assertEqual(expected_response_none, response_none) + def test_string_empty(self): + """Method to test whether an empty string is correctly identified.""" + + self.assertTrue(string_empty('')) + self.assertTrue(string_empty(' ')) + self.assertTrue(string_empty('\t')) + self.assertTrue(string_empty('\n')) + self.assertFalse(string_empty(None)) + self.assertFalse(string_empty('some value')) + self.assertFalse(string_empty(' some value ')) + self.assertFalse(string_empty(0)) + self.assertFalse(string_empty([])) + self.assertFalse(string_empty(False)) + if __name__ == '__main__': unittest.main() From ff378ac152731e3036c40fafcc5fa1ab397c3244 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 23 Jul 2019 12:08:21 +0530 Subject: [PATCH 20/30] =?UTF-8?q?chore(deps):=20update=20sqlalchemy=20requ?= =?UTF-8?q?irement=20from=20~=3D1.3.5=20to=20~=3D1.=E2=80=A6=20(#6227)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates the requirements on [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) to permit the latest version. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) 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 56f85ced4d..4d87c021e3 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -54,7 +54,7 @@ sentry-sdk[flask]~=0.10 healthcheck~=1.3 elasticsearch-dsl~=7.0 flask-redis~=0.4 -SQLAlchemy~=1.3.5 +SQLAlchemy~=1.3.6 Flask-Elasticsearch~=0.2 paypalrestsdk~=1.13 eventlet~=0.25 From 0a1849a7b4396fcd8f9d9f4001712eb29896b3b0 Mon Sep 17 00:00:00 2001 From: Saicharan Reddy Date: Tue, 23 Jul 2019 12:09:58 +0530 Subject: [PATCH 21/30] chore: Moved utility unit tests to appropriate folder (#6226) --- .../integration/api/helpers/test_utilities.py | 66 ------------------- tests/all/unit/api/helpers/test_utilities.py | 54 ++++++++++++++- 2 files changed, 53 insertions(+), 67 deletions(-) delete mode 100644 tests/all/integration/api/helpers/test_utilities.py diff --git a/tests/all/integration/api/helpers/test_utilities.py b/tests/all/integration/api/helpers/test_utilities.py deleted file mode 100644 index b71fe0cb7f..0000000000 --- a/tests/all/integration/api/helpers/test_utilities.py +++ /dev/null @@ -1,66 +0,0 @@ -import unittest -import string -import datetime - -from app import current_app as app -from app.api.helpers.exceptions import UnprocessableEntity -from tests.all.integration.utils import OpenEventTestCase -from app.api.helpers.utilities import dasherize, require_relationship, string_empty, str_generator, monthdelta, represents_int -from tests.all.integration.setup_database import Setup - - -class TestUtilitiesHelperValidation(OpenEventTestCase): - def setUp(self): - self.app = Setup.create_app() - - def test_dasherize(self): - """Method to test whether an attribute dasherizes or not""" - - with app.test_request_context(): - field = "starts_at" - dasherized_field = "starts-at" - result = dasherize(field) - self.assertEqual(result, dasherized_field) - - def test_require_relationship(self): - """Method to test relationship in request data""" - - with self.assertRaises(UnprocessableEntity): - data = ['event'] - require_relationship(['sponsor', 'event'], data) - - def test_monthdelta(self): - """Method to test difference in months result""" - - with app.test_request_context(): - test_date = datetime.datetime(2000, 6, 18) - test_future_date = monthdelta(test_date, 3) - self.assertEqual(test_future_date, datetime.datetime(2000, 9, 18)) - - def test_represents_int(self): - """Method to test representation of int""" - - with app.test_request_context(): - self.assertTrue(represents_int(4)) - self.assertFalse(represents_int('test')) - - def test_str_generator(self): - """Method to test str_generator.""" - - with app.test_request_context(): - generated_string = str_generator() - self.assertEqual(len(generated_string), 6) - self.assertRegex(generated_string, r'^[A-Z0-9]+$') - self.assertNotRegex(generated_string, r'^[a-z]+$') - - generated_string = str_generator(8, chars=string.ascii_lowercase) - self.assertEqual(len(generated_string), 8) - self.assertRegex(generated_string, r'^[a-z]+$') - self.assertNotRegex(generated_string, r'^[A-Z0-9]+$') - - generated_string = str_generator(chars='ABC253') - self.assertRegex(generated_string, r'^[ABC253]+$') - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/all/unit/api/helpers/test_utilities.py b/tests/all/unit/api/helpers/test_utilities.py index 3b5d95cfc5..6f6211d36c 100644 --- a/tests/all/unit/api/helpers/test_utilities.py +++ b/tests/all/unit/api/helpers/test_utilities.py @@ -1,6 +1,13 @@ import unittest +import string +import datetime + + +from app.api.helpers.exceptions import UnprocessableEntity from app.api.helpers.utilities import get_filename_from_cd -from app.api.helpers.utilities import string_empty +from app.api.helpers.utilities import string_empty, dasherize, represents_int, str_generator, \ + require_relationship, monthdelta + class TestUtilitiesHelperValidation(unittest.TestCase): def test_get_filename_from_cd(self): @@ -17,6 +24,21 @@ def test_get_filename_from_cd(self): self.assertEqual(expected_response_first, response_first) self.assertEqual(expected_response_none, response_none) + def test_dasherize(self): + """Method to test whether an attribute dasherizes or not""" + + field = "starts_at" + dasherized_field = "starts-at" + result = dasherize(field) + self.assertEqual(result, dasherized_field) + + def test_represents_int(self): + """Method to test representation of int""" + + self.assertTrue(represents_int(4)) + self.assertFalse(represents_int('test')) + + def test_string_empty(self): """Method to test whether an empty string is correctly identified.""" @@ -31,6 +53,36 @@ def test_string_empty(self): self.assertFalse(string_empty([])) self.assertFalse(string_empty(False)) + def test_str_generator(self): + """Method to test str_generator.""" + + generated_string = str_generator() + self.assertEqual(len(generated_string), 6) + self.assertRegex(generated_string, r'^[A-Z0-9]+$') + self.assertNotRegex(generated_string, r'^[a-z]+$') + + generated_string = str_generator(8, chars=string.ascii_lowercase) + self.assertEqual(len(generated_string), 8) + self.assertRegex(generated_string, r'^[a-z]+$') + self.assertNotRegex(generated_string, r'^[A-Z0-9]+$') + + generated_string = str_generator(chars='ABC253') + self.assertRegex(generated_string, r'^[ABC253]+$') + + def test_require_relationship(self): + """Method to test relationship in request data""" + + with self.assertRaises(UnprocessableEntity): + data = ['event'] + require_relationship(['sponsor', 'event'], data) + + def test_monthdelta(self): + """Method to test difference in months result""" + + test_date = datetime.datetime(2000, 6, 18) + test_future_date = monthdelta(test_date, 3) + self.assertEqual(test_future_date, datetime.datetime(2000, 9, 18)) + if __name__ == '__main__': unittest.main() From fc02893b21801f15eecbb40443ee1e57c03829da Mon Sep 17 00:00:00 2001 From: Samesh Lakhotia <43701530+sameshl@users.noreply.github.com> Date: Tue, 23 Jul 2019 23:28:03 +0530 Subject: [PATCH 22/30] docs: Update order expiry time Admin Only tag (#6230) changed the tag from `Yes` to `-` closes #6185 --- docs/api/api_blueprint.apib | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/api_blueprint.apib b/docs/api/api_blueprint.apib index 65871837c5..e2e08f3631 100644 --- a/docs/api/api_blueprint.apib +++ b/docs/api/api_blueprint.apib @@ -21882,7 +21882,7 @@ To update or get any attribute of this data layer, you will need admin access. H | `app-name` | Name of the application (Eg. Event Yay!, Open Event) | string | - | | `tagline` | Tagline (Eg. Event Management and Ticketing, Home) | string | - | | `secret` | App Secret | string | **yes** | -| `order-expiry-time` | Expiry time for orders in minutes | Integer(default: 15) | **yes** | +| `order-expiry-time` | Expiry time for orders in minutes | Integer(default: 15) | - | | `is-paypal-activated` | Whether paypal payment is configured or not | boolean | - | | `is-stripe-activated` | Whether stripe payment is configured or not | boolean | - | | `is-omise-activated` | Whether omise payment is configured or not | boolean | - | From 100ad5be87a7ca2c9dd5ea41f8ae04dabd5d0343 Mon Sep 17 00:00:00 2001 From: Shreyansh Dwivedi Date: Wed, 24 Jul 2019 12:36:52 +0530 Subject: [PATCH 23/30] fix: restoration of deleted event (#6218) fixes hound issue adds an event field to the function --- app/api/events.py | 26 ++++++- app/api/schema/events.py | 26 ------- .../integration/api/validation/test_events.py | 70 ------------------- 3 files changed, 25 insertions(+), 97 deletions(-) delete mode 100644 tests/all/integration/api/validation/test_events.py diff --git a/app/api/events.py b/app/api/events.py index 13f5312f52..5649e9ddf7 100644 --- a/app/api/events.py +++ b/app/api/events.py @@ -12,7 +12,7 @@ from app.api.data_layers.EventCopyLayer import EventCopyLayer from app.api.helpers.db import save_to_db, safe_query from app.api.helpers.events import create_custom_forms_for_attendees -from app.api.helpers.exceptions import ForbiddenException, ConflictException +from app.api.helpers.exceptions import ForbiddenException, ConflictException, UnprocessableEntity from app.api.helpers.permission_manager import has_access from app.api.helpers.utilities import dasherize from app.api.schema.events import EventSchemaPublic, EventSchema @@ -88,6 +88,26 @@ def validate_event(user, modules, data): "Online Event does not have any locaton") +def validate_date(event, data): + if event: + if 'starts_at' not in data: + data['starts_at'] = event.starts_at + + if 'ends_at' not in data: + data['ends_at'] = event.ends_at + + if not data.get('starts_at') or not data.get('ends_at'): + raise UnprocessableEntity({'pointer': '/data/attributes/date'}, + "enter required fields starts-at/ends-at") + + if data['starts_at'] >= data['ends_at']: + raise UnprocessableEntity({'pointer': '/data/attributes/ends-at'}, + "ends-at should be after starts-at") + + if datetime.timestamp(data['starts_at']) <= datetime.timestamp(datetime.now()): + raise UnprocessableEntity({'pointer': '/data/attributes/starts-at'}, + "starts-at should be after current date-time") + class EventList(ResourceList): def before_get(self, args, kwargs): """ @@ -155,6 +175,7 @@ def before_post(self, args, kwargs, data=None): user = User.query.filter_by(id=kwargs['user_id']).first() modules = Module.query.first() validate_event(user, modules, data) + validate_date(None, data) def after_create_object(self, event, data, view_kwargs): """ @@ -471,6 +492,9 @@ def before_update_object(self, event, data, view_kwargs): :param view_kwargs: :return: """ + if data.get('starts_at') != event.starts_at or data.get('ends_at') != event.ends_at: + validate_date(event, data) + if has_access('is_admin') and data.get('deleted_at') != event.deleted_at: if len(event.orders) != 0: raise ForbiddenException({'source': ''}, "Event associated with orders cannot be deleted") diff --git a/app/api/schema/events.py b/app/api/schema/events.py index 74ef403167..532b5e25af 100644 --- a/app/api/schema/events.py +++ b/app/api/schema/events.py @@ -24,32 +24,6 @@ class Meta: self_view_many = 'v1.event_list' inflect = dasherize - @validates_schema(pass_original=True) - def validate_date(self, data, original_data): - if 'id' in original_data['data']: - try: - event = Event.query.filter_by(id=original_data['data']['id']).one() - except NoResultFound: - raise ObjectNotFound({'source': 'data/id'}, "Event id not found") - - if 'starts_at' not in data: - data['starts_at'] = event.starts_at - - if 'ends_at' not in data: - data['ends_at'] = event.ends_at - - if 'starts_at' not in data or 'ends_at' not in data: - raise UnprocessableEntity({'pointer': '/data/attributes/date'}, - "enter required fields starts-at/ends-at") - - if data['starts_at'] >= data['ends_at']: - raise UnprocessableEntity({'pointer': '/data/attributes/ends-at'}, - "ends-at should be after starts-at") - - if datetime.timestamp(data['starts_at']) <= datetime.timestamp(datetime.now()): - raise UnprocessableEntity({'pointer': '/data/attributes/starts-at'}, - "starts-at should be after current date-time") - @validates_schema(pass_original=True) def validate_timezone(self, data, original_data): if 'id' in original_data['data']: diff --git a/tests/all/integration/api/validation/test_events.py b/tests/all/integration/api/validation/test_events.py deleted file mode 100644 index fa7b374c65..0000000000 --- a/tests/all/integration/api/validation/test_events.py +++ /dev/null @@ -1,70 +0,0 @@ -import unittest -from datetime import datetime -from pytz import timezone - -from app import current_app as app -from tests.all.integration.utils import OpenEventTestCase -from app.api.helpers.exceptions import UnprocessableEntity -from app.api.schema.events import EventSchema -from app.factories.event import EventFactoryBasic -from app.models import db -from app.api.helpers.db import save_to_db -from tests.all.integration.setup_database import Setup - - -class TestEventValidation(OpenEventTestCase): - def setUp(self): - self.app = Setup.create_app() - - def test_date_pass(self): - """ - Events Validate Date - Tests if the function runs without an exception - :return: - """ - schema = EventSchema() - original_data = { - 'data': {} - } - data = { - 'starts_at': datetime(2099, 8, 4, 12, 30, 45).replace(tzinfo=timezone('UTC')), - 'ends_at': datetime(2099, 9, 4, 12, 30, 45).replace(tzinfo=timezone('UTC')) - } - EventSchema.validate_date(schema, data, original_data) - - def test_date_start_gt_end(self): - """ - Events Validate Date - Tests if exception is raised when ends_at is before starts_at - :return: - """ - schema = EventSchema() - original_data = { - 'data': {} - } - data = { - 'starts_at': datetime(2099, 9, 4, 12, 30, 45).replace(tzinfo=timezone('UTC')), - 'ends_at': datetime(2099, 8, 4, 12, 30, 45).replace(tzinfo=timezone('UTC')) - } - with self.assertRaises(UnprocessableEntity): - EventSchema.validate_date(schema, data, original_data) - - def test_date_db_populate(self): - """ - Events Validate Date - Tests if validation works on values stored in db and not given in 'data' - :return: - """ - with app.test_request_context(): - schema = EventSchema() - obj = EventFactoryBasic() - save_to_db(obj) - - original_data = { - 'data': { - 'id': 1 - } - } - data = {} - EventSchema.validate_date(schema, data, original_data) - - -if __name__ == '__main__': - unittest.main() From 0ed4363f7e7c09f5b08fbf313cb43287e1a769b2 Mon Sep 17 00:00:00 2001 From: Saicharan Reddy Date: Wed, 24 Jul 2019 12:59:15 +0530 Subject: [PATCH 24/30] fix: Replaced hardcoded with FE URL from settings (#6233) --- app/api/helpers/mail.py | 12 ++++++++---- app/api/helpers/system_mails.py | 8 +++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/app/api/helpers/mail.py b/app/api/helpers/mail.py index b7797f8fac..ea5fe54e13 100644 --- a/app/api/helpers/mail.py +++ b/app/api/helpers/mail.py @@ -335,11 +335,13 @@ def send_email_to_attendees(order, purchaser_id, attachments=None): action=TICKET_PURCHASED, subject=MAILS[TICKET_PURCHASED]['subject'].format( event_name=order.event.name, - invoice_id=order.invoice_number + invoice_id=order.invoice_number, + frontend_url=get_settings()['frontend_url'] ), html=MAILS[TICKET_PURCHASED]['message'].format( pdf_url=holder.pdf_url, - event_name=order.event.name + event_name=order.event.name, + frontend_url=get_settings()['frontend_url'] ), attachments=attachments ) @@ -366,11 +368,13 @@ def send_order_cancel_email(order): action=TICKET_CANCELLED, subject=MAILS[TICKET_CANCELLED]['subject'].format( event_name=order.event.name, - invoice_id=order.invoice_number + invoice_id=order.invoice_number, + frontend_url=get_settings()['frontend_url'] ), html=MAILS[TICKET_CANCELLED]['message'].format( event_name=order.event.name, order_url=make_frontend_url('/orders/{identifier}'.format(identifier=order.identifier)), - cancel_note=order.cancel_note + cancel_note=order.cancel_note, + frontend_url=get_settings()['frontend_url'] ) ) diff --git a/app/api/helpers/system_mails.py b/app/api/helpers/system_mails.py index a1d4e160cf..459173933a 100644 --- a/app/api/helpers/system_mails.py +++ b/app/api/helpers/system_mails.py @@ -9,6 +9,8 @@ MONTHLY_PAYMENT_FOLLOWUP_EMAIL, EVENT_IMPORTED, EVENT_IMPORT_FAIL, TICKET_PURCHASED_ORGANIZER, TICKET_CANCELLED, \ TICKET_PURCHASED_ATTENDEE, PASSWORD_CHANGE, PASSWORD_RESET_AND_VERIFY, USER_EVENT_ROLE, TEST_MAIL + + MAILS = { EVENT_PUBLISH: { 'recipient': 'Owner, Organizer, Speaker', @@ -157,7 +159,7 @@ u"
Your order has been processed successfully." + u"
You can find your Tickets and Order Invoice attached to this mail." u"

Looking forward to seeing you at the event." - u"
Login to manage your orders at https://eventyay.com
" + u"
Login to manage your orders at {frontend_url} " ) }, TICKET_PURCHASED_ATTENDEE: { @@ -178,7 +180,7 @@ u"Hi, {buyer_email} just bought tickets for the event {event_name}" u"
The order has been processed successfully." + u"
Click here to view/download the invoice." - u"
Login to manage the orders at https://eventyay.com " + u"
Login to manage the orders at {frontend_url} " ) }, TICKET_CANCELLED: { @@ -189,7 +191,7 @@ u"
Please contact the organizer for more info" + u"
Message from the organizer: {cancel_note}" u"
Click here to view/download the invoice." - u"
Login to manage the orders at https://eventyay.com " + u"
Login to manage the orders at {frontend_url} " ) }, EVENT_EXPORTED: { From ec24c30963e6333c32238db8eac3423abe2bf30b Mon Sep 17 00:00:00 2001 From: Abhinav Khare Date: Fri, 26 Jul 2019 12:59:28 +0530 Subject: [PATCH 25/30] fix: Make discount code & access code URL follow json api conventions (#6238) --- app/api/__init__.py | 4 ++-- docs/api/api_blueprint.apib | 4 ++-- tests/hook_main.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/api/__init__.py b/app/api/__init__.py index f084ec8c09..3f87b0080a 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -474,7 +474,7 @@ '/tickets//discount-codes') api.route(DiscountCodeDetail, 'discount_code_detail', '/discount-codes/', '/events//discount-code', '/event-invoices//discount-code', - '/event//discount-code/') + '/events//discount-codes/') api.route(DiscountCodeRelationshipRequired, 'discount_code_event', '/discount-codes//relationships/event') api.route(DiscountCodeRelationshipOptional, 'discount_code_events', @@ -542,7 +542,7 @@ '/events//access-codes', '/users//access-codes', '/tickets//access-codes') api.route(AccessCodeDetail, 'access_code_detail', '/access-codes/', - '/event//access-code/') + '/events//access-codes/') api.route(AccessCodeRelationshipRequired, 'access_code_event', '/access-codes//relationships/event') api.route(AccessCodeRelationshipOptional, 'access_code_user', diff --git a/docs/api/api_blueprint.apib b/docs/api/api_blueprint.apib index e2e08f3631..7c60931e73 100644 --- a/docs/api/api_blueprint.apib +++ b/docs/api/api_blueprint.apib @@ -17815,7 +17815,7 @@ Delete a single discount code (Check permission to delete). } } -## Get Discount Code Detail using the code [/v1/event/{event_id}/discount-code/{code}] +## Get Discount Code Detail using the code [/v1/events/{event_id}/discount-codes/{code}] + Parameters + code: DC101 (string) - code associated with a discount code. (DC101 is an example of code) + event_id: 1 (integer) - ID of the Event in the form of an integer @@ -18429,7 +18429,7 @@ Delete a single Access code. } } -## Access Code Detail using the Code [/v1/event/{event_id}/access-code/{code}] +## Access Code Detail using the Code [/v1/events/{event_id}/access-codes/{code}] + Parameters + code: AC101 (string) - code of the access-code. + event_id: 1 (integer) - ID of the Event in the form of an integer. diff --git a/tests/hook_main.py b/tests/hook_main.py index 55ac61d907..e266dafba0 100644 --- a/tests/hook_main.py +++ b/tests/hook_main.py @@ -2804,7 +2804,7 @@ def discount_delete(transaction): @hooks.before("Discount Codes > Get Discount Code Detail using the code > Get Discount Code Detail") def discount_code_get_detail_using_code(transaction): """ - GET event/1/discount-codes/DC101 + GET events/1/discount-codes/DC101 :param transaction: :return: """ @@ -2941,7 +2941,7 @@ def access_code_delete(transaction): @hooks.before("Access Codes > Access Code Detail using the Code > Access Code Detail") def access_code_get_detail_using_code(transaction): """ - GET event/1/access-code/AC101 + GET events/1/access-codes/AC101 :param transaction: :return: """ From 7dd52822a910c099f8e20a523652bce9d3e9badf Mon Sep 17 00:00:00 2001 From: Saicharan Reddy Date: Fri, 26 Jul 2019 13:02:58 +0530 Subject: [PATCH 26/30] fix: send mail only if SMTP was configured(Event Export) (#6228) Correct usage of func Add missing SMTP parameter --- app/api/helpers/tasks.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/app/api/helpers/tasks.py b/app/api/helpers/tasks.py index 04a7fd4b2c..4fd7e23933 100644 --- a/app/api/helpers/tasks.py +++ b/app/api/helpers/tasks.py @@ -17,7 +17,7 @@ from app.api.helpers.utilities import strip_tags from app.models.session import Session from app.models.speaker import Speaker - +from app.api.helpers.mail import check_smtp_config """ Define all API v2 celery tasks here @@ -174,6 +174,7 @@ def resize_speaker_images_task(self, speaker_id, photo_url): def export_event_task(self, email, event_id, settings): event = safe_query(db, Event, 'id', event_id, 'event_id') user = db.session.query(User).filter_by(email=email).first() + smtp_encryption = get_settings()['smtp_encryption'] try: logging.info('Exporting started') path = event_export_task_base(event_id, settings) @@ -183,15 +184,21 @@ def export_event_task(self, email, event_id, settings): result = { 'download_url': download_url } + logging.info('Exporting done.. sending email') - send_export_mail(email=email, event_name=event.name, download_url=download_url) + if check_smtp_config(smtp_encryption): + send_export_mail(email=email, event_name=event.name, download_url=download_url) + else: + logging.warning('Error in sending export success email') send_notif_after_export(user=user, event_name=event.name, download_url=download_url) except Exception as e: result = {'__error': True, 'result': str(e)} logging.warning('Error in exporting.. sending email') - send_export_mail(email=email, event_name=event.name, error_text=str(e)) + if check_smtp_config(smtp_encryption): + send_export_mail(email=email, event_name=event.name, error_text=str(e)) + else: + logging.warning('Error in sending export error email') send_notif_after_export(user=user, event_name=event.name, error_text=str(e)) - return result From 5563826416d8b431e636677569dd5945853a095b Mon Sep 17 00:00:00 2001 From: Uddeshya Singh Date: Sat, 27 Jul 2019 22:28:36 +0530 Subject: [PATCH 27/30] fix: add tax information in event copy action (#6241) --- app/api/event_copy.py | 95 +++++++++++++++---------------------------- 1 file changed, 32 insertions(+), 63 deletions(-) diff --git a/app/api/event_copy.py b/app/api/event_copy.py index 988e8edb56..63ad2ee1df 100644 --- a/app/api/event_copy.py +++ b/app/api/event_copy.py @@ -15,6 +15,7 @@ from app.models.ticket import Ticket from app.models.track import Track from app.models.users_events_role import UsersEventsRoles +from app.models.tax import Tax event_copy = Blueprint('event_copy', __name__, url_prefix='/v1/events') @@ -23,6 +24,14 @@ def start_sponsor_logo_generation_task(event_id): from .helpers.tasks import sponsor_logos_url_task sponsor_logos_url_task.delay(event_id=event_id) + +def copy_to_event(object, event): + db.session.expunge(object) # expunge the object + make_transient(object) + object.event_id = event.id + delattr(object, 'id') + save_to_db(object) + @event_copy.route('//copy', methods=['POST']) def create_event_copy(identifier): id = 'identifier' @@ -36,15 +45,16 @@ def create_event_copy(identifier): return abort( make_response(jsonify(error="Access Forbidden"), 403) ) - tickets = Ticket.query.filter_by(event_id=event.id).all() - social_links = SocialLink.query.filter_by(event_id=event.id).all() - sponsors = Sponsor.query.filter_by(event_id=event.id).all() - microlocations = Microlocation.query.filter_by(event_id=event.id).all() - tracks = Track.query.filter_by(event_id=event.id).all() - custom_forms = CustomForms.query.filter_by(event_id=event.id).all() - discount_codes = DiscountCode.query.filter_by(event_id=event.id).all() - speaker_calls = SpeakersCall.query.filter_by(event_id=event.id).all() - user_event_roles = UsersEventsRoles.query.filter_by(event_id=event.id).all() + tickets = Ticket.query.filter_by(event_id=event.id, deleted_at=None).all() + social_links = SocialLink.query.filter_by(event_id=event.id, deleted_at=None).all() + sponsors = Sponsor.query.filter_by(event_id=event.id, deleted_at=None).all() + microlocations = Microlocation.query.filter_by(event_id=event.id, deleted_at=None).all() + tracks = Track.query.filter_by(event_id=event.id, deleted_at=None).all() + custom_forms = CustomForms.query.filter_by(event_id=event.id, deleted_at=None).all() + discount_codes = DiscountCode.query.filter_by(event_id=event.id, deleted_at=None).all() + speaker_calls = SpeakersCall.query.filter_by(event_id=event.id, deleted_at=None).all() + user_event_roles = UsersEventsRoles.query.filter_by(event_id=event.id, deleted_at=None).all() + taxes = Tax.query.filter_by(event_id=event.id, deleted_at=None).all() db.session.expunge(event) # expunge the object from session make_transient(event) @@ -52,81 +62,40 @@ def create_event_copy(identifier): event.identifier = get_new_event_identifier() save_to_db(event) + # Ensure tax information is copied + for tax in taxes: + copy_to_event(tax, event) + # Removes access_codes, order_tickets, ticket_tags for the new tickets created. for ticket in tickets: - ticket_id = ticket.id - db.session.expunge(ticket) # expunge the object from session - make_transient(ticket) - ticket.event_id = event.id - delattr(ticket, 'id') - save_to_db(ticket) + copy_to_event(ticket, event) for link in social_links: - link_id = link.id - db.session.expunge(link) # expunge the object from session - make_transient(link) - link.event_id = event.id - delattr(link, 'id') - save_to_db(link) + copy_to_event(link, event) for sponsor in sponsors: - sponsor_id = sponsor.id - db.session.expunge(sponsor) # expunge the object from session - make_transient(sponsor) - sponsor.event_id = event.id - delattr(sponsor, 'id') - save_to_db(sponsor) + copy_to_event(sponsor, event) start_sponsor_logo_generation_task(event.id) for location in microlocations: - location_id = location.id - db.session.expunge(location) # expunge the object from session - make_transient(location) - location.event_id = event.id - delattr(location, 'id') - save_to_db(location) + copy_to_event(location, event) # No sessions are copied for new tracks for track in tracks: - track_id = track.id - db.session.expunge(track) # expunge the object from session - make_transient(track) - track.event_id = event.id - delattr(track, 'id') - save_to_db(track) + copy_to_event(track, event) for call in speaker_calls: - call_id = call.id - db.session.expunge(call) # expunge the object from session - make_transient(call) - call.event_id = event.id - delattr(call, 'id') - save_to_db(call) + copy_to_event(call, event) for code in discount_codes: - code_id = code.id - db.session.expunge(code) # expunge the object from session - make_transient(code) - code.event_id = event.id - delattr(code, 'id') - save_to_db(code) + copy_to_event(code, event) for form in custom_forms: - form_id = form.id - db.session.expunge(form) # expunge the object from session - make_transient(form) - form.event_id = event.id - delattr(form, 'id') - save_to_db(form) + copy_to_event(form, event) for user_role in user_event_roles: - user_role_id = user_role.id - db.session.expunge(user_role) - make_transient(user_role) - user_role.event_id = event.id - delattr(user_role, 'id') - save_to_db(user_role) + copy_to_event(user_role, event) return jsonify({ 'id': event.id, From 92d001ba7bcefa1c873db8159bccb9214c43835e Mon Sep 17 00:00:00 2001 From: Kush Trivedi Date: Mon, 29 Jul 2019 09:51:34 +0530 Subject: [PATCH 28/30] chore(deps): Including python-dotenv in requirements (#6237) --- requirements.txt | 2 +- requirements/common.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5eaadb8e90..3e2d68e90c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ --r requirements/prod.txt +-r requirements/prod.txt \ No newline at end of file diff --git a/requirements/common.txt b/requirements/common.txt index 4d87c021e3..6820c0c2a0 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -35,6 +35,7 @@ forex-python~=1.5 oauth2~=1.9 qrcode~=5.3 python-magic~=0.4 +python-dotenv~=0.10.3 python-geoip~=1.2 marrow.mailer~=4.0 flask-cors~=3.0.8 From 3c747b0a27976e12d03d3b0da70337c9cd868cac Mon Sep 17 00:00:00 2001 From: Abhinav Khare Date: Tue, 30 Jul 2019 01:32:06 +0530 Subject: [PATCH 29/30] feat: Make discount codes & access codes support event_identifier (#6251) --- app/api/__init__.py | 6 ++++-- app/api/access_codes.py | 9 ++++++++- app/api/discount_codes.py | 6 ++++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/app/api/__init__.py b/app/api/__init__.py index 3f87b0080a..56566a2d91 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -474,7 +474,8 @@ '/tickets//discount-codes') api.route(DiscountCodeDetail, 'discount_code_detail', '/discount-codes/', '/events//discount-code', '/event-invoices//discount-code', - '/events//discount-codes/') + '/events//discount-codes/', + '/events//discount-codes/') api.route(DiscountCodeRelationshipRequired, 'discount_code_event', '/discount-codes//relationships/event') api.route(DiscountCodeRelationshipOptional, 'discount_code_events', @@ -542,7 +543,8 @@ '/events//access-codes', '/users//access-codes', '/tickets//access-codes') api.route(AccessCodeDetail, 'access_code_detail', '/access-codes/', - '/events//access-codes/') + '/events//access-codes/', + '/events//access-codes/',) api.route(AccessCodeRelationshipRequired, 'access_code_event', '/access-codes//relationships/event') api.route(AccessCodeRelationshipOptional, 'access_code_user', diff --git a/app/api/access_codes.py b/app/api/access_codes.py index fc46704146..c2190a5bed 100644 --- a/app/api/access_codes.py +++ b/app/api/access_codes.py @@ -15,6 +15,7 @@ from app.models.access_code import AccessCode from app.models.ticket import Ticket from app.models.user import User +from app.models.event import Event class AccessCodeListPost(ResourceList): @@ -110,7 +111,13 @@ def before_get(self, args, kwargs): :param kwargs: :return: """ - # Any registered user can fetch access code details using the code. + # Any user can fetch access code details using the code. + + if kwargs.get('access_event_identifier'): + event = safe_query( + db, Event, 'identifier', kwargs['discount_event_identifier'], + 'event_identifier') + kwargs['access_event_id'] = event.id if kwargs.get('code') and kwargs.get('access_event_id'): access = db.session.query(AccessCode).filter_by(code=kwargs.get('code'), event_id=kwargs.get('access_event_id')).first() diff --git a/app/api/discount_codes.py b/app/api/discount_codes.py index 01eb42e7e0..5fb3e36c96 100644 --- a/app/api/discount_codes.py +++ b/app/api/discount_codes.py @@ -198,6 +198,12 @@ def before_get(self, args, kwargs): 'event_identifier') kwargs['event_id'] = event.id + if kwargs.get('discount_event_identifier'): + event = safe_query( + db, Event, 'identifier', kwargs['discount_event_identifier'], + 'event_identifier') + kwargs['discount_event_id'] = event.id + if kwargs.get('event_id') and has_access('is_admin'): event = safe_query(db, Event, 'id', kwargs['event_id'], 'event_id') if event.discount_code_id: From 78fb8777ae1b5878270bb4290c85bc794e67eba0 Mon Sep 17 00:00:00 2001 From: Abhinav Khare Date: Tue, 30 Jul 2019 02:20:47 +0530 Subject: [PATCH 30/30] feat: verify unverified user if he resets password (#6254) --- app/api/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/auth.py b/app/api/auth.py index a4cf7fa4ce..35b9eb6d2d 100644 --- a/app/api/auth.py +++ b/app/api/auth.py @@ -278,7 +278,7 @@ def reset_password_patch(): return NotFoundError({'source': ''}, 'User Not Found').respond() else: user.password = password - if user.was_registered_with_order: + if not user.is_verified: user.is_verified = True save_to_db(user)