diff --git a/.env.example b/.env.example index 6cf1689ef7..23670277bd 100644 --- a/.env.example +++ b/.env.example @@ -2,7 +2,7 @@ DATABASE_URL=postgresql://open_event_user:opev_pass@127.0.0.1:5432/oevent INTEGRATE_SOCKETIO=false TEST_DATABASE_URL=postgresql://open_event_user:opev_pass@127.0.0.1:5432/opev_test APP_CONFIG=config.DevelopmentConfig -ENABLE_ELASTICSEARCH=true +ENABLE_ELASTICSEARCH=false ELASTICSEARCH_HOST=localhost:9200 POSTGRES_USER=open_event_user diff --git a/CHANGELOG.md b/CHANGELOG.md index adf819468f..5b1d9acc92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ ## Changelog +##### v1.10.0 (2019-12-22): + +- Fix event and speaker image resizing, and add management command to resize event and speaker images which remained to be resized. +Run `python manage.py fix_event_and_speaker_images` to resize images which weren't resized due to the bug +- Optimize link generation of relationships with up to 10X speedup +- Add scheduled job to automatically remove orphan ticket holders with no order ID +- Add created and modified times in ticket holder +- Allow new tickets to have same name as deleted tickets +- Fix PayTM payment gateway + ##### v1.9.0 (2019-11-28): - Fix billing info requirements from attendees diff --git a/Dockerfile b/Dockerfile index 4f4c16c6d2..87f2352e36 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,4 @@ -FROM python:3.7.4-alpine as base -LABEL maintainer="Niranjan Rajendran " +FROM python:3.7-alpine as base #### @@ -9,25 +8,19 @@ WORKDIR /install RUN apk update && \ apk add --virtual build-deps git gcc python3-dev musl-dev jpeg-dev zlib-dev libevent-dev file-dev libffi-dev openssl && \ - apk add postgresql-dev && \ - pip install setuptools + apk add postgresql-dev ADD requirements.txt /requirements.txt ADD requirements /requirements/ -RUN wget https://bootstrap.pypa.io/ez_setup.py && python ez_setup.py - -ENV PYTHONPATH /install/lib/python3.7/site-packages -RUN pip install --install-option="--prefix=/install" setuptools && \ - LIBRARY_PATH=/lib:/usr/lib pip install --install-option="--prefix=/install" -r /requirements.txt +RUN pip install --prefix=/install --no-warn-script-location -r /requirements.txt #### FROM base COPY --from=builder /install /usr/local -RUN apk --no-cache add postgresql-dev ca-certificates libxslt jpeg zlib file libxml2 git && \ - pip install git+https://github.com/fossasia/flask-rest-jsonapi.git@0.12.6.1#egg=flask-rest-jsonapi +RUN apk --no-cache add postgresql-libs ca-certificates libxslt jpeg zlib file libxml2 WORKDIR /data/app ADD . . diff --git a/app/__init__.py b/app/__init__.py index 4df584942c..04553605d0 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -18,6 +18,7 @@ from flask_login import current_user from flask_jwt_extended import JWTManager from flask_limiter import Limiter +from flask_limiter.util import get_ipaddr from datetime import timedelta from flask_cors import CORS from flask_rest_jsonapi.errors import jsonapi_errors @@ -40,7 +41,8 @@ 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 + expire_pending_tickets, send_monthly_event_invoice, event_invoices_mark_due, \ + delete_ticket_holders_no_order_id from app.models.event import Event from app.models.role_invite import RoleInvite from app.views.healthcheck import health_check_celery, health_check_db, health_check_migrations, check_migrations @@ -49,6 +51,7 @@ from app.views.redis_store import redis_store from app.views.celery_ import celery from app.templates.flask_ext.jinja.filters import init_filters +from app.extensions import shell BASE_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -56,7 +59,7 @@ static_dir = os.path.dirname(os.path.dirname(__file__)) + "/static" template_dir = os.path.dirname(__file__) + "/templates" app = Flask(__name__, static_folder=static_dir, template_folder=template_dir) -limiter = Limiter(app) +limiter = Limiter(app, key_func=get_ipaddr) env.read_envfile() @@ -196,6 +199,8 @@ def create_app(): # redis redis_store.init_app(app) + shell.init_app(app) + # elasticsearch if app.config['ENABLE_ELASTICSEARCH']: client.init_app(app) @@ -270,6 +275,7 @@ def update_sent_state(sender=None, headers=None, **kwargs): scheduler.add_job(expire_pending_tickets, 'cron', minute=45) scheduler.add_job(send_monthly_event_invoice, 'cron', day=1, month='1-12') scheduler.add_job(event_invoices_mark_due, 'cron', hour=5) +scheduler.add_job(delete_ticket_holders_no_order_id, 'cron', minute=5) scheduler.start() diff --git a/app/api/auth.py b/app/api/auth.py index 0ece9c9326..a91d807ecb 100644 --- a/app/api/auth.py +++ b/app/api/auth.py @@ -13,7 +13,6 @@ 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 sqlalchemy.orm.exc import NoResultFound @@ -289,7 +288,7 @@ def resend_verification_email(): '3/hour', key_func=lambda: request.json['data']['email'], error_message='Limit for this action exceeded' ) @limiter.limit( - '1/minute', key_func=get_remote_address, error_message='Limit for this action exceeded' + '1/minute', error_message='Limit for this action exceeded' ) def reset_password_post(): try: diff --git a/app/api/custom/orders.py b/app/api/custom/orders.py index b841f5ce87..be86dda685 100644 --- a/app/api/custom/orders.py +++ b/app/api/custom/orders.py @@ -1,6 +1,5 @@ from flask import Blueprint, jsonify, request from flask_jwt_extended import current_user, jwt_required -from flask_limiter.util import get_remote_address from sqlalchemy.orm.exc import NoResultFound @@ -50,7 +49,7 @@ def ticket_attendee_authorized(order_identifier): '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' + '60/minute', error_message='Limit for this action exceeded' ) def resend_emails(): """ diff --git a/app/api/events.py b/app/api/events.py index 672c560f9e..528631bc2e 100644 --- a/app/api/events.py +++ b/app/api/events.py @@ -56,8 +56,8 @@ def validate_event(user, modules, data): if not user.can_create_event(): raise ForbiddenException({'source': ''}, "Please verify your Email") - elif data.get('is_ticketing_enabled', True) and not modules.ticket_include: - raise ForbiddenException({'source': '/data/attributes/is-ticketing-enabled'}, + elif not modules.ticket_include: + raise ForbiddenException({'source': ''}, "Ticketing is not enabled in the system") if data.get('can_pay_by_paypal', False) or data.get('can_pay_by_cheque', False) or \ data.get('can_pay_by_bank', False) or data.get('can_pay_by_stripe', False): diff --git a/app/api/helpers/checksum.py b/app/api/helpers/checksum.py index 8095dbaf10..3325c56518 100644 --- a/app/api/helpers/checksum.py +++ b/app/api/helpers/checksum.py @@ -102,8 +102,8 @@ def __encode__(to_encode, iv, key): # Pad to_encode = __pad__(to_encode) # Encrypt - c = AES.new(key, AES.MODE_CBC, iv) - to_encode = c.encrypt(to_encode) + c = AES.new(key.encode('UTF-8'), AES.MODE_CBC, iv.encode('UTF-8')) + to_encode = c.encrypt(to_encode.encode('UTF-8')) # Encode to_encode = base64.b64encode(to_encode) return to_encode.decode("UTF-8") @@ -113,7 +113,7 @@ def __decode__(to_decode, iv, key): # Decode to_decode = base64.b64decode(to_decode) # Decrypt - c = AES.new(key, AES.MODE_CBC, iv) + c = AES.new(key.encode('UTF-8'), AES.MODE_CBC, iv.encode('UTF-8')) to_decode = c.decrypt(to_decode) if type(to_decode) == bytes: # convert bytes array to str. diff --git a/app/api/helpers/files.py b/app/api/helpers/files.py index ede5e401fa..646a7c3bbb 100644 --- a/app/api/helpers/files.py +++ b/app/api/helpers/files.py @@ -5,6 +5,7 @@ import urllib.parse import urllib.request import uuid +import requests import PIL from PIL import Image @@ -77,7 +78,7 @@ def create_save_resized_image(image_file, basewidth=None, maintain_aspect=None, if not image_file: return None filename = '{filename}.{ext}'.format(filename=get_file_name(), ext=ext) - data = urllib.request.urlopen(image_file).read() + data = requests.get(image_file).content image_file = io.BytesIO(data) try: im = Image.open(image_file) @@ -129,7 +130,14 @@ def create_save_image_sizes(image_file, image_sizes_type, unique_identifier=None try: image_sizes = ImageSizes.query.filter_by(type=image_sizes_type).one() except NoResultFound: - image_sizes = ImageSizes(image_sizes_type, 1300, 500, True, 100, 75, 30, True, 100, 500, 200, True, 100) + image_sizes = ImageSizes(image_sizes_type, full_width=1300, + full_height=500, full_aspect=True, full_quality=80, + icon_width=75, icon_height=30, icon_aspect=True, + icon_quality=80, thumbnail_width=500, thumbnail_height=200, + thumbnail_aspect=True, thumbnail_quality=80, logo_width=500, + logo_height=200, icon_size_width_height=35, icon_size_quality=80, + small_size_width_height=50, small_size_quality=80, + thumbnail_size_width_height=500) # Get an unique identifier from uuid if not provided if unique_identifier is None: diff --git a/app/api/helpers/mail.py b/app/api/helpers/mail.py index 8cd2d681f1..17d8352fab 100644 --- a/app/api/helpers/mail.py +++ b/app/api/helpers/mail.py @@ -363,18 +363,21 @@ def send_email_to_attendees(order, purchaser_id, attachments=None): def send_order_cancel_email(order): + cancel_msg = '' + if order.cancel_note: + cancel_msg = u"
Message from the organizer: {cancel_note}".format(cancel_note=order.cancel_note) + send_email( to=order.user.email, action=TICKET_CANCELLED, subject=MAILS[TICKET_CANCELLED]['subject'].format( event_name=order.event.name, 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, - frontend_url=get_settings()['frontend_url'] + frontend_url=get_settings()['frontend_url'], + cancel_msg=cancel_msg, + app_name=get_settings()['app_name'] ) ) diff --git a/app/api/helpers/scheduled_jobs.py b/app/api/helpers/scheduled_jobs.py index 9ed45d5ac8..0a7f3eba5c 100644 --- a/app/api/helpers/scheduled_jobs.py +++ b/app/api/helpers/scheduled_jobs.py @@ -21,6 +21,7 @@ from app.models.session import Session from app.models.ticket import Ticket from app.models.ticket_fee import TicketFees, get_fee +from app.models.ticket_holder import TicketHolder from app.settings import get_settings @@ -155,6 +156,16 @@ def expire_pending_tickets(): db.session.commit() +def delete_ticket_holders_no_order_id(): + from app import current_app as app + with app.app_context(): + order_expiry_time = get_settings()['order_expiry_time'] + TicketHolder.query.filter(TicketHolder.order_id == None, TicketHolder.deleted_at.is_(None), + TicketHolder.created_at + datetime.timedelta(minutes=order_expiry_time) + < datetime.datetime.utcnow()).delete(synchronize_session=False) + db.session.commit() + + def event_invoices_mark_due(): from app import current_app as app with app.app_context(): diff --git a/app/api/helpers/system_mails.py b/app/api/helpers/system_mails.py index 1d2a824e42..2371bef355 100644 --- a/app/api/helpers/system_mails.py +++ b/app/api/helpers/system_mails.py @@ -186,11 +186,13 @@ 'recipient': 'User', 'subject': u'Your order for {event_name} has been cancelled ({invoice_id})', 'message': ( - u"Hi,Your order for {event_name} has been cancelled has been cancelled by the organizer" - 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 {frontend_url} " + u"Hello," + u"
your order for {event_name} has been cancelled by the organizer." + u"
Please contact the organizer for more info." + + u"{cancel_msg}" + u"
To manage orders please login to {frontend_url} and visit \"My Tickets\"." + u"
Best regards," + u"
{app_name} Team" ) }, EVENT_EXPORTED: { diff --git a/app/api/helpers/tasks.py b/app/api/helpers/tasks.py index deecc5d8a6..8ce7361aa2 100644 --- a/app/api/helpers/tasks.py +++ b/app/api/helpers/tasks.py @@ -126,7 +126,7 @@ def send_email_task_smtp(payload, smtp_config, headers=None): @celery.task(base=RequestContextTask, name='resize.event.images', bind=True) def resize_event_images_task(self, event_id, original_image_url): - event = safe_query(db, Event, 'id', event_id, 'event_id') + event = Event.query.get(event_id) try: logging.info('Event image resizing tasks started {}'.format(original_image_url)) uploaded_images = create_save_image_sizes(original_image_url, 'event-image', event.id) @@ -135,7 +135,7 @@ def resize_event_images_task(self, event_id, original_image_url): event.icon_image_url = uploaded_images['icon_image_url'] save_to_db(event) logging.info('Resized images saved successfully for event with id: {}'.format(event_id)) - except (urllib.error.HTTPError, urllib.error.URLError): + except (requests.exceptions.HTTPError, requests.exceptions.InvalidURL): logging.exception('Error encountered while generating resized images for event with id: {}'.format(event_id)) @@ -152,7 +152,7 @@ def resize_user_images_task(self, user_id, original_image_url): user.icon_image_url = uploaded_images['icon_image_url'] save_to_db(user) logging.info('Resized images saved successfully for user with id: {}'.format(user_id)) - except (urllib.error.HTTPError, urllib.error.URLError): + except (requests.exceptions.HTTPError, requests.exceptions.InvalidURL): logging.exception('Error encountered while generating resized images for user with id: {}'.format(user_id)) @@ -166,13 +166,13 @@ def sponsor_logos_url_task(self, event_id): sponsor.logo_url = new_logo_url save_to_db(sponsor) logging.info('Sponsor logo url successfully generated') - except(urllib.error.HTTPError, urllib.error.URLError): + except(requests.exceptions.HTTPError, requests.exceptions.InvalidURL): logging.exception('Error encountered while logo generation') @celery.task(base=RequestContextTask, name='resize.speaker.images', bind=True) def resize_speaker_images_task(self, speaker_id, photo_url): - speaker = safe_query(db, Speaker, 'id', speaker_id, 'speaker_id') + speaker = Speaker.query.get(speaker_id) try: logging.info('Speaker image resizing tasks started for speaker with id {}'.format(speaker_id)) uploaded_images = create_save_image_sizes(photo_url, 'speaker-image', speaker_id) @@ -181,7 +181,7 @@ def resize_speaker_images_task(self, speaker_id, photo_url): speaker.icon_image_url = uploaded_images['icon_image_url'] save_to_db(speaker) logging.info('Resized images saved successfully for speaker with id: {}'.format(speaker_id)) - except (urllib.error.HTTPError, urllib.error.URLError): + except (requests.exceptions.HTTPError, requests.exceptions.InvalidURL): logging.exception('Error encountered while generating resized images for event with id: {}'.format(speaker_id)) diff --git a/app/api/schema/__init__.py b/app/api/schema/__init__.py index e69de29bb2..240b53017a 100644 --- a/app/api/schema/__init__.py +++ b/app/api/schema/__init__.py @@ -0,0 +1,11 @@ +# Monkey Patch Marshmallow JSONAPI +from marshmallow_jsonapi.flask import Relationship + + +def serialize(self, attr, obj, accessor=None): + if self.include_resource_linkage or self.include_data: + return super(Relationship, self).serialize(attr, obj, accessor) + return self._serialize(None, attr, obj) + + +Relationship.serialize = serialize diff --git a/app/api/schema/events.py b/app/api/schema/events.py index a27bd71ddc..77fccea6da 100644 --- a/app/api/schema/events.py +++ b/app/api/schema/events.py @@ -63,8 +63,6 @@ def validate_timezone(self, data, original_data): owner_name = fields.Str(allow_none=True) is_map_shown = fields.Bool(default=False) has_owner_info = fields.Bool(default=False) - has_sessions = fields.Bool(default=0, dump_only=True) - has_speakers = fields.Bool(default=0, dump_only=True) owner_description = fields.Str(allow_none=True) is_sessions_speakers_enabled = fields.Bool(default=False) privacy = fields.Str(default="public") @@ -72,14 +70,10 @@ def validate_timezone(self, data, original_data): ticket_url = fields.Url(allow_none=True) code_of_conduct = fields.Str(allow_none=True) schedule_published_on = fields.DateTime(allow_none=True) - is_ticketing_enabled = fields.Bool(default=False) is_featured = fields.Bool(default=False) is_ticket_form_enabled = fields.Bool(default=True) payment_country = fields.Str(allow_none=True) payment_currency = fields.Str(allow_none=True) - tickets_available = fields.Float(dump_only=True) - tickets_sold = fields.Float(dump_only=True) - revenue = fields.Float(dump_only=True) paypal_email = fields.Str(allow_none=True) is_tax_enabled = fields.Bool(default=False) is_billing_info_mandatory = fields.Bool(default=False) @@ -100,7 +94,6 @@ def validate_timezone(self, data, original_data): pentabarf_url = fields.Url(dump_only=True) ical_url = fields.Url(dump_only=True) xcal_url = fields.Url(dump_only=True) - average_rating = fields.Float(dump_only=True) refund_policy = fields.String(dump_only=True, default='All sales are final. No refunds shall be issued in any case.') is_stripe_linked = fields.Boolean(dump_only=True, allow_none=True, default=False) diff --git a/app/api/schema/orders.py b/app/api/schema/orders.py index d9fe5200dd..3b99223eb9 100644 --- a/app/api/schema/orders.py +++ b/app/api/schema/orders.py @@ -117,7 +117,7 @@ def initial_values(self, data): type_="event") event_invoice = Relationship(attribute='invoice', - self_view='v1.order_invoice', + self_view='v1.order_event_invoice', self_view_kwargs={'order_identifier': ''}, related_view='v1.event_invoice_detail', related_view_kwargs={'id': ''}, diff --git a/app/api/schema/users.py b/app/api/schema/users.py index c2b6dc379a..9ac02be823 100644 --- a/app/api/schema/users.py +++ b/app/api/schema/users.py @@ -100,7 +100,7 @@ class Meta: type_='feedback') event_invoice = Relationship( attribute='event_invoice', - self_view='v1.user_event_invoice', + self_view='v1.user_event_invoices', self_view_kwargs={'id': ''}, related_view='v1.event_invoice_list', related_view_kwargs={'user_id': ''}, diff --git a/app/api/server_version.py b/app/api/server_version.py index e58449295d..a9024b0ef8 100644 --- a/app/api/server_version.py +++ b/app/api/server_version.py @@ -1,6 +1,6 @@ from flask import jsonify, Blueprint -SERVER_VERSION = '1.9.0' +SERVER_VERSION = '1.10.0' info_route = Blueprint('info', __name__) _build = {'version': SERVER_VERSION} diff --git a/app/api/tickets.py b/app/api/tickets.py index 8faac8e15e..29f25a6dd7 100644 --- a/app/api/tickets.py +++ b/app/api/tickets.py @@ -41,9 +41,6 @@ def before_post(self, args, kwargs, data): deleted_at=None)) > 0: raise ConflictException({'pointer': '/data/attributes/name'}, "Ticket already exists") - if get_count(db.session.query(Event).filter_by(id=int(data['event']), is_ticketing_enabled=False)) > 0: - raise MethodNotAllowed({'parameter': 'event_id'}, "Ticketing is disabled for this Event") - def before_create_object(self, data, view_kwargs): """ before create method to check if paid ticket has a paymentMethod enabled diff --git a/app/extensions/shell.py b/app/extensions/shell.py new file mode 100644 index 0000000000..d90851afd5 --- /dev/null +++ b/app/extensions/shell.py @@ -0,0 +1,20 @@ +from app.models import db +from app.models.event import Event +from app.models.session import Session +from app.models.ticket import Ticket +from app.models.order import Order +from app.models.setting import Setting +from app.models.speaker import Speaker +from app.models.track import Track +from app.models.user import User + + +def init_app(app): + + @app.shell_context_processor + def shell_context(): + return dict( + db=db, Event=Event, Session=Session, + Ticket=Ticket, Order=Order, Setting=Setting, + Speaker=Speaker, Track=Track, User=User + ) diff --git a/app/factories/attendee.py b/app/factories/attendee.py index ed8369af2b..7f06217e49 100644 --- a/app/factories/attendee.py +++ b/app/factories/attendee.py @@ -2,6 +2,8 @@ import app.factories.common as common from app.factories.event import EventFactoryBasic +from app.factories.ticket import TicketFactory +from app.factories.order import OrderFactory from app.models.ticket_holder import db, TicketHolder @@ -11,6 +13,8 @@ class Meta: sqlalchemy_session = db.session event = factory.RelatedFactory(EventFactoryBasic) + ticket = factory.RelatedFactory(TicketFactory) + order = factory.RelatedFactory(OrderFactory) firstname = common.string_ lastname = common.string_ email = common.email_ @@ -22,3 +26,6 @@ class Meta: pdf_url = common.url_ event_id = 1 ticket_id = None + order_id = None + created_at = common.date_ + modified_at = common.date_ diff --git a/app/models/ticket.py b/app/models/ticket.py index da91b9c6a7..02f0af3ee6 100644 --- a/app/models/ticket.py +++ b/app/models/ticket.py @@ -22,7 +22,7 @@ class Ticket(SoftDeletionModel): __tablename__ = 'tickets' - __table_args__ = (db.UniqueConstraint('name', 'event_id', name='name_event_uc'),) + __table_args__ = (db.UniqueConstraint('name', 'event_id', 'deleted_at', name='name_event_deleted_at_uc'),) id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String, nullable=False) diff --git a/app/models/ticket_holder.py b/app/models/ticket_holder.py index 0124923edd..6c51aad1df 100644 --- a/app/models/ticket_holder.py +++ b/app/models/ticket_holder.py @@ -46,6 +46,8 @@ class TicketHolder(SoftDeletionModel): checkout_times: str = db.Column(db.String) attendee_notes: str = db.Column(db.String) event_id: int = db.Column(db.Integer, db.ForeignKey('events.id', ondelete='CASCADE')) + created_at: datetime = db.Column(db.DateTime(timezone=True), default=datetime.utcnow) + modified_at: datetime = db.Column(db.DateTime(timezone=True), default=datetime.utcnow, onupdate=datetime.utcnow) complex_field_values: str = db.Column(db.JSON) user = db.relationship('User', foreign_keys=[email], primaryjoin='User.email == TicketHolder.email', viewonly=True, backref='attendees') diff --git a/docs/api/blueprint/event/events.apib b/docs/api/blueprint/event/events.apib index 81afe53555..25b7ab867a 100644 --- a/docs/api/blueprint/event/events.apib +++ b/docs/api/blueprint/event/events.apib @@ -25,7 +25,6 @@ These endpoints help you to create the basic event structure. To add other parts | `sub-topic` | Sub-topic of the event | string | - | | `ticket-url` | The ticketing URL of the event | string | - | | `code-of-conduct` | Event's code of conduct | string | - | -| `is-ticketing-enabled` | Does the event use Open Event ticketing system | boolean (default: `true`) | - | | `thumbnail-image-url` | URL of the uploaded thumbnail | string | - | | `large-image-url` | URL of the large uploaded banner image | string | - | | `original-image-url` | URL of the original image | string | - | @@ -48,7 +47,6 @@ These endpoints help you to create the basic event structure. To add other parts | `identifier` | - | string | - | | `external-event-url` | - | string | - | | `has-owner-info` | - | boolean(default: `false`) | - | -| `average-rating` | Average rating of event | float | - | | `refund-policy` | Refund policy | string | - | | `is-stripe-linked` | Shows if the event has a linked stripe account. | boolean(default: `false`) | - | @@ -268,11 +266,9 @@ Get a list of events. "privacy": "public", "code-of-conduct": "example", "state": "draft", - "average-rating": null, "latitude": 1.23456789, "starts-at": "2016-12-13T23:59:59.123456+00:00", "searchable-location-name": "example", - "is-ticketing-enabled": true, "can-pay-by-cheque": true, "can-pay-by-omise": false, "description": "example", @@ -343,10 +339,8 @@ Create a new event using a `name`, `starts-at`, `ends-at`, `timezone` and an opt "privacy": "public", "state": "draft", "is-event-online": false, - "average-rating": null, "ticket-url": "http://example.com", "code-of-conduct": "example", - "is-ticketing-enabled": "false", "payment-country": "US", "payment-currency": "USD", "paypal-email": "example@example.com", @@ -569,11 +563,9 @@ Create a new event using a `name`, `starts-at`, `ends-at`, `timezone` and an opt "privacy": "public", "code-of-conduct": "example", "state": "draft", - "average-rating": null, "latitude": 1.23456789, "starts-at": "2016-12-13T23:59:59.123456+00:00", "searchable-location-name": "example", - "is-ticketing-enabled": false, "can-pay-by-cheque": false, "description": "example", "pentabarf-url": null, @@ -824,11 +816,9 @@ Get a single event. "is-event-online": false, "code-of-conduct": "example", "state": "draft", - "average-rating": null, "latitude": 1.23456789, "starts-at": "2016-12-13T23:59:59.123456+00:00", "searchable-location-name": "example", - "is-ticketing-enabled": true, "can-pay-by-cheque": true, "description": "example", "pentabarf-url": null, @@ -902,10 +892,8 @@ All other fields are optional. Add attributes you want to modify. "is-sessions-speakers-enabled": "false", "privacy": "private", "state": "draft", - "average-rating": null, "ticket-url": "http://example1.com", "code-of-conduct": "example1", - "is-ticketing-enabled": "false", "payment-country": "US", "payment-currency": "USD", "paypal-email": "example1@example1.com", @@ -1128,12 +1116,10 @@ All other fields are optional. Add attributes you want to modify. "privacy": "private", "code-of-conduct": "example1", "state": "published", - "average-rating": null, "is-event-online": false, "latitude": 12.23456789, "starts-at": "2016-12-14T18:29:59.123456+00:00", "searchable-location-name": "example1", - "is-ticketing-enabled": false, "can-pay-by-cheque": false, "description": "example1", "pentabarf-url": null, @@ -1390,11 +1376,9 @@ Get a list of events. "has-owner-info": false, "state": "draft", "is-event-online": false, - "average-rating": null, "latitude": 1.23456789, "starts-at": "2016-12-13T23:59:59.123456+00:00", "searchable-location-name": "Draft", - "is-ticketing-enabled": true, "can-pay-by-cheque": true, "description": "example", "pentabarf-url": null, @@ -1627,11 +1611,9 @@ Get a list of events. "privacy": "public", "has-owner-info": false, "state": "draft", - "average-rating": null, "latitude": 1.23456789, "starts-at": "2016-12-13T23:59:59.123456+00:00", "searchable-location-name": "Draft", - "is-ticketing-enabled": true, "can-pay-by-cheque": true, "description": "example", "pentabarf-url": null, @@ -1855,11 +1837,9 @@ Get a list of events. "privacy": "public", "has-owner-info": false, "state": "draft", - "average-rating": null, "latitude": 1.23456789, "starts-at": "2016-12-13T23:59:59.123456+00:00", "searchable-location-name": "Draft", - "is-ticketing-enabled": true, "can-pay-by-cheque": true, "description": "example", "pentabarf-url": null, @@ -2092,11 +2072,9 @@ Get a list of events. "has-owner-info": false, "state": "draft", "is-event-online": false, - "average-rating": null, "latitude": 1.23456789, "starts-at": "2016-12-13T23:59:59.123456+00:00", "searchable-location-name": "Draft", - "is-ticketing-enabled": true, "can-pay-by-cheque": true, "description": "example", "pentabarf-url": null, @@ -2321,11 +2299,9 @@ Get a list of events. "has-owner-info": false, "can-pay-by-omise": false, "state": "draft", - "average-rating": null, "latitude": 1.23456789, "starts-at": "2016-12-13T23:59:59.123456+00:00", "searchable-location-name": "Draft", - "is-ticketing-enabled": true, "can-pay-by-cheque": true, "description": "example", "pentabarf-url": null, @@ -2549,11 +2525,9 @@ Get a list of events. "state": "draft", "can-pay-by-omise": false, "is-event-online": false, - "average-rating": null, "latitude": 1.23456789, "starts-at": "2016-12-13T23:59:59.123456+00:00", "searchable-location-name": "Draft", - "is-ticketing-enabled": true, "can-pay-by-cheque": true, "description": "example", "pentabarf-url": null, @@ -2775,11 +2749,9 @@ Get a list of events. "has-owner-info": false, "state": "draft", "is-event-online": false, - "average-rating": null, "latitude": 1.23456789, "starts-at": "2016-12-13T23:59:59.123456+00:00", "searchable-location-name": "Draft", - "is-ticketing-enabled": true, "can-pay-by-cheque": true, "can-pay-by-omise": false, "description": "example", @@ -3003,11 +2975,9 @@ Get a list of events. "privacy": "public", "has-owner-info": false, "state": "draft", - "average-rating": null, "latitude": 1.23456789, "starts-at": "2016-12-13T23:59:59.123456+00:00", "searchable-location-name": "Draft", - "is-ticketing-enabled": true, "can-pay-by-cheque": true, "description": "example", "pentabarf-url": null, @@ -3229,11 +3199,9 @@ Get a list of events. "has-owner-info": false, "state": "draft", "is-event-online": false, - "average-rating": null, "latitude": 1.23456789, "starts-at": "2016-12-13T23:59:59.123456+00:00", "searchable-location-name": "Draft", - "is-ticketing-enabled": true, "can-pay-by-cheque": true, "description": "example", "pentabarf-url": null, @@ -3457,11 +3425,9 @@ Get a list of events. "is-event-online": false, "has-owner-info": false, "state": "draft", - "average-rating": null, "latitude": 1.23456789, "starts-at": "2016-12-13T23:59:59.123456+00:00", "searchable-location-name": "Draft", - "is-ticketing-enabled": true, "can-pay-by-cheque": true, "description": "example", "pentabarf-url": null, @@ -3683,12 +3649,10 @@ Get a list of events. "privacy": "public", "has-owner-info": false, "state": "draft", - "average-rating": null, "is-event-online": false, "latitude": 1.23456789, "starts-at": "2016-12-13T23:59:59.123456+00:00", "searchable-location-name": "Draft", - "is-ticketing-enabled": true, "can-pay-by-cheque": true, "description": "example", "pentabarf-url": null, @@ -3910,11 +3874,9 @@ Get a list of events. "privacy": "public", "has-owner-info": false, "state": "draft", - "average-rating": null, "latitude": 1.23456789, "starts-at": "2016-12-13T23:59:59.123456+00:00", "searchable-location-name": "Draft", - "is-ticketing-enabled": true, "can-pay-by-cheque": true, "description": "example", "pentabarf-url": null, @@ -4138,11 +4100,9 @@ Get a list of events. "privacy": "public", "has-owner-info": false, "state": "draft", - "average-rating": null, "latitude": 1.23456789, "starts-at": "2016-12-13T23:59:59.123456+00:00", "searchable-location-name": "Draft", - "is-ticketing-enabled": true, "can-pay-by-cheque": true, "description": "example", "pentabarf-url": null, @@ -4365,11 +4325,9 @@ Get a list of events. "privacy": "public", "has-owner-info": false, "state": "draft", - "average-rating": null, "latitude": 1.23456789, "starts-at": "2016-12-13T23:59:59.123456+00:00", "searchable-location-name": "Draft", - "is-ticketing-enabled": true, "can-pay-by-cheque": true, "description": "example", "pentabarf-url": null, @@ -4592,11 +4550,9 @@ Get a list of events. "privacy": "public", "has-owner-info": false, "state": "draft", - "average-rating": null, "latitude": 1.23456789, "starts-at": "2016-12-13T23:59:59.123456+00:00", "searchable-location-name": "Draft", - "is-ticketing-enabled": true, "can-pay-by-cheque": true, "description": "example", "pentabarf-url": null, @@ -4818,12 +4774,10 @@ Get a list of events. "privacy": "public", "has-owner-info": false, "state": "draft", - "average-rating": null, "latitude": 1.23456789, "starts-at": "2016-12-13T23:59:59.123456+00:00", "is-event-online": false, "searchable-location-name": "Draft", - "is-ticketing-enabled": true, "can-pay-by-cheque": true, "description": "example", "pentabarf-url": null, @@ -5046,11 +5000,9 @@ Get a list of events. "is-event-online": false, "has-owner-info": false, "state": "draft", - "average-rating": null, "latitude": 1.23456789, "starts-at": "2016-12-13T23:59:59.123456+00:00", "searchable-location-name": "Draft", - "is-ticketing-enabled": true, "can-pay-by-cheque": true, "description": "example", "pentabarf-url": null, @@ -5272,11 +5224,9 @@ Get a list of events. "privacy": "public", "has-owner-info": false, "state": "draft", - "average-rating": null, "latitude": 1.23456789, "starts-at": "2016-12-13T23:59:59.123456+00:00", "searchable-location-name": "Draft", - "is-ticketing-enabled": true, "can-pay-by-cheque": true, "description": "example", "pentabarf-url": null, @@ -5499,11 +5449,9 @@ Get a list of events. "privacy": "public", "has-owner-info": false, "state": "draft", - "average-rating": null, "latitude": 1.23456789, "starts-at": "2016-12-13T23:59:59.123456+00:00", "searchable-location-name": "Draft", - "is-ticketing-enabled": true, "can-pay-by-cheque": true, "description": "example", "pentabarf-url": null, @@ -5726,12 +5674,10 @@ Get a list of events. "privacy": "public", "has-owner-info": false, "state": "draft", - "average-rating": null, "is-event-online": false, "latitude": 1.23456789, "starts-at": "2016-12-13T23:59:59.123456+00:00", "searchable-location-name": "Draft", - "is-ticketing-enabled": true, "can-pay-by-cheque": true, "description": "example", "pentabarf-url": null, @@ -5953,11 +5899,9 @@ Get a list of events. "can-pay-by-omise": false, "has-owner-info": false, "state": "draft", - "average-rating": null, "latitude": 1.23456789, "starts-at": "2016-12-13T23:59:59.123456+00:00", "searchable-location-name": "Draft", - "is-ticketing-enabled": true, "can-pay-by-cheque": true, "description": "example", "pentabarf-url": null, @@ -6181,11 +6125,9 @@ Get a list of events. "has-owner-info": false, "state": "draft", "is-event-online": false, - "average-rating": null, "latitude": 1.23456789, "starts-at": "2016-12-13T23:59:59.123456+00:00", "searchable-location-name": "Draft", - "is-ticketing-enabled": true, "can-pay-by-cheque": true, "description": "example", "pentabarf-url": null, @@ -6407,12 +6349,10 @@ Get a list of events. "is-event-online": false, "has-owner-info": false, "state": "draft", - "average-rating": null, "can-pay-by-omise": false, "latitude": 1.23456789, "starts-at": "2016-12-13T23:59:59.123456+00:00", "searchable-location-name": "Draft", - "is-ticketing-enabled": true, "can-pay-by-cheque": true, "description": "example", "pentabarf-url": null, @@ -6633,11 +6573,9 @@ Get a list of events. "privacy": "public", "has-owner-info": false, "state": "draft", - "average-rating": null, "latitude": 1.23456789, "starts-at": "2016-12-13T23:59:59.123456+00:00", "searchable-location-name": "Draft", - "is-ticketing-enabled": true, "can-pay-by-cheque": true, "description": "example", "pentabarf-url": null, @@ -6894,12 +6832,10 @@ Get a list of events. "is-event-online": false, "has-owner-info": false, "state": "draft", - "average-rating": null, "can-pay-by-omise": false, "latitude": null, "starts-at": "2002-05-30T04:00:10+00:00", "searchable-location-name": null, - "is-ticketing-enabled": true, "can-pay-by-cheque": false, "description": "", "pentabarf-url": null, @@ -7154,11 +7090,9 @@ Get a list of events. "has-owner-info": false, "is-event-online": false, "state": "Draft", - "average-rating": null, "latitude": null, "starts-at": "2002-05-30T04:00:10+00:00", "searchable-location-name": null, - "is-ticketing-enabled": true, "can-pay-by-omise": false, "can-pay-by-cheque": false, "description": "", diff --git a/docs/api/blueprint/user/users.apib b/docs/api/blueprint/user/users.apib index 67734822f6..7321145838 100644 --- a/docs/api/blueprint/user/users.apib +++ b/docs/api/blueprint/user/users.apib @@ -204,7 +204,7 @@ Create a new user using an email, password and an optional name. { "email": "email@example.com", "password": "password", - "avatar_url": "http://example.com/example.png", + "avatar_url": "https://avatars2.githubusercontent.com/u/1583873", "first-name": "John", "last-name": "Doe", "details": "example", @@ -550,7 +550,7 @@ Authorized user should be same as user in request body or must be admin. "data": { "attributes": { "password": "password", - "avatar_url": "http://example.com/example.png", + "avatar_url": "https://avatars2.githubusercontent.com/u/1583873", "first-name": "John", "last-name": "Doe", "details": "example1", @@ -558,10 +558,7 @@ Authorized user should be same as user in request body or must be admin. "facebook-url": "http://facebook.com/facebook", "twitter-url": "http://twitter.com/twitter", "instagram-url": "http://instagram.com/instagram", - "google-plus-url": "http://plus.google.com/plus.google", - "thumbnail-image-url": "http://example.com/example.png", - "small-image-url": "http://example.com/example.png", - "icon-image-url": "http://example.com/example.png" + "google-plus-url": "http://plus.google.com/plus.google" }, "type": "user", "id": "2" diff --git a/manage.py b/manage.py index 9593e05e23..ba060695df 100644 --- a/manage.py +++ b/manage.py @@ -10,8 +10,13 @@ from populate_db import populate from flask_migrate import stamp from sqlalchemy.engine import reflection - +from sqlalchemy import or_ from tests.all.integration.auth_helper import create_super_admin +from app.api.helpers.tasks import resize_event_images_task +from app.api.helpers.tasks import resize_speaker_images_task +import logging + +logger = logging.getLogger(__name__) @manager.command @@ -37,6 +42,26 @@ def add_event_identifier(): save_to_db(event) +@manager.command +def fix_event_and_speaker_images(): + events = Event.query.filter(Event.original_image_url.isnot(None), + or_(Event.thumbnail_image_url == None, Event.large_image_url == None, + Event.icon_image_url == None)).all() + logger.info('Resizing images of %s events...', len(events)) + for event in events: + logger.info('Resizing Event %s', event.id) + resize_event_images_task.delay(event.id, event.original_image_url) + + speakers = Speaker.query.filter(Speaker.photo_url.isnot(None), + or_(Speaker.icon_image_url == None, + Speaker.small_image_url == None, Speaker.thumbnail_image_url == None)).all() + + logger.info('Resizing images of %s speakers...', len(speakers)) + for speaker in speakers: + logging.info('Resizing Speaker %s', speaker.id) + resize_speaker_images_task.delay(speaker.id, speaker.photo_url) + + @manager.command def fix_digit_identifier(): events = Event.query.filter(Event.identifier.op('~')('^[0-9\.]+$')).all() diff --git a/migrations/versions/rev-2019-12-04-00:37:24-0223c881d135_.py b/migrations/versions/rev-2019-12-04-00:37:24-0223c881d135_.py new file mode 100644 index 0000000000..64234c3611 --- /dev/null +++ b/migrations/versions/rev-2019-12-04-00:37:24-0223c881d135_.py @@ -0,0 +1,30 @@ +"""empty message + +Revision ID: 0223c881d135 +Revises: d1c2b8711223 +Create Date: 2019-12-04 00:37:24.171728 + +""" + +from alembic import op +import sqlalchemy as sa +import sqlalchemy_utils + + +# revision identifiers, used by Alembic. +revision = '0223c881d135' +down_revision = 'd1c2b8711223' + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_unique_constraint('name_event_deleted_at_uc', 'tickets', ['name', 'event_id', 'deleted_at']) + op.drop_constraint('name_event_uc', 'tickets', type_='unique') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_unique_constraint('name_event_uc', 'tickets', ['name', 'event_id']) + op.drop_constraint('name_event_deleted_at_uc', 'tickets', type_='unique') + # ### end Alembic commands ### diff --git a/migrations/versions/rev-2019-12-13-20:34:35-8621f70992ba_.py b/migrations/versions/rev-2019-12-13-20:34:35-8621f70992ba_.py new file mode 100644 index 0000000000..70f06d2aa4 --- /dev/null +++ b/migrations/versions/rev-2019-12-13-20:34:35-8621f70992ba_.py @@ -0,0 +1,30 @@ +"""empty message + +Revision ID: 8621f70992ba +Revises: 0223c881d135 +Create Date: 2019-12-13 20:34:35.014066 + +""" + +from alembic import op +import sqlalchemy as sa +import sqlalchemy_utils + + +# revision identifiers, used by Alembic. +revision = '8621f70992ba' +down_revision = '0223c881d135' + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('ticket_holders', sa.Column('created_at', sa.DateTime(timezone=True), nullable=True)) + op.add_column('ticket_holders', sa.Column('modified_at', sa.DateTime(timezone=True), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('ticket_holders', 'modified_at') + op.drop_column('ticket_holders', 'created_at') + # ### end Alembic commands ### diff --git a/requirements/common.txt b/requirements/common.txt index a9d31258ab..9588a44d91 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -13,16 +13,16 @@ requests-oauthlib~=1.3 icalendar~=4.0.4 requests[security]~=2.22 psycopg2-binary~=2.8.4 -SQLAlchemy-Utils~=0.35.0 +SQLAlchemy-Utils~=0.36.0 itsdangerous~=1.1 humanize~=0.5.1 -celery~=4.3 +celery~=4.4 redis~=3.3 apscheduler~=3.6.3 pillow~=6.2.1 gunicorn~=20.0 boto~=2.49 -geoip2~=2.9.0 +geoip2~=3.0.0 SQLAlchemy-Continuum~=1.3.9 arrow~=0.15.4 unicode-slugify~=0.1 @@ -46,7 +46,7 @@ pytz diff-match-patch blinker~=1.4 envparse~=0.2 --e git+https://github.com/fossasia/flask-rest-jsonapi@0.12.6.1#egg=flask-rest-jsonapi +git+https://github.com/fossasia/flask-rest-jsonapi@0.12.6.1 wtforms~=2.2 flask-admin~=1.5 google-compute-engine~=2.8 @@ -55,10 +55,10 @@ sentry-sdk[flask]~=0.13 healthcheck~=1.3 elasticsearch-dsl~=7.0.0 flask-redis~=0.4 -SQLAlchemy~=1.3.11 +SQLAlchemy~=1.3.12 Flask-Elasticsearch~=0.2 paypalrestsdk~=1.13 eventlet~=0.25 -pyyaml~=5.1 +pyyaml~=5.2 sendgrid~=6.1 marshmallow~=2.15.2 diff --git a/requirements/tests.txt b/requirements/tests.txt index cd9b643931..2b345c826e 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -1,4 +1,4 @@ -r dev.txt -coverage~=4.5 +coverage~=5.0 dredd_hooks~=0.2 diff --git a/runtime.txt b/runtime.txt index 42731f2fbe..aefcfbece7 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-3.7.4 +python-3.7.5 diff --git a/tests/all/integration/api/helpers/test_scheduled_jobs.py b/tests/all/integration/api/helpers/test_scheduled_jobs.py index f638f87c7d..4cfb779706 100644 --- a/tests/all/integration/api/helpers/test_scheduled_jobs.py +++ b/tests/all/integration/api/helpers/test_scheduled_jobs.py @@ -2,8 +2,10 @@ from app import current_app as app, db from app.factories.event_invoice import EventInvoiceFactory +from app.factories.attendee import AttendeeFactory from app.models.event_invoice import EventInvoice -from app.api.helpers.scheduled_jobs import event_invoices_mark_due +from app.models.ticket_holder import TicketHolder +from app.api.helpers.scheduled_jobs import event_invoices_mark_due, delete_ticket_holders_no_order_id from tests.all.integration.utils import OpenEventTestCase @@ -29,3 +31,43 @@ def test_event_invoices_mark_due(self): self.assertEqual(status_new, "due") self.assertNotEqual(status_paid, "due") + + def test_delete_ticket_holders_with_no_order_id(self): + """Method to test deleting ticket holders with no order id after expiry time""" + + with app.test_request_context(): + attendee = AttendeeFactory() + db.session.commit() + attendee_id = attendee.id + delete_ticket_holders_no_order_id() + ticket_holder = TicketHolder.query.get(attendee_id) + self.assertIs(ticket_holder, None) + + def test_delete_ticket_holder_created_currently(self): + """Method to test not deleting ticket holders with no order id but created within expiry time""" + + with app.test_request_context(): + attendee = AttendeeFactory(created_at=datetime.datetime.utcnow(), + modified_at=datetime.datetime.utcnow()) + + db.session.commit() + attendee_id = attendee.id + delete_ticket_holders_no_order_id() + ticket_holder = TicketHolder.query.get(attendee_id) + self.assertIsNot(ticket_holder, None) + + def test_delete_ticket_holder_with_valid_order_id(self): + """Method to test not deleting ticket holders with order id after expiry time""" + + with app.test_request_context(): + attendee = AttendeeFactory(order_id=1, ticket_id=1, + created_at=datetime.datetime.utcnow() - + datetime.timedelta(minutes=15), + modified_at=datetime.datetime.utcnow() - + datetime.timedelta(minutes=15)) + + db.session.commit() + attendee_id = attendee.id + delete_ticket_holders_no_order_id() + ticket_holder = TicketHolder.query.get(attendee_id) + self.assertIsNot(ticket_holder, None) diff --git a/tests/hook_main.py b/tests/hook_main.py index 672498956d..d678c8495e 100644 --- a/tests/hook_main.py +++ b/tests/hook_main.py @@ -336,6 +336,8 @@ def event_post(transaction): :return: """ with stash['app'].app_context(): + module = ModuleFactory() + db.session.add(module) RoleFactory(name=OWNER) # TODO: Change to get_or_create in event after_created db.session.commit() @@ -361,6 +363,8 @@ def event_patch(transaction): :return: """ with stash['app'].app_context(): + module = ModuleFactory() + db.session.add(module) event = EventFactoryBasic() db.session.add(event) db.session.commit()