Esempio n. 1
0
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())
Esempio n. 2
0
    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)
Esempio n. 3
0
    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
Esempio n. 4
0
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_
Esempio n. 5
0
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
Esempio n. 6
0
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
Esempio n. 7
0
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_