Skip to content

Commit

Permalink
Merge pull request #6400 from fossasia/development
Browse files Browse the repository at this point in the history
chore: Release v1.6.0
  • Loading branch information
iamareebjamal authored Sep 7, 2019
2 parents f74582e + a85bde9 commit 43ac5c6
Show file tree
Hide file tree
Showing 139 changed files with 27,826 additions and 26,122 deletions.
9 changes: 8 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ before-install:
- npm -g install npm@latest

install:
- npm install -g [email protected]
- npm install -g aglio
- npm install -g [email protected]
- pip3 install -r requirements/tests.txt

env:
Expand All @@ -30,6 +31,7 @@ before_script:
- export DATABASE_URL=postgresql://postgres@localhost:5432/test
- export TEST_DATABASE_URL=postgresql://postgres@localhost:5432/test
- bash scripts/test_multiple_heads.sh
- aglio --input docs/api/api_blueprint_source.apib --compile --output docs/api/api_blueprint.apib
- dredd

script:
Expand All @@ -38,3 +40,8 @@ script:
after_success:
- 'bash <(curl -s https://codecov.io/bash)'
- bash scripts/push_api_docs.sh

branches:
only:
- master
- development
11 changes: 9 additions & 2 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from app.api.helpers.cache import cache
from werkzeug.middleware.profiler import ProfilerMiddleware
from app.views import BlueprintsManager
from app.api.helpers.auth import AuthManager
from app.api.helpers.auth import AuthManager, is_token_blacklisted
from app.api.helpers.scheduled_jobs import send_after_event_mail, send_event_fee_notification, \
send_event_fee_notification_followup, change_session_state_on_event_completion, \
expire_pending_tickets, send_monthly_event_invoice, event_invoices_mark_due
Expand Down Expand Up @@ -104,9 +104,16 @@ def create_app():
# set up jwt
app.config['JWT_HEADER_TYPE'] = 'JWT'
app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(days=1)
app.config['JWT_REFRESH_TOKEN_EXPIRES'] = timedelta(days=365)
app.config['JWT_ERROR_MESSAGE_KEY'] = 'error'
app.config['JWT_TOKEN_LOCATION'] = ['cookies', 'headers']
app.config['JWT_REFRESH_COOKIE_PATH'] = '/v1/auth/token/refresh'
app.config['JWT_SESSION_COOKIE'] = False
app.config['JWT_BLACKLIST_ENABLED'] = True
app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = ['refresh']
_jwt = JWTManager(app)
_jwt.user_loader_callback_loader(jwt_user_loader)
_jwt.token_in_blacklist_loader(is_token_blacklisted)

# setup celery
app.config['CELERY_BROKER_URL'] = app.config['REDIS_URL']
Expand Down Expand Up @@ -240,7 +247,7 @@ def update_sent_state(sender=None, headers=None, **kwargs):

scheduler.add_job(send_after_event_mail, 'cron', hour=5, minute=30)
scheduler.add_job(send_event_fee_notification, 'cron', day=1)
scheduler.add_job(send_event_fee_notification_followup, 'cron', day=15)
scheduler.add_job(send_event_fee_notification_followup, 'cron', day=1, month='1-12')
scheduler.add_job(change_session_state_on_event_completion, 'cron', hour=5, minute=30)
scheduler.add_job(expire_pending_tickets, 'cron', minute=45)
scheduler.add_job(send_monthly_event_invoice, 'cron', day=1, month='1-12')
Expand Down
35 changes: 25 additions & 10 deletions app/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
AttendeeRelationshipRequired, AttendeeListPost
from app.api.bootstrap import api
from app.api.custom_forms import CustomFormList, CustomFormListPost, CustomFormDetail, CustomFormRelationshipRequired
from app.api.custom_form_options import CustomFormOptionList, CustomFormOptionDetail, CustomFormOptionRelationship
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, \
Expand Down Expand Up @@ -89,11 +90,12 @@
# users
api.route(UserList, 'user_list', '/users', '/events/<int:event_id>/organizers')
api.route(UserDetail, 'user_detail', '/users/<int:id>', '/notifications/<int:notification_id>/user',
'/event-invoices/<int:event_invoice_id>/user', '/speakers/<int:speaker_id>/user',
'/event-invoices/<int:event_invoice_id>/user', '/event-invoices/<event_invoice_identifier>/user',
'/access-codes/<int:access_code_id>/marketer', '/email-notifications/<int:email_notification_id>/user',
'/discount-codes/<int:discount_code_id>/marketer', '/sessions/<int:session_id>/creator',
'/attendees/<int:attendee_id>/user', '/feedbacks/<int:feedback_id>/user', '/events/<int:event_id>/owner',
'/alternate-emails/<int:user_email_id>/user', '/favourite-events/<int:user_favourite_event_id>/user')
'/alternate-emails/<int:user_email_id>/user', '/favourite-events/<int:user_favourite_event_id>/user',
'/speakers/<int:speaker_id>/user')
api.route(UserRelationship, 'user_notification', '/users/<int:id>/relationships/notifications')
api.route(UserRelationship, 'user_feedback', '/users/<int:id>/relationships/feedbacks')
api.route(UserRelationship, 'user_event_invoices', '/users/<int:id>/relationships/event-invoices')
Expand Down Expand Up @@ -261,14 +263,14 @@
'/sponsors/<int:sponsor_id>/event', '/tracks/<int:track_id>/event',
'/speakers-calls/<int:speakers_call_id>/event', '/session-types/<int:session_type_id>/event',
'/event-copyrights/<int:copyright_id>/event', '/tax/<int:tax_id>/event',
'/event-invoices/<int:event_invoice_id>/event', '/discount-codes/<int:discount_code_id>/event',
'/event-invoices/<int:event_invoice_id>/event', '/event-invoices/<event_invoice_identifier>/event',
'/sessions/<int:session_id>/event', '/ticket-tags/<int:ticket_tag_id>/event',
'/role-invites/<int:role_invite_id>/event', '/speakers/<int:speaker_id>/event',
'/access-codes/<int:access_code_id>/event', '/email-notifications/<int:email_notification_id>/event',
'/attendees/<int:attendee_id>/event', '/custom-forms/<int:custom_form_id>/event',
'/orders/<order_identifier>/event', '/faqs/<int:faq_id>/event', '/faq-types/<int:faq_type_id>/event',
'/feedbacks/<int:feedback_id>/event', '/stripe-authorizations/<int:stripe_authorization_id>/event',
'/user-favourite-events/<int:user_favourite_event_id>/event')
'/user-favourite-events/<int:user_favourite_event_id>/event', '/discount-codes/<int:discount_code_id>/event')
api.route(EventRelationship, 'event_ticket', '/events/<int:id>/relationships/tickets',
'/events/<identifier>/relationships/tickets')
api.route(EventRelationship, 'event_ticket_tag', '/events/<int:id>/relationships/ticket-tags',
Expand Down Expand Up @@ -465,15 +467,20 @@
# event invoices
api.route(EventInvoiceList, 'event_invoice_list', '/event-invoices', '/events/<int:event_id>/event-invoices',
'/events/<event_identifier>/event-invoices', '/users/<int:user_id>/event-invoices')
api.route(EventInvoiceDetail, 'event_invoice_detail', '/event-invoices/<int:id>')
api.route(EventInvoiceDetail, 'event_invoice_detail', '/event-invoices/<int:id>',
'/event-invoices/<event_invoice_identifier>')
api.route(EventInvoiceRelationshipRequired, 'event_invoice_user',
'/event-invoices/<int:id>/relationships/user')
'/event-invoices/<int:id>/relationships/user',
'/event-invoices/<event_invoice_identifier>/relationships/user')
api.route(EventInvoiceRelationshipRequired, 'event_invoice_event',
'/event-invoices/<int:id>/relationships/event')
'/event-invoices/<int:id>/relationships/event',
'/event-invoices/<event_invoice_identifier>/relationships/event')
api.route(EventInvoiceRelationshipRequired, 'event_invoice_order',
'/event-invoices/<int:id>/relationships/order')
'/event-invoices/<int:id>/relationships/order',
'/event-invoices/<event_invoice_identifier>/relationships/order')
api.route(EventInvoiceRelationshipOptional, 'event_invoice_discount_code',
'/event-invoices/<int:id>/relationships/discount-code')
'/event-invoices/<int:id>/relationships/discount-code',
'/event-invoices/<event_invoice_identifier>/relationships/discount-code')

# discount codes
api.route(DiscountCodeListPost, 'discount_code_list_post', '/discount-codes')
Expand All @@ -483,6 +490,7 @@
api.route(DiscountCodeDetail, 'discount_code_detail', '/discount-codes/<int:id>',
'/events/<int:event_id>/discount-code', '/event-invoices/<int:event_invoice_id>/discount-code',
'/events/<int:discount_event_id>/discount-codes/<code>',
'/event-invoices/<event_invoice_identifier>/discount-code',
'/events/<discount_event_identifier>/discount-codes/<code>')
api.route(DiscountCodeRelationshipRequired, 'discount_code_event',
'/discount-codes/<int:id>/relationships/event')
Expand Down Expand Up @@ -568,10 +576,17 @@
api.route(CustomFormListPost, 'custom_form_list_post', '/custom-forms')
api.route(CustomFormList, 'custom_form_list', '/events/<int:event_id>/custom-forms',
'/events/<event_identifier>/custom-forms')
api.route(CustomFormDetail, 'custom_form_detail', '/custom-forms/<int:id>')
api.route(CustomFormDetail, 'custom_form_detail', '/custom-forms/<int:id>',
'/custom-form-options/<int:custom_form_option_id>/custom-form')
api.route(CustomFormRelationshipRequired, 'custom_form_event',
'/custom-forms/<int:id>/relationships/event')

# custom form options
api.route(CustomFormOptionList, 'custom_form_option_list', '/custom-forms/<int:custom_form_id>/custom-form-options')
api.route(CustomFormOptionDetail, 'custom_form_option_detail', '/custom-form-options/<int:id>')
api.route(CustomFormOptionRelationship, 'custom_form_option_form',
'/custom-form-options/<int:id>/relationships/custom-form')

# FAQ
api.route(FaqListPost, 'faq_list_post', '/faqs')
api.route(FaqList, 'faq_list', '/events/<int:event_id>/faqs', '/events/<event_identifier>/faqs',
Expand Down
5 changes: 0 additions & 5 deletions app/api/attendees.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,6 @@ def before_update_object(self, obj, data, kwargs):
raise UnprocessableEntity(
{'pointer': '/data/relationships/ticket'}, "Invalid Ticket"
)
if not user.is_verified and ticket.price == 0:
raise UnprocessableEntity(
{'pointer': '/data/relationships/ticket'},
"Unverified user cannot buy free tickets"
)

if 'device_name_checkin' in data:
if 'checkin_times' not in data or data['checkin_times'] is None:
Expand Down
97 changes: 84 additions & 13 deletions app/api/auth.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import base64
import base64
import logging
import random
import string
from datetime import timedelta
from functools import wraps

import requests
from flask import request, jsonify, make_response, Blueprint, send_file
from flask_jwt_extended import jwt_required, current_user, create_access_token
from flask_jwt_extended import (
jwt_required, jwt_refresh_token_required,
fresh_jwt_required, unset_jwt_cookies,
current_user, create_access_token,
create_refresh_token, set_refresh_cookies,
get_jwt_identity)
from flask_limiter.util import get_remote_address
from healthcheck import EnvironmentDump
from flask_rest_jsonapi.exceptions import ObjectNotFound
Expand All @@ -16,24 +21,26 @@
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.auth import AuthManager, blacklist_token
from app.api.helpers.jwt import jwt_authenticate
from app.api.helpers.errors import ForbiddenError, UnprocessableEntityError, NotFoundError, BadRequestError
from app.api.helpers.files import make_frontend_url
from app.api.helpers.mail import send_email_to_attendees
from app.api.helpers.mail import send_email_with_action, \
send_email_confirmation
from app.api.helpers.notification import send_notification_with_action
from app.api.helpers.order import create_pdf_tickets_for_holder
from app.api.helpers.order import create_pdf_tickets_for_holder, calculate_order_amount
from app.api.helpers.storage import UPLOAD_PATHS
from app.api.helpers.storage import generate_hash
from app.api.helpers.third_party_auth import GoogleOAuth, FbOAuth, TwitterOAuth, InstagramOAuth
from app.api.helpers.ticketing import TicketingManager
from app.api.helpers.utilities import get_serializer, str_generator
from app.api.helpers.permission_manager import has_access
from app.models import db
from app.models.mail import PASSWORD_RESET, PASSWORD_CHANGE, \
PASSWORD_RESET_AND_VERIFY
from app.models.notification import PASSWORD_CHANGE as PASSWORD_CHANGE_NOTIF
from app.models.discount_code import DiscountCode
from app.models.order import Order
from app.models.user import User
from app.models.event_invoice import EventInvoice
Expand All @@ -45,9 +52,7 @@
auth_routes = Blueprint('auth', __name__, url_prefix='/v1/auth')


@authorised_blueprint.route('/auth/session', methods=['POST'])
@auth_routes.route('/login', methods=['POST'])
def login():
def authenticate(allow_refresh_token=False, existing_identity=None):
data = request.get_json()
username = data.get('email', data.get('username'))
password = data.get('password')
Expand All @@ -57,13 +62,64 @@ def login():
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:
if not identity or (existing_identity and identity != existing_identity):
# For fresh login, credentials should match existing user
return jsonify(error='Invalid Credentials'), 401

remember_me = data.get('remember-me')
include_in_response = data.get('include-in-response')
add_refresh_token = allow_refresh_token and remember_me

expiry_time = timedelta(minutes=90) if add_refresh_token else None
access_token = create_access_token(identity.id, fresh=True, expires_delta=expiry_time)
response_data = {'access_token': access_token}

if add_refresh_token:
refresh_token = create_refresh_token(identity.id)
if include_in_response:
response_data['refresh_token'] = refresh_token

response = jsonify(response_data)

if add_refresh_token and not include_in_response:
set_refresh_cookies(response, refresh_token)

return response


@authorised_blueprint.route('/auth/session', methods=['POST'])
@auth_routes.route('/login', methods=['POST'])
def login():
return authenticate(allow_refresh_token=True)


@auth_routes.route('/fresh-login', methods=['POST'])
@jwt_required
def fresh_login():
return authenticate(existing_identity=current_user)


@auth_routes.route('/token/refresh', methods=['POST'])
@jwt_refresh_token_required
def refresh_token():
current_user = get_jwt_identity()
new_token = create_access_token(identity=current_user, fresh=False)
return jsonify({'access_token': new_token})


@auth_routes.route('/logout', methods=['POST'])
def logout():
response = jsonify({'success': True})
unset_jwt_cookies(response)
return response


@auth_routes.route('/blacklist', methods=['POST'])
@jwt_required
def blacklist_token_rquest():
blacklist_token(current_user)
return jsonify({'success': True})


@auth_routes.route('/oauth/<provider>', methods=['GET'])
def redirect_uri(provider):
Expand Down Expand Up @@ -290,7 +346,7 @@ def reset_password_patch():


@auth_routes.route('/change-password', methods=['POST'])
@jwt_required
@fresh_jwt_required
def change_password():
old_password = request.json['data']['old-password']
new_password = request.json['data']['new-password']
Expand Down Expand Up @@ -449,3 +505,18 @@ def resend_emails():
"Only placed and completed orders have confirmation").respond()
else:
return ForbiddenError({'source': ''}, "Co-Organizer Access Required").respond()


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

return jsonify(calculate_order_amount(tickets, discount_code))
46 changes: 46 additions & 0 deletions app/api/custom_form_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from flask_rest_jsonapi import ResourceDetail, ResourceList, ResourceRelationship
from app.models import db
from app.api.schema.custom_form_options import CustomFormOptionSchema
from app.models.custom_form_option import CustomFormOptions


class CustomFormOptionList(ResourceList):
"""
Create and List Custom Form Options
"""

def query(self, view_kwargs):
query_ = self.session.query(CustomFormOptions)
if view_kwargs.get('custom_form_id'):
query_ = self.session.query(CustomFormOptions).filter(
getattr(CustomFormOptions, 'custom_form_id') == view_kwargs['custom_form_id'])
return query_

schema = CustomFormOptionSchema
data_layer = {'session': db.session,
'model': CustomFormOptions,
'methods': {
'query': query
}}


class CustomFormOptionDetail(ResourceDetail):
"""
CustomForm Resource
"""

schema = CustomFormOptionSchema
data_layer = {'session': db.session,
'model': CustomFormOptions
}


class CustomFormOptionRelationship(ResourceRelationship):
"""
CustomForm Resource
"""

schema = CustomFormOptionSchema
data_layer = {'session': db.session,
'model': CustomFormOptions
}
8 changes: 8 additions & 0 deletions app/api/discount_codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,14 @@ def before_get_object(self, view_kwargs):
else:
view_kwargs['id'] = None

if view_kwargs.get('event_invoice_identifier') and has_access('is_admin'):
event_invoice = safe_query(self, EventInvoice, 'identifier', view_kwargs['event_invoice_identifier'],
'event_invoice_identifier')
if event_invoice.discount_code_id:
view_kwargs['id'] = event_invoice.discount_code_id
else:
view_kwargs['id'] = None

if view_kwargs.get('id'):
try:
discount = self.session.query(
Expand Down
Loading

0 comments on commit 43ac5c6

Please sign in to comment.