fossasia/open-event-orga-server

View on GitHub
app/api/users.py

Summary

Maintainability
D
3 days
Test Coverage
import logging
from datetime import datetime

from flask import Blueprint, abort, jsonify, make_response, request
from flask_jwt_extended import current_user, verify_fresh_jwt_in_request
from flask_rest_jsonapi import ResourceDetail, ResourceList, ResourceRelationship
from sqlalchemy import or_
from sqlalchemy.orm.exc import NoResultFound

from app.api.bootstrap import api
from app.api.helpers.db import get_count, safe_query_by, safe_query_kwargs
from app.api.helpers.errors import ConflictError, ForbiddenError, UnprocessableEntityError
from app.api.helpers.mail import send_email_change_user_email, send_user_register_email
from app.api.helpers.permission_manager import has_access
from app.api.helpers.permissions import is_user_itself
from app.api.helpers.user import (
    modify_email_for_user_to_be_deleted,
    modify_email_for_user_to_be_restored,
)
from app.api.schema.users import UserSchema, UserSchemaPublic
from app.models import db
from app.models.access_code import AccessCode
from app.models.discount_code import DiscountCode
from app.models.email_notification import EmailNotification
from app.models.event import Event
from app.models.event_invoice import EventInvoice
from app.models.feedback import Feedback
from app.models.group import Group
from app.models.notification import Notification
from app.models.order import Order
from app.models.session import Session
from app.models.speaker import Speaker
from app.models.ticket_holder import TicketHolder
from app.models.user import User
from app.models.user_follow_group import UserFollowGroup
from app.models.users_events_role import UsersEventsRoles
from app.models.video_stream_moderator import VideoStreamModerator

logger = logging.getLogger(__name__)

user_misc_routes = Blueprint('user_misc', __name__, url_prefix='/v1')


class UserList(ResourceList):
    """
    List and create Users
    """

    def before_create_object(self, data, view_kwargs):
        """
        method to check if there is an existing user with same email which is received in data to create a new user
        and if the password is at least 8 characters long
        :param data:
        :param view_kwargs:
        :return:
        """
        if len(data['password']) < 8:
            logging.error('Password should be at least 8 characters long')
            raise UnprocessableEntityError(
                {'source': '/data/attributes/password'},
                'Password should be at least 8 characters long',
            )
        if (
            db.session.query(User.id).filter_by(email=data['email'].strip()).scalar()
            is not None
        ):
            logging.error('Email already exists')
            raise ConflictError(
                {'pointer': '/data/attributes/email'}, "Email already exists"
            )

        if data.get('is_verified'):
            logging.error("You are not allowed to submit this field")
            raise UnprocessableEntityError(
                {'pointer': '/data/attributes/is-verified'},
                "You are not allowed to submit this field",
            )

    def after_create_object(self, user, data, view_kwargs):
        """
        method to send-
        email notification
        mail link for register verification
        add image urls
        :param user:
        :param data:
        :param view_kwargs:
        :return:
        """

        send_user_register_email(user)
        # TODO Handle in a celery task
        # if data.get('original_image_url'):
        #     try:
        #         uploaded_images = create_save_image_sizes(data['original_image_url'], 'speaker-image', user.id)
        #     except (urllib.error.HTTPError, urllib.error.URLError):
        #         raise UnprocessableEntityError(
        #             {'source': 'attributes/original-image-url'}, 'Invalid Image URL'
        #         )
        #     uploaded_images['small_image_url'] = uploaded_images['thumbnail_image_url']
        #     del uploaded_images['large_image_url']
        #     self.session.query(User).filter_by(id=user.id).update(uploaded_images)

        if data.get('avatar_url'):
            start_image_resizing_tasks(user, data['avatar_url'])

    decorators = (api.has_permission('is_admin', methods="GET"),)
    schema = UserSchema
    data_layer = {
        'session': db.session,
        'model': User,
        'methods': {
            'before_create_object': before_create_object,
            'after_create_object': after_create_object,
        },
    }


class UserDetail(ResourceDetail):
    """
    User detail by id
    """

    def before_get(self, args, kwargs):
        if current_user.is_admin or current_user.is_super_admin or current_user:
            self.schema = UserSchema
        else:
            self.schema = UserSchemaPublic

    def before_get_object(self, view_kwargs):
        """
        before get method for user object
        :param view_kwargs:
        :return:
        """
        if view_kwargs.get('notification_id') is not None:
            notification = safe_query_kwargs(
                Notification,
                view_kwargs,
                'notification_id',
            )
            if notification.user_id is not None:
                view_kwargs['id'] = notification.user_id
            else:
                view_kwargs['id'] = None

        if view_kwargs.get('feedback_id') is not None:
            feedback = safe_query_kwargs(Feedback, view_kwargs, 'feedback_id')
            if feedback.user_id is not None:
                view_kwargs['id'] = feedback.user_id
            else:
                view_kwargs['id'] = None

        if view_kwargs.get('attendee_id') is not None:
            attendee = safe_query_kwargs(TicketHolder, view_kwargs, 'attendee_id')
            if attendee.user is not None:
                if not has_access(
                    'is_user_itself', user_id=attendee.user.id
                ) or not has_access('is_coorganizer', event_id=attendee.event_id):
                    raise ForbiddenError({'source': ''}, 'Access Forbidden')
                view_kwargs['id'] = attendee.user.id
            else:
                view_kwargs['id'] = None

        if view_kwargs.get('event_invoice_id') is not None:
            event_invoice = safe_query_kwargs(
                EventInvoice,
                view_kwargs,
                'event_invoice_id',
            )
            if event_invoice.user_id is not None:
                view_kwargs['id'] = event_invoice.user_id
            else:
                view_kwargs['id'] = None

        if view_kwargs.get('event_invoice_identifier') is not None:
            event_invoice = safe_query_kwargs(
                EventInvoice, view_kwargs, 'event_invoice_identifier', 'identifier'
            )
            if event_invoice.user_id is not None:
                view_kwargs['id'] = event_invoice.user_id
            else:
                view_kwargs['id'] = None

        if view_kwargs.get('users_events_role_id') is not None:
            users_events_role = safe_query_kwargs(
                UsersEventsRoles,
                view_kwargs,
                'users_events_role_id',
            )
            if users_events_role.user_id is not None:
                view_kwargs['id'] = users_events_role.user_id
            else:
                view_kwargs['id'] = None

        if view_kwargs.get('speaker_id') is not None:
            speaker = safe_query_kwargs(Speaker, view_kwargs, 'speaker_id')
            if speaker.user_id is not None:
                view_kwargs['id'] = speaker.user_id
            else:
                view_kwargs['id'] = None

        if view_kwargs.get('session_id') is not None:
            session = safe_query_kwargs(Session, view_kwargs, 'session_id')
            if session.creator_id is not None:
                view_kwargs['id'] = session.creator_id
            else:
                view_kwargs['id'] = None

        if view_kwargs.get('access_code_id') is not None:
            access_code = safe_query_kwargs(AccessCode, view_kwargs, 'access_code_id')
            if access_code.marketer_id is not None:
                view_kwargs['id'] = access_code.marketer_id
            else:
                view_kwargs['id'] = None

        if view_kwargs.get('event_id') is not None:
            event = safe_query_kwargs(Event, view_kwargs, 'event_id')
            if event.owner is not None:
                view_kwargs['id'] = event.owner.id
            else:
                view_kwargs['id'] = None

        if view_kwargs.get('group_id') is not None:
            group = safe_query_kwargs(Group, view_kwargs, 'group_id')
            if group.user_id is not None:
                view_kwargs['id'] = group.user_id
            else:
                view_kwargs['id'] = None

        if view_kwargs.get('discount_code_id') is not None:
            discount_code = safe_query_kwargs(
                DiscountCode,
                view_kwargs,
                'discount_code_id',
            )
            if discount_code.marketer_id is not None:
                view_kwargs['id'] = discount_code.marketer_id
            else:
                view_kwargs['id'] = None

        if view_kwargs.get('email_notification_id') is not None:
            email_notification = safe_query_kwargs(
                EmailNotification,
                view_kwargs,
                'email_notification_id',
            )
            if email_notification.user_id is not None:
                view_kwargs['id'] = email_notification.user_id
            else:
                view_kwargs['id'] = None

        if view_kwargs.get('video_stream_moderator_id') is not None:
            moderator = safe_query_kwargs(
                VideoStreamModerator,
                view_kwargs,
                'video_stream_moderator_id',
            )
            user = safe_query_by(User, moderator.email, param='email')
            if user is not None:
                view_kwargs['id'] = user.id
            else:
                view_kwargs['id'] = None

        if view_kwargs.get('user_follow_group_id') is not None:
            user_follow_group = safe_query_kwargs(
                UserFollowGroup, view_kwargs, 'user_follow_group_id'
            )
            if user_follow_group.id is not None:
                view_kwargs['id'] = user_follow_group.user_id
            else:
                view_kwargs['id'] = None

    def before_update_object(self, user, data, view_kwargs):
        # TODO: Make a celery task for this
        # if data.get('avatar_url') and data['original_image_url'] != user.original_image_url:
        #     try:
        #         uploaded_images = create_save_image_sizes(data['original_image_url'], 'speaker-image', user.id)
        #     except (urllib.error.HTTPError, urllib.error.URLError):
        #         raise UnprocessableEntityError(
        #             {'source': 'attributes/original-image-url'}, 'Invalid Image URL'
        #         )
        #     data['original_image_url'] = uploaded_images['original_image_url']
        #     data['small_image_url'] = uploaded_images['thumbnail_image_url']
        #     data['thumbnail_image_url'] = uploaded_images['thumbnail_image_url']
        #     data['icon_image_url'] = uploaded_images['icon_image_url']

        if data.get('deleted_at') != user.deleted_at:
            if has_access('is_user_itself', user_id=user.id) or has_access('is_admin'):
                if data.get('deleted_at'):
                    event_exists = db.session.query(
                        Event.query.filter_by(deleted_at=None)
                        .join(Event.users)
                        .filter(User.id == user.id)
                        .exists()
                    ).scalar()
                    if event_exists:
                        logging.error("Users associated with events cannot be deleted")
                        raise ForbiddenError(
                            {'source': ''},
                            "Users associated with events cannot be deleted",
                        )
                    # TODO(Areeb): Deduplicate the query. Present in video stream model as well
                    order_exists = db.session.query(
                        TicketHolder.query.filter_by(user=user)
                        .join(Order)
                        .join(Order.event)
                        .filter(Event.ends_at > datetime.now())
                        .filter(
                            or_(
                                Order.status == 'completed',
                                Order.status == 'placed',
                                Order.status == 'initializing',
                                Order.status == 'pending',
                            )
                        )
                        .exists()
                    ).scalar()
                    # If any pending or completed order exists, we cannot delete the user
                    if order_exists:
                        logger.warning(
                            'User %s has pending or completed orders, hence cannot be deleted',
                            user,
                        )
                        raise ForbiddenError(
                            {'source': ''},
                            "Users associated with orders cannot be deleted",
                        )
                    modify_email_for_user_to_be_deleted(user)
                else:
                    modify_email_for_user_to_be_restored(user)
                    data['email'] = user.email
                user.deleted_at = data.get('deleted_at')
            else:
                logging.info("You are not authorized to update this information.")
                raise ForbiddenError(
                    {'source': ''}, "You are not authorized to update this information."
                )

        if (
            not has_access('is_admin')
            and data.get('is_verified') is not None
            and data.get('is_verified') != user.is_verified
        ):
            logging.info("Admin access is required to update this information.")
            raise ForbiddenError(
                {'pointer': '/data/attributes/is-verified'},
                "Admin access is required to update this information.",
            )

        users_email = data.get('email', None)
        if users_email is not None:
            users_email = users_email.strip()

        if users_email is not None and users_email != user.email:
            try:
                db.session.query(User).filter_by(email=users_email).one()
            except NoResultFound:
                verify_fresh_jwt_in_request()
                view_kwargs['email_changed'] = user.email
            else:
                logging.error("Email already exists")
                raise ConflictError(
                    {'pointer': '/data/attributes/email'}, "Email already exists"
                )

        if (
            has_access('is_super_admin')
            and data.get('is_admin')
            and data.get('is_admin') != user.is_admin
        ):
            user.is_admin = not user.is_admin

        if (
            has_access('is_admin')
            and ('is_sales_admin' in data)
            and data.get('is_sales_admin') != user.is_sales_admin
        ):
            user.is_sales_admin = not user.is_sales_admin

        if (
            has_access('is_admin')
            and ('is_marketer' in data)
            and data.get('is_marketer') != user.is_marketer
        ):
            user.is_marketer = not user.is_marketer

        if data.get('avatar_url'):
            start_image_resizing_tasks(user, data['avatar_url'])

    def after_update_object(self, user, data, view_kwargs):
        """
        method to mail user about email change
        :param user:
        :param data:
        :param view_kwargs:
        :return:
        """
        if view_kwargs.get('email_changed'):
            send_email_change_user_email(user, view_kwargs.get('email_changed'))

    decorators = (
        api.has_permission(
            'is_user_itself',
            fetch="user_id,id",
            fetch_as="user_id",
            model=[
                Notification,
                Feedback,
                UsersEventsRoles,
                Session,
                EventInvoice,
                AccessCode,
                DiscountCode,
                EmailNotification,
                Speaker,
                User,
                UserFollowGroup,
                Group,
            ],
            fetch_key_url="notification_id, feedback_id, users_events_role_id, session_id, \
                  event_invoice_id, access_code_id, discount_code_id, email_notification_id, speaker_id, id, user_follow_group_id, group_id",
            leave_if=lambda a: a.get('attendee_id'),
        ),
    )
    schema = UserSchema
    data_layer = {
        'session': db.session,
        'model': User,
        'methods': {
            'before_get_object': before_get_object,
            'before_update_object': before_update_object,
            'after_update_object': after_update_object,
        },
    }


class UserRelationship(ResourceRelationship):
    """
    User Relationship
    """

    decorators = (is_user_itself,)
    schema = UserSchema
    data_layer = {'session': db.session, 'model': User}


@user_misc_routes.route('/users/check_email', methods=['POST'])
@user_misc_routes.route('/users/checkEmail', methods=['POST'])  # deprecated
def is_email_available():
    email = request.json.get('email', None)
    if email:
        if get_count(db.session.query(User).filter_by(deleted_at=None, email=email)):
            return jsonify(exists=True)
        return jsonify(exists=False)
    abort(make_response(jsonify(error="Email field missing"), 422))


def start_image_resizing_tasks(user, original_image_url):
    user_id = str(user.id)
    from .helpers.tasks import resize_user_images_task

    resize_user_images_task.delay(user_id, original_image_url)