Exemple #1
0
def onboarding():
    current_user = view_helpers.get_current_user()

    current_user.last_show_onboarding = datetime.now()
    current_user.save()

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_IMPRESSION,
        rmclogger.LOG_EVENT_ONBOARDING,
        current_user.id,
    )

    friends = m.User.objects(id__in=current_user.friend_ids).only(
        *(m.User.CORE_FIELDS + ['course_history']))

    user_objs = []
    for user in [current_user] + list(friends):
        user_objs.append(user.to_dict())

    return flask.render_template(
        'onboarding_page.html',
        page_script='onboarding_page.js',
        current_user_id=current_user.id,
        user_objs=user_objs,
    )
Exemple #2
0
def index():
    # Redirect logged-in users to profile
    # TODO(Sandy): If we request extra permissions from FB, we'll need to show
    # them the landing page to let them to Connect again and accept the new
    # permissions. Alternatively, we could use other means of requesting for
    # new perms
    request = flask.request
    logout = bool(request.values.get('logout'))
    referrer_id = request.values.get('meow') or request.values.get('referrer')

    if logout:
        view_helpers.logout_current_user()

    if not logout and not referrer_id and view_helpers.get_current_user():
        return flask.make_response(flask.redirect('profile'))

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_IMPRESSION,
        rmclogger.LOG_EVENT_LANDING,
    )

    return flask.render_template(
        'index_page.html',
        page_script='index_page.js',
    )
Exemple #3
0
def onboarding():
    current_user = view_helpers.get_current_user()

    current_user.last_show_onboarding = datetime.now()
    current_user.save()

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_IMPRESSION,
        rmclogger.LOG_EVENT_ONBOARDING,
        current_user.id,
    )

    friends = m.User.objects(
        id__in=current_user.friend_ids
    ).only(*(m.User.CORE_FIELDS + ['course_history']))

    user_objs = []
    for user in [current_user] + list(friends):
        user_objs.append(user.to_dict())

    return flask.render_template('onboarding_page.html',
        page_script='onboarding_page.js',
        current_user_id=current_user.id,
        user_objs=user_objs,
    )
Exemple #4
0
    def add_course(self, course_id, term_id, program_year_id=None):
        """Creates a UserCourse and adds it to the user's course_history.

        Idempotent.

        Returns the resulting UserCourse.
        """
        user_course = _user_course.UserCourse.objects(user_id=self.id, course_id=course_id).first()

        if user_course is None:
            if _course.Course.objects.with_id(course_id) is None:
                # Non-existant course according to our data
                rmclogger.log_event(rmclogger.LOG_CATEGORY_DATA_MODEL, rmclogger.LOG_EVENT_UNKNOWN_COURSE_ID, course_id)
                return None

            user_course = _user_course.UserCourse(
                user_id=self.id, course_id=course_id, term_id=term_id, program_year_id=program_year_id
            )
        else:
            # Record only the latest attempt for duplicate/failed courses
            if term_id > user_course.term_id or user_course.term_id == _term.Term.SHORTLIST_TERM_ID:
                user_course.term_id = term_id
                user_course.program_year_id = program_year_id

        user_course.save()

        if user_course.id not in self.course_history:
            self.course_history.append(user_course.id)
            self.save()

        return user_course
Exemple #5
0
def remove_course():
    current_user = view_helpers.get_current_user()

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_API,
        rmclogger.LOG_EVENT_REMOVE_COURSE, {
            'request_form': flask.request.form,
            'user_id': current_user.id,
        },
    )

    user_course = m.UserCourse.objects(
        user_id=current_user.id,
        course_id=flask.request.form.get('course_id'),
        term_id=flask.request.form.get('term_id'),
    ).first()

    if not user_course:
        logging.warn("No UserCourse found matching request values %s" %
                flask.request.values)
        # TODO(david): Use api.py:not_found in my onboarding-v2 branch
        return ''

    current_user.update(pull__course_history=user_course.id)
    user_course.delete()

    return ''
Exemple #6
0
def remove_course():
    current_user = view_helpers.get_current_user()

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_API,
        rmclogger.LOG_EVENT_REMOVE_COURSE,
        {
            'request_form': flask.request.form,
            'user_id': current_user.id,
        },
    )

    user_course = m.UserCourse.objects(
        user_id=current_user.id,
        course_id=flask.request.form.get('course_id'),
        term_id=flask.request.form.get('term_id'),
    ).first()

    if not user_course:
        logging.warn("No UserCourse found matching request values %s" %
                     flask.request.values)
        # TODO(david): Use api.py:not_found in my onboarding-v2 branch
        return ''

    current_user.update(pull__course_history=user_course.id)
    user_course.delete()

    return ''
Exemple #7
0
def signup_email():
    """Create a new account using data encoded in the POST body.

    Expects the following form data:
        first_name: E.g. 'Taylor'
        last_name: E.g. 'Swift'
        email: E.g. '*****@*****.**'
        password: E.g. 'iknewyouweretrouble'

    Responds with the session cookie via the `set-cookie` header on success.
    Send the associated cookie for all subsequent API requests that accept
    user authentication.
    """
    # Prevent a CSRF attack from replacing a logged-in user's account with
    # a new account with known credentials
    current_user = view_helpers.get_current_user()
    if current_user:
        return api_util.jsonify({'message': 'A user is already logged in.'})

    params = flask.request.form.copy()

    # Don't log the password
    password = params.pop('password', None)

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_API,
        rmclogger.LOG_EVENT_SIGNUP,
        {
            'params': params,
            'type': rmclogger.LOGIN_TYPE_STRING_EMAIL,
        },
    )

    first_name = params.get('first_name')
    last_name = params.get('last_name')
    email = params.get('email')

    if not first_name:
        raise api_util.ApiBadRequestError('Must provide first name.')

    if not last_name:
        raise api_util.ApiBadRequestError('Must provide last name.')

    if not email:
        raise api_util.ApiBadRequestError('Must provide email.')

    if not password:
        raise api_util.ApiBadRequestError('Must provide password.')

    try:
        user = m.User.create_new_user_from_email(first_name, last_name, email,
                                                 password)
    except m.User.UserCreationError as e:
        raise api_util.ApiBadRequestError(e.message)

    view_helpers.login_as_user(user)

    return api_util.jsonify(
        {'message': 'Created and logged in user %s' % user.name})
Exemple #8
0
def signup_email():
    """Create a new account using data encoded in the POST body.

    Expects the following form data:
        first_name: E.g. 'Taylor'
        last_name: E.g. 'Swift'
        email: E.g. '*****@*****.**'
        password: E.g. 'iknewyouweretrouble'

    Responds with the session cookie via the `set-cookie` header on success.
    Send the associated cookie for all subsequent API requests that accept
    user authentication.
    """
    # Prevent a CSRF attack from replacing a logged-in user's account with
    # a new account with known credentials
    current_user = view_helpers.get_current_user()
    if current_user:
        return api_util.jsonify({'message': 'A user is already logged in.'})

    params = flask.request.form.copy()

    # Don't log the password
    password = params.pop('password', None)

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_API,
        rmclogger.LOG_EVENT_SIGNUP, {
            'params': params,
            'type': rmclogger.LOGIN_TYPE_STRING_EMAIL,
        },
    )

    first_name = params.get('first_name')
    last_name = params.get('last_name')
    email = params.get('email')

    if not first_name:
        raise api_util.ApiBadRequestError('Must provide first name.')

    if not last_name:
        raise api_util.ApiBadRequestError('Must provide last name.')

    if not email:
        raise api_util.ApiBadRequestError('Must provide email.')

    if not password:
        raise api_util.ApiBadRequestError('Must provide password.')

    try:
        user = m.User.create_new_user_from_email(
                first_name, last_name, email, password)
    except m.User.UserCreationError as e:
        raise api_util.ApiBadRequestError(e.message)

    view_helpers.login_as_user(user)

    return api_util.jsonify({
        'message': 'Created and logged in user %s' % user.name
    })
Exemple #9
0
def course_page(course_id):
    course = m.Course.objects.with_id(course_id)
    if not course:
        # TODO(david): 404 page
        flask.abort(404)

    current_user = view_helpers.get_current_user()

    course_dict_list, user_course_dict_list, user_course_list = (
            m.Course.get_course_and_user_course_dicts(
                [course], current_user, include_all_users=True,
                include_friends=True, full_user_courses=True,
                include_sections=True))

    professor_dict_list = m.Professor.get_full_professors_for_course(
            course, current_user)

    user_dicts = {}
    if current_user:
        # TODO(Sandy): This is poorly named because its not only friends...
        friend_ids = ({uc_dict['user_id'] for uc_dict in user_course_dict_list}
                - set([current_user.id]))
        friends = m.User.objects(id__in=friend_ids).only(*m.User.CORE_FIELDS)

        for friend in friends:
            user_dicts[friend.id] = friend.to_dict()
        user_dicts[current_user.id] = current_user.to_dict(
                include_course_ids=True)

    tip_dict_list = course.get_reviews(current_user, user_course_list)

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_IMPRESSION,
        rmclogger.LOG_EVENT_SINGLE_COURSE, {
            'current_user': current_user.id if current_user else None,
            'course_id': course_id,
        },
    )

    exam_objs = m.Exam.objects(course_id=course_id)
    exam_dict_list = [e.to_dict() for e in exam_objs]

    exam_updated_date = None
    if exam_objs:
        exam_updated_date = exam_objs[0].id.generation_time

    return flask.render_template('course_page.html',
        page_script='course_page.js',
        course_obj=course_dict_list[0],
        professor_objs=professor_dict_list,
        tip_objs=tip_dict_list,
        user_course_objs=user_course_dict_list,
        user_objs=user_dicts.values(),
        exam_objs=exam_dict_list,
        exam_updated_date=exam_updated_date,
        current_user_id=current_user.id if current_user else None,
    )
Exemple #10
0
def course_page(course_id):
    course = m.Course.objects.with_id(course_id)
    if not course:
        # TODO(david): 404 page
        flask.abort(404)

    current_user = view_helpers.get_current_user()

    course_dict_list, user_course_dict_list, user_course_list = (
            m.Course.get_course_and_user_course_dicts(
                [course], current_user, include_all_users=True,
                include_friends=True, full_user_courses=True,
                include_sections=True))

    professor_dict_list = m.Professor.get_full_professors_for_course(
            course, current_user)

    user_dicts = {}
    if current_user:
        # TODO(Sandy): This is poorly named because its not only friends...
        friend_ids = ({uc_dict['user_id'] for uc_dict in user_course_dict_list}
                - set([current_user.id]))
        friends = m.User.objects(id__in=friend_ids).only(*m.User.CORE_FIELDS)

        for friend in friends:
            user_dicts[friend.id] = friend.to_dict()
        user_dicts[current_user.id] = current_user.to_dict(
                include_course_ids=True)

    tip_dict_list = course.get_reviews(current_user, user_course_list)

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_IMPRESSION,
        rmclogger.LOG_EVENT_SINGLE_COURSE, {
            'current_user': current_user.id if current_user else None,
            'course_id': course_id,
        },
    )

    exam_objs = m.Exam.objects(course_id=course_id)
    exam_dict_list = [e.to_dict() for e in exam_objs]

    exam_updated_date = None
    if exam_objs:
        exam_updated_date = exam_objs[0].id.generation_time

    return flask.render_template('course_page.html',
        page_script='course_page.js',
        course_obj=course_dict_list[0],
        professor_objs=professor_dict_list,
        tip_objs=tip_dict_list,
        user_course_objs=user_course_dict_list,
        user_objs=user_dicts.values(),
        exam_objs=exam_dict_list,
        exam_updated_date=exam_updated_date,
        current_user_id=current_user.id if current_user else None,
    )
Exemple #11
0
def login_facebook():
    """Attempt to login a user with FB credentials encoded in the POST body.

    Expects the following form data:
        fb_access_token: Facebook user access token. This is used to verify
            that the user did authenticate with Facebook and is authenticated
            to our app. The user's FB ID is also obtained from this token.

    Responds with the session cookie via the `set-cookie` header on success.
    Send the associated cookie for all subsequent API requests that accept
    user authentication.

    Also returns the CSRF token, which must be sent as the value of the
    "X-CSRF-Token" header for all non-GET requests.
    """
    # Prevent a CSRF attack from replacing a logged-in user's account with the
    # attacker's.
    current_user = view_helpers.get_current_user()
    if current_user:
        return api_util.jsonify({'message': 'A user is already logged in.'})

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_API,
        rmclogger.LOG_EVENT_SIGNUP,
        {
            'type': rmclogger.LOGIN_TYPE_STRING_FACEBOOK,
        },
    )

    req = flask.request
    fb_access_token = req.form.get('fb_access_token')

    # We perform a check to confirm the fb_access_token is indeed the person
    # identified by fbid, and that it was our app that generated the token.
    token_info = facebook.get_access_token_info(fb_access_token)

    if not token_info['is_valid'] or not token_info.get('user_id'):
        raise api_util.ApiForbiddenError(
            'The given FB credentials are invalid.')

    fbid = str(token_info['user_id'])
    user = m.User.objects(fbid=fbid).first()

    if not user:
        raise api_util.ApiForbiddenError('No user with fbid %s exists. '
                                         'Create an account at uwflow.com.' %
                                         fbid)

    view_helpers.login_as_user(user)
    # TODO(sandy): We don't need to do this anymore, just use the endpoint
    csrf_token = view_helpers.generate_csrf_token()

    return api_util.jsonify({
        'message': 'Logged in user %s' % user.name,
        'csrf_token': csrf_token,
    })
Exemple #12
0
def login_facebook():
    """Attempt to login a user with FB credentials encoded in the POST body.

    Expects the following form data:
        fb_access_token: Facebook user access token. This is used to verify
            that the user did authenticate with Facebook and is authenticated
            to our app. The user's FB ID is also obtained from this token.

    Responds with the session cookie via the `set-cookie` header on success.
    Send the associated cookie for all subsequent API requests that accept
    user authentication.

    Also returns the CSRF token, which must be sent as the value of the
    "X-CSRF-Token" header for all non-GET requests.
    """
    # Prevent a CSRF attack from replacing a logged-in user's account with the
    # attacker's.
    current_user = view_helpers.get_current_user()
    if current_user:
        return api_util.jsonify({'message': 'A user is already logged in.'})

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_API,
        rmclogger.LOG_EVENT_SIGNUP, {
            'type': rmclogger.LOGIN_TYPE_STRING_FACEBOOK,
        },
    )

    req = flask.request
    fb_access_token = req.form.get('fb_access_token')

    # We perform a check to confirm the fb_access_token is indeed the person
    # identified by fbid, and that it was our app that generated the token.
    token_info = facebook.get_access_token_info(fb_access_token)

    if not token_info['is_valid'] or not token_info.get('user_id'):
        raise api_util.ApiForbiddenError(
                'The given FB credentials are invalid.')

    fbid = str(token_info['user_id'])
    user = m.User.objects(fbid=fbid).first()

    if not user:
        raise api_util.ApiForbiddenError('No user with fbid %s exists. '
                                         'Create an account at uwflow.com.'
                                         % fbid)

    view_helpers.login_as_user(user)
    # TODO(sandy): We don't need to do this anymore, just use the endpoint
    csrf_token = view_helpers.generate_csrf_token()

    return api_util.jsonify({
        'message': 'Logged in user %s' % user.name,
        'csrf_token': csrf_token,
    })
Exemple #13
0
def privacy():
    # current_user CAN be None, but that's okay for logging
    current_user = view_helpers.get_current_user()
    user_id = current_user.id if current_user else None
    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_IMPRESSION,
        rmclogger.LOG_EVENT_PRIVACY_POLICY,
        user_id,
    )

    return flask.render_template('privacy_page.html')
Exemple #14
0
def about_page():
    # current_user CAN be None, but that's okay for logging
    current_user = view_helpers.get_current_user()
    user_id = current_user.id if current_user else None
    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_IMPRESSION,
        rmclogger.LOG_EVENT_ABOUT,
        user_id,
    )

    return flask.render_template('about_page.html')
Exemple #15
0
def about_page():
    # current_user CAN be None, but that's okay for logging
    current_user = view_helpers.get_current_user()
    user_id = current_user.id if current_user else None
    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_IMPRESSION,
        rmclogger.LOG_EVENT_ABOUT,
        user_id,
    )

    return flask.render_template('about_page.html')
Exemple #16
0
def privacy():
    # current_user CAN be None, but that's okay for logging
    current_user = view_helpers.get_current_user()
    user_id = current_user.id if current_user else None
    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_IMPRESSION,
        rmclogger.LOG_EVENT_PRIVACY_POLICY,
        user_id,
    )

    return flask.render_template('privacy_page.html')
Exemple #17
0
def renew_fb():
    '''Renew the current user's Facebook access token.

    The client should make this call periodically (once every couple months,
    see User.should_renew_fb_token) to keep the access token up to date.

    Takes a Facebook signed request object from the post params in the form of:
    {
        'fb_signed_request': obj
    }
    '''
    req = flask.request
    current_user = view_helpers.get_current_user()

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_API,
        rmclogger.LOG_EVENT_RENEW_FB, {
            'user_id': current_user.id,
            'request_form': req.form,
        }
    )

    fbsr = req.form.get('fb_signed_request')
    if fbsr is None:
        logging.warn('No fbsr set')
        raise exceptions.ImATeapot('No fbsr set')

    fb_data = facebook.get_fb_data(fbsr, app.config)
    access_token = fb_data['access_token']
    expires_on = fb_data['expires_on']
    is_invalid = fb_data['is_invalid']

    if not is_invalid:
        current_user.fb_access_token_expiry_date = expires_on
        current_user.fb_access_token = access_token
        current_user.fb_access_token_invalid = is_invalid

        # Update the user's fb friend list, since it's likely outdated by now
        try:
            current_user.update_fb_friends(
                    facebook.get_friend_list(access_token))
        except:
            # Not sure why this would happen. Usually it's due to invalid
            # access_token, but we JUST got the token, so it should be valid
            logging.warn(
                    "/api/renew-fb: get_friend_list failed with token (%s)"
                    % access_token)

        current_user.save()

    return ''
Exemple #18
0
def upload_transcript():
    req = flask.request

    user = view_helpers.get_current_user()
    user_id = user.id

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_API,
        rmclogger.LOG_EVENT_TRANSCRIPT, {
            'user_id': user_id,
            'requset_form': req.form,
        },
    )

    def get_term_id(term_name):
        season, year = term_name.split()
        return m.Term.get_id_from_year_season(year, season)

    transcript_data = util.json_loads(req.form['transcriptData'])
    courses_by_term = transcript_data['coursesByTerm']

    # TODO(Sandy): Batch request fetch to mongo instead of fetch while looping
    for term in courses_by_term:
        term_id = get_term_id(term['name'])
        program_year_id = term['programYearId']

        for course_id in term['courseIds']:
            # TODO(Sandy): Fill in course weight and grade info here
            user.add_course(course_id.lower(), term_id, program_year_id)

    if courses_by_term:
        last_term = courses_by_term[0]
        term_id = get_term_id(last_term['name'])
        user.last_term_id = term_id
        user.last_program_year_id = last_term['programYearId']
    user.program_name = transcript_data['programName']

    student_id = transcript_data.get('studentId')
    if student_id:
        user.student_id = str(student_id)

    user.cache_mutual_course_ids(view_helpers.get_redis_instance())
    user.transcripts_imported += 1
    user.save()

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_TRANSCRIPT,
        rmclogger.LOG_EVENT_UPLOAD,
        user_id
    )
    return ''
Exemple #19
0
def renew_fb():
    '''Renew the current user's Facebook access token.

    The client should make this call periodically (once every couple months,
    see User.should_renew_fb_token) to keep the access token up to date.

    Takes a Facebook signed request object from the post params in the form of:
    {
        'fb_signed_request': obj
    }
    '''
    req = flask.request
    current_user = view_helpers.get_current_user()

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_API,
        rmclogger.LOG_EVENT_RENEW_FB, {
            'user_id': current_user.id,
            'request_form': req.form,
        }
    )

    fbsr = req.form.get('fb_signed_request')
    if fbsr is None:
        logging.warn('No fbsr set')
        raise exceptions.ImATeapot('No fbsr set')

    fb_data = facebook.get_fb_data(fbsr, app.config)
    access_token = fb_data['access_token']
    expires_on = fb_data['expires_on']
    is_invalid = fb_data['is_invalid']

    if not is_invalid:
        current_user.fb_access_token_expiry_date = expires_on
        current_user.fb_access_token = access_token
        current_user.fb_access_token_invalid = is_invalid

        # Update the user's fb friend list, since it's likely outdated by now
        try:
            current_user.update_fb_friends(
                    facebook.get_friend_list(access_token))
        except:
            # Not sure why this would happen. Usually it's due to invalid
            # access_token, but we JUST got the token, so it should be valid
            logging.warn(
                    "/api/renew-fb: get_friend_list failed with token (%s)"
                    % access_token)

        current_user.save()

    return ''
Exemple #20
0
def upload_transcript():
    req = flask.request

    user = view_helpers.get_current_user()
    user_id = user.id

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_API,
        rmclogger.LOG_EVENT_TRANSCRIPT, {
            'user_id': user_id,
            'requset_form': req.form,
        },
    )

    def get_term_id(term_name):
        season, year = term_name.split()
        return m.Term.get_id_from_year_season(year, season)

    transcript_data = util.json_loads(req.form['transcriptData'])
    courses_by_term = transcript_data['coursesByTerm']

    # TODO(Sandy): Batch request fetch to mongo instead of fetch while looping
    for term in courses_by_term:
        term_id = get_term_id(term['name'])
        program_year_id = term['programYearId']

        for course_id in term['courseIds']:
            # TODO(Sandy): Fill in course weight and grade info here
            user.add_course(course_id.lower(), term_id, program_year_id)

    if courses_by_term:
        last_term = courses_by_term[0]
        term_id = get_term_id(last_term['name'])
        user.last_term_id = term_id
        user.last_program_year_id = last_term['programYearId']
    user.program_name = transcript_data['programName']

    student_id = transcript_data.get('studentId')
    if student_id:
        user.student_id = str(student_id)

    user.cache_mutual_course_ids(view_helpers.get_redis_instance())
    user.transcripts_imported += 1
    user.save()

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_TRANSCRIPT,
        rmclogger.LOG_EVENT_UPLOAD,
        user_id
    )
    return ''
Exemple #21
0
def remove_transcript():
    current_user = view_helpers.get_current_user()
    current_user.course_history = []
    current_user.save()

    # Remove cached mutual courses
    current_user.remove_mutual_course_ids(view_helpers.get_redis_instance())

    # Remove term_id from user_courses
    # TODO(mack): Display message notifying users how many reviews they will
    # lose by removing their transcript.
    m.UserCourse.objects(user_id=current_user.id).delete()

    rmclogger.log_event(rmclogger.LOG_CATEGORY_TRANSCRIPT,
                        rmclogger.LOG_EVENT_REMOVE, current_user.id)
    return ''
Exemple #22
0
def login_email():
    """Attempt to log in a user with the credentials encoded in the POST body.

    Expects the following form data:
        email: E.g. '*****@*****.**'
        password: E.g. 'iknewyouweretrouble'

    Responds with the session cookie via the `set-cookie` header on success.
    Send the associated cookie for all subsequent API requests that accept
    user authentication.
    """
    # Prevent a CSRF attack from replacing a logged-in user's account with the
    # attacker's.
    current_user = view_helpers.get_current_user()
    if current_user:
        return api_util.jsonify({'message': 'A user is already logged in.'})

    params = flask.request.form.copy()

    # Don't log the password
    password = params.pop('password', None)

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_API,
        rmclogger.LOG_EVENT_LOGIN,
        {
            'params': params,
            'type': rmclogger.LOGIN_TYPE_STRING_EMAIL,
        },
    )

    email = params.get('email')

    if not email:
        raise api_util.ApiBadRequestError('Must provide email.')

    if not password:
        raise api_util.ApiBadRequestError('Must provide password.')

    user = m.User.auth_user(email, password)

    if not user:
        raise api_util.ApiNotFoundError('Incorrect email or password.')

    view_helpers.login_as_user(user)

    return api_util.jsonify({'message': 'Logged in user %s' % user.name})
Exemple #23
0
def transcript_log():
    user = view_helpers.get_current_user()

    file_name = '%d.txt' % int(time.time())
    file_path = os.path.join(get_transcript_dir(), file_name)
    with open(file_path, 'w') as f:
        f.write(flask.request.form['transcript'].encode('utf-8'))

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_TRANSCRIPT,
        rmclogger.LOG_EVENT_PARSE_FAILED, {
            'user_id': user.id,
            'file_path': file_path,
        },
    )

    return ''
Exemple #24
0
def login_email():
    """Attempt to log in a user with the credentials encoded in the POST body.

    Expects the following form data:
        email: E.g. '*****@*****.**'
        password: E.g. 'iknewyouweretrouble'

    Responds with the session cookie via the `set-cookie` header on success.
    Send the associated cookie for all subsequent API requests that accept
    user authentication.
    """
    # Prevent a CSRF attack from replacing a logged-in user's account with the
    # attacker's.
    current_user = view_helpers.get_current_user()
    if current_user:
        return api_util.jsonify({'message': 'A user is already logged in.'})

    params = flask.request.form.copy()

    # Don't log the password
    password = params.pop('password', None)

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_API,
        rmclogger.LOG_EVENT_LOGIN, {
            'params': params,
            'type': rmclogger.LOGIN_TYPE_STRING_EMAIL,
        },
    )

    email = params.get('email')

    if not email:
        raise api_util.ApiBadRequestError('Must provide email.')

    if not password:
        raise api_util.ApiBadRequestError('Must provide password.')

    user = m.User.auth_user(email, password)

    if not user:
        raise api_util.ApiNotFoundError('Incorrect email or password.')

    view_helpers.login_as_user(user)

    return api_util.jsonify({'message': 'Logged in user %s' % user.name})
Exemple #25
0
def transcript_log():
    user = view_helpers.get_current_user()

    file_name = '%d.txt' % int(time.time())
    file_path = os.path.join(get_transcript_dir(), file_name)
    with open(file_path, 'w') as f:
        f.write(flask.request.form['transcript'].encode('utf-8'))

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_TRANSCRIPT,
        rmclogger.LOG_EVENT_PARSE_FAILED, {
            'user_id': user.id,
            'file_path': file_path,
        },
    )

    return ''
Exemple #26
0
def unsubscribe_page():
    current_user = view_helpers.get_current_user()
    req = flask.request
    unsubscribe_user_id = req.args.get('pasta')

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_IMPRESSION,
        rmclogger.LOG_EVENT_UNSUBSCRIBE, {
            'current_user': current_user.id if current_user else None,
            'unsubscribe_user': unsubscribe_user_id,
            'request_args': req.args,
        },
    )

    return flask.render_template('unsubscribe_page.html',
        page_script='unsubscribe_page.js',
        unsubscribe_user=unsubscribe_user_id,
    )
Exemple #27
0
def unsubscribe_page():
    current_user = view_helpers.get_current_user()
    req = flask.request
    unsubscribe_user_id = req.args.get('pasta')

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_IMPRESSION,
        rmclogger.LOG_EVENT_UNSUBSCRIBE, {
            'current_user': current_user.id if current_user else None,
            'unsubscribe_user': unsubscribe_user_id,
            'request_args': req.args,
        },
    )

    return flask.render_template('unsubscribe_page.html',
        page_script='unsubscribe_page.js',
        unsubscribe_user=unsubscribe_user_id,
    )
Exemple #28
0
def unsubscribe_user():
    current_user = view_helpers.get_current_user()
    req = flask.request
    unsubscribe_user_id = req.form.get('pasta')

    if not unsubscribe_user_id:
        logging.warn('Missing user_id (%s)' % unsubscribe_user_id)
        return flask.redirect('/')

    try:
        unsubscribe_user_id = bson.ObjectId(unsubscribe_user_id)
    except:
        logging.warn('Invalid user_id (%s)' % unsubscribe_user_id)
        return flask.redirect('/')

    user = m.User.objects.with_id(unsubscribe_user_id)
    if user:
        user.email_unsubscribed = True
        user.save()

        # TODO(Sandy): Temporary until we enforce logged in unsub's or just
        # generate and send out a hash next time
        notes = "Legit unsub"
        if current_user:
            if current_user.id != unsubscribe_user_id:
                notes = "Suspicious: Non-matching logged in user_id/unsub_id"
        else:
            notes = "Suspicious: No logged in user_id"

        rmclogger.log_event(
            rmclogger.LOG_CATEGORY_API,
            rmclogger.LOG_EVENT_UNSUBSCRIBE_USER,
            {
                'current_user': current_user.id if current_user else None,
                'unsubscribe_user': unsubscribe_user_id,
                'request_form': req.form,
                'notes': notes,
            },
        )
    else:
        logging.warn('User object (%s) not found' % unsubscribe_user_id)
        return flask.redirect('/')

    return flask.redirect('/')
Exemple #29
0
def remove_transcript():
    current_user = view_helpers.get_current_user()
    current_user.course_history = []
    current_user.save()

    # Remove cached mutual courses
    current_user.remove_mutual_course_ids(view_helpers.get_redis_instance())

    # Remove term_id from user_courses
    # TODO(mack): Display message notifying users how many reviews they will
    # lose by removing their transcript.
    m.UserCourse.objects(user_id=current_user.id).delete()

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_TRANSCRIPT,
        rmclogger.LOG_EVENT_REMOVE,
        current_user.id
    )
    return ''
Exemple #30
0
def unsubscribe_user():
    current_user = view_helpers.get_current_user()
    req = flask.request
    unsubscribe_user_id = req.form.get('pasta')

    if not unsubscribe_user_id:
        logging.warn('Missing user_id (%s)' % unsubscribe_user_id)
        return flask.redirect('/')

    try:
        unsubscribe_user_id = bson.ObjectId(unsubscribe_user_id)
    except:
        logging.warn('Invalid user_id (%s)' % unsubscribe_user_id)
        return flask.redirect('/')

    user = m.User.objects.with_id(unsubscribe_user_id)
    if user:
        user.email_unsubscribed = True
        user.save()

        # TODO(Sandy): Temporary until we enforce logged in unsub's or just
        # generate and send out a hash next time
        notes = "Legit unsub"
        if current_user:
            if current_user.id != unsubscribe_user_id:
                notes = "Suspicious: Non-matching logged in user_id/unsub_id"
        else:
            notes = "Suspicious: No logged in user_id"

        rmclogger.log_event(
            rmclogger.LOG_CATEGORY_API,
            rmclogger.LOG_EVENT_UNSUBSCRIBE_USER, {
                'current_user': current_user.id if current_user else None,
                'unsubscribe_user': unsubscribe_user_id,
                'request_form': req.form,
                'notes': notes,
            },
        )
    else:
        logging.warn('User object (%s) not found' % unsubscribe_user_id)
        return flask.redirect('/')

    return flask.redirect('/')
Exemple #31
0
def render_schedule_page(profile_user):
    profile_dict = profile_user.to_dict()
    profile_dict.update({
        'last_program_year_id':
        profile_user.get_latest_program_year_id(),
    })

    # TODO(david): Show exam slots here as well (see render_profile_page)
    schedule_item_dicts = profile_user.get_schedule_item_dicts()

    course_ids = [si['course_id'] for si in schedule_item_dicts]
    courses = m.Course.objects(id__in=course_ids)
    course_dicts = [c.to_dict() for c in courses]

    current_user = view_helpers.get_current_user()
    current_user_id = None
    if current_user:
        current_user_id = current_user.id

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_IMPRESSION,
        rmclogger.LOG_EVENT_SCHEDULE_VIEW,
        {
            'current_user': current_user_id,
            'profile_user': profile_user.id,
        },
    )

    user_dicts = [profile_dict]
    if current_user and current_user_id != profile_user.id:
        user_dicts.append(current_user.to_dict())

    return flask.render_template(
        'schedule_page.html',
        page_script='schedule_page.js',
        profile_obj=profile_dict,
        user_objs=user_dicts,
        profile_user_id=profile_user.id,
        current_user_id=current_user_id,
        schedule_item_objs=schedule_item_dicts,
        course_objs=course_dicts,
        show_printable=flask.request.values.get('print'),
    )
Exemple #32
0
def schedule_log():
    user = view_helpers.get_current_user()

    file_name = '%d.txt' % int(time.time())
    file_path = os.path.join(get_schedule_dir(), file_name)
    with open(file_path, 'w') as f:
        f.write(flask.request.form['schedule'].encode('utf-8'))

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_SCHEDULE,
        rmclogger.LOG_EVENT_PARSE_FAILED, {
            'user_id': user.id,
            'file_path': file_path,
        },
    )

    user.last_bad_schedule_paste = flask.request.form.get('schedule')
    user.last_bad_schedule_paste_date = datetime.now()
    user.save()

    return ''
Exemple #33
0
def schedule_log():
    user = view_helpers.get_current_user()

    file_name = '%d.txt' % int(time.time())
    file_path = os.path.join(get_schedule_dir(), file_name)
    with open(file_path, 'w') as f:
        f.write(flask.request.form['schedule'].encode('utf-8'))

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_SCHEDULE,
        rmclogger.LOG_EVENT_PARSE_FAILED, {
            'user_id': user.id,
            'file_path': file_path,
        },
    )

    user.last_bad_schedule_paste = flask.request.form.get('schedule')
    user.last_bad_schedule_paste_date = datetime.now()
    user.save()

    return ''
Exemple #34
0
    def add_course(self, course_id, term_id, program_year_id=None):
        """Creates a UserCourse and adds it to the user's course_history.

        Idempotent.

        Returns the resulting UserCourse.
        """
        user_course = _user_course.UserCourse.objects(
            user_id=self.id, course_id=course_id).first()

        if user_course is None:
            if _course.Course.objects.with_id(course_id) is None:
                # Non-existant course according to our data
                rmclogger.log_event(
                    rmclogger.LOG_CATEGORY_DATA_MODEL,
                    rmclogger.LOG_EVENT_UNKNOWN_COURSE_ID,
                    course_id
                )
                return None

            user_course = _user_course.UserCourse(
                user_id=self.id,
                course_id=course_id,
                term_id=term_id,
                program_year_id=program_year_id,
            )
        else:
            # Record only the latest attempt for duplicate/failed courses
            if (term_id > user_course.term_id or
                user_course.term_id == _term.Term.SHORTLIST_TERM_ID):
                user_course.term_id = term_id
                user_course.program_year_id = program_year_id

        user_course.save()

        if user_course.id not in self.course_history:
            self.course_history.append(user_course.id)
            self.save()

        return user_course
Exemple #35
0
def render_schedule_page(profile_user):
    profile_dict = profile_user.to_dict()
    profile_dict.update({
        'last_program_year_id': profile_user.get_latest_program_year_id(),
    })

    # TODO(david): Show exam slots here as well (see render_profile_page)
    schedule_item_dicts = profile_user.get_schedule_item_dicts()

    course_ids = [si['course_id'] for si in schedule_item_dicts]
    courses = m.Course.objects(id__in=course_ids)
    course_dicts = [c.to_dict() for c in courses]

    current_user = view_helpers.get_current_user()
    current_user_id = None
    if current_user:
        current_user_id = current_user.id

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_IMPRESSION,
        rmclogger.LOG_EVENT_SCHEDULE_VIEW, {
            'current_user': current_user_id,
            'profile_user': profile_user.id,
        },
    )

    user_dicts = [profile_dict]
    if current_user and current_user_id != profile_user.id:
        user_dicts.append(current_user.to_dict())

    return flask.render_template('schedule_page.html',
        page_script='schedule_page.js',
        profile_obj=profile_dict,
        user_objs=user_dicts,
        profile_user_id=profile_user.id,
        current_user_id=current_user_id,
        schedule_item_objs=schedule_item_dicts,
        course_objs=course_dicts,
        show_printable=flask.request.values.get('print'),
    )
Exemple #36
0
def index():
    # Redirect logged-in users to profile
    # TODO(Sandy): If we request extra permissions from FB, we'll need to show
    # them the landing page to let them to Connect again and accept the new
    # permissions. Alternatively, we could use other means of requesting for
    # new perms
    request = flask.request
    logout = bool(request.values.get('logout'))
    referrer_id = request.values.get('meow') or request.values.get('referrer')

    if logout:
        view_helpers.logout_current_user()

    if not logout and not referrer_id and view_helpers.get_current_user():
        return flask.make_response(flask.redirect('profile'))

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_IMPRESSION,
        rmclogger.LOG_EVENT_LANDING,
    )

    return flask.render_template('index_page.html',
        page_script='index_page.js',
    )
Exemple #37
0
def login():
    req = flask.request

    fbsr = req.form.get('fb_signed_request')

    # TODO(Sandy): Change log category because this isn't API?
    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_API,
        rmclogger.LOG_EVENT_LOGIN, {
            'fbsr': fbsr,
            'request_form': req.form,
        },
    )

    if (fbsr is None):
        raise exceptions.ImATeapot('No fbsr set')

    fb_data = facebook.get_fb_data(fbsr, app.config)
    fbid = fb_data['fbid']
    fb_access_token = fb_data['access_token']
    fb_access_token_expiry_date = fb_data['expires_on']
    is_invalid = fb_data['is_invalid']

    user = m.User.objects(fbid=fbid).first()
    if user:
        # Existing user. Update with latest FB info
        user.fb_access_token = fb_access_token
        user.fb_access_token_expiry_date = fb_access_token_expiry_date
        user.fb_access_token_invalid = is_invalid
        user.save()
        view_helpers.login_as_user(user)

        rmclogger.log_event(
            rmclogger.LOG_CATEGORY_IMPRESSION,
            rmclogger.LOG_EVENT_LOGIN, {
                'new_user': False,
                'user_id': user.id,
            },
        )

        return ''

    # Sign up the new user
    friend_fbids = flask.json.loads(req.form.get('friend_fbids'))
    gender = req.form.get('gender')
    first_name = req.form.get('first_name')
    middle_name = req.form.get('middle_name')
    last_name = req.form.get('last_name')
    email = req.form.get('email')

    now = datetime.now()
    user_obj = {
        'fbid': fbid,
        'first_name': first_name,
        'middle_name': middle_name,
        'last_name': last_name,
        'email': email,
        'gender': gender,
        'fb_access_token': fb_access_token,
        'fb_access_token_expiry_date': fb_access_token_expiry_date,
        # TODO(Sandy): Count visits properly
        'join_date': now,
        'join_source': m.User.JoinSource.FACEBOOK,
        'num_visits': 1,
        'last_visited': now,
        'friend_fbids': friend_fbids,
        # TODO(Sandy): Fetch from client side and pass here: name, email,
        # school, program, faculty
    }
    referrer_id = req.form.get('referrer_id')
    if referrer_id:
        try:
            user_obj['referrer_id'] = bson.ObjectId(referrer_id)
        except:
            pass

    user = m.User(**user_obj)
    user.save()
    view_helpers.login_as_user(user)

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_IMPRESSION,
        rmclogger.LOG_EVENT_LOGIN, {
            'new_user': True,
            'user_id': user.id,
            'referrer_id': referrer_id,
        },
    )

    return ''
Exemple #38
0
def upload_schedule():
    req = flask.request
    user = view_helpers.get_current_user()

    schedule_data = util.json_loads(req.form.get('schedule_data'))
    processed_items = schedule_data['processed_items']
    failed_items = schedule_data['failed_items']
    term_name = schedule_data['term_name']
    term_id = m.Term.id_from_name(term_name)

    # FIXME TODO(david): Save these in models and display on schedule
    #failed_items = schedule_data['failed_items']

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_API,
        rmclogger.LOG_EVENT_SCHEDULE,
        {
            'schedule_data': schedule_data,
            'term_id': term_id,
            'user_id': user.id,
        },
    )

    now = datetime.now()

    user.last_good_schedule_paste = req.form.get('schedule_text')
    user.last_good_schedule_paste_date = now
    user.save()

    # Remove existing schedule items for the user for the given term
    for usi in m.UserScheduleItem.objects(user_id=user.id, term_id=term_id):
        usi.delete()

    for item in processed_items:
        try:
            # Create this UserScheduleItem
            first_name, last_name = m.Professor.guess_names(item['prof_name'])
            prof_id = m.Professor.get_id_from_name(
                first_name=first_name,
                last_name=last_name,
            )
            if first_name and last_name:
                if not m.Professor.objects.with_id(prof_id):
                    m.Professor(
                        id=prof_id,
                        first_name=first_name,
                        last_name=last_name,
                    ).save()

            usi = m.UserScheduleItem(
                user_id=user.id,
                class_num=item['class_num'],
                building=item['building'],
                room=item.get('room'),
                section_type=item['section_type'].upper(),
                section_num=item['section_num'],
                start_date=datetime.utcfromtimestamp(item['start_date']),
                end_date=datetime.utcfromtimestamp(item['end_date']),
                course_id=item['course_id'],
                prof_id=prof_id,
                term_id=term_id,
            )
            try:
                usi.save()
            except me.NotUniqueError as ex:
                # Likely the case where the user pastes in two or more valid
                # schedules into the same input box
                logging.info(
                    'Duplicate error on UserScheduleItem .save(): %s' % (ex))

            # Add this item to the user's course history
            # FIXME(Sandy): See if we can get program_year_id from Quest
            # Or just increment their last one
            user.add_course(usi.course_id, usi.term_id)

        except KeyError:
            logging.error("Invalid item in uploaded schedule: %s" % (item))

    # Add courses that failed to fully parse, probably due to unavailable times
    for course_id in set(failed_items):
        fsi = m.FailedScheduleItem(
            user_id=user.id,
            course_id=course_id,
            parsed_date=now,
        )

        try:
            fsi.save()
        except me.NotUniqueError as ex:
            # This should never happen since we're iterating over a set
            logging.warn('WTF this should never happen.')
            logging.warn('Duplicate error FailedScheduleItem.save(): %s' % ex)

        user.add_course(course_id, term_id)

    user.schedules_imported += 1
    user.save()

    schedule_screenshot.update_screenshot_async(user)

    rmclogger.log_event(rmclogger.LOG_CATEGORY_SCHEDULE,
                        rmclogger.LOG_EVENT_UPLOAD, user.id)

    return ''
Exemple #39
0
    def search(params, current_user=None):
        """Search for courses based on various parameters.

        Arguments:
            params: Dict of search parameters (all optional):
                keywords: Keywords to search on
                sort_mode: Name of a sort mode. See Course.SORT_MODES. The
                    'friends_taken' sort mode defaults to 'popular' if no
                    current_user.
                direction: 1 for ascending, -1 for descending
                count: Max items to return (aka. limit)
                offset: Index of first search result to return (aka. skip)
                exclude_taken_courses: "yes" to exclude courses current_user
                    has taken.
            current_user: The user making the request.

        Returns:
            A tuple (courses, has_more):
                courses: Search results
                has_more: Whether there could be more search results
        """
        keywords = params.get("keywords")
        sort_mode = params.get("sort_mode", "popular")
        default_direction = _SORT_MODES_BY_NAME[sort_mode]["direction"]
        direction = int(params.get("direction", default_direction))
        count = int(params.get("count", 10))
        offset = int(params.get("offset", 0))
        exclude_taken_courses = params.get("exclude_taken_courses") == "yes"

        # TODO(david): These logging things should be done asynchronously
        rmclogger.log_event(rmclogger.LOG_CATEGORY_COURSE_SEARCH, rmclogger.LOG_EVENT_SEARCH_PARAMS, params)

        filters = {}
        if keywords:
            # Clean keywords to just alphanumeric and space characters
            keywords_cleaned = re.sub(r"[^\w ]", " ", keywords)

            def regexify_keywords(keyword):
                keyword = keyword.lower()
                return re.compile("^%s" % re.escape(keyword))

            keyword_regexes = map(regexify_keywords, keywords_cleaned.split())
            filters["_keywords__all"] = keyword_regexes

        if exclude_taken_courses:
            if current_user:
                ucs = current_user.get_user_courses().only("course_id", "term_id")
                filters["id__nin"] = [uc.course_id for uc in ucs if not term.Term.is_shortlist_term(uc.term_id)]
            else:
                logging.error("Anonymous user tried excluding taken courses")

        if sort_mode == "friends_taken" and current_user:
            import user

            friends = user.User.objects(id__in=current_user.friend_ids).only("course_history")

            num_friends_by_course = collections.Counter()
            for friend in friends:
                num_friends_by_course.update(friend.course_ids)

            filters["id__in"] = num_friends_by_course.keys()
            existing_courses = Course.objects(**filters).only("id")
            existing_course_ids = set(c.id for c in existing_courses)
            for course_id in num_friends_by_course.keys():
                if course_id not in existing_course_ids:
                    del num_friends_by_course[course_id]

            sorted_course_count_tuples = sorted(
                num_friends_by_course.items(), key=lambda (_, total): total, reverse=direction < 0
            )[offset : offset + count]

            sorted_course_ids = [course_id for (course_id, total) in sorted_course_count_tuples]

            unsorted_courses = Course.objects(id__in=sorted_course_ids)
            course_by_id = {course.id: course for course in unsorted_courses}
            courses = [course_by_id[cid] for cid in sorted_course_ids]

        else:
            sort_options = _SORT_MODES_BY_NAME[sort_mode]

            if sort_options["is_rating"]:
                suffix = "positive" if direction < 0 else "negative"
                order_by = "-%s.sorting_score_%s" % (sort_options["field"], suffix)
            else:
                sign = "-" if direction < 0 else ""
                order_by = "%s%s" % (sign, sort_options["field"])

            unsorted_courses = Course.objects(**filters)
            sorted_courses = unsorted_courses.order_by(order_by)
            courses = sorted_courses.skip(offset).limit(count)

        has_more = len(courses) == count

        return courses, has_more
Exemple #40
0
def search_courses():
    # TODO(mack): create enum of sort options
    # num_friends, num_ratings, overall, interest, easiness

    request = flask.request
    keywords = request.values.get('keywords')
    sort_mode = request.values.get('sort_mode', 'popular')
    default_direction = COURSES_SORT_MODES_BY_NAME[sort_mode]['direction']
    direction = int(request.values.get('direction', default_direction))
    count = int(request.values.get('count', 10))
    offset = int(request.values.get('offset', 0))
    exclude_taken_courses = request.values.get('exclude_taken_courses')

    current_user = view_helpers.get_current_user()

    # TODO(david): These logging things should be done asynchronously
    rmclogger.log_event(rmclogger.LOG_CATEGORY_COURSE_SEARCH,
                        rmclogger.LOG_EVENT_SEARCH_PARAMS, request.values)

    filters = {}
    if keywords:
        # Clean keywords to just alphanumeric and space characters
        keywords = re.sub(r'[^\w ]', ' ', keywords)

        keywords = re.sub('\s+', ' ', keywords)
        keywords = keywords.split(' ')

        def regexify_keywords(keyword):
            keyword = keyword.lower()
            return re.compile('^%s' % keyword)

        keywords = map(regexify_keywords, keywords)
        filters['_keywords__all'] = keywords

    if exclude_taken_courses == "yes":
        if current_user:
            ucs = (current_user.get_user_courses().only(
                'course_id', 'term_id'))
            filters['id__nin'] = [
                uc.course_id for uc in ucs
                if not m.term.Term.is_shortlist_term(uc.term_id)
            ]
        else:
            logging.error('Anonymous user tried excluding taken courses')

    if sort_mode == 'friends_taken':
        # TODO(mack): should only do if user is logged in
        friends = m.User.objects(
            id__in=current_user.friend_ids).only('course_history')
        # TODO(mack): need to majorly optimize this
        num_friends_by_course = {}
        for friend in friends:
            for course_id in friend.course_ids:
                if not course_id in num_friends_by_course:
                    num_friends_by_course[course_id] = 0
                num_friends_by_course[course_id] += 1

        filters['id__in'] = num_friends_by_course.keys()
        existing_courses = m.Course.objects(**filters).only('id')
        existing_course_ids = set(c.id for c in existing_courses)
        for course_id in num_friends_by_course.keys():
            if course_id not in existing_course_ids:
                del num_friends_by_course[course_id]

        sorted_course_count_tuples = sorted(
            num_friends_by_course.items(),
            key=lambda (_, total): total,
            reverse=direction < 0,
        )[offset:offset + count]

        sorted_course_ids = [
            course_id for (course_id, total) in sorted_course_count_tuples
        ]

        unsorted_limited_courses = m.Course.objects(id__in=sorted_course_ids)

        limited_courses_by_id = {}
        for course in unsorted_limited_courses:
            limited_courses_by_id[course.id] = course

        limited_courses = []
        for course_id in sorted_course_ids:
            limited_courses.append(limited_courses_by_id[course_id])

    else:
        sort_options = COURSES_SORT_MODES_BY_NAME[sort_mode]

        if sort_mode in RATING_SORT_MODES:
            sort_instr = '-' + sort_options['field']
            sort_instr += "_positive" if direction < 0 else "_negative"
        else:
            sort_instr = ''
            if direction < 0:
                sort_instr = '-'
            sort_instr += sort_options['field']

        unsorted_courses = m.Course.objects(**filters)
        sorted_courses = unsorted_courses.order_by(sort_instr)
        limited_courses = sorted_courses.skip(offset).limit(count)

    has_more = len(limited_courses) == count

    course_dict_list, user_course_dict_list, user_course_list = (
        m.Course.get_course_and_user_course_dicts(limited_courses,
                                                  current_user,
                                                  include_friends=True,
                                                  full_user_courses=False,
                                                  include_sections=True))
    professor_dict_list = m.Professor.get_reduced_professors_for_courses(
        limited_courses)

    user_dict_list = []
    if current_user:
        user_ids = [
            uc['user_id'] for uc in user_course_dict_list
            if uc['user_id'] != current_user.id
        ]
        users = m.User.objects(id__in=user_ids).only(*m.User.CORE_FIELDS)
        user_dict_list = [u.to_dict() for u in users]

    return util.json_dumps({
        'user_objs': user_dict_list,
        'course_objs': course_dict_list,
        'professor_objs': professor_dict_list,
        'user_course_objs': user_course_dict_list,
        'has_more': has_more,
    })
Exemple #41
0
def render_profile_page(profile_user_id, current_user=None):
    # TODO(mack): for dict maps, use .update() rather than overwriting to
    # avoid subtle overwrites by data that has fields filled out

    LAST_TERM_ID = util.get_current_term_id()

    # PART ONE - VALIDATION

    current_user = current_user or view_helpers.get_current_user()

    try:
        if profile_user_id:
            profile_user_id = bson.ObjectId(profile_user_id)
    except:
        logging.warn('Invalid profile_user_id (%s)' % profile_user_id)
        return view_helpers.redirect_to_profile(current_user)

    if not profile_user_id:
        return view_helpers.redirect_to_profile(current_user)

    if profile_user_id == current_user.id:
        own_profile = True
        profile_user = current_user
    else:
        own_profile = False

        # Allow only friends to view profile
        if not (profile_user_id in current_user.friend_ids or
                (current_user.is_admin and flask.request.values.get('admin'))):
            logging.info("User (%s) tried to access non-friend profile (%s)" %
                         (current_user.id, profile_user_id))
            return view_helpers.redirect_to_profile(current_user)

        profile_user = m.User.objects.with_id(profile_user_id)
        # Technically we don't need this check due to above (under normal
        # operation). Though have this anyway as a failsafe
        if profile_user is None:
            logging.warn('profile_user is None')
            return view_helpers.redirect_to_profile(current_user)

    if own_profile:
        profile_user_secret_id = profile_user.get_secret_id()
    else:
        profile_user_secret_id = None

    show_import_schedule = False
    # Redirect the user appropriately... to /onboarding if they have no course
    # history, and to wherever they logged in from if they just logged in
    # TODO(david): Should have frontend decide whether to take us to /profile
    #     or /onboarding and not redirect in one of these two places
    if own_profile:
        redirect_url = flask.request.values.get('next')

        show_onboarding = False
        if not current_user.has_course_history:
            if not current_user.last_show_onboarding:
                show_onboarding = True
            else:
                time_delta = datetime.now() - current_user.last_show_onboarding
                # If they haven't imported any courses yet and the last time
                # the user was on the onboarding page is more than 5 days ago,
                # show the onboarding page again
                if time_delta.days > RESHOW_ONBOARDING_DELAY_DAYS:
                    show_onboarding = True

        # See https://uwflow.uservoice.com/admin/tickets/62
        if profile_user_id == '50b8ce2cd89d62310645ca78':
            show_onboarding = False

        if show_onboarding:
            onboarding_url = '/onboarding'
            if flask.request.query_string:
                onboarding_url = '%s?%s' % (onboarding_url,
                                            flask.request.query_string)
            return flask.make_response(flask.redirect(onboarding_url))
        else:
            redirect_url = flask.request.values.get('next')
            if redirect_url:
                return flask.make_response(flask.redirect(redirect_url))

        # Show the import schedule view if it's been long enough
        if not current_user.has_schedule:
            if current_user.last_show_import_schedule:
                time_delta = (datetime.now() -
                              current_user.last_show_import_schedule)
                # User didn't import schedule yet, reshow every few days
                if time_delta.days > RESHOW_SCHEDULE_DELAY_DAYS:
                    show_import_schedule = True
            else:
                show_import_schedule = True

            if show_import_schedule:
                # TODO(Sandy): Do this on modal dismiss instead
                current_user.last_show_import_schedule = datetime.now()
                current_user.save()

    # PART TWO - DATA FETCHING

    # Get the mutual course ids of friends of profile user
    mutual_course_ids_by_friend = {}
    if own_profile:
        mutual_course_ids_by_friend = profile_user.get_mutual_course_ids(
            view_helpers.get_redis_instance())

    def get_friend_course_ids_in_term(friend_ids, term_id):
        user_courses = m.UserCourse.objects(term_id=term_id,
                                            user_id__in=friend_ids).only(
                                                'user_id', 'course_id')

        last_term_course_ids_by_friend = {}
        for uc in user_courses:
            last_term_course_ids_by_friend.setdefault(uc.user_id,
                                                      []).append(uc.course_id)
        return last_term_course_ids_by_friend

    # Get the course ids of last term courses of friends of profile user
    last_term_course_ids_by_friend = get_friend_course_ids_in_term(
        profile_user.friend_ids, LAST_TERM_ID)

    # Get the course ids of courses profile user has taken
    profile_course_ids = set(profile_user.course_ids)

    # Fetch courses for transcript, which need more detailed information
    # than other courses (such as mutual and last term courses for friends)
    transcript_courses = list(m.Course.objects(id__in=profile_course_ids))

    # Fetch remainining courses that need less data. This will be mutual
    # and last term courses for profile user's friends
    friend_course_ids = set()
    friend_courses = []
    if own_profile:
        for course_ids in mutual_course_ids_by_friend.values():
            friend_course_ids = friend_course_ids.union(course_ids)
        for course_ids in last_term_course_ids_by_friend.values():
            friend_course_ids = friend_course_ids.union(course_ids)
        friend_course_ids = friend_course_ids - profile_course_ids
        friend_courses = m.Course.objects(id__in=friend_course_ids).only(
            'id', 'name')

    # Fetch simplified information for friends of profile user
    # (for friend sidebar)
    friends = profile_user.get_friends()

    # Fetch all professors for all courses
    professor_objs = m.Professor.get_reduced_professors_for_courses(
        transcript_courses)

    # PART THREE - TRANSFORM DATA TO DICTS

    # Convert professors to dicts
    professor_dicts = {}
    for professor_obj in professor_objs:
        professor_dicts[professor_obj['id']] = professor_obj

    # Convert courses to dicts
    course_dict_list, user_course_dict_list, user_course_list = (
        m.Course.get_course_and_user_course_dicts(transcript_courses,
                                                  current_user,
                                                  include_friends=own_profile))
    course_dicts = {}
    for course_dict in course_dict_list:
        course_dicts[course_dict['id']] = course_dict
    user_course_dicts = {}
    for user_course_dict in user_course_dict_list:
        user_course_dicts[user_course_dict['id']] = user_course_dict

    profile_uc_dict_list = []

    # We only need to fetch usercourses for profile user if it is not the
    # current user since m.Course.get_course_and_user_course_dicts() will
    # have already fetched usercourses for the current user
    if not own_profile:
        # Get the user courses of profile user
        profile_uc_dict_list = [
            uc.to_dict() for uc in profile_user.get_user_courses()
        ]
        # Get a mapping from course id to user_course for profile user
        profile_user_course_by_course = {}
        for uc_dict in profile_uc_dict_list:
            profile_user_course_by_course[uc_dict['course_id']] = uc_dict

    # Fill in with information about profile user
    for course in transcript_courses:
        course_dict = course_dicts[course.id]

        if not own_profile:
            # This has already been done for current user
            profile_uc_dict = profile_user_course_by_course.get(course.id)
            profile_user_course_id = profile_uc_dict['id']
            user_course_dicts[profile_user_course_id] = profile_uc_dict

            # Since we only fetched the user courses of the logged in user in
            # m.Course.get_course_and_user_course_dicts() above, gotta also
            # add the user courses of the profile user here
            user_course_dict_list.append(profile_uc_dict)
        else:
            profile_user_course_id = course_dict.get('user_course_id')
            if profile_user_course_id:
                profile_uc_dict_list.append(
                    user_course_dicts[profile_user_course_id])

        course_dict['profile_user_course_id'] = profile_user_course_id

    for course in friend_courses:
        course_dicts[course.id] = course.to_dict()

    def filter_course_ids(course_ids):
        return [
            course_id for course_id in course_ids if course_id in course_dicts
        ]

    # Convert friend users to dicts
    user_dicts = {}
    # TODO(mack): should really be named current_term
    last_term = m.Term(id=LAST_TERM_ID)
    for friend in friends:
        user_dict = friend.to_dict(extended=False)

        if own_profile:
            user_dict.update({
                'last_term_name':
                last_term.name,
                'last_term_course_ids':
                filter_course_ids(
                    last_term_course_ids_by_friend.get(friend.id, [])),
                'mutual_course_ids':
                filter_course_ids(
                    mutual_course_ids_by_friend.get(friend.id, [])),
            })

        user_dicts[friend.id] = user_dict

    # Convert profile user to dict
    # TODO(mack): This must be after friend user dicts since it can override
    # data in it. Remove this restriction
    profile_dict = profile_user.to_dict(include_course_ids=True)
    profile_dict.update({
        'last_program_year_id':
        profile_user.get_latest_program_year_id(),
    })
    user_dicts.setdefault(profile_user.id, {}).update(profile_dict)

    # Convert current user to dict
    # TODO(mack): This must be after friend user dicts since it can override
    # data in it. Remove this restriction
    if not own_profile:
        user_dicts.setdefault(current_user.id, {}).update(
            current_user.to_dict(include_course_ids=True))

    def get_ordered_transcript(profile_uc_dict_list):
        transcript_by_term = {}

        for uc_dict in profile_uc_dict_list:
            (transcript_by_term.setdefault(uc_dict['term_id'],
                                           []).append(uc_dict))

        ordered_transcript = []
        for term_id, uc_dicts in sorted(transcript_by_term.items(),
                                        reverse=True):
            curr_term = m.Term(id=term_id)
            term_dict = {
                'id':
                curr_term.id,
                'name':
                curr_term.name,
                'program_year_id':
                uc_dicts[0].get('program_year_id'),
                'course_ids': [
                    uc_dict['course_id'] for uc_dict in uc_dicts
                    if uc_dict['course_id'] in course_dicts
                ],
            }
            ordered_transcript.append(term_dict)

        return ordered_transcript, transcript_by_term

    # Store courses by term as transcript using the current user's friends
    ordered_transcript, transcript_by_term = get_ordered_transcript(
        profile_uc_dict_list)

    # Fetch exam schedules and schedule items
    current_term_id = util.get_current_term_id()

    current_term_courses = transcript_by_term.get(current_term_id, [])
    current_course_ids = [c['course_id'] for c in current_term_courses]

    exam_objs = profile_user.get_current_term_exams(current_course_ids)
    exam_dicts = [e.to_dict() for e in exam_objs]
    exam_updated_date = None
    if exam_objs:
        exam_updated_date = exam_objs[0].id.generation_time

    # Set the course to prompt the user to review if it's time
    course_id_to_review = None
    if own_profile and profile_user.should_prompt_review():
        profile_user_courses = filter(lambda uc: uc.user_id == profile_user.id,
                                      user_course_list)
        uc_to_review = m.UserCourse.select_course_to_review(
            profile_user_courses)
        course_id_to_review = uc_to_review and uc_to_review.course_id

        if uc_to_review:
            uc_to_review.select_for_review(current_user)

    # NOTE: This implictly requires that the courses on the schedule are on the
    # transcript, since these course objects are needed by the schedule  on the
    # frontend. This should be the case since when we add a schedule item, a
    # corresponding item is added to the transcript.
    schedule_item_dicts = profile_user.get_schedule_item_dicts(exam_objs)
    failed_schedule_item_dicts = profile_user.get_failed_schedule_item_dicts()

    referrals = m.User.objects(referrer_id=current_user.id)
    referral_objs = [referral.to_dict() for referral in referrals]

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_IMPRESSION,
        rmclogger.LOG_EVENT_PROFILE,
        {
            'current_user': current_user.id,
            'profile_user': profile_user.id,
        },
    )

    schedule_screenshot.update_screenshot_async(profile_user)

    scholarships_dict = []

    if profile_user.id == current_user.id:
        scholarships = m.Scholarship.objects()
        # Filter scholarships based on program
        closed_scholarship_ids_set = set(profile_user.closed_scholarship_ids)
        scholarships = [
            s for s in scholarships
            if profile_user.short_program_name in s.programs
            and s.id not in closed_scholarship_ids_set
        ]
        scholarships_dict = [s.to_dict() for s in scholarships]

    recommendation_dict = []
    recommended_course_ids = []
    if profile_user.id == current_user.id:
        recommended_course_ids = current_user.recommended_courses
        recommendation_dict = [
            m.Course.objects(id=course_id).first().to_dict()
            for course_id in recommended_course_ids
        ]

    return flask.render_template(
        'profile_page.html',
        page_script='profile_page.js',
        transcript_obj=ordered_transcript,
        user_objs=user_dicts.values(),
        referral_objs=referral_objs,
        user_course_objs=user_course_dicts.values(),
        course_objs=course_dicts.values(),
        professor_objs=professor_dicts.values(),
        # TODO(mack): currently needed by jinja to do server-side rendering
        # figure out a cleaner way to do this w/o passing another param
        profile_obj=profile_dict,
        profile_user_id=profile_user.id,
        current_user_id=current_user.id,
        profile_user_secret_id=profile_user_secret_id,
        own_profile=own_profile,
        has_courses=profile_user.has_course_history,
        exam_objs=exam_dicts,
        exam_updated_date=exam_updated_date,
        schedule_item_objs=schedule_item_dicts,
        failed_schedule_item_objs=failed_schedule_item_dicts,
        has_shortlisted=profile_user.has_shortlisted,
        show_import_schedule=show_import_schedule,
        show_import_schedule_button=own_profile
        and (not profile_user.has_schedule),
        course_id_to_review=course_id_to_review,
        scholarship_objs=scholarships_dict,
        recommended_objs=recommendation_dict,
    )
Exemple #42
0
def render_profile_page(profile_user_id, current_user=None):
    # TODO(mack): for dict maps, use .update() rather than overwriting to
    # avoid subtle overwrites by data that has fields filled out

    LAST_TERM_ID = util.get_current_term_id()

    # PART ONE - VALIDATION

    current_user = current_user or view_helpers.get_current_user()

    try:
        if profile_user_id:
            profile_user_id = bson.ObjectId(profile_user_id)
    except:
        logging.warn('Invalid profile_user_id (%s)' % profile_user_id)
        return view_helpers.redirect_to_profile(current_user)

    if not profile_user_id:
        return view_helpers.redirect_to_profile(current_user)

    if profile_user_id == current_user.id:
        own_profile = True
        profile_user = current_user
    else:
        own_profile = False

        # Allow only friends to view profile
        if not (profile_user_id in current_user.friend_ids or (
                current_user.is_admin and flask.request.values.get('admin'))):
            logging.info("User (%s) tried to access non-friend profile (%s)"
                    % (current_user.id, profile_user_id))
            return view_helpers.redirect_to_profile(current_user)

        profile_user = m.User.objects.with_id(profile_user_id)
        # Technically we don't need this check due to above (under normal
        # operation). Though have this anyway as a failsafe
        if profile_user is None:
            logging.warn('profile_user is None')
            return view_helpers.redirect_to_profile(current_user)

    if own_profile:
        profile_user_secret_id = profile_user.get_secret_id()
    else:
        profile_user_secret_id = None

    show_import_schedule = False
    # Redirect the user appropriately... to /onboarding if they have no course
    # history, and to wherever they logged in from if they just logged in
    # TODO(david): Should have frontend decide whether to take us to /profile
    #     or /onboarding and not redirect in one of these two places
    if own_profile:
        redirect_url = flask.request.values.get('next')

        show_onboarding = False
        if not current_user.has_course_history:
            if not current_user.last_show_onboarding:
                show_onboarding = True
            else:
                time_delta = datetime.now() - current_user.last_show_onboarding
                # If they haven't imported any courses yet and the last time
                # the user was on the onboarding page is more than 5 days ago,
                # show the onboarding page again
                if time_delta.days > RESHOW_ONBOARDING_DELAY_DAYS:
                    show_onboarding = True

        # See https://uwflow.uservoice.com/admin/tickets/62
        if profile_user_id == '50b8ce2cd89d62310645ca78':
            show_onboarding = False

        if show_onboarding:
            onboarding_url = '/onboarding'
            if flask.request.query_string:
                onboarding_url = '%s?%s' % (
                        onboarding_url, flask.request.query_string)
            return flask.make_response(flask.redirect(onboarding_url))
        else:
            redirect_url = flask.request.values.get('next')
            if redirect_url:
                return flask.make_response(flask.redirect(redirect_url))

        # Show the import schedule view if it's been long enough
        if not current_user.has_schedule:
            if current_user.last_show_import_schedule:
                time_delta = (datetime.now() -
                              current_user.last_show_import_schedule)
                # User didn't import schedule yet, reshow every few days
                if time_delta.days > RESHOW_SCHEDULE_DELAY_DAYS:
                    show_import_schedule = True
            else:
                show_import_schedule = True

            if show_import_schedule:
                # TODO(Sandy): Do this on modal dismiss instead
                current_user.last_show_import_schedule = datetime.now()
                current_user.save()

    # PART TWO - DATA FETCHING

    # Get the mutual course ids of friends of profile user
    mutual_course_ids_by_friend = {}
    if own_profile:
        mutual_course_ids_by_friend = profile_user.get_mutual_course_ids(
            view_helpers.get_redis_instance())

    def get_friend_course_ids_in_term(friend_ids, term_id):
        user_courses = m.UserCourse.objects(
                term_id=term_id, user_id__in=friend_ids).only(
                    'user_id', 'course_id')

        last_term_course_ids_by_friend = {}
        for uc in user_courses:
            last_term_course_ids_by_friend.setdefault(
                    uc.user_id, []).append(uc.course_id)
        return last_term_course_ids_by_friend

    # Get the course ids of last term courses of friends of profile user
    last_term_course_ids_by_friend = get_friend_course_ids_in_term(
            profile_user.friend_ids, LAST_TERM_ID)

    # Get the course ids of courses profile user has taken
    profile_course_ids = set(profile_user.course_ids)

    # Fetch courses for transcript, which need more detailed information
    # than other courses (such as mutual and last term courses for friends)
    transcript_courses = list(m.Course.objects(id__in=profile_course_ids))

    # Fetch remainining courses that need less data. This will be mutual
    # and last term courses for profile user's friends
    friend_course_ids = set()
    friend_courses = []
    if own_profile:
        for course_ids in mutual_course_ids_by_friend.values():
            friend_course_ids = friend_course_ids.union(course_ids)
        for course_ids in last_term_course_ids_by_friend.values():
            friend_course_ids = friend_course_ids.union(course_ids)
        friend_course_ids = friend_course_ids - profile_course_ids
        friend_courses = m.Course.objects(
                id__in=friend_course_ids).only('id', 'name')

    # Fetch simplified information for friends of profile user
    # (for friend sidebar)
    friends = profile_user.get_friends()

    # Fetch all professors for all courses
    professor_objs = m.Professor.get_reduced_professors_for_courses(
            transcript_courses)

    # PART THREE - TRANSFORM DATA TO DICTS

    # Convert professors to dicts
    professor_dicts = {}
    for professor_obj in professor_objs:
        professor_dicts[professor_obj['id']] = professor_obj

    # Convert courses to dicts
    course_dict_list, user_course_dict_list, user_course_list = (
            m.Course.get_course_and_user_course_dicts(
                transcript_courses, current_user, include_friends=own_profile))
    course_dicts = {}
    for course_dict in course_dict_list:
        course_dicts[course_dict['id']] = course_dict
    user_course_dicts = {}
    for user_course_dict in user_course_dict_list:
        user_course_dicts[user_course_dict['id']] = user_course_dict

    profile_uc_dict_list = []

    # We only need to fetch usercourses for profile user if it is not the
    # current user since m.Course.get_course_and_user_course_dicts() will
    # have already fetched usercourses for the current user
    if not own_profile:
        # Get the user courses of profile user
        profile_uc_dict_list = [
                uc.to_dict() for uc in profile_user.get_user_courses()]
        # Get a mapping from course id to user_course for profile user
        profile_user_course_by_course = {}
        for uc_dict in profile_uc_dict_list:
            profile_user_course_by_course[uc_dict['course_id']] = uc_dict

    # Fill in with information about profile user
    for course in transcript_courses:
        course_dict = course_dicts[course.id]

        if not own_profile:
            # This has already been done for current user
            profile_uc_dict = profile_user_course_by_course.get(course.id)
            profile_user_course_id = profile_uc_dict['id']
            user_course_dicts[profile_user_course_id] = profile_uc_dict

            # Since we only fetched the user courses of the logged in user in
            # m.Course.get_course_and_user_course_dicts() above, gotta also
            # add the user courses of the profile user here
            user_course_dict_list.append(profile_uc_dict)
        else:
            profile_user_course_id = course_dict.get('user_course_id')
            if profile_user_course_id:
                profile_uc_dict_list.append(
                        user_course_dicts[profile_user_course_id])

        course_dict['profile_user_course_id'] = profile_user_course_id

    for course in friend_courses:
        course_dicts[course.id] = course.to_dict()

    def filter_course_ids(course_ids):
        return [course_id for course_id in course_ids
                if course_id in course_dicts]

    # Convert friend users to dicts
    user_dicts = {}
    # TODO(mack): should really be named current_term
    last_term = m.Term(id=LAST_TERM_ID)
    for friend in friends:
        user_dict = friend.to_dict(extended=False)

        if own_profile:
            user_dict.update({
                'last_term_name': last_term.name,
                'last_term_course_ids': filter_course_ids(
                    last_term_course_ids_by_friend.get(friend.id, [])),
                'mutual_course_ids': filter_course_ids(
                    mutual_course_ids_by_friend.get(friend.id, [])),
            })

        user_dicts[friend.id] = user_dict

    # Convert profile user to dict
    # TODO(mack): This must be after friend user dicts since it can override
    # data in it. Remove this restriction
    profile_dict = profile_user.to_dict(include_course_ids=True)
    profile_dict.update({
        'last_program_year_id': profile_user.get_latest_program_year_id(),
    })
    user_dicts.setdefault(profile_user.id, {}).update(profile_dict)

    # Convert current user to dict
    # TODO(mack): This must be after friend user dicts since it can override
    # data in it. Remove this restriction
    if not own_profile:
        user_dicts.setdefault(current_user.id, {}).update(
                current_user.to_dict(include_course_ids=True))

    def get_ordered_transcript(profile_uc_dict_list):
        transcript_by_term = {}

        for uc_dict in profile_uc_dict_list:
            (transcript_by_term.setdefault(uc_dict['term_id'], [])
                               .append(uc_dict))

        ordered_transcript = []
        for term_id, uc_dicts in sorted(transcript_by_term.items(),
                                        reverse=True):
            curr_term = m.Term(id=term_id)
            term_dict = {
                'id': curr_term.id,
                'name': curr_term.name,
                'program_year_id': uc_dicts[0].get('program_year_id'),
                'course_ids': [uc_dict['course_id'] for uc_dict in uc_dicts
                    if uc_dict['course_id'] in course_dicts],
            }
            ordered_transcript.append(term_dict)

        return ordered_transcript, transcript_by_term

    # Store courses by term as transcript using the current user's friends
    ordered_transcript, transcript_by_term = get_ordered_transcript(
            profile_uc_dict_list)

    # Fetch exam schedules and schedule items
    current_term_id = util.get_current_term_id()

    current_term_courses = transcript_by_term.get(current_term_id, [])
    current_course_ids = [c['course_id'] for c in current_term_courses]

    exam_objs = profile_user.get_current_term_exams(current_course_ids)
    exam_dicts = [e.to_dict() for e in exam_objs]
    exam_updated_date = None
    if exam_objs:
        exam_updated_date = exam_objs[0].id.generation_time

    # Set the course to prompt the user to review if it's time
    course_id_to_review = None
    if own_profile and profile_user.should_prompt_review():
        profile_user_courses = filter(lambda uc: uc.user_id == profile_user.id,
                user_course_list)
        uc_to_review = m.UserCourse.select_course_to_review(
                profile_user_courses)
        course_id_to_review = uc_to_review and uc_to_review.course_id

        if uc_to_review:
            uc_to_review.select_for_review(current_user)

    # NOTE: This implictly requires that the courses on the schedule are on the
    # transcript, since these course objects are needed by the schedule  on the
    # frontend. This should be the case since when we add a schedule item, a
    # corresponding item is added to the transcript.
    schedule_item_dicts = profile_user.get_schedule_item_dicts(exam_objs)
    failed_schedule_item_dicts = profile_user.get_failed_schedule_item_dicts()

    referrals = m.User.objects(referrer_id=current_user.id)
    referral_objs = [referral.to_dict() for referral in referrals]

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_IMPRESSION,
        rmclogger.LOG_EVENT_PROFILE, {
            'current_user': current_user.id,
            'profile_user': profile_user.id,
        },
    )

    schedule_screenshot.update_screenshot_async(profile_user)

    scholarships_dict = []

    if profile_user.id == current_user.id:
        scholarships = m.Scholarship.objects()
        # Filter scholarships based on program
        closed_scholarship_ids_set = set(profile_user.closed_scholarship_ids)
        scholarships = [s for s in scholarships if
                profile_user.short_program_name in s.programs and
                s.id not in closed_scholarship_ids_set]
        scholarships_dict = [s.to_dict() for s in scholarships]

    recommendation_dict = []
    recommended_course_ids = []
    if profile_user.id == current_user.id:
        recommended_course_ids = current_user.recommended_courses
        recommendation_dict = [m.Course.objects(id=course_id).first().to_dict()
                               for course_id in recommended_course_ids]

    return flask.render_template('profile_page.html',
        page_script='profile_page.js',
        transcript_obj=ordered_transcript,
        user_objs=user_dicts.values(),
        referral_objs=referral_objs,
        user_course_objs=user_course_dicts.values(),
        course_objs=course_dicts.values(),
        professor_objs=professor_dicts.values(),
        # TODO(mack): currently needed by jinja to do server-side rendering
        # figure out a cleaner way to do this w/o passing another param
        profile_obj=profile_dict,
        profile_user_id=profile_user.id,
        current_user_id=current_user.id,
        profile_user_secret_id=profile_user_secret_id,
        own_profile=own_profile,
        has_courses=profile_user.has_course_history,
        exam_objs=exam_dicts,
        exam_updated_date=exam_updated_date,
        schedule_item_objs=schedule_item_dicts,
        failed_schedule_item_objs=failed_schedule_item_dicts,
        has_shortlisted=profile_user.has_shortlisted,
        show_import_schedule=show_import_schedule,
        show_import_schedule_button=own_profile and (not
                profile_user.has_schedule),
        course_id_to_review=course_id_to_review,
        scholarship_objs=scholarships_dict,
        recommended_objs=recommendation_dict,
    )
Exemple #43
0
def login_with_facebook():
    """Login or create an account using Facebook connect

    Upon successful login or account creation, returns a 'secure cookie'
    (provided by Flask) containing the session data.

    Takes a Facebook signed request in the form of:
    {
        'fb_signed_request': obj
    }
    """
    req = flask.request

    fbsr = req.form.get('fb_signed_request')

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_GENERIC,
        rmclogger.LOG_EVENT_LOGIN,
        {
            'fbsr': fbsr,
            'request_form': req.form,
            'type': rmclogger.LOGIN_TYPE_STRING_FACEBOOK,
        },
    )

    if (fbsr is None):
        raise exceptions.ImATeapot('No fbsr set')

    fb_data = facebook.get_fb_data(fbsr, app.config)
    fbid = fb_data['fbid']
    fb_access_token = fb_data['access_token']
    fb_access_token_expiry_date = fb_data['expires_on']
    is_invalid = fb_data['is_invalid']

    user = m.User.objects(fbid=fbid).first()
    if user:
        # Existing user. Update with their latest Facebook info
        user.fb_access_token = fb_access_token
        user.fb_access_token_expiry_date = fb_access_token_expiry_date
        user.fb_access_token_invalid = is_invalid
        user.save()

        # Authenticate
        view_helpers.login_as_user(user)

        rmclogger.log_event(
            rmclogger.LOG_CATEGORY_IMPRESSION,
            rmclogger.LOG_EVENT_LOGIN,
            {
                'new_user': False,
                'user_id': user.id,
                'type': rmclogger.LOGIN_TYPE_STRING_FACEBOOK,
            },
        )
    else:
        # New user, or existing email logins user.
        now = datetime.now()
        email = req.form.get('email')
        user_data = {
            'fb_access_token': fb_access_token,
            'fb_access_token_expiry_date': fb_access_token_expiry_date,
            'fbid': fbid,
            'friend_fbids': flask.json.loads(req.form.get('friend_fbids')),
            'gender': req.form.get('gender'),
            'last_visited': now,
        }

        user = m.User.objects(email=email).first() if email else None
        if user:
            # Update existing account with Facebook data
            referrer_id = None
            for k, v in user_data.iteritems():
                user[k] = v
            user.save()
        else:
            # Create an account with their Facebook data
            user_data.update({
                'email': email,
                'first_name': req.form.get('first_name'),
                'join_date': now,
                'join_source': m.User.JoinSource.FACEBOOK,
                'last_name': req.form.get('last_name'),
                'middle_name': req.form.get('middle_name'),
            })

            referrer_id = req.form.get('referrer_id')
            if referrer_id:
                try:
                    user_data['referrer_id'] = bson.ObjectId(referrer_id)
                except bson.errors.InvalidId:
                    pass

            user = m.User(**user_data)
            user.save()

        # Authenticate
        view_helpers.login_as_user(user)

        rmclogger.log_event(
            rmclogger.LOG_CATEGORY_IMPRESSION,
            rmclogger.LOG_EVENT_LOGIN,
            {
                'new_user': True,
                'user_id': user.id,
                'referrer_id': referrer_id,
                'type': rmclogger.LOGIN_TYPE_STRING_FACEBOOK,
            },
        )

    return ''
Exemple #44
0
def user_course():
    uc_data = util.json_loads(flask.request.data)
    user = view_helpers.get_current_user()

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_API,
        rmclogger.LOG_EVENT_USER_COURSE,
        {
            'uc_data': uc_data,
            'user_id': user.id,
        },
    )

    # Validate request object
    course_id = uc_data.get('course_id')
    term_id = uc_data.get('term_id')
    if course_id is None or term_id is None:
        logging.error("/api/user/course got course_id (%s) and term_id (%s)" %
                      (course_id, term_id))
        # TODO(david): Perhaps we should have a request error function that
        # returns a 400
        raise exceptions.ImATeapot('No course_id or term_id set')

    if not m.UserCourse.can_review(term_id):
        logging.warning("%s attempted to rate %s in future/shortlist term %s" %
                        (user.id, course_id, term_id))
        raise exceptions.ImATeapot(
            "Can't review a course in the future or shortlist")

    # Fetch existing UserCourse
    uc = m.UserCourse.objects(user_id=user.id,
                              course_id=uc_data['course_id'],
                              term_id=uc_data['term_id']).first()

    if uc is None:
        logging.error("/api/user/course User course not found for "
                      "user_id=%s course_id=%s term_id=%s" %
                      (user.id, course_id, term_id))
        # TODO(david): Perhaps we should have a request error function that
        # returns a 400
        raise exceptions.ImATeapot('No user course found')

    orig_points = uc.num_points

    # TODO(Sandy): Consider the case where the user picked a professor and
    # rates them, but then changes the professor. We need to remove the ratings
    # from the old prof's aggregated ratings and add them to the new prof's
    # Maybe create professor if newly added
    if uc_data.get('new_prof_added'):

        new_prof_name = uc_data['new_prof_added']

        # TODO(mack): should do guess_names first, and use that to
        # generate the id
        prof_id = m.Professor.get_id_from_name(new_prof_name)
        uc.professor_id = prof_id

        # TODO(Sandy): Have some kind of sanity check for professor names.
        # Don't allow ridiculousness like "Santa Claus", "aksnlf",
        # "swear words"
        if m.Professor.objects(id=prof_id).count() == 0:
            first_name, last_name = m.Professor.guess_names(new_prof_name)
            m.Professor(
                id=prof_id,
                first_name=first_name,
                last_name=last_name,
            ).save()

        course = m.Course.objects.with_id(uc.course_id)
        course.professor_ids = list(set(course.professor_ids) | {prof_id})
        course.save()

        logging.info("Added new course professor %s (name: %s)" %
                     (prof_id, new_prof_name))
    elif uc_data.get('professor_id'):
        uc.professor_id = uc_data['professor_id']
    else:
        uc.professor_id = None

    now = datetime.now()

    if uc_data.get('course_review'):
        # New course review data
        uc_data['course_review']['comment_date'] = now
        uc.course_review.update(**uc_data['course_review'])

    if uc_data.get('professor_review'):
        # New prof review data
        uc_data['professor_review']['comment_date'] = now
        uc.professor_review.update(**uc_data['professor_review'])

    uc.save()

    points_gained = uc.num_points - orig_points
    user.award_points(points_gained, view_helpers.get_redis_instance())
    user.save()

    return util.json_dumps({
        'professor_review.comment_date':
        uc['professor_review']['comment_date'],
        'course_review.comment_date':
        uc['course_review']['comment_date'],
        'points_gained':
        points_gained,
    })
Exemple #45
0
def user_course():
    uc_data = util.json_loads(flask.request.data)
    user = view_helpers.get_current_user()

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_API,
        rmclogger.LOG_EVENT_USER_COURSE, {
            'uc_data': uc_data,
            'user_id': user.id,
        },
    )

    # Validate request object
    course_id = uc_data.get('course_id')
    term_id = uc_data.get('term_id')
    if course_id is None or term_id is None:
        logging.error("/api/user/course got course_id (%s) and term_id (%s)" %
            (course_id, term_id))
        # TODO(david): Perhaps we should have a request error function that
        # returns a 400
        raise exceptions.ImATeapot('No course_id or term_id set')

    if not m.UserCourse.can_review(term_id):
        logging.warning("%s attempted to rate %s in future/shortlist term %s"
                % (user.id, course_id, term_id))
        raise exceptions.ImATeapot(
                "Can't review a course in the future or shortlist")

    # Fetch existing UserCourse
    uc = m.UserCourse.objects(
        user_id=user.id,
        course_id=uc_data['course_id'],
        term_id=uc_data['term_id']
    ).first()

    if uc is None:
        logging.error("/api/user/course User course not found for "
            "user_id=%s course_id=%s term_id=%s" %
            (user.id, course_id, term_id))
        # TODO(david): Perhaps we should have a request error function that
        # returns a 400
        raise exceptions.ImATeapot('No user course found')

    orig_points = uc.num_points

    # TODO(Sandy): Consider the case where the user picked a professor and
    # rates them, but then changes the professor. We need to remove the ratings
    # from the old prof's aggregated ratings and add them to the new prof's
    # Maybe create professor if newly added
    if uc_data.get('new_prof_added'):

        new_prof_name = uc_data['new_prof_added']

        # TODO(mack): should do guess_names first, and use that to
        # generate the id
        prof_id = m.Professor.get_id_from_name(new_prof_name)
        uc.professor_id = prof_id

        # TODO(Sandy): Have some kind of sanity check for professor names.
        # Don't allow ridiculousness like "Santa Claus", "aksnlf",
        # "swear words"
        if m.Professor.objects(id=prof_id).count() == 0:
            first_name, last_name = m.Professor.guess_names(new_prof_name)
            m.Professor(
                id=prof_id,
                first_name=first_name,
                last_name=last_name,
            ).save()

        course = m.Course.objects.with_id(uc.course_id)
        course.professor_ids = list(set(course.professor_ids) | {prof_id})
        course.save()

        logging.info("Added new course professor %s (name: %s)" % (prof_id,
                new_prof_name))
    elif uc_data.get('professor_id'):
        uc.professor_id = uc_data['professor_id']
    else:
        uc.professor_id = None

    now = datetime.now()

    if uc_data.get('course_review'):
        # New course review data
        uc_data['course_review']['comment_date'] = now
        uc.course_review.update(**uc_data['course_review'])

    if uc_data.get('professor_review'):
        # New prof review data
        uc_data['professor_review']['comment_date'] = now
        uc.professor_review.update(**uc_data['professor_review'])

    uc.save()

    points_gained = uc.num_points - orig_points
    user.award_points(points_gained, view_helpers.get_redis_instance())
    user.save()

    return util.json_dumps({
        'professor_review.comment_date': uc['professor_review'][
            'comment_date'],
        'course_review.comment_date': uc['course_review']['comment_date'],
        'points_gained': points_gained,
    })
Exemple #46
0
def login_with_facebook():
    """Login or create an account using Facebook connect

    Upon successful login or account creation, returns a 'secure cookie'
    (provided by Flask) containing the session data.

    Takes a Facebook signed request in the form of:
    {
        'fb_signed_request': obj
    }
    """
    req = flask.request

    fbsr = req.form.get('fb_signed_request')

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_GENERIC,
        rmclogger.LOG_EVENT_LOGIN, {
            'fbsr': fbsr,
            'request_form': req.form,
            'type': rmclogger.LOGIN_TYPE_STRING_FACEBOOK,
        },
    )

    if (fbsr is None):
        raise exceptions.ImATeapot('No fbsr set')

    fb_data = facebook.get_fb_data(fbsr, app.config)
    fbid = fb_data['fbid']
    fb_access_token = fb_data['access_token']
    fb_access_token_expiry_date = fb_data['expires_on']
    is_invalid = fb_data['is_invalid']

    user = m.User.objects(fbid=fbid).first()
    if user:
        # Existing user. Update with their latest Facebook info
        user.fb_access_token = fb_access_token
        user.fb_access_token_expiry_date = fb_access_token_expiry_date
        user.fb_access_token_invalid = is_invalid
        user.save()

        # Authenticate
        view_helpers.login_as_user(user)

        rmclogger.log_event(
            rmclogger.LOG_CATEGORY_IMPRESSION,
            rmclogger.LOG_EVENT_LOGIN, {
                'new_user': False,
                'user_id': user.id,
                'type': rmclogger.LOGIN_TYPE_STRING_FACEBOOK,
            },
        )
    else:
        # New user. Sign up with their Facebook info
        now = datetime.now()
        user_obj = {
            'email': req.form.get('email'),
            'fb_access_token': fb_access_token,
            'fb_access_token_expiry_date': fb_access_token_expiry_date,
            'fbid': fbid,
            'first_name': req.form.get('first_name'),
            'friend_fbids': flask.json.loads(req.form.get('friend_fbids')),
            'gender': req.form.get('gender'),
            'join_date': now,
            'join_source': m.User.JoinSource.FACEBOOK,
            'last_name': req.form.get('last_name'),
            'last_visited': now,
            'middle_name': req.form.get('middle_name'),
        }

        referrer_id = req.form.get('referrer_id')
        if referrer_id:
            try:
                user_obj['referrer_id'] = bson.ObjectId(referrer_id)
            except bson.errors.InvalidId:
                pass

        # Create the user
        user = m.User(**user_obj)
        user.save()

        # Authenticate
        view_helpers.login_as_user(user)

        rmclogger.log_event(
            rmclogger.LOG_CATEGORY_IMPRESSION,
            rmclogger.LOG_EVENT_LOGIN, {
                'new_user': True,
                'user_id': user.id,
                'referrer_id': referrer_id,
                'type': rmclogger.LOGIN_TYPE_STRING_FACEBOOK,
            },
        )

    return ''
Exemple #47
0
    def search(params, current_user=None):
        """Search for courses based on various parameters.

        Arguments:
            params: Dict of search parameters (all optional):
                keywords: Keywords to search on
                sort_mode: Name of a sort mode. See Course.SORT_MODES. The
                    'friends_taken' sort mode defaults to 'popular' if no
                    current_user.
                direction: 1 for ascending, -1 for descending
                count: Max items to return (aka. limit)
                offset: Index of first search result to return (aka. skip)
                exclude_taken_courses: "yes" to exclude courses current_user
                    has taken.
            current_user: The user making the request.

        Returns:
            A tuple (courses, has_more):
                courses: Search results
                has_more: Whether there could be more search results
        """
        keywords = params.get('keywords')
        sort_mode = params.get('sort_mode', 'popular')
        default_direction = _SORT_MODES_BY_NAME[sort_mode]['direction']
        direction = int(params.get('direction', default_direction))
        count = int(params.get('count', 10))
        offset = int(params.get('offset', 0))
        exclude_taken_courses = (params.get('exclude_taken_courses') == "yes")

        # TODO(david): These logging things should be done asynchronously
        rmclogger.log_event(
            rmclogger.LOG_CATEGORY_COURSE_SEARCH,
            rmclogger.LOG_EVENT_SEARCH_PARAMS,
            params
        )

        filters = {}
        if keywords:
            # Clean keywords to just alphanumeric and space characters
            keywords_cleaned = re.sub(r'[^\w ]', ' ', keywords)

            def regexify_keywords(keyword):
                keyword = keyword.lower()
                return re.compile('^%s' % re.escape(keyword))

            keyword_regexes = map(regexify_keywords, keywords_cleaned.split())
            filters['_keywords__all'] = keyword_regexes

        if exclude_taken_courses:
            if current_user:
                ucs = (current_user.get_user_courses()
                        .only('course_id', 'term_id'))
                filters['id__nin'] = [
                    uc.course_id for uc in ucs
                    if not term.Term.is_shortlist_term(uc.term_id)
                ]
            else:
                logging.error('Anonymous user tried excluding taken courses')

        if sort_mode == 'friends_taken' and current_user:
            import user
            friends = user.User.objects(id__in=current_user.friend_ids).only(
                    'course_history')

            num_friends_by_course = collections.Counter()
            for friend in friends:
                num_friends_by_course.update(friend.course_ids)

            filters['id__in'] = num_friends_by_course.keys()
            existing_courses = Course.objects(**filters).only('id')
            existing_course_ids = set(c.id for c in existing_courses)
            for course_id in num_friends_by_course.keys():
                if course_id not in existing_course_ids:
                    del num_friends_by_course[course_id]

            sorted_course_count_tuples = sorted(
                num_friends_by_course.items(),
                key=lambda (_, total): total,
                reverse=direction < 0,
            )[offset:offset + count]

            sorted_course_ids = [course_id for (course_id, total)
                    in sorted_course_count_tuples]

            unsorted_courses = Course.objects(id__in=sorted_course_ids)
            course_by_id = {course.id: course for course in unsorted_courses}
            courses = [course_by_id[cid] for cid in sorted_course_ids]

        else:
            sort_options = _SORT_MODES_BY_NAME[sort_mode]

            if sort_options['is_rating']:
                suffix = 'positive' if direction < 0 else 'negative'
                order_by = '-%s.sorting_score_%s' % (sort_options['field'],
                        suffix)
            else:
                sign = '-' if direction < 0 else ''
                order_by = '%s%s' % (sign, sort_options['field'])

            unsorted_courses = Course.objects(**filters)
            sorted_courses = unsorted_courses.order_by(order_by)
            courses = sorted_courses.skip(offset).limit(count)

        has_more = len(courses) == count

        return courses, has_more
Exemple #48
0
def login():
    req = flask.request

    fbsr = req.form.get('fb_signed_request')

    # TODO(Sandy): Change log category because this isn't API?
    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_API,
        rmclogger.LOG_EVENT_LOGIN,
        {
            'fbsr': fbsr,
            'request_form': req.form,
        },
    )

    if (fbsr is None):
        raise exceptions.ImATeapot('No fbsr set')

    fb_data = facebook.get_fb_data(fbsr, app.config)
    fbid = fb_data['fbid']
    fb_access_token = fb_data['access_token']
    fb_access_token_expiry_date = fb_data['expires_on']
    is_invalid = fb_data['is_invalid']

    user = m.User.objects(fbid=fbid).first()
    if user:
        # Existing user. Update with latest FB info
        user.fb_access_token = fb_access_token
        user.fb_access_token_expiry_date = fb_access_token_expiry_date
        user.fb_access_token_invalid = is_invalid
        user.save()
        view_helpers.login_as_user(user)

        rmclogger.log_event(
            rmclogger.LOG_CATEGORY_IMPRESSION,
            rmclogger.LOG_EVENT_LOGIN,
            {
                'new_user': False,
                'user_id': user.id,
            },
        )

        return ''

    # Sign up the new user
    friend_fbids = flask.json.loads(req.form.get('friend_fbids'))
    gender = req.form.get('gender')
    first_name = req.form.get('first_name')
    middle_name = req.form.get('middle_name')
    last_name = req.form.get('last_name')
    email = req.form.get('email')

    now = datetime.now()
    user_obj = {
        'fbid': fbid,
        'first_name': first_name,
        'middle_name': middle_name,
        'last_name': last_name,
        'email': email,
        'gender': gender,
        'fb_access_token': fb_access_token,
        'fb_access_token_expiry_date': fb_access_token_expiry_date,
        # TODO(Sandy): Count visits properly
        'join_date': now,
        'join_source': m.User.JoinSource.FACEBOOK,
        'num_visits': 1,
        'last_visited': now,
        'friend_fbids': friend_fbids,
        # TODO(Sandy): Fetch from client side and pass here: name, email,
        # school, program, faculty
    }
    referrer_id = req.form.get('referrer_id')
    if referrer_id:
        try:
            user_obj['referrer_id'] = bson.ObjectId(referrer_id)
        except:
            pass

    user = m.User(**user_obj)
    user.save()
    view_helpers.login_as_user(user)

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_IMPRESSION,
        rmclogger.LOG_EVENT_LOGIN,
        {
            'new_user': True,
            'user_id': user.id,
            'referrer_id': referrer_id,
        },
    )

    return ''
Exemple #49
0
def search_courses():
    # TODO(mack): create enum of sort options
    # num_friends, num_ratings, overall, interest, easiness

    request = flask.request
    keywords = request.values.get('keywords')
    sort_mode = request.values.get('sort_mode', 'popular')
    default_direction = COURSES_SORT_MODES_BY_NAME[sort_mode]['direction']
    direction = int(request.values.get('direction', default_direction))
    count = int(request.values.get('count', 10))
    offset = int(request.values.get('offset', 0))
    exclude_taken_courses = request.values.get('exclude_taken_courses')

    current_user = view_helpers.get_current_user()

    # TODO(david): These logging things should be done asynchronously
    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_COURSE_SEARCH,
        rmclogger.LOG_EVENT_SEARCH_PARAMS,
        request.values
    )

    filters = {}
    if keywords:
        # Clean keywords to just alphanumeric and space characters
        keywords = re.sub(r'[^\w ]', ' ', keywords)

        keywords = re.sub('\s+', ' ', keywords)
        keywords = keywords.split(' ')

        def regexify_keywords(keyword):
            keyword = keyword.lower()
            return re.compile('^%s' % keyword)

        keywords = map(regexify_keywords, keywords)
        filters['_keywords__all'] = keywords

    if exclude_taken_courses == "yes":
        if current_user:
            ucs = (current_user.get_user_courses()
                    .only('course_id', 'term_id'))
            filters['id__nin'] = [
                uc.course_id for uc in ucs
                if not m.term.Term.is_shortlist_term(uc.term_id)
            ]
        else:
            logging.error('Anonymous user tried excluding taken courses')

    if sort_mode == 'friends_taken':
        # TODO(mack): should only do if user is logged in
        friends = m.User.objects(id__in=current_user.friend_ids).only(
                'course_history')
        # TODO(mack): need to majorly optimize this
        num_friends_by_course = {}
        for friend in friends:
            for course_id in friend.course_ids:
                if not course_id in num_friends_by_course:
                    num_friends_by_course[course_id] = 0
                num_friends_by_course[course_id] += 1

        filters['id__in'] = num_friends_by_course.keys()
        existing_courses = m.Course.objects(**filters).only('id')
        existing_course_ids = set(c.id for c in existing_courses)
        for course_id in num_friends_by_course.keys():
            if course_id not in existing_course_ids:
                del num_friends_by_course[course_id]

        sorted_course_count_tuples = sorted(
            num_friends_by_course.items(),
            key=lambda (_, total): total,
            reverse=direction < 0,
        )[offset:offset + count]

        sorted_course_ids = [course_id for (course_id, total)
                in sorted_course_count_tuples]

        unsorted_limited_courses = m.Course.objects(id__in=sorted_course_ids)

        limited_courses_by_id = {}
        for course in unsorted_limited_courses:
            limited_courses_by_id[course.id] = course

        limited_courses = []
        for course_id in sorted_course_ids:
            limited_courses.append(limited_courses_by_id[course_id])

    else:
        sort_options = COURSES_SORT_MODES_BY_NAME[sort_mode]

        if sort_mode in RATING_SORT_MODES:
            sort_instr = '-' + sort_options['field']
            sort_instr += "_positive" if direction < 0 else "_negative"
        else:
            sort_instr = ''
            if direction < 0:
                sort_instr = '-'
            sort_instr += sort_options['field']

        unsorted_courses = m.Course.objects(**filters)
        sorted_courses = unsorted_courses.order_by(sort_instr)
        limited_courses = sorted_courses.skip(offset).limit(count)

    has_more = len(limited_courses) == count

    course_dict_list, user_course_dict_list, user_course_list = (
            m.Course.get_course_and_user_course_dicts(
                limited_courses, current_user, include_friends=True,
                full_user_courses=False, include_sections=True))
    professor_dict_list = m.Professor.get_reduced_professors_for_courses(
            limited_courses)

    user_dict_list = []
    if current_user:
        user_ids = [uc['user_id'] for uc in user_course_dict_list
                if uc['user_id'] != current_user.id]
        users = m.User.objects(id__in=user_ids).only(*m.User.CORE_FIELDS)
        user_dict_list = [u.to_dict() for u in users]

    return util.json_dumps({
        'user_objs': user_dict_list,
        'course_objs': course_dict_list,
        'professor_objs': professor_dict_list,
        'user_course_objs': user_course_dict_list,
        'has_more': has_more,
    })
Exemple #50
0
def upload_schedule():
    req = flask.request
    user = view_helpers.get_current_user()

    schedule_data = util.json_loads(req.form.get('schedule_data'))
    processed_items = schedule_data['processed_items']
    failed_items = schedule_data['failed_items']
    term_name = schedule_data['term_name']
    term_id = m.Term.id_from_name(term_name)

    # FIXME TODO(david): Save these in models and display on schedule
    #failed_items = schedule_data['failed_items']

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_API,
        rmclogger.LOG_EVENT_SCHEDULE, {
            'schedule_data': schedule_data,
            'term_id': term_id,
            'user_id': user.id,
        },
    )

    now = datetime.now()

    user.last_good_schedule_paste = req.form.get('schedule_text')
    user.last_good_schedule_paste_date = now
    user.save()

    # Remove existing schedule items for the user for the given term
    for usi in m.UserScheduleItem.objects(user_id=user.id, term_id=term_id):
        usi.delete()

    for item in processed_items:
        try:
            # Create this UserScheduleItem
            first_name, last_name = m.Professor.guess_names(item['prof_name'])
            prof_id = m.Professor.get_id_from_name(
                first_name=first_name,
                last_name=last_name,
            )
            if first_name and last_name:
                if not m.Professor.objects.with_id(prof_id):
                    m.Professor(
                        id=prof_id,
                        first_name=first_name,
                        last_name=last_name,
                    ).save()

            usi = m.UserScheduleItem(
                user_id=user.id,
                class_num=item['class_num'],
                building=item['building'],
                room=item.get('room'),
                section_type=item['section_type'].upper(),
                section_num=item['section_num'],
                start_date=datetime.utcfromtimestamp(item['start_date']),
                end_date=datetime.utcfromtimestamp(item['end_date']),
                course_id=item['course_id'],
                prof_id=prof_id,
                term_id=term_id,
            )
            try:
                usi.save()
            except me.NotUniqueError as ex:
                # Likely the case where the user pastes in two or more valid
                # schedules into the same input box
                logging.info('Duplicate error on UserScheduleItem .save(): %s'
                        % (ex))

            # Add this item to the user's course history
            # FIXME(Sandy): See if we can get program_year_id from Quest
            # Or just increment their last one
            user.add_course(usi.course_id, usi.term_id)

        except KeyError:
            logging.error("Invalid item in uploaded schedule: %s" % (item))

    # Add courses that failed to fully parse, probably due to unavailable times
    for course_id in set(failed_items):
        fsi = m.FailedScheduleItem(
            user_id=user.id,
            course_id=course_id,
            parsed_date=now,
        )

        try:
            fsi.save()
        except me.NotUniqueError as ex:
            # This should never happen since we're iterating over a set
            logging.warn('WTF this should never happen.')
            logging.warn('Duplicate error FailedScheduleItem.save(): %s' % ex)

        user.add_course(course_id, term_id)

    user.schedules_imported += 1
    user.save()

    schedule_screenshot.update_screenshot_async(user)

    rmclogger.log_event(
        rmclogger.LOG_CATEGORY_SCHEDULE,
        rmclogger.LOG_EVENT_UPLOAD,
        user.id
    )

    return ''