class CritiqueCourse(me.Document): meta = { 'indexes': [ 'course_id', 'professor_id', ], } # id = me.ObjectIdField(primary_key=True) course_id = me.StringField(required=True) # TODO(mack): need section_id or equiv # course_id = me.StringField(required=True, unique_with='section_id') # section_id = me.IntField(required=True) professor_id = me.StringField(required=True) term_id = me.StringField(required=True) interest = me.EmbeddedDocumentField(rating.AggregateRating, default=rating.AggregateRating()) easiness = me.EmbeddedDocumentField(rating.AggregateRating, default=rating.AggregateRating()) overall_course = me.EmbeddedDocumentField(rating.AggregateRating, default=rating.AggregateRating()) clarity = me.EmbeddedDocumentField(rating.AggregateRating, default=rating.AggregateRating()) passion = me.EmbeddedDocumentField(rating.AggregateRating, default=rating.AggregateRating()) overall_prof = me.EmbeddedDocumentField(rating.AggregateRating, default=rating.AggregateRating())
def update_redis_ratings_for_course(self, course_id, changes): # TOOO(mack): use redis pipeline for this for change in changes: rating_name = change['name'] agg_rating = self.get_course_rating_from_redis( course_id, rating_name) if not agg_rating: agg_rating = _rating.AggregateRating() agg_rating.update_aggregate_after_replacement( change['old'], change['new']) self.set_course_rating_in_redis(course_id, rating_name, agg_rating)
def get_course_rating_from_redis(self, course_id, rating_name): rating_json = r.get( self.get_professor_course_redis_key(course_id, rating_name)) if rating_json: rating_loaded = json_util.loads(rating_json) return _rating.AggregateRating( rating=rating_loaded['rating'], count=rating_loaded['count'], ) return None
class Professor(me.Document): meta = { 'indexes': [ 'clarity.rating', 'clarity.count', 'easiness.rating', 'easiness.count', 'passion.rating', 'passion.count', ], } #FIXME(Sandy): Becker actually shows up as byron_becker # eg. byron_weber_becker id = me.StringField(primary_key=True) # TODO(mack): available in menlo data # department_id = me.StringField() # eg. Byron Weber first_name = me.StringField(required=True) # eg. Becker last_name = me.StringField(required=True) # eg. ['MATH', 'CS'] departments_taught = me.ListField(me.StringField()) clarity = me.EmbeddedDocumentField(_rating.AggregateRating, default=_rating.AggregateRating()) easiness = me.EmbeddedDocumentField(_rating.AggregateRating, default=_rating.AggregateRating()) passion = me.EmbeddedDocumentField(_rating.AggregateRating, default=_rating.AggregateRating()) @classmethod def get_id_from_name(cls, first_name, last_name=None): if not last_name: return re.sub(r'\s+', '_', first_name.lower()) first_name = first_name.lower() last_name = last_name.lower() return re.sub(r'\s+', '_', '%s %s' % (first_name, last_name)) @staticmethod def guess_names(combined_name): """Returns first, last name given a string.""" names = re.split(r'\s+', combined_name) return (' '.join(names[:-1]), names[-1]) @property def name(self): return '%s %s' % (self.first_name, self.last_name) def save(self, *args, **kwargs): if not self.id: self.id = Professor.get_id_from_name(self.first_name, self.last_name) super(Professor, self).save(*args, **kwargs) def get_ratings(self): ratings_dict = { 'clarity': self.clarity.to_dict(), 'easiness': self.easiness.to_dict(), 'passion': self.passion.to_dict(), } ratings_dict['overall'] = _rating.get_overall_rating( ratings_dict.values()).to_dict() return util.dict_to_list(ratings_dict) # TODO(mack): redis key should be namespaced under course_professor # or something.... # TODO(mack): store all ratings under single hash which is # supposed to be more memory efficient (and probably faster # fetching as well) # example course_id is math117 def get_professor_course_redis_key(self, course_id, rating_name): return ':'.join([course_id, self.id, rating_name]) def set_course_rating_in_redis(self, course_id, rating_name, aggregate_rating): redis_key = self.get_professor_course_redis_key(course_id, rating_name) r.set(redis_key, aggregate_rating.to_json()) def get_course_rating_from_redis(self, course_id, rating_name): rating_json = r.get( self.get_professor_course_redis_key(course_id, rating_name)) if rating_json: rating_loaded = json_util.loads(rating_json) return _rating.AggregateRating( rating=rating_loaded['rating'], count=rating_loaded['count'], ) return None def update_redis_ratings_for_course(self, course_id, changes): # TOOO(mack): use redis pipeline for this for change in changes: rating_name = change['name'] agg_rating = self.get_course_rating_from_redis( course_id, rating_name) if not agg_rating: agg_rating = _rating.AggregateRating() agg_rating.update_aggregate_after_replacement( change['old'], change['new']) self.set_course_rating_in_redis(course_id, rating_name, agg_rating) # TODO(david): This should go on ProfCourse def get_ratings_for_course(self, course_id): rating_dict = {} for name in ['clarity', 'easiness', 'passion']: agg_rating = self.get_course_rating_from_redis(course_id, name) if agg_rating: rating_dict[name] = agg_rating.to_dict() rating_dict['overall'] = _rating.get_overall_rating( rating_dict.values()).to_dict() return util.dict_to_list(rating_dict) def get_ratings_for_career(self): """Returns an aggregate of all the ratings for a prof""" courses_taught = self.get_courses_taught() clarity = 0 clarity_count = 0 passion = 0 passion_count = 0 for c in courses_taught: ratings = self.get_ratings_for_course(c) for r in ratings: if r.get('name') == 'clarity': clarity += round(r.get('count') * r.get('rating')) clarity_count += r.get('count') elif r.get('name') == 'passion': passion += round(r.get('count') * r.get('rating')) passion_count += r.get('count') overall_count = clarity_count + passion_count overall = clarity + passion return [{ 'count': clarity_count, 'name': 'clarity', 'rating': safe_division(clarity, clarity_count) }, { 'count': passion_count, 'name': 'passion', 'rating': safe_division(passion, passion_count) }, { 'count': overall_count, 'name': 'overall', 'rating': safe_division(overall, overall_count) }] @classmethod def get_reduced_professors_for_courses(cls, courses): professor_ids = set() for course in courses: professor_ids = professor_ids.union(course.professor_ids) professors = cls.objects(id__in=professor_ids).only( 'first_name', 'last_name') return [p.to_dict() for p in professors] @classmethod def get_full_professors_for_course(cls, course, current_user): professors = cls.objects(id__in=course.professor_ids) return [ p.to_dict(course_id=course.id, current_user=current_user) for p in professors ] def get_reviews_for_course(self, course_id, current_user=None): ucs = user_course.get_reviews_for_course_prof(course_id, self.id) # Quality filter. # TODO(david): Eventually do this in mongo query or enforce quality # metrics on front-end ucs = filter( lambda uc: len(uc.professor_review.comment) >= _review. ProfessorReview.MIN_REVIEW_LENGTH, ucs) prof_review_dicts = [ uc.professor_review.to_dict(current_user, getattr(uc, 'user_id', None)) for uc in ucs ] # Try to not show older reviews, if we have enough results date_getter = lambda review: review['comment_date'] prof_review_dicts = util.publicly_visible_ratings_and_reviews_filter( prof_review_dicts, date_getter, util.MIN_NUM_REVIEWS) return prof_review_dicts def get_reviews_for_self(self): """Returns all reviews for a prof, over all courses taught""" menlo_reviews = user_course.MenloCourse.objects( professor_id=self.id, ).only('professor_review', 'course_id') user_reviews = user_course.UserCourse.objects( professor_id=self.id, ).only('professor_review', 'user_id', 'term_id', 'course_id') return itertools.chain(menlo_reviews, user_reviews) def get_reviews_for_all_courses(self, current_user): """Returns all reviews for a prof as a dict, organized by course id""" courses_taught = self.get_courses_taught() course_reviews = [] for course in courses_taught: course_reviews.append({ 'course_id': course, 'reviews': self.get_reviews_for_course(course, current_user) }) return course_reviews def get_courses_taught(self): """Returns an array of course_id's for each course the prof taught""" ucs = self.get_reviews_for_self() ucs = filter( lambda uc: len(uc.professor_review.comment) >= _review. ProfessorReview.MIN_REVIEW_LENGTH, ucs) courses_taught = set(uc['course_id'] for uc in ucs) return sorted(courses_taught) def get_departments_taught(self): """Returns an array of the departments the prof has taught in""" ucs = self.get_reviews_for_self() ucs = filter( lambda uc: len(uc.professor_review.comment) >= _review. ProfessorReview.MIN_REVIEW_LENGTH, ucs) departments_taught = set( _COURSE_NAME_REGEX.match(uc['course_id']).group(1).upper() for uc in ucs) return sorted(departments_taught) def to_dict(self, course_id=None, current_user=None): dict_ = { 'id': self.id, #'first_name': self.first_name, #'last_name': self.last_name, #'ratings': self.get_ratings(), 'name': self.name, } if course_id: ratings = self.get_ratings_for_course(course_id) reviews = self.get_reviews_for_course(course_id, current_user) dict_.update({ 'course_ratings': ratings, 'course_reviews': reviews, }) return dict_
class Course(me.Document): meta = { 'indexes': [ '_keywords', 'interest.rating', 'interest.count', 'easiness.rating', 'easiness.count', 'usefulness.rating', 'usefulness.count', 'overall.rating', 'overall.count', ], } # eg. earth121l id = me.StringField(primary_key=True) # eg. earth department_id = me.StringField(required=True) # eg. 121l number = me.StringField(required=True) # eg. Introductory Earth Sciences Laboratory 1 name = me.StringField(required=True) # Description about the course description = me.StringField(required=True) easiness = me.EmbeddedDocumentField(rating.AggregateRating, default=rating.AggregateRating()) interest = me.EmbeddedDocumentField(rating.AggregateRating, default=rating.AggregateRating()) usefulness = me.EmbeddedDocumentField(rating.AggregateRating, default=rating.AggregateRating()) # TODO(mack): deprecate overall rating overall = me.EmbeddedDocumentField(rating.AggregateRating, default=rating.AggregateRating()) professor_ids = me.ListField(me.StringField()) antireqs = me.StringField() coreqs = me.StringField() prereqs = me.StringField() # NOTE: The word term is overloaded based on where it's used. Here, it mean # which terms of the year is the course being offered? # NOTE: THIS FIELD IS ***DEPRECATED***, because the data source we get # info about this is not reliable. There may not exist such reliable # data at all -- course offerings are decided on an annual basis. # TODO(david): Remove this field and replace it with info from sections. # e.g. ['01', '05', '09'] terms_offered = me.ListField(me.StringField()) # eg. ['earth', '121l', 'earth121l', 'Introductory', # 'Earth' 'Sciences', 'Laboratory', '1'] _keywords = me.ListField(me.StringField(), required=True) SORT_MODES = _SORT_MODES @property def code(self): matches = re.findall(r'^([a-z]+)(.*)$', self.id)[0] department = matches[0] number = matches[1] return '%s %s' % (department.upper(), number.upper()) def save(self, *args, **kwargs): if not self.id: # id should not be set during first save self.id = self.department_id + self.number super(Course, self).save(*args, **kwargs) def get_ratings(self): # Ordered for consistency with CourseReview.rating_fields; see #109. return collections.OrderedDict([ ('usefulness', self.usefulness.to_dict()), ('easiness', self.easiness.to_dict()), ('interest', self.interest.to_dict()), ]) def get_reviews(self, current_user=None, user_courses=None): """Return a list of all user reviews ("tips") about this course. Does not include professor reviews. Arguments: current_user: The current user. Used for revealing more author information if possible (eg. reviews written by friends who allow their friends to know that they wrote it). user_courses: An optional list of all user_courses that's associated with this course to speed up this function. """ if not user_courses: limit_fields = ['course_id', 'user_id', 'course_review'] user_courses = _user_course.UserCourse.objects( course_id=self.id).only(*limit_fields) reviews = [] for uc in user_courses: if (len(uc.course_review.comment) < review.CourseReview.MIN_REVIEW_LENGTH): continue reviews.append(uc.course_review.to_dict(current_user, uc.user_id, uc.id)) # Filter out old reviews if we have enough results. date_getter = lambda review: review['comment_date'] reviews = util.publicly_visible_ratings_and_reviews_filter( reviews, date_getter, util.MIN_NUM_REVIEWS) return reviews # TODO(mack): this function is way too overloaded, even to separate into # multiple functions based on usage @classmethod def get_course_and_user_course_dicts(cls, courses, current_user, include_friends=False, include_all_users=False, full_user_courses=False, include_sections=False): limited_user_course_fields = [ 'program_year_id', 'term_id', 'user_id', 'course_id'] course_dicts = [course.to_dict() for course in courses] course_ids = [c['id'] for c in course_dicts] if include_sections: for course_dict in course_dicts: # By default, we'll send down section info for current and next # term for each course we return. sections = section.Section.get_for_course_and_recent_terms( course_dict['id']) course_dict['sections'] = [s.to_dict() for s in sections] ucs = [] if not current_user: if include_all_users: ucs = _user_course.UserCourse.objects( course_id__in=course_ids) if not full_user_courses: ucs.only(*limited_user_course_fields) ucs = list(ucs) uc_dicts = [uc.to_dict() for uc in ucs] return course_dicts, uc_dicts, ucs else: return course_dicts, [], [] uc_dicts = [] if include_all_users or include_friends: query = { 'course_id__in': course_ids, } # If we're just including friends if not include_all_users: query['user_id__in'] = current_user.friend_ids if full_user_courses: if not include_all_users: query.setdefault('user_id__in', []).append(current_user.id) ucs = list(_user_course.UserCourse.objects(**query)) uc_dicts = [uc.to_dict() for uc in ucs] else: ucs = list(_user_course.UserCourse.objects(**query).only( *limited_user_course_fields)) friend_uc_fields = ['id', 'user_id', 'course_id', 'term_id', 'term_name'] uc_dicts = [uc.to_dict(friend_uc_fields) for uc in ucs] # TODO(mack): optimize to not always get full user course # for current_user current_ucs = list(_user_course.UserCourse.objects( user_id=current_user.id, course_id__in=course_ids, id__nin=[uc_dict['id'] for uc_dict in uc_dicts], )) ucs += current_ucs uc_dicts += [uc.to_dict() for uc in current_ucs] current_user_course_by_course = {} friend_user_courses_by_course = {} current_friends_set = set(current_user.friend_ids) current_user_course_ids = set(current_user.course_history) for uc_dict in uc_dicts: if uc_dict['id'] in current_user_course_ids: current_user_course_by_course[uc_dict['course_id']] = uc_dict elif include_friends: if uc_dict['user_id'] in current_friends_set: friend_user_courses_by_course.setdefault( uc_dict['course_id'], []).append(uc_dict) for course_dict in course_dicts: current_uc = current_user_course_by_course.get( course_dict['id']) current_uc_id = current_uc['id'] if current_uc else None course_dict['user_course_id'] = current_uc_id if include_friends: friend_ucs = friend_user_courses_by_course.get( course_dict['id'], []) friend_uc_ids = [uc['id'] for uc in friend_ucs] course_dict['friend_user_course_ids'] = friend_uc_ids return course_dicts, uc_dicts, ucs @staticmethod def code_to_id(course_code): return "".join(course_code.split()).lower() @staticmethod 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 to_dict(self): """Returns information about a course to be sent down an API. Args: course: The course object. """ return { 'id': self.id, 'code': self.code, 'name': self.name, 'description': self.description, # TODO(mack): create user models for friends #'friends': [1647810326, 518430508, 541400376], 'ratings': util.dict_to_list(self.get_ratings()), 'overall': self.overall.to_dict(), 'professor_ids': self.professor_ids, 'prereqs': self.prereqs, } def __repr__(self): return "<Course: %s>" % self.code
class Course(me.Document): meta = { 'indexes': [ '_keywords', 'interest.rating', 'interest.count', 'easiness.rating', 'easiness.count', 'usefulness.rating', 'usefulness.count', 'overall.rating', 'overall.count', ], } # eg. earth121l id = me.StringField(primary_key=True) # eg. earth department_id = me.StringField(required=True) # eg. 121l number = me.StringField(required=True) # eg. Introductory Earth Sciences Laboratory 1 name = me.StringField(required=True) # Description about the course description = me.StringField(required=True) easiness = me.EmbeddedDocumentField(rating.AggregateRating, default=rating.AggregateRating()) interest = me.EmbeddedDocumentField(rating.AggregateRating, default=rating.AggregateRating()) usefulness = me.EmbeddedDocumentField(rating.AggregateRating, default=rating.AggregateRating()) # TODO(mack): deprecate overall rating overall = me.EmbeddedDocumentField(rating.AggregateRating, default=rating.AggregateRating()) professor_ids = me.ListField(me.StringField()) antireqs = me.StringField() coreqs = me.StringField() prereqs = me.StringField() # NOTE: The word term is overloaded based on where it's used. Here, it mean # which terms of the year is the course being offered? # NOTE: THIS FIELD IS ***DEPRECATED***, because the data source we get # info about this is not reliable. There may not exist such reliable # data at all -- course offerings are decided on an annual basis. # TODO(david): Remove this field and replace it with info from sections. # e.g. ['01', '05', '09'] terms_offered = me.ListField(me.StringField()) # eg. ['earth', '121l', 'earth121l', 'Introductory', # 'Earth' 'Sciences', 'Laboratory', '1'] _keywords = me.ListField(me.StringField(), required=True) @property def code(self): matches = re.findall(r'^([a-z]+)(.*)$', self.id)[0] department = matches[0] number = matches[1] return '%s %s' % (department.upper(), number.upper()) def save(self, *args, **kwargs): if not self.id: # id should not be set during first save self.id = self.department_id + self.number super(Course, self).save(*args, **kwargs) def get_ratings(self): return { 'interest': self.interest.to_dict(), 'usefulness': self.usefulness.to_dict(), 'easiness': self.easiness.to_dict(), } def get_reviews(self, current_user=None, user_courses=None): """Return a list of all user reviews ("tips") about this course. Does not include professor reviews. Arguments: current_user: The current user. Used for revealing more author information if possible (eg. reviews written by friends who allow their friends to know that they wrote it). user_courses: An optional list of all user_courses that's associated with this course to speed up this function. """ if not user_courses: limit_fields = ['course_id', 'user_id', 'course_review'] user_courses = _user_course.UserCourse.objects( course_id=self.id).only(*limit_fields) reviews = [] for uc in user_courses: if (len(uc.course_review.comment) < review.CourseReview.MIN_REVIEW_LENGTH): continue reviews.append(uc.course_review.to_dict(current_user, uc.user_id)) # Filter out old reviews if we have enough results. date_getter = lambda review: review['comment_date'] reviews = util.publicly_visible_ratings_and_reviews_filter( reviews, date_getter, util.MIN_NUM_REVIEWS) return reviews # TODO(mack): this function is way too overloaded, even to separate into # multiple functions based on usage @classmethod def get_course_and_user_course_dicts(cls, courses, current_user, include_friends=False, include_all_users=False, full_user_courses=False, include_sections=False): limited_user_course_fields = [ 'program_year_id', 'term_id', 'user_id', 'course_id' ] course_dicts = [course.to_dict() for course in courses] course_ids = [c['id'] for c in course_dicts] if include_sections: for course_dict in course_dicts: # By default, we'll send down section info for current and next # term for each course we return. sections = section.Section.get_for_course_and_recent_terms( course_dict['id']) course_dict['sections'] = [s.to_dict() for s in sections] ucs = [] if not current_user: if include_all_users: ucs = _user_course.UserCourse.objects(course_id__in=course_ids) if not full_user_courses: ucs.only(*limited_user_course_fields) ucs = list(ucs) uc_dicts = [uc.to_dict() for uc in ucs] return course_dicts, uc_dicts, ucs else: return course_dicts, [], [] uc_dicts = [] if include_all_users or include_friends: query = { 'course_id__in': course_ids, } # If we're just including friends if not include_all_users: query['user_id__in'] = current_user.friend_ids if full_user_courses: if not include_all_users: query.setdefault('user_id__in', []).append(current_user.id) ucs = list(_user_course.UserCourse.objects(**query)) uc_dicts = [uc.to_dict() for uc in ucs] else: ucs = list( _user_course.UserCourse.objects(**query).only( *limited_user_course_fields)) friend_uc_fields = [ 'id', 'user_id', 'course_id', 'term_id', 'term_name' ] uc_dicts = [uc.to_dict(friend_uc_fields) for uc in ucs] # TODO(mack): optimize to not always get full user course # for current_user current_ucs = list( _user_course.UserCourse.objects( user_id=current_user.id, course_id__in=course_ids, id__nin=[uc_dict['id'] for uc_dict in uc_dicts], )) ucs += current_ucs uc_dicts += [uc.to_dict() for uc in current_ucs] current_user_course_by_course = {} friend_user_courses_by_course = {} current_friends_set = set(current_user.friend_ids) current_user_course_ids = set(current_user.course_history) for uc_dict in uc_dicts: if uc_dict['id'] in current_user_course_ids: current_user_course_by_course[uc_dict['course_id']] = uc_dict elif include_friends: if uc_dict['user_id'] in current_friends_set: friend_user_courses_by_course.setdefault( uc_dict['course_id'], []).append(uc_dict) for course_dict in course_dicts: current_uc = current_user_course_by_course.get(course_dict['id']) current_uc_id = current_uc['id'] if current_uc else None course_dict['user_course_id'] = current_uc_id if include_friends: friend_ucs = friend_user_courses_by_course.get( course_dict['id'], []) friend_uc_ids = [uc['id'] for uc in friend_ucs] course_dict['friend_user_course_ids'] = friend_uc_ids return course_dicts, uc_dicts, ucs @staticmethod def code_to_id(course_code): return "".join(course_code.split()).lower() def to_dict(self): """Returns information about a course to be sent down an API. Args: course: The course object. """ return { 'id': self.id, 'code': self.code, 'name': self.name, 'description': self.description, # TODO(mack): create user models for friends #'friends': [1647810326, 518430508, 541400376], 'ratings': util.dict_to_list(self.get_ratings()), 'overall': self.overall.to_dict(), 'professor_ids': self.professor_ids, 'prereqs': self.prereqs, } def __repr__(self): return "<Course: %s>" % self.code
class Professor(me.Document): meta = { 'indexes': [ 'clarity.rating', 'clarity.count', 'easiness.rating', 'easiness.count', 'passion.rating', 'passion.count', ], } #FIXME(Sandy): Becker actually shows up as byron_becker # eg. byron_weber_becker id = me.StringField(primary_key=True) # TODO(mack): available in menlo data # department_id = me.StringField() # eg. Byron Weber first_name = me.StringField(required=True) # eg. Becker last_name = me.StringField(required=True) clarity = me.EmbeddedDocumentField(_rating.AggregateRating, default=_rating.AggregateRating()) easiness = me.EmbeddedDocumentField(_rating.AggregateRating, default=_rating.AggregateRating()) passion = me.EmbeddedDocumentField(_rating.AggregateRating, default=_rating.AggregateRating()) @classmethod def get_id_from_name(cls, first_name, last_name=None): if not last_name: return re.sub(r'\s+', '_', first_name.lower()) first_name = first_name.lower() last_name = last_name.lower() return re.sub(r'\s+', '_', '%s %s' % (first_name, last_name)) @staticmethod def guess_names(combined_name): """Returns first, last name given a string.""" names = re.split(r'\s+', combined_name) return (' '.join(names[:-1]), names[-1]) @property def name(self): return '%s %s' % (self.first_name, self.last_name) def save(self, *args, **kwargs): if not self.id: self.id = Professor.get_id_from_name(self.first_name, self.last_name) super(Professor, self).save(*args, **kwargs) def get_ratings(self): ratings_dict = { 'clarity': self.clarity.to_dict(), 'easiness': self.easiness.to_dict(), 'passion': self.passion.to_dict(), } ratings_dict['overall'] = _rating.get_overall_rating( ratings_dict.values()).to_dict() return util.dict_to_list(ratings_dict) # TODO(mack): redis key should be namespaced under course_professor # or something.... # TODO(mack): store all ratings under single hash which is # supposed to be more memory efficient (and probably faster # fetching as well) def get_professor_course_redis_key(self, course_id, rating_name): return ':'.join([course_id, self.id, rating_name]) def set_course_rating_in_redis(self, course_id, rating_name, aggregate_rating): redis_key = self.get_professor_course_redis_key(course_id, rating_name) r.set(redis_key, aggregate_rating.to_json()) def get_course_rating_from_redis(self, course_id, rating_name): rating_json = r.get( self.get_professor_course_redis_key(course_id, rating_name)) if rating_json: rating_loaded = json_util.loads(rating_json) return _rating.AggregateRating( rating=rating_loaded['rating'], count=rating_loaded['count'], ) return None def update_redis_ratings_for_course(self, course_id, changes): # TOOO(mack): use redis pipeline for this for change in changes: rating_name = change['name'] agg_rating = self.get_course_rating_from_redis( course_id, rating_name) if not agg_rating: agg_rating = _rating.AggregateRating() agg_rating.update_aggregate_after_replacement( change['old'], change['new']) self.set_course_rating_in_redis(course_id, rating_name, agg_rating) # TODO(david): This should go on ProfCourse def get_ratings_for_course(self, course_id): rating_dict = {} for name in ['clarity', 'easiness', 'passion']: agg_rating = self.get_course_rating_from_redis(course_id, name) if agg_rating: rating_dict[name] = agg_rating.to_dict() rating_dict['overall'] = _rating.get_overall_rating( rating_dict.values()).to_dict() return util.dict_to_list(rating_dict) @classmethod def get_reduced_professors_for_courses(cls, courses): professor_ids = set() for course in courses: professor_ids = professor_ids.union(course.professor_ids) professors = cls.objects(id__in=professor_ids).only( 'first_name', 'last_name') return [p.to_dict() for p in professors] @classmethod def get_full_professors_for_course(cls, course, current_user): professors = cls.objects(id__in=course.professor_ids) return [ p.to_dict(course_id=course.id, current_user=current_user) for p in professors ] def get_reviews_for_course(self, course_id, current_user=None): ucs = user_course.get_reviews_for_course_prof(course_id, self.id) # Quality filter. # TODO(david): Eventually do this in mongo query or enforce quality # metrics on front-end ucs = filter( lambda uc: len(uc.professor_review.comment) >= _review. ProfessorReview.MIN_REVIEW_LENGTH, ucs) prof_review_dicts = [ uc.professor_review.to_dict(current_user, getattr(uc, 'user_id', None)) for uc in ucs ] # Try to not show older reviews, if we have enough results date_getter = lambda review: review['comment_date'] prof_review_dicts = util.publicly_visible_ratings_and_reviews_filter( prof_review_dicts, date_getter, util.MIN_NUM_REVIEWS) return prof_review_dicts def to_dict(self, course_id=None, current_user=None): dict_ = { 'id': self.id, #'first_name': self.first_name, #'last_name': self.last_name, #'ratings': self.get_ratings(), 'name': self.name, } if course_id: ratings = self.get_ratings_for_course(course_id) reviews = self.get_reviews_for_course(course_id, current_user) dict_.update({ 'course_ratings': ratings, 'course_reviews': reviews, }) return dict_