class GradedAssignment(models.Model): """ Model representing a single launch of a graded assignment by an individual user. There will be a row created here only if the LTI consumer may require a result to be returned from the LTI launch (determined by the presence of the lis_result_sourcedid parameter in the launch POST). There will be only one row created for a given usage/consumer combination; repeated launches of the same content by the same user from the same LTI consumer will not add new rows to the table. Some LTI-specified fields use the prefix lis_; this refers to the IMS Learning Information Services standard from which LTI inherits some properties """ user = models.ForeignKey(User, db_index=True) course_key = CourseKeyField(max_length=255, db_index=True) usage_key = UsageKeyField(max_length=255, db_index=True) outcome_service = models.ForeignKey(OutcomeService) lis_result_sourcedid = models.CharField(max_length=255, db_index=True) version_number = models.IntegerField(default=0) class Meta(object): """ Uniqueness constraints. """ unique_together = ('outcome_service', 'lis_result_sourcedid')
class PersistentSubsectionGrade(TimeStampedModel): """ A django model tracking persistent grades at the subsection level. """ class Meta(object): unique_together = [ # * Specific grades can be pulled using all three columns, # * Progress page can pull all grades for a given (course_id, user_id) # * Course staff can see all grades for a course using (course_id,) ('course_id', 'user_id', 'usage_key'), ] # primary key will need to be large for this table id = UnsignedBigIntAutoField(primary_key=True) # pylint: disable=invalid-name # uniquely identify this particular grade object user_id = models.IntegerField(blank=False) course_id = CourseKeyField(blank=False, max_length=255) usage_key = UsageKeyField(blank=False, max_length=255) # Information relating to the state of content when grade was calculated subtree_edited_timestamp = models.DateTimeField( 'last content edit timestamp', blank=False) course_version = models.CharField('guid of latest course version', blank=True, max_length=255) # earned/possible refers to the number of points achieved and available to achieve. # graded refers to the subset of all problems that are marked as being graded. earned_all = models.FloatField(blank=False) possible_all = models.FloatField(blank=False) earned_graded = models.FloatField(blank=False) possible_graded = models.FloatField(blank=False) # track which blocks were visible at the time of grade calculation visible_blocks = models.ForeignKey(VisibleBlocks, db_column='visible_blocks_hash', to_field='hashed') # use custom manager objects = PersistentSubsectionGradeQuerySet.as_manager() def __unicode__(self): """ Returns a string representation of this model. """ return u"{} user: {}, course version: {}, subsection {} ({}). {}/{} graded, {}/{} all".format( type(self).__name__, self.user_id, self.course_version, self.usage_key, self.visible_blocks_id, self.earned_graded, self.possible_graded, self.earned_all, self.possible_all, ) @classmethod def save_grade(cls, **kwargs): """ Wrapper for create_grade or update_grade, depending on which applies. Takes the same arguments as both of those methods. """ user_id = kwargs.pop('user_id') usage_key = kwargs.pop('usage_key') try: with transaction.atomic(): grade, is_created = cls.objects.get_or_create( user_id=user_id, course_id=usage_key.course_key, usage_key=usage_key, defaults=kwargs, ) log.info( u"Persistent Grades: Grade model saved: {0}".format(grade)) except IntegrityError: cls.update_grade(user_id=user_id, usage_key=usage_key, **kwargs) log.warning( u"Persistent Grades: Integrity error trying to save grade for user: {0}, usage key: {1}, defaults: {2}" .format(user_id, usage_key, **kwargs)) else: if not is_created: grade.update(**kwargs) @classmethod def read_grade(cls, user_id, usage_key): """ Reads a grade from database Arguments: user_id: The user associated with the desired grade usage_key: The location of the subsection associated with the desired grade Raises PersistentSubsectionGrade.DoesNotExist if applicable """ return cls.objects.get( user_id=user_id, course_id=usage_key. course_key, # course_id is included to take advantage of db indexes usage_key=usage_key, ) @classmethod def update_grade( cls, user_id, usage_key, course_version, subtree_edited_timestamp, earned_all, possible_all, earned_graded, possible_graded, visible_blocks, ): """ Updates a previously existing grade. This is distinct from update() in that `grade.update()` operates on an existing grade object, while this is a classmethod that pulls the grade from the database, and then updates it. If you already have a grade object, use the update() method on that object to avoid an extra round-trip to the database. Use this classmethod if all you have are a user and the usage key of an existing grade. Requires all the arguments listed in docstring for create_grade """ grade = cls.read_grade( user_id=user_id, usage_key=usage_key, ) grade.update( course_version=course_version, subtree_edited_timestamp=subtree_edited_timestamp, earned_all=earned_all, possible_all=possible_all, earned_graded=earned_graded, possible_graded=possible_graded, visible_blocks=visible_blocks, ) def update( self, course_version, subtree_edited_timestamp, earned_all, possible_all, earned_graded, possible_graded, visible_blocks, ): """ Modify an existing PersistentSubsectionGrade object, saving the new version. """ visible_blocks_hash = VisibleBlocks.objects.hash_from_blockrecords( BlockRecordList.from_list(visible_blocks)) self.course_version = course_version or "" self.subtree_edited_timestamp = subtree_edited_timestamp self.earned_all = earned_all self.possible_all = possible_all self.earned_graded = earned_graded self.possible_graded = possible_graded self.visible_blocks_id = visible_blocks_hash # pylint: disable=attribute-defined-outside-init self.save() log.info(u"Persistent Grades: Grade model updated: {0}".format(self))
class CourseOverview(TimeStampedModel): """ Model for storing and caching basic information about a course. This model contains basic course metadata such as an ID, display name, image URL, and any other information that would be necessary to display a course as part of a user dashboard or enrollment API. """ # IMPORTANT: Bump this whenever you modify this model and/or add a migration. VERSION = 1 # Cache entry versioning. version = IntegerField() # Course identification id = CourseKeyField(db_index=True, primary_key=True, max_length=255) # pylint: disable=invalid-name _location = UsageKeyField(max_length=255) display_name = TextField(null=True) display_number_with_default = TextField() display_org_with_default = TextField() # Start/end dates start = DateTimeField(null=True) end = DateTimeField(null=True) advertised_start = TextField(null=True) # URLs course_image_url = TextField() facebook_url = TextField(null=True) social_sharing_url = TextField(null=True) end_of_course_survey_url = TextField(null=True) # Certification data certificates_display_behavior = TextField(null=True) certificates_show_before_end = BooleanField() cert_html_view_enabled = BooleanField() has_any_active_web_certificate = BooleanField() cert_name_short = TextField() cert_name_long = TextField() # Grading lowest_passing_grade = DecimalField(max_digits=5, decimal_places=2, null=True) # Access parameters days_early_for_beta = FloatField(null=True) mobile_available = BooleanField() visible_to_staff_only = BooleanField() _pre_requisite_courses_json = TextField( ) # JSON representation of list of CourseKey strings # Enrollment details enrollment_start = DateTimeField(null=True) enrollment_end = DateTimeField(null=True) enrollment_domain = TextField(null=True) invitation_only = BooleanField(default=False) max_student_enrollments_allowed = IntegerField(null=True) @classmethod def _create_from_course(cls, course): """ Creates a CourseOverview object from a CourseDescriptor. Does not touch the database, simply constructs and returns an overview from the given course. Arguments: course (CourseDescriptor): any course descriptor object Returns: CourseOverview: overview extracted from the given course """ from lms.djangoapps.certificates.api import get_active_web_certificate from lms.djangoapps.courseware.courses import course_image_url # Workaround for a problem discovered in https://openedx.atlassian.net/browse/TNL-2806. # If the course has a malformed grading policy such that # course._grading_policy['GRADE_CUTOFFS'] = {}, then # course.lowest_passing_grade will raise a ValueError. # Work around this for now by defaulting to None. try: lowest_passing_grade = course.lowest_passing_grade except ValueError: lowest_passing_grade = None display_name = course.display_name start = course.start end = course.end if isinstance(course.id, CCXLocator): from ccx.utils import get_ccx_from_ccx_locator # pylint: disable=import-error ccx = get_ccx_from_ccx_locator(course.id) display_name = ccx.display_name start = ccx.start end = ccx.due return cls( version=cls.VERSION, id=course.id, _location=course.location, display_name=display_name, display_number_with_default=course.display_number_with_default, display_org_with_default=course.display_org_with_default, start=start, end=end, advertised_start=course.advertised_start, course_image_url=course_image_url(course), facebook_url=course.facebook_url, social_sharing_url=course.social_sharing_url, certificates_display_behavior=course.certificates_display_behavior, certificates_show_before_end=course.certificates_show_before_end, cert_html_view_enabled=course.cert_html_view_enabled, has_any_active_web_certificate=(get_active_web_certificate(course) is not None), cert_name_short=course.cert_name_short, cert_name_long=course.cert_name_long, lowest_passing_grade=lowest_passing_grade, end_of_course_survey_url=course.end_of_course_survey_url, days_early_for_beta=course.days_early_for_beta, mobile_available=course.mobile_available, visible_to_staff_only=course.visible_to_staff_only, _pre_requisite_courses_json=json.dumps( course.pre_requisite_courses), enrollment_start=course.enrollment_start, enrollment_end=course.enrollment_end, enrollment_domain=course.enrollment_domain, invitation_only=course.invitation_only, max_student_enrollments_allowed=course. max_student_enrollments_allowed, ) @classmethod def load_from_module_store(cls, course_id): """ Load a CourseDescriptor, create a new CourseOverview from it, cache the overview, and return it. Arguments: course_id (CourseKey): the ID of the course overview to be loaded. Returns: CourseOverview: overview of the requested course. Raises: - CourseOverview.DoesNotExist if the course specified by course_id was not found. - IOError if some other error occurs while trying to load the course from the module store. """ store = modulestore() with store.bulk_operations(course_id): course = store.get_course(course_id) if isinstance(course, CourseDescriptor): course_overview = cls._create_from_course(course) try: course_overview.save() except IntegrityError: # There is a rare race condition that will occur if # CourseOverview.get_from_id is called while a # another identical overview is already in the process # of being created. # One of the overviews will be saved normally, while the # other one will cause an IntegrityError because it tries # to save a duplicate. # (see: https://openedx.atlassian.net/browse/TNL-2854). pass return course_overview elif course is not None: raise IOError( "Error while loading course {} from the module store: {}", unicode(course_id), course.error_msg if isinstance( course, ErrorDescriptor) else unicode(course)) else: raise cls.DoesNotExist() @classmethod def get_from_id(cls, course_id): """ Load a CourseOverview object for a given course ID. First, we try to load the CourseOverview from the database. If it doesn't exist, we load the entire course from the modulestore, create a CourseOverview object from it, and then cache it in the database for future use. Arguments: course_id (CourseKey): the ID of the course overview to be loaded. Returns: CourseOverview: overview of the requested course. Raises: - CourseOverview.DoesNotExist if the course specified by course_id was not found. - IOError if some other error occurs while trying to load the course from the module store. """ try: course_overview = cls.objects.get(id=course_id) if course_overview.version != cls.VERSION: # Throw away old versions of CourseOverview, as they might contain stale data. course_overview.delete() course_overview = None except cls.DoesNotExist: course_overview = None return course_overview or cls.load_from_module_store(course_id) def clean_id(self, padding_char='='): """ Returns a unique deterministic base32-encoded ID for the course. Arguments: padding_char (str): Character used for padding at end of base-32 -encoded string, defaulting to '=' """ return course_metadata_utils.clean_course_key(self.location.course_key, padding_char) @property def location(self): """ Returns the UsageKey of this course. UsageKeyField has a strange behavior where it fails to parse the "run" of a course out of the serialized form of a Mongo Draft UsageKey. This method is a wrapper around _location attribute that fixes the problem by calling map_into_course, which restores the run attribute. """ if self._location.run is None: self._location = self._location.map_into_course(self.id) return self._location @property def number(self): """ Returns this course's number. This is a "number" in the sense of the "course numbers" that you see at lots of universities. For example, given a course "Intro to Computer Science" with the course key "edX/CS-101/2014", the course number would be "CS-101" """ return course_metadata_utils.number_for_course_location(self.location) @property def url_name(self): """ Returns this course's URL name. """ return course_metadata_utils.url_name_for_course_location( self.location) @property def display_name_with_default(self): """ Return reasonable display name for the course. """ return course_metadata_utils.display_name_with_default(self) def has_started(self): """ Returns whether the the course has started. """ return course_metadata_utils.has_course_started(self.start) def has_ended(self): """ Returns whether the course has ended. """ return course_metadata_utils.has_course_ended(self.end) def starts_within(self, days): """ Returns True if the course starts with-in given number of days otherwise returns False. """ return course_metadata_utils.course_starts_within(self.start, days) def start_datetime_text(self, format_string="SHORT_DATE"): """ Returns the desired text corresponding the course's start date and time in UTC. Prefers .advertised_start, then falls back to .start. """ return course_metadata_utils.course_start_datetime_text( self.start, self.advertised_start, format_string, ugettext, strftime_localized) @property def start_date_is_still_default(self): """ Checks if the start date set for the course is still default, i.e. .start has not been modified, and .advertised_start has not been set. """ return course_metadata_utils.course_start_date_is_default( self.start, self.advertised_start, ) def end_datetime_text(self, format_string="SHORT_DATE"): """ Returns the end date or datetime for the course formatted as a string. """ return course_metadata_utils.course_end_datetime_text( self.end, format_string, strftime_localized) def may_certify(self): """ Returns whether it is acceptable to show the student a certificate download link. """ return course_metadata_utils.may_certify_for_course( self.certificates_display_behavior, self.certificates_show_before_end, self.has_ended()) @property def pre_requisite_courses(self): """ Returns a list of ID strings for this course's prerequisite courses. """ return json.loads(self._pre_requisite_courses_json) @classmethod def get_all_course_keys(cls): """ Returns all course keys from course overviews. """ return [ CourseKey.from_string(course_overview['id']) for course_overview in CourseOverview.objects.values('id') ]
class CourseOverview(TimeStampedModel): """ Model for storing and caching basic information about a course. This model contains basic course metadata such as an ID, display name, image URL, and any other information that would be necessary to display a course as part of: user dashboard (enrolled courses) course catalog (courses to enroll in) course about (meta data about the course) """ class Meta(object): app_label = 'course_overviews' # IMPORTANT: Bump this whenever you modify this model and/or add a migration. VERSION = 4 # Cache entry versioning. version = IntegerField() # Course identification id = CourseKeyField(db_index=True, primary_key=True, max_length=255) _location = UsageKeyField(max_length=255) org = TextField(max_length=255, default='outdated_entry') display_name = TextField(null=True) display_number_with_default = TextField() display_org_with_default = TextField() # Start/end dates start = DateTimeField(null=True) end = DateTimeField(null=True) advertised_start = TextField(null=True) announcement = DateTimeField(null=True) # URLs course_image_url = TextField() social_sharing_url = TextField(null=True) end_of_course_survey_url = TextField(null=True) # Certification data certificates_display_behavior = TextField(null=True) certificates_show_before_end = BooleanField(default=False) cert_html_view_enabled = BooleanField(default=False) has_any_active_web_certificate = BooleanField(default=False) cert_name_short = TextField() cert_name_long = TextField() # Grading lowest_passing_grade = DecimalField(max_digits=5, decimal_places=2, null=True) # Access parameters days_early_for_beta = FloatField(null=True) mobile_available = BooleanField(default=False) visible_to_staff_only = BooleanField(default=False) _pre_requisite_courses_json = TextField( ) # JSON representation of list of CourseKey strings # Enrollment details enrollment_start = DateTimeField(null=True) enrollment_end = DateTimeField(null=True) enrollment_domain = TextField(null=True) invitation_only = BooleanField(default=False) max_student_enrollments_allowed = IntegerField(null=True) # Catalog information catalog_visibility = TextField(null=True) short_description = TextField(null=True) course_video_url = TextField(null=True) effort = TextField(null=True) self_paced = BooleanField(default=False) @classmethod def _create_from_course(cls, course): """ Creates a CourseOverview object from a CourseDescriptor. Does not touch the database, simply constructs and returns an overview from the given course. Arguments: course (CourseDescriptor): any course descriptor object Returns: CourseOverview: overview extracted from the given course """ from lms.djangoapps.certificates.api import get_active_web_certificate from openedx.core.lib.courses import course_image_url log.info('Creating course overview for %s.', unicode(course.id)) # Workaround for a problem discovered in https://openedx.atlassian.net/browse/TNL-2806. # If the course has a malformed grading policy such that # course._grading_policy['GRADE_CUTOFFS'] = {}, then # course.lowest_passing_grade will raise a ValueError. # Work around this for now by defaulting to None. try: lowest_passing_grade = course.lowest_passing_grade except ValueError: lowest_passing_grade = None display_name = course.display_name start = course.start end = course.end max_student_enrollments_allowed = course.max_student_enrollments_allowed if isinstance(course.id, CCXLocator): from lms.djangoapps.ccx.utils import get_ccx_from_ccx_locator ccx = get_ccx_from_ccx_locator(course.id) display_name = ccx.display_name start = ccx.start end = ccx.due max_student_enrollments_allowed = ccx.max_student_enrollments_allowed return cls( version=cls.VERSION, id=course.id, _location=course.location, org=course.location.org, display_name=display_name, display_number_with_default=course.display_number_with_default, display_org_with_default=course.display_org_with_default, start=start, end=end, advertised_start=course.advertised_start, announcement=course.announcement, course_image_url=course_image_url(course), social_sharing_url=course.social_sharing_url, certificates_display_behavior=course.certificates_display_behavior, certificates_show_before_end=course.certificates_show_before_end, cert_html_view_enabled=course.cert_html_view_enabled, has_any_active_web_certificate=(get_active_web_certificate(course) is not None), cert_name_short=course.cert_name_short, cert_name_long=course.cert_name_long, lowest_passing_grade=lowest_passing_grade, end_of_course_survey_url=course.end_of_course_survey_url, days_early_for_beta=course.days_early_for_beta, mobile_available=course.mobile_available, visible_to_staff_only=course.visible_to_staff_only, _pre_requisite_courses_json=json.dumps( course.pre_requisite_courses), enrollment_start=course.enrollment_start, enrollment_end=course.enrollment_end, enrollment_domain=course.enrollment_domain, invitation_only=course.invitation_only, max_student_enrollments_allowed=max_student_enrollments_allowed, catalog_visibility=course.catalog_visibility, short_description=CourseDetails.fetch_about_attribute( course.id, 'short_description'), effort=CourseDetails.fetch_about_attribute(course.id, 'effort'), course_video_url=CourseDetails.fetch_video_url(course.id), self_paced=course.self_paced, ) @classmethod def load_from_module_store(cls, course_id): """ Load a CourseDescriptor, create a new CourseOverview from it, cache the overview, and return it. Arguments: course_id (CourseKey): the ID of the course overview to be loaded. Returns: CourseOverview: overview of the requested course. Raises: - CourseOverview.DoesNotExist if the course specified by course_id was not found. - IOError if some other error occurs while trying to load the course from the module store. """ store = modulestore() with store.bulk_operations(course_id): course = store.get_course(course_id) if isinstance(course, CourseDescriptor): course_overview = cls._create_from_course(course) try: with transaction.atomic(): course_overview.save() CourseOverviewTab.objects.bulk_create([ CourseOverviewTab(tab_id=tab.tab_id, course_overview=course_overview) for tab in course.tabs ]) CourseOverviewImageSet.create_for_course( course_overview, course) except IntegrityError: # There is a rare race condition that will occur if # CourseOverview.get_from_id is called while a # another identical overview is already in the process # of being created. # One of the overviews will be saved normally, while the # other one will cause an IntegrityError because it tries # to save a duplicate. # (see: https://openedx.atlassian.net/browse/TNL-2854). pass return course_overview elif course is not None: raise IOError( "Error while loading course {} from the module store: {}", unicode(course_id), course.error_msg if isinstance( course, ErrorDescriptor) else unicode(course)) else: raise cls.DoesNotExist() @classmethod def get_from_id(cls, course_id): """ Load a CourseOverview object for a given course ID. First, we try to load the CourseOverview from the database. If it doesn't exist, we load the entire course from the modulestore, create a CourseOverview object from it, and then cache it in the database for future use. Arguments: course_id (CourseKey): the ID of the course overview to be loaded. Returns: CourseOverview: overview of the requested course. Raises: - CourseOverview.DoesNotExist if the course specified by course_id was not found. - IOError if some other error occurs while trying to load the course from the module store. """ try: course_overview = cls.objects.select_related('image_set').get( id=course_id) if course_overview.version < cls.VERSION: # Throw away old versions of CourseOverview, as they might contain stale data. course_overview.delete() course_overview = None except cls.DoesNotExist: course_overview = None # Regenerate the thumbnail images if they're missing (either because # they were never generated, or because they were flushed out after # a change to CourseOverviewImageConfig. if course_overview and not hasattr(course_overview, 'image_set'): CourseOverviewImageSet.create_for_course(course_overview) return course_overview or cls.load_from_module_store(course_id) def clean_id(self, padding_char='='): """ Returns a unique deterministic base32-encoded ID for the course. Arguments: padding_char (str): Character used for padding at end of base-32 -encoded string, defaulting to '=' """ return course_metadata_utils.clean_course_key(self.location.course_key, padding_char) @property def location(self): """ Returns the UsageKey of this course. UsageKeyField has a strange behavior where it fails to parse the "run" of a course out of the serialized form of a Mongo Draft UsageKey. This method is a wrapper around _location attribute that fixes the problem by calling map_into_course, which restores the run attribute. """ if self._location.run is None: self._location = self._location.map_into_course(self.id) return self._location @property def number(self): """ Returns this course's number. This is a "number" in the sense of the "course numbers" that you see at lots of universities. For example, given a course "Intro to Computer Science" with the course key "edX/CS-101/2014", the course number would be "CS-101" """ return course_metadata_utils.number_for_course_location(self.location) @property def url_name(self): """ Returns this course's URL name. """ return course_metadata_utils.url_name_for_course_location( self.location) @property def display_name_with_default(self): """ Return reasonable display name for the course. """ return course_metadata_utils.display_name_with_default(self) @property def display_name_with_default_escaped(self): """ DEPRECATED: use display_name_with_default Return html escaped reasonable display name for the course. Note: This newly introduced method should not be used. It was only introduced to enable a quick search/replace and the ability to slowly migrate and test switching to display_name_with_default, which is no longer escaped. """ return course_metadata_utils.display_name_with_default_escaped(self) def has_started(self): """ Returns whether the the course has started. """ return course_metadata_utils.has_course_started(self.start) def has_ended(self): """ Returns whether the course has ended. """ return course_metadata_utils.has_course_ended(self.end) def starts_within(self, days): """ Returns True if the course starts with-in given number of days otherwise returns False. """ return course_metadata_utils.course_starts_within(self.start, days) def start_datetime_text(self, format_string="SHORT_DATE"): """ Returns the desired text corresponding the course's start date and time in UTC. Prefers .advertised_start, then falls back to .start. """ return course_metadata_utils.course_start_datetime_text( self.start, self.advertised_start, format_string, ugettext, strftime_localized) @property def start_date_is_still_default(self): """ Checks if the start date set for the course is still default, i.e. .start has not been modified, and .advertised_start has not been set. """ return course_metadata_utils.course_start_date_is_default( self.start, self.advertised_start, ) def end_datetime_text(self, format_string="SHORT_DATE"): """ Returns the end date or datetime for the course formatted as a string. """ return course_metadata_utils.course_end_datetime_text( self.end, format_string, strftime_localized) @property def sorting_score(self): """ Returns a tuple that can be used to sort the courses according the how "new" they are. The "newness" score is computed using a heuristic that takes into account the announcement and (advertised) start dates of the course if available. The lower the number the "newer" the course. """ return course_metadata_utils.sorting_score(self.start, self.advertised_start, self.announcement) @property def start_type(self): """ Returns the type of the course's 'start' field. """ if self.advertised_start: return u'string' elif self.start != DEFAULT_START_DATE: return u'timestamp' else: return u'empty' @property def start_display(self): """ Returns the display value for the course's start date. """ if self.advertised_start: return self.advertised_start elif self.start != DEFAULT_START_DATE: return defaultfilters.date(self.start, "DATE_FORMAT") else: return None def may_certify(self): """ Returns whether it is acceptable to show the student a certificate download link. """ return course_metadata_utils.may_certify_for_course( self.certificates_display_behavior, self.certificates_show_before_end, self.has_ended()) @property def pre_requisite_courses(self): """ Returns a list of ID strings for this course's prerequisite courses. """ return json.loads(self._pre_requisite_courses_json) @classmethod def get_select_courses(cls, course_keys): """ Returns CourseOverview objects for the given course_keys. """ course_overviews = [] log.info('Generating course overview for %d courses.', len(course_keys)) log.debug( 'Generating course overview(s) for the following courses: %s', course_keys) for course_key in course_keys: try: course_overviews.append(CourseOverview.get_from_id(course_key)) except Exception as ex: # pylint: disable=broad-except log.exception( 'An error occurred while generating course overview for %s: %s', unicode(course_key), ex.message, ) log.info('Finished generating course overviews.') return course_overviews @classmethod def get_all_courses(cls, org=None, filter_=None): """ Returns all CourseOverview objects in the database. Arguments: org (string): Optional parameter that allows case-insensitive filtering by organization. filter_ (dict): Optional parameter that allows custom filtering. """ # Note: If a newly created course is not returned in this QueryList, # make sure the "publish" signal was emitted when the course was # created. For tests using CourseFactory, use emit_signals=True. course_overviews = CourseOverview.objects.all() if org: # In rare cases, courses belonging to the same org may be accidentally assigned # an org code with a different casing (e.g., Harvardx as opposed to HarvardX). # Case-insensitive exact matching allows us to deal with this kind of dirty data. course_overviews = course_overviews.filter(org__iexact=org) if filter_: course_overviews = course_overviews.filter(**filter_) return course_overviews @classmethod def get_all_course_keys(cls): """ Returns all course keys from course overviews. """ return [ CourseKey.from_string(course_overview['id']) for course_overview in CourseOverview.objects.values('id') ] def is_discussion_tab_enabled(self): """ Returns True if course has discussion tab and is enabled """ tabs = self.tabs.all() # creates circular import; hence explicitly referenced is_discussion_enabled for tab in tabs: if tab.tab_id == "discussion" and django_comment_client.utils.is_discussion_enabled( self.id): return True return False @property def image_urls(self): """ Return a dict with all known URLs for this course image. Current resolutions are: raw = original upload from the user small = thumbnail with dimensions CourseOverviewImageConfig.current().small large = thumbnail with dimensions CourseOverviewImageConfig.current().large If no thumbnails exist, the raw (originally uploaded) image will be returned for all resolutions. """ # This is either the raw image that the course team uploaded, or the # settings.DEFAULT_COURSE_ABOUT_IMAGE_URL if they didn't specify one. raw_image_url = self.course_image_url # Default all sizes to return the raw image if there is no # CourseOverviewImageSet associated with this CourseOverview. This can # happen because we're disabled via CourseOverviewImageConfig. urls = { 'raw': raw_image_url, 'small': raw_image_url, 'large': raw_image_url, } # If we do have a CourseOverviewImageSet, we still default to the raw # images if our thumbnails are blank (might indicate that there was a # processing error of some sort while trying to generate thumbnails). if hasattr( self, 'image_set') and CourseOverviewImageConfig.current().enabled: urls['small'] = self.image_set.small_url or raw_image_url urls['large'] = self.image_set.large_url or raw_image_url return urls @property def pacing(self): """ Returns the pacing for the course. Potential values: self: Self-paced courses instructor: Instructor-led courses """ return 'self' if self.self_paced else 'instructor' def __unicode__(self): """Represent ourselves with the course key.""" return unicode(self.id)
class PersistentSubsectionGrade(TimeStampedModel): """ A django model tracking persistent grades at the subsection level. """ class Meta(object): unique_together = [ # * Specific grades can be pulled using all three columns, # * Progress page can pull all grades for a given (course_id, user_id) # * Course staff can see all grades for a course using (course_id,) ('course_id', 'user_id', 'usage_key'), ] # primary key will need to be large for this table id = UnsignedBigIntAutoField(primary_key=True) # pylint: disable=invalid-name # uniquely identify this particular grade object user_id = models.IntegerField(blank=False) course_id = CourseKeyField(blank=False, max_length=255) # note: the usage_key may not have the run filled in for # old mongo courses. Use the full_usage_key property # instead when you want to use/compare the usage_key. usage_key = UsageKeyField(blank=False, max_length=255) # Information relating to the state of content when grade was calculated subtree_edited_timestamp = models.DateTimeField( 'last content edit timestamp', blank=False) course_version = models.CharField('guid of latest course version', blank=True, max_length=255) # earned/possible refers to the number of points achieved and available to achieve. # graded refers to the subset of all problems that are marked as being graded. earned_all = models.FloatField(blank=False) possible_all = models.FloatField(blank=False) earned_graded = models.FloatField(blank=False) possible_graded = models.FloatField(blank=False) # track which blocks were visible at the time of grade calculation visible_blocks = models.ForeignKey(VisibleBlocks, db_column='visible_blocks_hash', to_field='hashed') @property def full_usage_key(self): """ Returns the "correct" usage key value with the run filled in. """ if self.usage_key.run is None: # pylint: disable=no-member return self.usage_key.replace(course_key=self.course_id) else: return self.usage_key def __unicode__(self): """ Returns a string representation of this model. """ return u"{} user: {}, course version: {}, subsection {} ({}). {}/{} graded, {}/{} all".format( type(self).__name__, self.user_id, self.course_version, self.usage_key, self.visible_blocks_id, self.earned_graded, self.possible_graded, self.earned_all, self.possible_all, ) @classmethod def read_grade(cls, user_id, usage_key): """ Reads a grade from database Arguments: user_id: The user associated with the desired grade usage_key: The location of the subsection associated with the desired grade Raises PersistentSubsectionGrade.DoesNotExist if applicable """ return cls.objects.select_related('visible_blocks').get( user_id=user_id, course_id=usage_key. course_key, # course_id is included to take advantage of db indexes usage_key=usage_key, ) @classmethod def bulk_read_grades(cls, user_id, course_key): """ Reads all grades for the given user and course. Arguments: user_id: The user associated with the desired grades course_key: The course identifier for the desired grades """ return cls.objects.select_related('visible_blocks').filter( user_id=user_id, course_id=course_key, ) @classmethod def update_or_create_grade(cls, **kwargs): """ Wrapper for objects.update_or_create. """ cls._prepare_params_and_visible_blocks(kwargs) user_id = kwargs.pop('user_id') usage_key = kwargs.pop('usage_key') grade, _ = cls.objects.update_or_create( user_id=user_id, course_id=usage_key.course_key, usage_key=usage_key, defaults=kwargs, ) return grade @classmethod def create_grade(cls, **kwargs): """ Wrapper for objects.create. """ cls._prepare_params_and_visible_blocks(kwargs) return cls.objects.create(**kwargs) @classmethod def bulk_create_grades(cls, grade_params_iter, course_key): """ Bulk creation of grades. """ if not grade_params_iter: return map(cls._prepare_params, grade_params_iter) VisibleBlocks.bulk_get_or_create( [params['visible_blocks'] for params in grade_params_iter], course_key) map(cls._prepare_params_visible_blocks_id, grade_params_iter) return cls.objects.bulk_create([ PersistentSubsectionGrade(**params) for params in grade_params_iter ]) @classmethod def _prepare_params_and_visible_blocks(cls, params): """ Prepares the fields for the grade record, while creating the related VisibleBlocks, if needed. """ cls._prepare_params(params) params[ 'visible_blocks'] = VisibleBlocks.objects.create_from_blockrecords( params['visible_blocks']) @classmethod def _prepare_params(cls, params): """ Prepares the fields for the grade record. """ if not params.get('course_id', None): params['course_id'] = params['usage_key'].course_key params['course_version'] = params.get('course_version', None) or "" params['visible_blocks'] = BlockRecordList.from_list( params['visible_blocks'], params['course_id']) @classmethod def _prepare_params_visible_blocks_id(cls, params): """ Prepares the visible_blocks_id field for the grade record, using the hash of the visible_blocks field. Specifying the hashed field eliminates extra queries to get the VisibleBlocks record. Use this variation of preparing the params when you are sure of the existence of the VisibleBlock. """ params['visible_blocks_id'] = params['visible_blocks'].hash_value del params['visible_blocks']
class CourseOverview(django.db.models.Model): """ Model for storing and caching basic information about a course. This model contains basic course metadata such as an ID, display name, image URL, and any other information that would be necessary to display a course as part of a user dashboard or enrollment API. """ # Course identification id = CourseKeyField(db_index=True, primary_key=True, max_length=255) # pylint: disable=invalid-name _location = UsageKeyField(max_length=255) display_name = TextField(null=True) display_number_with_default = TextField() display_org_with_default = TextField() # Start/end dates start = DateTimeField(null=True) end = DateTimeField(null=True) advertised_start = TextField(null=True) # URLs course_image_url = TextField() facebook_url = TextField(null=True) social_sharing_url = TextField(null=True) end_of_course_survey_url = TextField(null=True) # Certification data certificates_display_behavior = TextField(null=True) certificates_show_before_end = BooleanField() cert_html_view_enabled = BooleanField() has_any_active_web_certificate = BooleanField() cert_name_short = TextField() cert_name_long = TextField() # Grading lowest_passing_grade = DecimalField(max_digits=5, decimal_places=2) # Access parameters days_early_for_beta = FloatField(null=True) mobile_available = BooleanField() visible_to_staff_only = BooleanField() _pre_requisite_courses_json = TextField() # JSON representation of list of CourseKey strings @staticmethod def _create_from_course(course): """ Creates a CourseOverview object from a CourseDescriptor. Does not touch the database, simply constructs and returns an overview from the given course. Arguments: course (CourseDescriptor): any course descriptor object Returns: CourseOverview: overview extracted from the given course """ from lms.djangoapps.certificates.api import get_active_web_certificate from lms.djangoapps.courseware.courses import course_image_url return CourseOverview( id=course.id, _location=course.location, display_name=course.display_name, display_number_with_default=course.display_number_with_default, display_org_with_default=course.display_org_with_default, start=course.start, end=course.end, advertised_start=course.advertised_start, course_image_url=course_image_url(course), facebook_url=course.facebook_url, social_sharing_url=course.social_sharing_url, certificates_display_behavior=course.certificates_display_behavior, certificates_show_before_end=course.certificates_show_before_end, cert_html_view_enabled=course.cert_html_view_enabled, has_any_active_web_certificate=(get_active_web_certificate(course) is not None), cert_name_short=course.cert_name_short, cert_name_long=course.cert_name_long, lowest_passing_grade=course.lowest_passing_grade, end_of_course_survey_url=course.end_of_course_survey_url, days_early_for_beta=course.days_early_for_beta, mobile_available=course.mobile_available, visible_to_staff_only=course.visible_to_staff_only, _pre_requisite_courses_json=json.dumps(course.pre_requisite_courses) ) @staticmethod def get_from_id(course_id): """ Load a CourseOverview object for a given course ID. First, we try to load the CourseOverview from the database. If it doesn't exist, we load the entire course from the modulestore, create a CourseOverview object from it, and then cache it in the database for future use. Arguments: course_id (CourseKey): the ID of the course overview to be loaded. Returns: CourseOverview: overview of the requested course. If loading course from the module store failed, returns None. Raises: - CourseOverview.DoesNotExist if the course specified by course_id was not found. - IOError if some other error occurs while trying to load the course from the module store. """ course_overview = None try: course_overview = CourseOverview.objects.get(id=course_id) except CourseOverview.DoesNotExist: store = modulestore() with store.bulk_operations(course_id): course = store.get_course(course_id) if isinstance(course, CourseDescriptor): course_overview = CourseOverview._create_from_course(course) course_overview.save() elif course is not None: raise IOError( "Error while loading course {} from the module store: {}", unicode(course_id), course.error_msg if isinstance(course, ErrorDescriptor) else unicode(course) ) else: raise CourseOverview.DoesNotExist() return course_overview def clean_id(self, padding_char='='): """ Returns a unique deterministic base32-encoded ID for the course. Arguments: padding_char (str): Character used for padding at end of base-32 -encoded string, defaulting to '=' """ return course_metadata_utils.clean_course_key(self.location.course_key, padding_char) @property def location(self): """ Returns the UsageKey of this course. UsageKeyField has a strange behavior where it fails to parse the "run" of a course out of the serialized form of a Mongo Draft UsageKey. This method is a wrapper around _location attribute that fixes the problem by calling map_into_course, which restores the run attribute. """ if self._location.run is None: self._location = self._location.map_into_course(self.id) return self._location @property def number(self): """ Returns this course's number. This is a "number" in the sense of the "course numbers" that you see at lots of universities. For example, given a course "Intro to Computer Science" with the course key "edX/CS-101/2014", the course number would be "CS-101" """ return course_metadata_utils.number_for_course_location(self.location) @property def url_name(self): """ Returns this course's URL name. """ return course_metadata_utils.url_name_for_course_location(self.location) @property def display_name_with_default(self): """ Return reasonable display name for the course. """ return course_metadata_utils.display_name_with_default(self) def has_started(self): """ Returns whether the the course has started. """ return course_metadata_utils.has_course_started(self.start) def has_ended(self): """ Returns whether the course has ended. """ return course_metadata_utils.has_course_ended(self.end) def start_datetime_text(self, format_string="SHORT_DATE"): """ Returns the desired text corresponding the course's start date and time in UTC. Prefers .advertised_start, then falls back to .start. """ return course_metadata_utils.course_start_datetime_text( self.start, self.advertised_start, format_string, ugettext, strftime_localized ) @property def start_date_is_still_default(self): """ Checks if the start date set for the course is still default, i.e. .start has not been modified, and .advertised_start has not been set. """ return course_metadata_utils.course_start_date_is_default( self.start, self.advertised_start, ) def end_datetime_text(self, format_string="SHORT_DATE"): """ Returns the end date or datetime for the course formatted as a string. """ return course_metadata_utils.course_end_datetime_text( self.end, format_string, strftime_localized ) def may_certify(self): """ Returns whether it is acceptable to show the student a certificate download link. """ return course_metadata_utils.may_certify_for_course( self.certificates_display_behavior, self.certificates_show_before_end, self.has_ended() ) @property def pre_requisite_courses(self): """ Returns a list of ID strings for this course's prerequisite courses. """ return json.loads(self._pre_requisite_courses_json)