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, )
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', )
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, )
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
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 ''
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})
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 })
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, )
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, })
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')
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')
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 ''
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 ''
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 ''
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})
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 ''
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, )
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('/')
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 ''
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'), )
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 ''
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
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'), )
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', )
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 ''
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 ''
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
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, })
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, )
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, )
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 ''
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, })
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, })
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 ''
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
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, })
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 ''