コード例 #1
0
class PersistentCourseGrade(TimeStampedModel):
    """
    A django model tracking persistent course grades.

    .. no_pii:
    """
    class Meta:
        app_label = "grades"
        # Indices:
        # (course_id, user_id) for individual grades
        # (course_id) for instructors to see all course grades, implicitly created via the unique_together constraint
        # (user_id) for course dashboard; explicitly declared as an index below
        # (passed_timestamp, course_id) for tracking when users first earned a passing grade.
        # (modified): find all the grades updated within a certain timespan
        # (modified, course_id): find all the grades updated within a certain timespan for a course
        unique_together = [
            ('course_id', 'user_id'),
        ]
        index_together = [('passed_timestamp', 'course_id'),
                          ('modified', 'course_id')]

    # primary key will need to be large for this table
    id = UnsignedBigIntAutoField(primary_key=True)  # pylint: disable=invalid-name
    user_id = models.IntegerField(blank=False, db_index=True)
    course_id = CourseKeyField(blank=False, max_length=255)

    # Information relating to the state of content when grade was calculated
    course_edited_timestamp = models.DateTimeField(
        'Last content edit timestamp', blank=True, null=True)
    course_version = models.CharField('Course content version identifier',
                                      blank=True,
                                      max_length=255)
    grading_policy_hash = models.CharField('Hash of grading policy',
                                           blank=False,
                                           max_length=255)

    # Information about the course grade itself
    percent_grade = models.FloatField(blank=False)
    letter_grade = models.CharField('Letter grade for course',
                                    blank=False,
                                    max_length=255)

    # Information related to course completion
    passed_timestamp = models.DateTimeField(
        'Date learner earned a passing grade', blank=True, null=True)

    _CACHE_NAMESPACE = "grades.models.PersistentCourseGrade"

    def __str__(self):
        """
        Returns a string representation of this model.
        """
        return ', '.join([
            "{} user: {}".format(type(self).__name__, self.user_id),
            f"course version: {self.course_version}",
            f"grading policy: {self.grading_policy_hash}",
            f"percent grade: {self.percent_grade}%",
            f"letter grade: {self.letter_grade}",
            f"passed timestamp: {self.passed_timestamp}",
        ])

    @classmethod
    def prefetch(cls, course_id, users):
        """
        Prefetches grades for the given users for the given course.
        """
        get_cache(cls._CACHE_NAMESPACE)[cls._cache_key(course_id)] = {
            grade.user_id: grade
            for grade in cls.objects.filter(
                user_id__in=[user.id for user in users], course_id=course_id)
        }

    @classmethod
    def clear_prefetched_data(cls, course_key):
        """
        Clears prefetched grades for this course from the RequestCache.
        """
        get_cache(cls._CACHE_NAMESPACE).pop(cls._cache_key(course_key), None)

    @classmethod
    def read(cls, user_id, course_id):
        """
        Reads a grade from database

        Arguments:
            user_id: The user associated with the desired grade
            course_id: The id of the course associated with the desired grade

        Raises PersistentCourseGrade.DoesNotExist if applicable
        """
        try:
            prefetched_grades = get_cache(
                cls._CACHE_NAMESPACE)[cls._cache_key(course_id)]
            try:
                return prefetched_grades[user_id]
            except KeyError:
                # user's grade is not in the prefetched dict, so
                # assume they have no grade
                raise cls.DoesNotExist  # lint-amnesty, pylint: disable=raise-missing-from
        except KeyError:
            # grades were not prefetched for the course, so fetch it
            return cls.objects.get(user_id=user_id, course_id=course_id)

    @classmethod
    def update_or_create(cls, user_id, course_id, **kwargs):
        """
        Creates a course grade in the database.
        Returns a PersistedCourseGrade object.
        """
        passed = kwargs.pop('passed')

        if kwargs.get('course_version', None) is None:
            kwargs['course_version'] = ""

        grade, _ = cls.objects.update_or_create(user_id=user_id,
                                                course_id=course_id,
                                                defaults=kwargs)
        if passed and not grade.passed_timestamp:
            grade.passed_timestamp = now()
            grade.save()

        cls._emit_grade_calculated_event(grade)
        cls._update_cache(course_id, user_id, grade)
        return grade

    @classmethod
    def _update_cache(cls, course_id, user_id, grade):
        course_cache = get_cache(cls._CACHE_NAMESPACE).get(
            cls._cache_key(course_id))
        if course_cache is not None:
            course_cache[user_id] = grade

    @classmethod
    def _cache_key(cls, course_id):
        return f"grades_cache.{course_id}"

    @staticmethod
    def _emit_grade_calculated_event(grade):
        events.course_grade_calculated(grade)
コード例 #2
0
class CourseEmail(Email):
    """
    Stores information for an email to a course.

    .. no_pii:
    """
    class Meta:
        app_label = "bulk_email"

    course_id = CourseKeyField(max_length=255, db_index=True)
    # to_option is deprecated and unused, but dropping db columns is hard so it's still here for legacy reasons
    to_option = models.CharField(max_length=64,
                                 choices=[("deprecated", "deprecated")])
    targets = models.ManyToManyField(Target)
    template_name = models.CharField(null=True, max_length=255)
    from_addr = models.CharField(null=True, max_length=255)

    def __str__(self):
        return self.subject

    @classmethod
    def create(cls,
               course_id,
               sender,
               targets,
               subject,
               html_message,
               text_message=None,
               template_name=None,
               from_addr=None):
        """
        Create an instance of CourseEmail.
        """
        # automatically generate the stripped version of the text from the HTML markup:
        if text_message is None:
            text_message = html_to_text(html_message)

        new_targets = []
        for target in targets:
            # split target, to handle cohort:cohort_name and track:mode_slug
            target_split = target.split(':', 1)
            # Ensure our desired target exists
            if target_split[0] not in EMAIL_TARGETS:  # lint-amnesty, pylint: disable=no-else-raise
                fmt = 'Course email being sent to unrecognized target: "{target}" for "{course}", subject "{subject}"'
                msg = fmt.format(target=target,
                                 course=course_id,
                                 subject=subject).encode('utf8')
                raise ValueError(msg)
            elif target_split[0] == SEND_TO_COHORT:
                # target_split[1] will contain the cohort name
                cohort = CohortTarget.ensure_valid_cohort(
                    target_split[1], course_id)
                new_target, _ = CohortTarget.objects.get_or_create(
                    target_type=target_split[0], cohort=cohort)
            elif target_split[0] == SEND_TO_TRACK:
                # target_split[1] contains the desired mode slug
                CourseModeTarget.ensure_valid_mode(target_split[1], course_id)

                # There could exist multiple CourseModes that match this query, due to differing currency types.
                # The currencies do not affect user lookup though, so we can just use the first result.
                mode = CourseMode.objects.filter(course_id=course_id,
                                                 mode_slug=target_split[1])[0]
                new_target, _ = CourseModeTarget.objects.get_or_create(
                    target_type=target_split[0], track=mode)
            else:
                new_target, _ = Target.objects.get_or_create(
                    target_type=target_split[0])
            new_targets.append(new_target)

        # create the task, then save it immediately:
        course_email = cls(
            course_id=course_id,
            sender=sender,
            subject=subject,
            html_message=html_message,
            text_message=text_message,
            template_name=template_name,
            from_addr=from_addr,
        )
        course_email.save(
        )  # Must exist in db before setting M2M relationship values
        course_email.targets.add(*new_targets)
        course_email.save()

        return course_email

    def get_template(self):
        """
        Returns the corresponding CourseEmailTemplate for this CourseEmail.
        """
        return CourseEmailTemplate.get_template(name=self.template_name)
コード例 #3
0
class CourseDiscussionSettings(models.Model):
    """
    Settings for course discussions

    .. no_pii:
    """
    course_id = CourseKeyField(
        unique=True,
        max_length=255,
        db_index=True,
        help_text="Which course are these settings associated with?",
    )
    discussions_id_map = JSONField(
        null=True,
        blank=True,
        help_text="Key/value store mapping discussion IDs to discussion XBlock usage keys.",
    )
    always_divide_inline_discussions = models.BooleanField(default=False)
    reported_content_email_notifications = models.BooleanField(default=False)
    _divided_discussions = models.TextField(db_column='divided_discussions', null=True, blank=True)  # JSON list

    COHORT = 'cohort'
    ENROLLMENT_TRACK = 'enrollment_track'
    NONE = 'none'
    ASSIGNMENT_TYPE_CHOICES = ((NONE, 'None'), (COHORT, 'Cohort'), (ENROLLMENT_TRACK, 'Enrollment Track'))
    division_scheme = models.CharField(max_length=20, choices=ASSIGNMENT_TYPE_CHOICES, default=NONE)

    class Meta:
        # use existing table that was originally created from django_comment_common app
        db_table = 'django_comment_common_coursediscussionsettings'

    @property
    def divided_discussions(self):
        """
        Jsonify the divided_discussions
        """
        return json.loads(self._divided_discussions)

    @divided_discussions.setter
    def divided_discussions(self, value):
        """
        Un-Jsonify the divided_discussions
        """
        self._divided_discussions = json.dumps(value)

    @request_cached()
    @classmethod
    def get(cls, course_key):
        """
        Get and/or create settings
        """
        try:
            course_discussion_settings = cls.objects.get(course_id=course_key)
        except cls.DoesNotExist:
            from openedx.core.djangoapps.course_groups.cohorts import get_legacy_discussion_settings
            legacy_discussion_settings = get_legacy_discussion_settings(course_key)
            course_discussion_settings, _ = cls.objects.get_or_create(
                course_id=course_key,
                defaults={
                    'always_divide_inline_discussions': legacy_discussion_settings['always_cohort_inline_discussions'],
                    'divided_discussions': legacy_discussion_settings['cohorted_discussions'],
                    'division_scheme': cls.COHORT if legacy_discussion_settings['is_cohorted'] else cls.NONE
                },
            )
        return course_discussion_settings

    def update(self, validated_data: dict):
        """
        Set discussion settings for a course

        Returns:
            A CourseDiscussionSettings object
        """
        fields = {
            'division_scheme': (str,)[0],
            'always_divide_inline_discussions': bool,
            'divided_discussions': list,
            'reported_content_email_notifications': bool,
        }
        for field, field_type in fields.items():
            if field in validated_data:
                if not isinstance(validated_data[field], field_type):
                    raise ValueError(f"Incorrect field type for `{field}`. Type must be `{field_type.__name__}`")
                setattr(self, field, validated_data[field])
        self.save()
        return self
コード例 #4
0
class Migration(migrations.Migration):

    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
    ]

    operations = [
        migrations.CreateModel(
            name='SurveyAnswer',
            fields=[
                ('id',
                 models.AutoField(verbose_name='ID',
                                  serialize=False,
                                  auto_created=True,
                                  primary_key=True)),
                ('created',
                 model_utils.fields.AutoCreatedField(
                     default=django.utils.timezone.now,
                     verbose_name='created',
                     editable=False)),
                ('modified',
                 model_utils.fields.AutoLastModifiedField(
                     default=django.utils.timezone.now,
                     verbose_name='modified',
                     editable=False)),
                ('field_name', models.CharField(max_length=255,
                                                db_index=True)),
                ('field_value', models.CharField(max_length=1024)),
                ('course_key',
                 CourseKeyField(max_length=255, null=True, db_index=True)),
            ],
            options={
                'abstract': False,
            },
        ),
        migrations.CreateModel(
            name='SurveyForm',
            fields=[
                ('id',
                 models.AutoField(verbose_name='ID',
                                  serialize=False,
                                  auto_created=True,
                                  primary_key=True)),
                ('created',
                 model_utils.fields.AutoCreatedField(
                     default=django.utils.timezone.now,
                     verbose_name='created',
                     editable=False)),
                ('modified',
                 model_utils.fields.AutoLastModifiedField(
                     default=django.utils.timezone.now,
                     verbose_name='modified',
                     editable=False)),
                ('name',
                 models.CharField(unique=True, max_length=255, db_index=True)),
                ('form', models.TextField()),
            ],
            options={
                'abstract': False,
            },
        ),
        migrations.AddField(
            model_name='surveyanswer',
            name='form',
            field=models.ForeignKey(to='survey.SurveyForm',
                                    on_delete=models.CASCADE),
        ),
        migrations.AddField(
            model_name='surveyanswer',
            name='user',
            field=models.ForeignKey(to=settings.AUTH_USER_MODEL,
                                    on_delete=models.CASCADE),
        ),
    ]
コード例 #5
0
class Bookmark(TimeStampedModel):
    """
    Bookmarks model.

    .. no_pii:
    """
    user = models.ForeignKey(User, db_index=True, on_delete=models.CASCADE)
    course_key = CourseKeyField(max_length=255, db_index=True)
    usage_key = UsageKeyField(max_length=255, db_index=True)
    _path = JSONField(db_column='path',
                      help_text='Path in course tree to the block')

    xblock_cache = models.ForeignKey('bookmarks.XBlockCache',
                                     on_delete=models.CASCADE)

    class Meta:
        """
        Bookmark metadata.
        """
        unique_together = ('user', 'usage_key')

    def __str__(self):
        return self.resource_id

    @classmethod
    def create(cls, data):
        """
        Create a Bookmark object.

        Arguments:
            data (dict): The data to create the object with.

        Returns:
            A Bookmark object.

        Raises:
            ItemNotFoundError: If no block exists for the usage_key.
        """
        data = dict(data)
        usage_key = data.pop('usage_key')

        with modulestore().bulk_operations(usage_key.course_key):
            block = modulestore().get_item(usage_key)

            xblock_cache = XBlockCache.create({
                'usage_key':
                usage_key,
                'display_name':
                block.display_name_with_default,
            })
            data['_path'] = prepare_path_for_serialization(
                Bookmark.updated_path(usage_key, xblock_cache))

        data['course_key'] = usage_key.course_key
        data['xblock_cache'] = xblock_cache

        user = data.pop('user')

        # Sometimes this ends up in data, but newer versions of Django will fail on having unknown keys in defaults
        data.pop('display_name', None)

        bookmark, created = cls.objects.get_or_create(usage_key=usage_key,
                                                      user=user,
                                                      defaults=data)
        return bookmark, created

    @property
    def resource_id(self):
        """
        Return the resource id: {username,usage_id}.
        """
        return f"{self.user.username},{self.usage_key}"

    @property
    def display_name(self):
        """
        Return the display_name from self.xblock_cache.

        Returns:
            String.
        """
        return self.xblock_cache.display_name  # pylint: disable=no-member

    @property
    def path(self):
        """
        Return the path to the bookmark's block after checking self.xblock_cache.

        Returns:
            List of dicts.
        """
        if self.modified < self.xblock_cache.modified:  # pylint: disable=no-member
            path = Bookmark.updated_path(self.usage_key, self.xblock_cache)
            self._path = prepare_path_for_serialization(path)
            self.save()  # Always save so that self.modified is updated.
            return path

        return parse_path_data(self._path)

    @staticmethod
    def updated_path(usage_key, xblock_cache):
        """
        Return the update-to-date path.

        xblock_cache.paths is the list of all possible paths to a block
        constructed by doing a DFS of the tree. However, in case of DAGS,
        which section jump_to_id() takes the user to depends on the
        modulestore. If xblock_cache.paths has only one item, we can
        just use it. Otherwise, we use path_to_location() to get the path
        jump_to_id() will take the user to.
        """
        if xblock_cache.paths and len(xblock_cache.paths) == 1:
            return xblock_cache.paths[0]

        return Bookmark.get_path(usage_key)

    @staticmethod
    def get_path(usage_key):
        """
        Returns data for the path to the block in the course graph.

        Note: In case of multiple paths to the block from the course
        root, this function returns a path arbitrarily but consistently,
        depending on the modulestore. In the future, we may want to
        extend it to check which of the paths, the user has access to
        and return its data.

        Arguments:
            block (XBlock): The block whose path is required.

        Returns:
            list of PathItems
        """
        with modulestore().bulk_operations(usage_key.course_key):
            try:
                path = search.path_to_location(modulestore(),
                                               usage_key,
                                               full_path=True)
            except ItemNotFoundError:
                log.error('Block with usage_key: %s not found.', usage_key)
                return []
            except NoPathToItem:
                log.error('No path to block with usage_key: %s.', usage_key)
                return []

            path_data = []
            for ancestor_usage_key in path:
                if ancestor_usage_key != usage_key and ancestor_usage_key.block_type != 'course':
                    try:
                        block = modulestore().get_item(ancestor_usage_key)
                    except ItemNotFoundError:
                        return []  # No valid path can be found.
                    path_data.append(
                        PathItem(usage_key=block.location,
                                 display_name=block.display_name_with_default))

        return path_data
コード例 #6
0
ファイル: models.py プロジェクト: fdns/eol-edx
class CertificateGenerationCourseSetting(TimeStampedModel):
    """
    Enable or disable certificate generation for a particular course.

    In general, we should only enable self-generated certificates
    for a course once we successfully generate example certificates
    for the course.  This is enforced in the UI layer, but
    not in the data layer.

    .. no_pii:
    """
    course_key = CourseKeyField(max_length=255, db_index=True)

    self_generation_enabled = models.BooleanField(
        default=False,
        help_text=
        (u"Allow students to generate their own certificates for the course. "
         u"Enabling this does NOT affect usage of the management command used "
         u"for batch certificate generation."))
    language_specific_templates_enabled = models.BooleanField(
        default=False,
        help_text=(
            u"Render translated certificates rather than using the platform's "
            u"default language. Available translations are controlled by the "
            u"certificate template."))
    include_hours_of_effort = models.NullBooleanField(
        default=None,
        help_text=
        (u"Display estimated time to complete the course, which is equal to the maximum hours of effort per week "
         u"times the length of the course in weeks. This attribute will only be displayed in a certificate when the "
         u"attributes 'Weeks to complete' and 'Max effort' have been provided for the course run and its certificate "
         u"template includes Hours of Effort."))

    class Meta(object):
        get_latest_by = 'created'
        app_label = "certificates"

    @classmethod
    def get(cls, course_key):
        """ Retrieve certificate generation settings for a course.

        Arguments:
            course_key (CourseKey): The identifier for the course.

        Returns:
            CertificateGenerationCourseSetting
        """
        try:
            latest = cls.objects.filter(course_key=course_key).latest()
        except cls.DoesNotExist:
            return None
        else:
            return latest

    @classmethod
    def is_self_generation_enabled_for_course(cls, course_key):
        """Check whether self-generated certificates are enabled for a course.

        Arguments:
            course_key (CourseKey): The identifier for the course.

        Returns:
            boolean

        """
        try:
            latest = cls.objects.filter(course_key=course_key).latest()
        except cls.DoesNotExist:
            return False
        else:
            return latest.self_generation_enabled

    @classmethod
    def set_self_generatation_enabled_for_course(cls, course_key, is_enabled):
        """Enable or disable self-generated certificates for a course.

        Arguments:
            course_key (CourseKey): The identifier for the course.
            is_enabled (boolean): Whether to enable or disable self-generated certificates.

        """
        default = {'self_generation_enabled': is_enabled}
        CertificateGenerationCourseSetting.objects.update_or_create(
            course_key=course_key, defaults=default)
コード例 #7
0
ファイル: models.py プロジェクト: shurmanov/edx-platform
class RestrictedCourse(models.Model):
    """Course with access restrictions.

    Restricted courses can block users at two points:

    1) When enrolling in a course.
    2) When attempting to access a course the user is already enrolled in.

    The second case can occur when new restrictions
    are put into place; for example, when new countries
    are embargoed.

    Restricted courses can be configured to display
    messages to users when they are blocked.
    These displayed on pages served by the embargo app.

    """
    COURSE_LIST_CACHE_KEY = 'embargo.restricted_courses'
    MESSAGE_URL_CACHE_KEY = 'embargo.message_url_path.{access_point}.{course_key}'

    ENROLL_MSG_KEY_CHOICES = tuple([
        (msg_key, msg.description)
        for msg_key, msg in ENROLL_MESSAGES.iteritems()
    ])

    COURSEWARE_MSG_KEY_CHOICES = tuple([
        (msg_key, msg.description)
        for msg_key, msg in COURSEWARE_MESSAGES.iteritems()
    ])

    course_key = CourseKeyField(
        max_length=255, db_index=True, unique=True,
        help_text=ugettext_lazy(u"The course key for the restricted course.")
    )

    enroll_msg_key = models.CharField(
        max_length=255,
        choices=ENROLL_MSG_KEY_CHOICES,
        default='default',
        help_text=ugettext_lazy(u"The message to show when a user is blocked from enrollment.")
    )

    access_msg_key = models.CharField(
        max_length=255,
        choices=COURSEWARE_MSG_KEY_CHOICES,
        default='default',
        help_text=ugettext_lazy(u"The message to show when a user is blocked from accessing a course.")
    )

    disable_access_check = models.BooleanField(
        default=False,
        help_text=ugettext_lazy(
            u"Allow users who enrolled in an allowed country "
            u"to access restricted courses from excluded countries."
        )
    )

    @classmethod
    def is_restricted_course(cls, course_id):
        """
        Check if the course is in restricted list

        Args:
            course_id (str): course_id to look for

        Returns:
            Boolean
            True if course is in restricted course list.
        """
        return unicode(course_id) in cls._get_restricted_courses_from_cache()

    @classmethod
    def is_disabled_access_check(cls, course_id):
        """
        Check if the course is in restricted list has disabled_access_check

        Args:
            course_id (str): course_id to look for

        Returns:
            Boolean
            disabled_access_check attribute of restricted course
        """

        # checking is_restricted_course method also here to make sure course exists in the list otherwise in case of
        # no course found it will throw the key not found error on 'disable_access_check'
        return (
            cls.is_restricted_course(unicode(course_id))
            and cls._get_restricted_courses_from_cache().get(unicode(course_id))["disable_access_check"]
        )

    @classmethod
    def _get_restricted_courses_from_cache(cls):
        """
        Cache all restricted courses and returns the dict of course_keys and disable_access_check that are restricted
        """
        restricted_courses = cache.get(cls.COURSE_LIST_CACHE_KEY)
        if restricted_courses is None:
            restricted_courses = {
                unicode(course.course_key): {
                    'disable_access_check': course.disable_access_check
                }
                for course in RestrictedCourse.objects.all()
            }
            cache.set(cls.COURSE_LIST_CACHE_KEY, restricted_courses)
        return restricted_courses

    def snapshot(self):
        """Return a snapshot of all access rules for this course.

        This is useful for recording an audit trail of rule changes.
        The returned dictionary is JSON-serializable.

        Returns:
            dict

        Example Usage:
        >>> restricted_course.snapshot()
        {
            'enroll_msg': 'default',
            'access_msg': 'default',
            'country_rules': [
                {'country': 'IR', 'rule_type': 'blacklist'},
                {'country': 'CU', 'rule_type': 'blacklist'}
            ]
        }

        """
        country_rules_for_course = (
            CountryAccessRule.objects
        ).select_related('country').filter(restricted_course=self)

        return {
            'enroll_msg': self.enroll_msg_key,
            'access_msg': self.access_msg_key,
            'country_rules': [
                {
                    'country': unicode(rule.country.country),
                    'rule_type': rule.rule_type
                }
                for rule in country_rules_for_course
            ]
        }

    def message_key_for_access_point(self, access_point):
        """Determine which message to show the user.

        The message can be configured per-course and depends
        on how the user is trying to access the course
        (trying to enroll or accessing courseware).

        Arguments:
            access_point (str): Either "courseware" or "enrollment"

        Returns:
            str: The message key.  If the access point is not valid,
                returns None instead.

        """
        if access_point == 'enrollment':
            return self.enroll_msg_key
        elif access_point == 'courseware':
            return self.access_msg_key

    def __unicode__(self):
        return unicode(self.course_key)

    @classmethod
    def message_url_path(cls, course_key, access_point):
        """Determine the URL path for the message explaining why the user was blocked.

        This is configured per-course.  See `RestrictedCourse` in the `embargo.models`
        module for more details.

        Arguments:
            course_key (CourseKey): The location of the course.
            access_point (str): How the user was trying to access the course.
                Can be either "enrollment" or "courseware".

        Returns:
            unicode: The URL path to a page explaining why the user was blocked.

        Raises:
            InvalidAccessPoint: Raised if access_point is not a supported value.

        """
        if access_point not in ['enrollment', 'courseware']:
            raise InvalidAccessPoint(access_point)

        # First check the cache to see if we already have
        # a URL for this (course_key, access_point) tuple
        cache_key = cls.MESSAGE_URL_CACHE_KEY.format(
            access_point=access_point,
            course_key=course_key
        )
        url = cache.get(cache_key)

        # If there's a cache miss, we'll need to retrieve the message
        # configuration from the database
        if url is None:
            url = cls._get_message_url_path_from_db(course_key, access_point)
            cache.set(cache_key, url)

        return url

    @classmethod
    def _get_message_url_path_from_db(cls, course_key, access_point):
        """Retrieve the "blocked" message from the database.

        Arguments:
            course_key (CourseKey): The location of the course.
            access_point (str): How the user was trying to access the course.
                Can be either "enrollment" or "courseware".

        Returns:
            unicode: The URL path to a page explaining why the user was blocked.

        """
        # Fallback in case we're not able to find a message path
        # Presumably if the caller is requesting a URL, the caller
        # has already determined that the user should be blocked.
        # We use generic messaging unless we find something more specific,
        # but *always* return a valid URL path.
        default_path = reverse(
            'embargo:blocked_message',
            kwargs={
                'access_point': 'courseware',
                'message_key': 'default'
            }
        )

        # First check whether this is a restricted course.
        # The list of restricted courses is cached, so this does
        # not require a database query.
        if not cls.is_restricted_course(course_key):
            return default_path

        # Retrieve the message key from the restricted course
        # for this access point, then determine the URL.
        try:
            course = cls.objects.get(course_key=course_key)
            msg_key = course.message_key_for_access_point(access_point)
            return reverse(
                'embargo:blocked_message',
                kwargs={
                    'access_point': access_point,
                    'message_key': msg_key
                }
            )
        except cls.DoesNotExist:
            # This occurs only if there's a race condition
            # between cache invalidation and database access.
            return default_path

    @classmethod
    def invalidate_cache_for_course(cls, course_key):
        """Invalidate the caches for the restricted course. """
        cache.delete(cls.COURSE_LIST_CACHE_KEY)
        log.info("Invalidated cached list of restricted courses.")

        for access_point in ['enrollment', 'courseware']:
            msg_cache_key = cls.MESSAGE_URL_CACHE_KEY.format(
                access_point=access_point,
                course_key=course_key
            )
            cache.delete(msg_cache_key)
        log.info("Invalidated cached messaging URLs ")
コード例 #8
0
class VerificationDeadline(TimeStampedModel):
    """
    Represent a verification deadline for a particular course.

    The verification deadline is the datetime after which
    users are no longer allowed to submit photos for initial verification
    in a course.

    Note that this is NOT the same as the "upgrade" deadline, after
    which a user is no longer allowed to upgrade to a verified enrollment.

    If no verification deadline record exists for a course,
    then that course does not have a deadline.  This means that users
    can submit photos at any time.

    .. no_pii:
    """
    class Meta(object):
        app_label = "verify_student"

    course_key = CourseKeyField(
        max_length=255,
        db_index=True,
        unique=True,
        help_text=ugettext_lazy(u"The course for which this deadline applies"),
    )

    deadline = models.DateTimeField(help_text=ugettext_lazy(
        u"The datetime after which users are no longer allowed "
        "to submit photos for verification."))

    # The system prefers to set this automatically based on default settings. But
    # if the field is set manually we want a way to indicate that so we don't
    # overwrite the manual setting of the field.
    deadline_is_explicit = models.BooleanField(default=False)

    ALL_DEADLINES_CACHE_KEY = "verify_student.all_verification_deadlines"

    @classmethod
    def set_deadline(cls, course_key, deadline, is_explicit=False):
        """
        Configure the verification deadline for a course.

        If `deadline` is `None`, then the course will have no verification
        deadline.  In this case, users will be able to verify for the course
        at any time.

        Arguments:
            course_key (CourseKey): Identifier for the course.
            deadline (datetime or None): The verification deadline.

        """
        if deadline is None:
            VerificationDeadline.objects.filter(course_key=course_key).delete()
        else:
            record, created = VerificationDeadline.objects.get_or_create(
                course_key=course_key,
                defaults={
                    "deadline": deadline,
                    "deadline_is_explicit": is_explicit
                })

            if not created:
                record.deadline = deadline
                record.deadline_is_explicit = is_explicit
                record.save()

    @classmethod
    def deadlines_for_courses(cls, course_keys):
        """
        Retrieve verification deadlines for particular courses.

        Arguments:
            course_keys (list): List of `CourseKey`s.

        Returns:
            dict: Map of course keys to datetimes (verification deadlines)

        """
        all_deadlines = cache.get(cls.ALL_DEADLINES_CACHE_KEY)
        if all_deadlines is None:
            all_deadlines = {
                deadline.course_key: deadline.deadline
                for deadline in VerificationDeadline.objects.all()
            }
            cache.set(cls.ALL_DEADLINES_CACHE_KEY, all_deadlines)

        return {
            course_key: all_deadlines[course_key]
            for course_key in course_keys if course_key in all_deadlines
        }

    @classmethod
    def deadline_for_course(cls, course_key):
        """
        Retrieve the verification deadline for a particular course.

        Arguments:
            course_key (CourseKey): The identifier for the course.

        Returns:
            datetime or None

        """
        try:
            deadline = cls.objects.get(course_key=course_key)
            return deadline.deadline
        except cls.DoesNotExist:
            return None
class Migration(migrations.Migration):

    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
        ('video_pipeline', '0002_auto_20171114_0704'),
    ]

    operations = [
        migrations.CreateModel(
            name='CourseVideoUploadsEnabledByDefault',
            fields=[
                ('id',
                 models.AutoField(verbose_name='ID',
                                  serialize=False,
                                  auto_created=True,
                                  primary_key=True)),
                ('change_date',
                 models.DateTimeField(auto_now_add=True,
                                      verbose_name='Change date')),
                ('enabled',
                 models.BooleanField(default=False, verbose_name='Enabled')),
                ('course_id', CourseKeyField(max_length=255, db_index=True)),
                ('changed_by',
                 models.ForeignKey(on_delete=django.db.models.deletion.PROTECT,
                                   editable=False,
                                   to=settings.AUTH_USER_MODEL,
                                   null=True,
                                   verbose_name='Changed by')),
            ],
            options={
                'ordering': ('-change_date', ),
                'abstract': False,
            },
        ),
        migrations.CreateModel(
            name='VideoUploadsEnabledByDefault',
            fields=[
                ('id',
                 models.AutoField(verbose_name='ID',
                                  serialize=False,
                                  auto_created=True,
                                  primary_key=True)),
                ('change_date',
                 models.DateTimeField(auto_now_add=True,
                                      verbose_name='Change date')),
                ('enabled',
                 models.BooleanField(default=False, verbose_name='Enabled')),
                ('enabled_for_all_courses',
                 models.BooleanField(default=False)),
                ('changed_by',
                 models.ForeignKey(on_delete=django.db.models.deletion.PROTECT,
                                   editable=False,
                                   to=settings.AUTH_USER_MODEL,
                                   null=True,
                                   verbose_name='Changed by')),
            ],
            options={
                'ordering': ('-change_date', ),
                'abstract': False,
            },
        ),
    ]
コード例 #10
0
class Migration(migrations.Migration):

    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
    ]

    operations = [
        migrations.CreateModel(
            name='CohortMembership',
            fields=[
                ('id',
                 models.AutoField(verbose_name='ID',
                                  serialize=False,
                                  auto_created=True,
                                  primary_key=True)),
                ('course_id', CourseKeyField(max_length=255)),
            ],
        ),
        migrations.CreateModel(
            name='CourseCohort',
            fields=[
                ('id',
                 models.AutoField(verbose_name='ID',
                                  serialize=False,
                                  auto_created=True,
                                  primary_key=True)),
                ('assignment_type',
                 models.CharField(default=b'manual',
                                  max_length=20,
                                  choices=[(b'random', b'Random'),
                                           (b'manual', b'Manual')])),
            ],
        ),
        migrations.CreateModel(
            name='CourseCohortsSettings',
            fields=[
                ('id',
                 models.AutoField(verbose_name='ID',
                                  serialize=False,
                                  auto_created=True,
                                  primary_key=True)),
                ('is_cohorted', models.BooleanField(default=False)),
                ('course_id',
                 CourseKeyField(
                     help_text=
                     b'Which course are these settings associated with?',
                     unique=True,
                     max_length=255,
                     db_index=True)),
                ('_cohorted_discussions',
                 models.TextField(null=True,
                                  db_column=b'cohorted_discussions',
                                  blank=True)),
                ('always_cohort_inline_discussions',
                 models.BooleanField(default=True)),
            ],
        ),
        migrations.CreateModel(
            name='CourseUserGroup',
            fields=[
                ('id',
                 models.AutoField(verbose_name='ID',
                                  serialize=False,
                                  auto_created=True,
                                  primary_key=True)),
                ('name',
                 models.CharField(
                     help_text=
                     b'What is the name of this group?  Must be unique within a course.',
                     max_length=255)),
                ('course_id',
                 CourseKeyField(
                     help_text=b'Which course is this group associated with?',
                     max_length=255,
                     db_index=True)),
                ('group_type',
                 models.CharField(max_length=20,
                                  choices=[(b'cohort', b'Cohort')])),
                ('users',
                 models.ManyToManyField(help_text=b'Who is in this group?',
                                        related_name='course_groups',
                                        to=settings.AUTH_USER_MODEL,
                                        db_index=True)),
            ],
        ),
        migrations.CreateModel(
            name='CourseUserGroupPartitionGroup',
            fields=[
                ('id',
                 models.AutoField(verbose_name='ID',
                                  serialize=False,
                                  auto_created=True,
                                  primary_key=True)),
                ('partition_id',
                 models.IntegerField(
                     help_text=
                     b'contains the id of a cohorted partition in this course')
                 ),
                ('group_id',
                 models.IntegerField(
                     help_text=
                     b'contains the id of a specific group within the cohorted partition'
                 )),
                ('created_at', models.DateTimeField(auto_now_add=True)),
                ('updated_at', models.DateTimeField(auto_now=True)),
                ('course_user_group',
                 models.OneToOneField(to='course_groups.CourseUserGroup')),
            ],
        ),
        migrations.AddField(
            model_name='coursecohort',
            name='course_user_group',
            field=models.OneToOneField(related_name='cohort',
                                       to='course_groups.CourseUserGroup'),
        ),
        migrations.AddField(
            model_name='cohortmembership',
            name='course_user_group',
            field=models.ForeignKey(to='course_groups.CourseUserGroup'),
        ),
        migrations.AddField(
            model_name='cohortmembership',
            name='user',
            field=models.ForeignKey(to=settings.AUTH_USER_MODEL),
        ),
        migrations.AlterUniqueTogether(
            name='courseusergroup',
            unique_together=set([('name', 'course_id')]),
        ),
        migrations.AlterUniqueTogether(
            name='cohortmembership',
            unique_together=set([('user', 'course_id')]),
        ),
    ]
コード例 #11
0
class VerificationDeadline(TimeStampedModel):
    """
    Represent a verification deadline for a particular course.

    The verification deadline is the datetime after which
    users are no longer allowed to submit photos for initial verification
    in a course.

    Note that this is NOT the same as the "upgrade" deadline, after
    which a user is no longer allowed to upgrade to a verified enrollment.

    If no verification deadline record exists for a course,
    then that course does not have a deadline.  This means that users
    can submit photos at any time.

    .. no_pii:
    """
    class Meta:
        app_label = "verify_student"

    course_key = CourseKeyField(
        max_length=255,
        db_index=True,
        unique=True,
        help_text=ugettext_lazy("The course for which this deadline applies"),
    )

    deadline = models.DateTimeField(help_text=ugettext_lazy(
        "The datetime after which users are no longer allowed "
        "to submit photos for verification."))

    # The system prefers to set this automatically based on default settings. But
    # if the field is set manually we want a way to indicate that so we don't
    # overwrite the manual setting of the field.
    deadline_is_explicit = models.BooleanField(default=False)

    @classmethod
    def set_deadline(cls, course_key, deadline, is_explicit=False):
        """
        Configure the verification deadline for a course.

        If `deadline` is `None`, then the course will have no verification
        deadline.  In this case, users will be able to verify for the course
        at any time.

        Arguments:
            course_key (CourseKey): Identifier for the course.
            deadline (datetime or None): The verification deadline.

        """
        if deadline is None:
            VerificationDeadline.objects.filter(course_key=course_key).delete()
        else:
            record, created = VerificationDeadline.objects.get_or_create(
                course_key=course_key,
                defaults={
                    "deadline": deadline,
                    "deadline_is_explicit": is_explicit
                })

            if not created:
                record.deadline = deadline
                record.deadline_is_explicit = is_explicit
                record.save()

    @classmethod
    def deadlines_for_enrollments(cls, enrollments_qs):
        """
        Retrieve verification deadlines for a user's enrolled courses.

        Arguments:
            enrollments_qs: CourseEnrollment queryset. For performance reasons
                            we want the queryset here instead of passing in a
                            big list of course_keys in an "SELECT IN" query.
                            If we have a queryset, Django is smart enough to do
                            a performant subquery at the MySQL layer instead of
                            passing down all the course_keys through Python.

        Returns:
            dict: Map of course keys to datetimes (verification deadlines)
        """
        verification_deadlines = VerificationDeadline.objects.filter(
            course_key__in=enrollments_qs.values('course_id'))
        return {
            deadline.course_key: deadline.deadline
            for deadline in verification_deadlines
        }

    @classmethod
    def deadline_for_course(cls, course_key):
        """
        Retrieve the verification deadline for a particular course.

        Arguments:
            course_key (CourseKey): The identifier for the course.

        Returns:
            datetime or None

        """
        try:
            deadline = cls.objects.get(course_key=course_key)
            return deadline.deadline
        except cls.DoesNotExist:
            return None
コード例 #12
0
ファイル: models.py プロジェクト: yrchen/edx-platform
class ContentLibraryBlockImportTask(models.Model):
    """
    Model of a task to import blocks from an external source (e.g. modulestore).
    """

    library = models.ForeignKey(
        ContentLibrary,
        on_delete=models.CASCADE,
        related_name='import_tasks',
    )

    TASK_CREATED = 'created'
    TASK_PENDING = 'pending'
    TASK_RUNNING = 'running'
    TASK_FAILED = 'failed'
    TASK_SUCCESSFUL = 'successful'

    TASK_STATE_CHOICES = (
        (TASK_CREATED, _('Task was created, but not queued to run.')),
        (TASK_PENDING, _('Task was created and queued to run.')),
        (TASK_RUNNING, _('Task is running.')),
        (TASK_FAILED, _('Task finished, but some blocks failed to import.')),
        (TASK_SUCCESSFUL, _('Task finished successfully.')),
    )

    state = models.CharField(
        choices=TASK_STATE_CHOICES,
        default=TASK_CREATED,
        max_length=30,
        verbose_name=_('state'),
        help_text=_('The state of the block import task.'),
    )

    progress = models.FloatField(
        default=0.0,
        verbose_name=_('progress'),
        help_text=_('A float from 0.0 to 1.0 representing the task progress.'),
    )

    course_id = CourseKeyField(
        max_length=255,
        db_index=True,
        verbose_name=_('course ID'),
        help_text=_('ID of the imported course.'),
    )

    created_at = models.DateTimeField(auto_now_add=True)

    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ['-created_at', '-updated_at']

    @classmethod
    @contextlib.contextmanager
    def execute(cls, import_task_id):
        """
        A context manager to manage a task that is being executed.
        """
        self = cls.objects.get(pk=import_task_id)
        self.state = self.TASK_RUNNING
        self.save()
        try:
            yield self
            self.state = self.TASK_SUCCESSFUL
        except:  # pylint: disable=broad-except
            self.state = self.TASK_FAILED
            raise
        finally:
            self.save()

    def save_progress(self, progress):
        self.progress = progress
        self.save(update_fields=['progress', 'updated_at'])

    def __str__(self):
        return f'{self.course_id} to {self.library} #{self.pk}'
コード例 #13
0
class CourseTeam(models.Model):
    """
    This model represents team related info.

    .. no_pii:
    """
    def __str__(self):
        return f"{self.name} in {self.course_id}"

    def __repr__(self):
        return (  # lint-amnesty, pylint: disable=missing-format-attribute
            "<CourseTeam"
            " id={0.id}"
            " team_id={0.team_id}"
            " team_size={0.team_size}"
            " topic_id={0.topic_id}"
            " course_id={0.course_id}"
            ">"
        ).format(self)

    class Meta:
        app_label = "teams"

    team_id = models.SlugField(max_length=255, unique=True)
    discussion_topic_id = models.SlugField(max_length=255, unique=True)
    name = models.CharField(max_length=255, db_index=True)
    course_id = CourseKeyField(max_length=255, db_index=True)
    topic_id = models.CharField(default='', max_length=255, db_index=True, blank=True)
    date_created = models.DateTimeField(auto_now_add=True)
    description = models.CharField(max_length=300)
    country = CountryField(default='', blank=True)
    language = LanguageField(
        default='',
        blank=True,
        help_text=ugettext_lazy("Optional language the team uses as ISO 639-1 code."),
    )
    # indexed for ordering
    last_activity_at = models.DateTimeField(default=utc_now, db_index=True)
    users = models.ManyToManyField(User, db_index=True, related_name='teams', through='CourseTeamMembership')
    team_size = models.IntegerField(default=0, db_index=True)  # indexed for ordering

    field_tracker = FieldTracker()

    # This field would divide the teams into two mutually exclusive groups
    # If the team is org protected, the members in a team is enrolled into a degree bearing institution
    # If the team is not org protected, the members in a team is part of the general edX learning community
    # We need this exclusion for learner privacy protection
    organization_protected = models.BooleanField(default=False)

    # Don't emit changed events when these fields change.
    FIELD_BLACKLIST = ['last_activity_at', 'team_size']

    @classmethod
    def create(
        cls,
        name,
        course_id,
        description,
        topic_id='',
        country='',
        language='',
        organization_protected=False
    ):
        """Create a complete CourseTeam object.

        Args:
            name (str): The name of the team to be created.
            course_id (str): The ID string of the course associated
              with this team.
            description (str): A description of the team.
            topic_id (str): An optional identifier for the topic the
              team formed around.
            country (str, optional): An optional country where the team
              is based, as ISO 3166-1 code.
            language (str, optional): An optional language which the
              team uses, as ISO 639-1 code.
            organization_protected (bool, optional): specifies whether the team should only
              contain members who are in a organization context, or not

        """
        unique_id = uuid4().hex
        team_id = slugify(name)[0:20] + '-' + unique_id
        discussion_topic_id = unique_id

        course_team = cls(
            team_id=team_id,
            discussion_topic_id=discussion_topic_id,
            name=name,
            course_id=course_id,
            topic_id=topic_id,
            description=description,
            country=country,
            language=language,
            organization_protected=organization_protected
        )

        return course_team

    def add_user(self, user):
        """Adds the given user to the CourseTeam."""
        from lms.djangoapps.teams.api import user_protection_status_matches_team

        if not CourseEnrollment.is_enrolled(user, self.course_id):
            raise NotEnrolledInCourseForTeam
        if CourseTeamMembership.user_in_team_for_teamset(user, self.course_id, self.topic_id):
            raise AlreadyOnTeamInTeamset
        if not user_protection_status_matches_team(user, self):
            raise AddToIncompatibleTeamError
        return CourseTeamMembership.objects.create(
            user=user,
            team=self
        )

    def reset_team_size(self):
        """Reset team_size to reflect the current membership count."""
        self.team_size = CourseTeamMembership.objects.filter(team=self).count()
        self.save()
コード例 #14
0
ファイル: models.py プロジェクト: fdns/eol-edx
class ProgramCourseEnrollment(TimeStampedModel):  # pylint: disable=model-missing-unicode
    """
    This is a model to represent a learner's enrollment in a course
    in the context of a program from the registrar service

    .. no_pii:
    """
    STATUSES = (
        ('active', 'active'),
        ('inactive', 'inactive'),
    )

    class Meta(object):
        app_label = "program_enrollments"

    program_enrollment = models.ForeignKey(
        ProgramEnrollment,
        on_delete=models.CASCADE,
        related_name="program_course_enrollments")
    course_enrollment = models.OneToOneField(
        CourseEnrollment,
        null=True,
        blank=True,
    )
    course_key = CourseKeyField(max_length=255)
    status = models.CharField(max_length=9, choices=STATUSES)
    historical_records = HistoricalRecords()

    def __str__(self):
        return '[ProgramCourseEnrollment id={}]'.format(self.id)

    @classmethod
    def create_program_course_enrollment(cls, program_enrollment, course_key,
                                         status):
        """
        Create ProgramCourseEnrollment for the given course and program enrollment
        """
        program_course_enrollment = ProgramCourseEnrollment.objects.create(
            program_enrollment=program_enrollment,
            course_key=course_key,
            status=status,
        )

        if program_enrollment.user:
            program_course_enrollment.enroll(program_enrollment.user)

        return program_course_enrollment.status

    def change_status(self, status):
        """
        Modify ProgramCourseEnrollment status and course_enrollment status if it exists
        """
        if status == self.status:
            return status

        self.status = status
        if self.course_enrollment:
            if status == ProgramCourseEnrollmentResponseStatuses.ACTIVE:
                self.course_enrollment.activate()
            elif status == ProgramCourseEnrollmentResponseStatuses.INACTIVE:
                self.course_enrollment.deactivate()
            else:
                message = (
                    "Changed {enrollment} status to {status}, not changing course_enrollment"
                    " status because status is not '{active}' or '{inactive}'")
                logger.warn(
                    message.format(
                        enrollment=self,
                        status=status,
                        active=ProgramCourseEnrollmentResponseStatuses.ACTIVE,
                        inactive=ProgramCourseEnrollmentResponseStatuses.
                        INACTIVE))
        elif self.program_enrollment.user:
            logger.warn(
                "User {user} {program_enrollment} {course_key} has no course_enrollment"
                .format(
                    user=self.program_enrollment.user,
                    program_enrollment=self.program_enrollment,
                    course_key=self.course_key,
                ))
        self.save()
        return self.status

    def enroll(self, user):
        """
        Create a CourseEnrollment to enroll user in course
        """
        try:
            self.course_enrollment = CourseEnrollment.enroll(
                user,
                self.course_key,
                mode=CourseMode.MASTERS,
                check_access=True,
            )
        except AlreadyEnrolledError:
            course_enrollment = CourseEnrollment.objects.get(
                user=user,
                course_id=self.course_key,
            )
            if course_enrollment.mode == CourseMode.AUDIT or course_enrollment.mode == CourseMode.HONOR:
                course_enrollment.mode = CourseMode.MASTERS
                course_enrollment.save()
            self.course_enrollment = course_enrollment
            message = (
                "Attempted to create course enrollment for user={user} and course={course}"
                " but an enrollment already exists. Existing enrollment will be used instead"
            )
            logger.info(message.format(user=user.id, course=self.course_key))
        if self.status == ProgramCourseEnrollmentResponseStatuses.INACTIVE:
            self.course_enrollment.deactivate()
        self.save()
コード例 #15
0
ファイル: models.py プロジェクト: fdns/eol-edx
class CertificateGenerationHistory(TimeStampedModel):
    """
    Model for storing Certificate Generation History.

    .. no_pii:
    """

    course_id = CourseKeyField(max_length=255)
    generated_by = models.ForeignKey(User, on_delete=models.CASCADE)
    instructor_task = models.ForeignKey(InstructorTask,
                                        on_delete=models.CASCADE)
    is_regeneration = models.BooleanField(default=False)

    def get_task_name(self):
        """
        Return "regenerated" if record corresponds to Certificate Regeneration task, otherwise returns 'generated'
        """
        # Translators: This is a past-tense verb that is used for task action messages.
        return _("regenerated") if self.is_regeneration else _("generated")

    def get_certificate_generation_candidates(self):
        """
        Return the candidates for certificate generation task. It could either be students or certificate statuses
        depending upon the nature of certificate generation task. Returned value could be one of the following,

        1. "All learners" Certificate Generation task was initiated for all learners of the given course.
        2. Comma separated list of certificate statuses, This usually happens when instructor regenerates certificates.
        3. "for exceptions", This is the case when instructor generates certificates for white-listed
            students.
        """
        task_input = self.instructor_task.task_input
        if not task_input.strip():
            # if task input is empty, it means certificates were generated for all learners
            # Translators: This string represents task was executed for all learners.
            return _("All learners")

        task_input_json = json.loads(task_input)

        # get statuses_to_regenerate from task_input convert statuses to human readable strings and return
        statuses = task_input_json.get('statuses_to_regenerate', None)
        if statuses:
            readable_statuses = [
                CertificateStatuses.readable_statuses.get(status)
                for status in statuses if
                CertificateStatuses.readable_statuses.get(status) is not None
            ]
            return ", ".join(readable_statuses)

        # If "student_set" is present in task_input, then this task only
        # generates certificates for white listed students. Note that
        # this key used to be "students", so we include that in this conditional
        # for backwards compatibility.
        if 'student_set' in task_input_json or 'students' in task_input_json:
            # Translators: This string represents task was executed for students having exceptions.
            return _("For exceptions")
        else:
            return _("All learners")

    class Meta(object):
        app_label = "certificates"

    def __unicode__(self):
        return u"certificates %s by %s on %s for %s" % \
               ("regenerated" if self.is_regeneration else "generated", self.generated_by, self.created, self.course_id)
コード例 #16
0
ファイル: models.py プロジェクト: edx/edx-when
class DummyCourse(models.Model):
    """
    .. no_pii:
    """

    id = CourseKeyField(db_index=True, primary_key=True, max_length=255)
コード例 #17
0
ファイル: models.py プロジェクト: fdns/eol-edx
class ExampleCertificateSet(TimeStampedModel):
    """
    A set of example certificates.

    Example certificates are used to verify that certificate
    generation is working for a particular course.

    A particular course may have several kinds of certificates
    (e.g. honor and verified), in which case we generate
    multiple example certificates for the course.

    .. no_pii:
    """
    course_key = CourseKeyField(max_length=255, db_index=True)

    class Meta(object):
        get_latest_by = 'created'
        app_label = "certificates"

    @classmethod
    @transaction.atomic
    def create_example_set(cls, course_key):
        """Create a set of example certificates for a course.

        Arguments:
            course_key (CourseKey)

        Returns:
            ExampleCertificateSet

        """
        # Import here instead of top of file since this module gets imported before
        # the course_modes app is loaded, resulting in a Django deprecation warning.
        from course_modes.models import CourseMode
        cert_set = cls.objects.create(course_key=course_key)

        ExampleCertificate.objects.bulk_create([
            ExampleCertificate(example_cert_set=cert_set,
                               description=mode.slug,
                               template=cls._template_for_mode(
                                   mode.slug, course_key))
            for mode in CourseMode.modes_for_course(course_key)
        ])

        return cert_set

    @classmethod
    def latest_status(cls, course_key):
        """Summarize the latest status of example certificates for a course.

        Arguments:
            course_key (CourseKey)

        Returns:
            list: List of status dictionaries.  If no example certificates
                have been started yet, returns None.

        """
        try:
            latest = cls.objects.filter(course_key=course_key).latest()
        except cls.DoesNotExist:
            return None

        queryset = ExampleCertificate.objects.filter(
            example_cert_set=latest).order_by('-created')
        return [cert.status_dict for cert in queryset]

    def __iter__(self):
        """Iterate through example certificates in the set.

        Yields:
            ExampleCertificate

        """
        queryset = (ExampleCertificate.objects
                    ).select_related('example_cert_set').filter(
                        example_cert_set=self)
        for cert in queryset:
            yield cert

    @staticmethod
    def _template_for_mode(mode_slug, course_key):
        """Calculate the template PDF based on the course mode. """
        return (
            u"certificate-template-{key.org}-{key.course}-verified.pdf".format(
                key=course_key) if mode_slug == 'verified' else
            u"certificate-template-{key.org}-{key.course}.pdf".format(
                key=course_key))
コード例 #18
0
class Migration(migrations.Migration):

    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
    ]

    operations = [
        migrations.CreateModel(
            name='CreditCourse',
            fields=[
                ('id',
                 models.AutoField(verbose_name='ID',
                                  serialize=False,
                                  auto_created=True,
                                  primary_key=True)),
                ('course_key',
                 CourseKeyField(unique=True, max_length=255, db_index=True)),
                ('enabled', models.BooleanField(default=False)),
            ],
        ),
        migrations.CreateModel(
            name='CreditEligibility',
            fields=[
                ('id',
                 models.AutoField(verbose_name='ID',
                                  serialize=False,
                                  auto_created=True,
                                  primary_key=True)),
                ('created',
                 model_utils.fields.AutoCreatedField(
                     default=django.utils.timezone.now,
                     verbose_name='created',
                     editable=False)),
                ('modified',
                 model_utils.fields.AutoLastModifiedField(
                     default=django.utils.timezone.now,
                     verbose_name='modified',
                     editable=False)),
                ('username', models.CharField(max_length=255, db_index=True)),
                ('deadline',
                 models.DateTimeField(
                     default=openedx.core.djangoapps.credit.models.
                     default_deadline_for_credit_eligibility,
                     help_text='Deadline for purchasing and requesting credit.'
                 )),
                ('course',
                 models.ForeignKey(related_name='eligibilities',
                                   to='credit.CreditCourse',
                                   on_delete=models.CASCADE)),
            ],
            options={
                'verbose_name_plural': 'Credit eligibilities',
            },
        ),
        migrations.CreateModel(
            name='CreditProvider',
            fields=[
                ('id',
                 models.AutoField(verbose_name='ID',
                                  serialize=False,
                                  auto_created=True,
                                  primary_key=True)),
                ('created',
                 model_utils.fields.AutoCreatedField(
                     default=django.utils.timezone.now,
                     verbose_name='created',
                     editable=False)),
                ('modified',
                 model_utils.fields.AutoLastModifiedField(
                     default=django.utils.timezone.now,
                     verbose_name='modified',
                     editable=False)),
                ('provider_id',
                 models.CharField(
                     help_text=
                     'Unique identifier for this credit provider. Only alphanumeric characters and hyphens (-) are allowed. The identifier is case-sensitive.',
                     unique=True,
                     max_length=255,
                     validators=[
                         django.core.validators.RegexValidator(
                             regex=u'[a-z,A-Z,0-9,\\-]+',
                             message=
                             u'Only alphanumeric characters and hyphens (-) are allowed',
                             code=u'invalid_provider_id')
                     ])),
                ('active',
                 models.BooleanField(
                     default=True,
                     help_text=
                     'Whether the credit provider is currently enabled.')),
                ('display_name',
                 models.CharField(
                     help_text='Name of the credit provider displayed to users',
                     max_length=255)),
                ('enable_integration',
                 models.BooleanField(
                     default=False,
                     help_text=
                     'When true, automatically notify the credit provider when a user requests credit. In order for this to work, a shared secret key MUST be configured for the credit provider in secure auth settings.'
                 )),
                ('provider_url',
                 models.URLField(
                     default=u'',
                     help_text=
                     'URL of the credit provider.  If automatic integration is enabled, this will the the end-point that we POST to to notify the provider of a credit request.  Otherwise, the user will be shown a link to this URL, so the user can request credit from the provider directly.'
                 )),
                ('provider_status_url',
                 models.URLField(
                     default=u'',
                     help_text=
                     'URL from the credit provider where the user can check the status of his or her request for credit.  This is displayed to students *after* they have requested credit.'
                 )),
                ('provider_description',
                 models.TextField(
                     default=u'',
                     help_text=
                     'Description for the credit provider displayed to users.')
                 ),
                ('fulfillment_instructions',
                 models.TextField(
                     help_text=
                     'Plain text or html content for displaying further steps on receipt page *after* paying for the credit to get credit for a credit course against a credit provider.',
                     null=True,
                     blank=True)),
                ('eligibility_email_message',
                 models.TextField(
                     default=u'',
                     help_text=
                     'Plain text or html content for displaying custom message inside credit eligibility email content which is sent when user has met all credit eligibility requirements.'
                 )),
                ('receipt_email_message',
                 models.TextField(
                     default=u'',
                     help_text=
                     'Plain text or html content for displaying custom message inside credit receipt email content which is sent *after* paying to get credit for a credit course.'
                 )),
                ('thumbnail_url',
                 models.URLField(
                     default=u'',
                     help_text='Thumbnail image url of the credit provider.',
                     max_length=255)),
            ],
            options={
                'abstract': False,
            },
        ),
        migrations.CreateModel(
            name='CreditRequest',
            fields=[
                ('id',
                 models.AutoField(verbose_name='ID',
                                  serialize=False,
                                  auto_created=True,
                                  primary_key=True)),
                ('created',
                 model_utils.fields.AutoCreatedField(
                     default=django.utils.timezone.now,
                     verbose_name='created',
                     editable=False)),
                ('modified',
                 model_utils.fields.AutoLastModifiedField(
                     default=django.utils.timezone.now,
                     verbose_name='modified',
                     editable=False)),
                ('uuid',
                 models.CharField(unique=True, max_length=32, db_index=True)),
                ('username', models.CharField(max_length=255, db_index=True)),
                ('parameters', jsonfield.fields.JSONField()),
                ('status',
                 models.CharField(default=u'pending',
                                  max_length=255,
                                  choices=[(u'pending', u'Pending'),
                                           (u'approved', u'Approved'),
                                           (u'rejected', u'Rejected')])),
                ('course',
                 models.ForeignKey(related_name='credit_requests',
                                   to='credit.CreditCourse',
                                   on_delete=models.CASCADE)),
                ('provider',
                 models.ForeignKey(related_name='credit_requests',
                                   to='credit.CreditProvider',
                                   on_delete=models.CASCADE)),
            ],
            options={
                'get_latest_by': 'created',
            },
        ),
        migrations.CreateModel(
            name='CreditRequirement',
            fields=[
                ('id',
                 models.AutoField(verbose_name='ID',
                                  serialize=False,
                                  auto_created=True,
                                  primary_key=True)),
                ('created',
                 model_utils.fields.AutoCreatedField(
                     default=django.utils.timezone.now,
                     verbose_name='created',
                     editable=False)),
                ('modified',
                 model_utils.fields.AutoLastModifiedField(
                     default=django.utils.timezone.now,
                     verbose_name='modified',
                     editable=False)),
                ('namespace', models.CharField(max_length=255)),
                ('name', models.CharField(max_length=255)),
                ('display_name', models.CharField(default=u'',
                                                  max_length=255)),
                ('order', models.PositiveIntegerField(default=0)),
                ('criteria', jsonfield.fields.JSONField()),
                ('active', models.BooleanField(default=True)),
                ('course',
                 models.ForeignKey(related_name='credit_requirements',
                                   to='credit.CreditCourse',
                                   on_delete=models.CASCADE)),
            ],
            options={
                'ordering': ['order'],
            },
        ),
        migrations.CreateModel(
            name='CreditRequirementStatus',
            fields=[
                ('id',
                 models.AutoField(verbose_name='ID',
                                  serialize=False,
                                  auto_created=True,
                                  primary_key=True)),
                ('created',
                 model_utils.fields.AutoCreatedField(
                     default=django.utils.timezone.now,
                     verbose_name='created',
                     editable=False)),
                ('modified',
                 model_utils.fields.AutoLastModifiedField(
                     default=django.utils.timezone.now,
                     verbose_name='modified',
                     editable=False)),
                ('username', models.CharField(max_length=255, db_index=True)),
                ('status',
                 models.CharField(max_length=32,
                                  choices=[(u'satisfied', u'satisfied'),
                                           (u'failed', u'failed'),
                                           (u'declined', u'declined')])),
                ('reason', jsonfield.fields.JSONField(default={})),
                ('requirement',
                 models.ForeignKey(related_name='statuses',
                                   to='credit.CreditRequirement',
                                   on_delete=models.CASCADE)),
            ],
        ),
        migrations.CreateModel(
            name='HistoricalCreditRequest',
            fields=[
                ('id',
                 models.IntegerField(verbose_name='ID',
                                     db_index=True,
                                     auto_created=True,
                                     blank=True)),
                ('created',
                 model_utils.fields.AutoCreatedField(
                     default=django.utils.timezone.now,
                     verbose_name='created',
                     editable=False)),
                ('modified',
                 model_utils.fields.AutoLastModifiedField(
                     default=django.utils.timezone.now,
                     verbose_name='modified',
                     editable=False)),
                ('uuid', models.CharField(max_length=32, db_index=True)),
                ('username', models.CharField(max_length=255, db_index=True)),
                ('parameters', jsonfield.fields.JSONField()),
                ('status',
                 models.CharField(default=u'pending',
                                  max_length=255,
                                  choices=[(u'pending', u'Pending'),
                                           (u'approved', u'Approved'),
                                           (u'rejected', u'Rejected')])),
                ('history_id',
                 models.AutoField(serialize=False, primary_key=True)),
                ('history_date', models.DateTimeField()),
                ('history_type',
                 models.CharField(max_length=1,
                                  choices=[('+', 'Created'), ('~', 'Changed'),
                                           ('-', 'Deleted')])),
                ('course',
                 models.ForeignKey(
                     related_name='+',
                     on_delete=django.db.models.deletion.DO_NOTHING,
                     db_constraint=False,
                     blank=True,
                     to='credit.CreditCourse',
                     null=True)),
                ('history_user',
                 models.ForeignKey(
                     related_name='+',
                     on_delete=django.db.models.deletion.SET_NULL,
                     to=settings.AUTH_USER_MODEL,
                     null=True)),
                ('provider',
                 models.ForeignKey(
                     related_name='+',
                     on_delete=django.db.models.deletion.DO_NOTHING,
                     db_constraint=False,
                     blank=True,
                     to='credit.CreditProvider',
                     null=True)),
            ],
            options={
                'ordering': ('-history_date', '-history_id'),
                'get_latest_by': 'history_date',
                'verbose_name': 'historical credit request',
            },
        ),
        migrations.CreateModel(
            name='HistoricalCreditRequirementStatus',
            fields=[
                ('id',
                 models.IntegerField(verbose_name='ID',
                                     db_index=True,
                                     auto_created=True,
                                     blank=True)),
                ('created',
                 model_utils.fields.AutoCreatedField(
                     default=django.utils.timezone.now,
                     verbose_name='created',
                     editable=False)),
                ('modified',
                 model_utils.fields.AutoLastModifiedField(
                     default=django.utils.timezone.now,
                     verbose_name='modified',
                     editable=False)),
                ('username', models.CharField(max_length=255, db_index=True)),
                ('status',
                 models.CharField(max_length=32,
                                  choices=[(u'satisfied', u'satisfied'),
                                           (u'failed', u'failed'),
                                           (u'declined', u'declined')])),
                ('reason', jsonfield.fields.JSONField(default={})),
                ('history_id',
                 models.AutoField(serialize=False, primary_key=True)),
                ('history_date', models.DateTimeField()),
                ('history_type',
                 models.CharField(max_length=1,
                                  choices=[('+', 'Created'), ('~', 'Changed'),
                                           ('-', 'Deleted')])),
                ('history_user',
                 models.ForeignKey(
                     related_name='+',
                     on_delete=django.db.models.deletion.SET_NULL,
                     to=settings.AUTH_USER_MODEL,
                     null=True)),
                ('requirement',
                 models.ForeignKey(
                     related_name='+',
                     on_delete=django.db.models.deletion.DO_NOTHING,
                     db_constraint=False,
                     blank=True,
                     to='credit.CreditRequirement',
                     null=True)),
            ],
            options={
                'ordering': ('-history_date', '-history_id'),
                'get_latest_by': 'history_date',
                'verbose_name': 'historical credit requirement status',
            },
        ),
        migrations.AlterUniqueTogether(
            name='creditrequirementstatus',
            unique_together=set([('username', 'requirement')]),
        ),
        migrations.AlterUniqueTogether(
            name='creditrequirement',
            unique_together=set([('namespace', 'name', 'course')]),
        ),
        migrations.AlterUniqueTogether(
            name='creditrequest',
            unique_together=set([('username', 'course', 'provider')]),
        ),
        migrations.AlterUniqueTogether(
            name='crediteligibility',
            unique_together=set([('username', 'course')]),
        ),
    ]
コード例 #19
0
ファイル: models.py プロジェクト: shurmanov/edx-platform
class CourseAccessRuleHistory(models.Model):
    """History of course access rule changes. """
    # pylint: disable=model-missing-unicode

    timestamp = models.DateTimeField(db_index=True, auto_now_add=True)
    course_key = CourseKeyField(max_length=255, db_index=True)
    snapshot = models.TextField(null=True, blank=True)

    DELETED_PLACEHOLDER = "DELETED"

    @classmethod
    def save_snapshot(cls, restricted_course, deleted=False):
        """Save a snapshot of access rules for a course.

        Arguments:
            restricted_course (RestrictedCourse)

        Keyword Arguments:
            deleted (boolean): If True, the restricted course
                is about to be deleted.  Create a placeholder
                snapshot recording that the course and all its
                rules was deleted.

        Returns:
            None

        """
        course_key = restricted_course.course_key

        # At the point this is called, the access rules may not have
        # been deleted yet.  When the rules *are* deleted, the
        # restricted course entry may no longer exist, so we
        # won't be able to take a snapshot of the rules.
        # To handle this, we save a placeholder "DELETED" entry
        # so that it's clear in the audit that the restricted
        # course (along with all its rules) was deleted.
        snapshot = (
            CourseAccessRuleHistory.DELETED_PLACEHOLDER if deleted
            else json.dumps(restricted_course.snapshot())
        )

        cls.objects.create(
            course_key=course_key,
            snapshot=snapshot
        )

    @staticmethod
    def snapshot_post_save_receiver(sender, instance, **kwargs):  # pylint: disable=unused-argument
        """Create a snapshot of course access rules when the rules are updated. """
        if isinstance(instance, RestrictedCourse):
            CourseAccessRuleHistory.save_snapshot(instance)
        elif isinstance(instance, CountryAccessRule):
            CourseAccessRuleHistory.save_snapshot(instance.restricted_course)

    @staticmethod
    def snapshot_post_delete_receiver(sender, instance, **kwargs):  # pylint: disable=unused-argument
        """Create a snapshot of course access rules when rules are deleted. """
        if isinstance(instance, RestrictedCourse):
            CourseAccessRuleHistory.save_snapshot(instance, deleted=True)
        elif isinstance(instance, CountryAccessRule):
            try:
                restricted_course = instance.restricted_course
            except RestrictedCourse.DoesNotExist:
                # When Django admin deletes a restricted course, it will
                # also delete the rules associated with that course.
                # At this point, we can't access the restricted course
                # from the rule beause it may already have been deleted.
                # If this happens, we don't need to record anything,
                # since we already record a placeholder "DELETED"
                # entry when the restricted course record is deleted.
                pass
            else:
                CourseAccessRuleHistory.save_snapshot(restricted_course)

    class Meta(object):
        get_latest_by = 'timestamp'
コード例 #20
0
ファイル: models.py プロジェクト: saadow123/1
class InstructorTask(models.Model):
    """
    Stores information about background tasks that have been submitted to
    perform work by an instructor (or course staff).
    Examples include grading and rescoring.

    `task_type` identifies the kind of task being performed, e.g. rescoring.
    `course_id` uses the course run's unique id to identify the course.
    `task_key` stores relevant input arguments encoded into key value for testing to see
           if the task is already running (together with task_type and course_id).
    `task_input` stores input arguments as JSON-serialized dict, for reporting purposes.
        Examples include url of problem being rescored, id of student if only one student being rescored.

    `task_id` stores the id used by celery for the background task.
    `task_state` stores the last known state of the celery task
    `task_output` stores the output of the celery task.
        Format is a JSON-serialized dict.  Content varies by task_type and task_state.

    `requester` stores id of user who submitted the task
    `created` stores date that entry was first created
    `updated` stores date that entry was last modified

    .. no_pii:
    """
    class Meta(object):
        app_label = "instructor_task"

    task_type = models.CharField(max_length=50, db_index=True)
    course_id = CourseKeyField(max_length=255, db_index=True)
    task_key = models.CharField(max_length=255, db_index=True)
    task_input = models.TextField()
    task_id = models.CharField(
        max_length=255, db_index=True)  # max_length from celery_taskmeta
    task_state = models.CharField(
        max_length=50, null=True,
        db_index=True)  # max_length from celery_taskmeta
    task_output = models.CharField(max_length=1024, null=True)
    requester = models.ForeignKey(User,
                                  db_index=True,
                                  on_delete=models.CASCADE)
    created = models.DateTimeField(auto_now_add=True, null=True)
    updated = models.DateTimeField(auto_now=True)
    subtasks = models.TextField(blank=True)  # JSON dictionary

    def __repr__(self):
        return 'InstructorTask<%r>' % ({
            'task_type': self.task_type,
            'course_id': self.course_id,
            'task_input': self.task_input,
            'task_id': self.task_id,
            'task_state': self.task_state,
            'task_output': self.task_output,
        }, )

    def __str__(self):
        return six.text_type(repr(self))

    @classmethod
    def create(cls, course_id, task_type, task_key, task_input, requester):
        """
        Create an instance of InstructorTask.
        """
        # create the task_id here, and pass it into celery:
        task_id = str(uuid4())
        json_task_input = json.dumps(task_input)

        # check length of task_input, and return an exception if it's too long
        if len(json_task_input) > TASK_INPUT_LENGTH:
            logger.error(
                u'Task input longer than: `%s` for `%s` of course: `%s`',
                TASK_INPUT_LENGTH, task_type, course_id)
            error_msg = _('An error has occurred. Task was not created.')
            raise AttributeError(error_msg)

        # create the task, then save it:
        instructor_task = cls(course_id=course_id,
                              task_type=task_type,
                              task_id=task_id,
                              task_key=task_key,
                              task_input=json_task_input,
                              task_state=QUEUING,
                              requester=requester)
        instructor_task.save_now()

        return instructor_task

    @transaction.atomic
    def save_now(self):
        """
        Writes InstructorTask immediately, ensuring the transaction is committed.
        """
        self.save()

    @staticmethod
    def create_output_for_success(returned_result):
        """
        Converts successful result to output format.

        Raises a ValueError exception if the output is too long.
        """
        # In future, there should be a check here that the resulting JSON
        # will fit in the column.  In the meantime, just return an exception.
        json_output = json.dumps(returned_result)
        if len(json_output) > 1023:
            raise ValueError(
                u"Length of task output is too long: {0}".format(json_output))
        return json_output

    @staticmethod
    def create_output_for_failure(exception, traceback_string):
        """
        Converts failed result information to output format.

        Traceback information is truncated or not included if it would result in an output string
        that would not fit in the database.  If the output is still too long, then the
        exception message is also truncated.

        Truncation is indicated by adding "..." to the end of the value.
        """
        tag = '...'
        task_progress = {
            'exception': type(exception).__name__,
            'message': text_type(exception)
        }
        if traceback_string is not None:
            # truncate any traceback that goes into the InstructorTask model:
            task_progress['traceback'] = traceback_string
        json_output = json.dumps(task_progress)
        # if the resulting output is too long, then first shorten the
        # traceback, and then the message, until it fits.
        too_long = len(json_output) - 1023
        if too_long > 0:
            if traceback_string is not None:
                if too_long >= len(traceback_string) - len(tag):
                    # remove the traceback entry entirely (so no key or value)
                    del task_progress['traceback']
                    too_long -= (len(traceback_string) + len('traceback'))
                else:
                    # truncate the traceback:
                    task_progress['traceback'] = traceback_string[:-(
                        too_long + len(tag))] + tag
                    too_long = 0
            if too_long > 0:
                # we need to shorten the message:
                task_progress['message'] = task_progress['message'][:-(
                    too_long + len(tag))] + tag
            json_output = json.dumps(task_progress)
        return json_output

    @staticmethod
    def create_output_for_revoked():
        """Creates standard message to store in output format for revoked tasks."""
        return json.dumps({'message': 'Task revoked before running'})
コード例 #21
0
class CustomCourseForEdX(models.Model):
    """
    A Custom Course.

    .. no_pii:
    """
    course_id = CourseKeyField(max_length=255, db_index=True)
    display_name = models.CharField(max_length=255)
    coach = models.ForeignKey(User, db_index=True, on_delete=models.CASCADE)
    # if not empty, this field contains a json serialized list of
    # the master course modules
    structure_json = models.TextField(verbose_name='Structure JSON',
                                      blank=True,
                                      null=True)

    class Meta(object):
        app_label = 'ccx'

    @lazy
    def course(self):
        """Return the CourseDescriptor of the course related to this CCX"""
        store = modulestore()
        with store.bulk_operations(self.course_id):
            course = store.get_course(self.course_id)
            if not course or isinstance(course, ErrorDescriptor):
                log.error("CCX {0} from {2} course {1}".format(  # pylint: disable=logging-format-interpolation
                    self.display_name, self.course_id,
                    "broken" if course else "non-existent"))
            return course

    @lazy
    def start(self):
        """Get the value of the override of the 'start' datetime for this CCX
        """
        # avoid circular import problems
        from .overrides import get_override_for_ccx
        return get_override_for_ccx(self, self.course, 'start')

    @lazy
    def due(self):
        """Get the value of the override of the 'due' datetime for this CCX
        """
        # avoid circular import problems
        from .overrides import get_override_for_ccx
        return get_override_for_ccx(self, self.course, 'due')

    @lazy
    def max_student_enrollments_allowed(self):
        """
        Get the value of the override of the 'max_student_enrollments_allowed'
        datetime for this CCX
        """
        # avoid circular import problems
        from .overrides import get_override_for_ccx
        return get_override_for_ccx(self, self.course,
                                    'max_student_enrollments_allowed')

    def has_started(self):
        """Return True if the CCX start date is in the past"""
        return datetime.now(utc) > self.start

    def has_ended(self):
        """Return True if the CCX due date is set and is in the past"""
        if self.due is None:
            return False

        return datetime.now(utc) > self.due

    @property
    def structure(self):
        """
        Deserializes a course structure JSON object
        """
        if self.structure_json:
            return json.loads(self.structure_json)
        return None

    @property
    def locator(self):
        """
        Helper property that gets a corresponding CCXLocator for this CCX.

        Returns:
            The CCXLocator corresponding to this CCX.
        """
        return CCXLocator.from_course_locator(self.course_id, unicode(self.id))
コード例 #22
0
class Migration(migrations.Migration):

    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
    ]

    operations = [
        migrations.CreateModel(
            name='Bookmark',
            fields=[
                ('id',
                 models.AutoField(verbose_name='ID',
                                  serialize=False,
                                  auto_created=True,
                                  primary_key=True)),
                ('created',
                 model_utils.fields.AutoCreatedField(
                     default=django.utils.timezone.now,
                     verbose_name='created',
                     editable=False)),
                ('modified',
                 model_utils.fields.AutoLastModifiedField(
                     default=django.utils.timezone.now,
                     verbose_name='modified',
                     editable=False)),
                ('course_key', CourseKeyField(max_length=255, db_index=True)),
                ('usage_key', UsageKeyField(max_length=255, db_index=True)),
                ('_path',
                 jsonfield.fields.JSONField(
                     help_text='Path in course tree to the block',
                     db_column='path')),
                ('user',
                 models.ForeignKey(to=settings.AUTH_USER_MODEL,
                                   on_delete=models.CASCADE)),
            ],
        ),
        migrations.CreateModel(
            name='XBlockCache',
            fields=[
                ('id',
                 models.AutoField(verbose_name='ID',
                                  serialize=False,
                                  auto_created=True,
                                  primary_key=True)),
                ('created',
                 model_utils.fields.AutoCreatedField(
                     default=django.utils.timezone.now,
                     verbose_name='created',
                     editable=False)),
                ('modified',
                 model_utils.fields.AutoLastModifiedField(
                     default=django.utils.timezone.now,
                     verbose_name='modified',
                     editable=False)),
                ('course_key', CourseKeyField(max_length=255, db_index=True)),
                ('usage_key',
                 UsageKeyField(unique=True, max_length=255, db_index=True)),
                ('display_name', models.CharField(default='', max_length=255)),
                ('_paths',
                 jsonfield.fields.JSONField(
                     default=[],
                     help_text=
                     'All paths in course tree to the corresponding block.',
                     db_column='paths')),
            ],
            options={
                'abstract': False,
            },
        ),
        migrations.AddField(
            model_name='bookmark',
            name='xblock_cache',
            field=models.ForeignKey(to='bookmarks.XBlockCache',
                                    on_delete=models.CASCADE),
        ),
        migrations.AlterUniqueTogether(
            name='bookmark',
            unique_together=set([('user', 'usage_key')]),
        ),
    ]
コード例 #23
0
class XBlockCache(TimeStampedModel):
    """
    XBlockCache model to store info about xblocks.

    .. no_pii:
    """

    course_key = CourseKeyField(max_length=255, db_index=True)
    usage_key = UsageKeyField(max_length=255, db_index=True, unique=True)

    display_name = models.CharField(max_length=255, default='')
    _paths = JSONField(
        db_column='paths',
        default=[],
        help_text='All paths in course tree to the corresponding block.')

    def __str__(self):
        return str(self.usage_key)

    @property
    def paths(self):
        """
        Return paths.

        Returns:
            list of list of PathItems.
        """
        return [parse_path_data(path)
                for path in self._paths] if self._paths else self._paths

    @paths.setter
    def paths(self, value):
        """
        Set paths.

        Arguments:
            value (list of list of PathItems): The list of paths to cache.
        """
        self._paths = [prepare_path_for_serialization(path)
                       for path in value] if value else value

    @classmethod
    def create(cls, data):
        """
        Create an XBlockCache object.

        Arguments:
            data (dict): The data to create the object with.

        Returns:
            An XBlockCache object.
        """
        data = dict(data)

        usage_key = data.pop('usage_key')
        usage_key = usage_key.replace(
            course_key=modulestore().fill_in_run(usage_key.course_key))

        data['course_key'] = usage_key.course_key
        xblock_cache, created = cls.objects.get_or_create(usage_key=usage_key,
                                                          defaults=data)

        if not created:
            new_display_name = data.get('display_name',
                                        xblock_cache.display_name)
            if xblock_cache.display_name != new_display_name:
                xblock_cache.display_name = new_display_name
                xblock_cache.save()

        return xblock_cache
コード例 #24
0
ファイル: models.py プロジェクト: fdns/eol-edx
class CertificateTemplate(TimeStampedModel):
    """A set of custom web certificate templates.

    Web certificate templates are Django web templates
    to replace PDF certificate.

    A particular course may have several kinds of certificate templates
    (e.g. honor and verified).

    .. no_pii:
    """
    name = models.CharField(
        max_length=255,
        help_text=_(u'Name of template.'),
    )
    description = models.CharField(
        max_length=255,
        null=True,
        blank=True,
        help_text=_(u'Description and/or admin notes.'),
    )
    template = models.TextField(help_text=_(u'Django template HTML.'), )
    organization_id = models.IntegerField(
        null=True,
        blank=True,
        db_index=True,
        help_text=_(u'Organization of template.'),
    )
    course_key = CourseKeyField(
        max_length=255,
        null=True,
        blank=True,
        db_index=True,
    )
    mode = models.CharField(
        max_length=125,
        choices=GeneratedCertificate.MODES,
        default=GeneratedCertificate.MODES.honor,
        null=True,
        blank=True,
        help_text=_(u'The course mode for this template.'),
    )
    is_active = models.BooleanField(
        help_text=_(u'On/Off switch.'),
        default=False,
    )
    language = models.CharField(
        max_length=2,
        blank=True,
        null=True,
        help_text=
        u'Only certificates for courses in the selected language will be rendered using this template. '
        u'Course language is determined by the first two letters of the language code.'
    )

    def __unicode__(self):
        return u'%s' % (self.name, )

    class Meta(object):
        get_latest_by = 'created'
        unique_together = (('organization_id', 'course_key', 'mode',
                            'language'), )
        app_label = "certificates"
コード例 #25
0
class CourseTeam(models.Model):
    """
    This model represents team related info.

    .. no_pii:
    """
    def __str__(self):
        return "{} in {}".format(self.name, self.course_id)

    def __repr__(self):
        return ("<CourseTeam"
                " id={0.id}"
                " team_id={0.team_id}"
                " team_size={0.team_size}"
                " topic_id={0.topic_id}"
                " course_id={0.course_id}"
                ">").format(self)

    class Meta(object):
        app_label = "teams"

    team_id = models.SlugField(max_length=255, unique=True)
    discussion_topic_id = models.SlugField(max_length=255, unique=True)
    name = models.CharField(max_length=255, db_index=True)
    course_id = CourseKeyField(max_length=255, db_index=True)
    topic_id = models.CharField(max_length=255, db_index=True, blank=True)
    date_created = models.DateTimeField(auto_now_add=True)
    description = models.CharField(max_length=300)
    country = CountryField(blank=True)
    language = LanguageField(
        blank=True,
        help_text=ugettext_lazy(
            "Optional language the team uses as ISO 639-1 code."),
    )
    last_activity_at = models.DateTimeField(
        db_index=True)  # indexed for ordering
    users = models.ManyToManyField(User,
                                   db_index=True,
                                   related_name='teams',
                                   through='CourseTeamMembership')
    team_size = models.IntegerField(default=0,
                                    db_index=True)  # indexed for ordering

    field_tracker = FieldTracker()

    # Don't emit changed events when these fields change.
    FIELD_BLACKLIST = ['last_activity_at', 'team_size']

    @classmethod
    def create(cls,
               name,
               course_id,
               description,
               topic_id=None,
               country=None,
               language=None):
        """Create a complete CourseTeam object.

        Args:
            name (str): The name of the team to be created.
            course_id (str): The ID string of the course associated
              with this team.
            description (str): A description of the team.
            topic_id (str): An optional identifier for the topic the
              team formed around.
            country (str, optional): An optional country where the team
              is based, as ISO 3166-1 code.
            language (str, optional): An optional language which the
              team uses, as ISO 639-1 code.

        """
        unique_id = uuid4().hex
        team_id = slugify(name)[0:20] + '-' + unique_id
        discussion_topic_id = unique_id

        course_team = cls(
            team_id=team_id,
            discussion_topic_id=discussion_topic_id,
            name=name,
            course_id=course_id,
            topic_id=topic_id if topic_id else '',
            description=description,
            country=country if country else '',
            language=language if language else '',
            last_activity_at=datetime.utcnow().replace(tzinfo=pytz.utc))

        return course_team

    def add_user(self, user):
        """Adds the given user to the CourseTeam."""
        if not CourseEnrollment.is_enrolled(user, self.course_id):
            raise NotEnrolledInCourseForTeam
        if CourseTeamMembership.user_in_team_for_course(user, self.course_id):
            raise AlreadyOnTeamInCourse
        return CourseTeamMembership.objects.create(user=user, team=self)

    def reset_team_size(self):
        """Reset team_size to reflect the current membership count."""
        self.team_size = CourseTeamMembership.objects.filter(team=self).count()
        self.save()
コード例 #26
0
ファイル: models.py プロジェクト: fdns/eol-edx
class CertificateWhitelist(models.Model):
    """
    Tracks students who are whitelisted, all users
    in this table will always qualify for a certificate
    regardless of their grade unless they are on the
    embargoed country restriction list
    (allow_certificate set to False in userprofile).

    .. no_pii:
    """
    class Meta(object):
        app_label = "certificates"

    objects = NoneToEmptyManager()

    user = models.ForeignKey(User, on_delete=models.CASCADE)
    course_id = CourseKeyField(max_length=255, blank=True, default=None)
    whitelist = models.BooleanField(default=0)
    created = AutoCreatedField(_('created'))
    notes = models.TextField(default=None, null=True)

    @classmethod
    def get_certificate_white_list(cls, course_id, student=None):
        """
        Return certificate white list for the given course as dict object,
        returned dictionary will have the following key-value pairs

        [{
            id:         'id (pk) of CertificateWhitelist item'
            user_id:    'User Id of the student'
            user_name:  'name of the student'
            user_email: 'email of the student'
            course_id:  'Course key of the course to whom certificate exception belongs'
            created:    'Creation date of the certificate exception'
            notes:      'Additional notes for the certificate exception'
        }, {...}, ...]

        """
        white_list = cls.objects.filter(course_id=course_id, whitelist=True)
        if student:
            white_list = white_list.filter(user=student)
        result = []
        generated_certificates = GeneratedCertificate.eligible_certificates.filter(
            course_id=course_id,
            user__in=[exception.user for exception in white_list],
            status=CertificateStatuses.downloadable)
        generated_certificates = {
            certificate['user']: certificate['created_date']
            for certificate in generated_certificates.values(
                'user', 'created_date')
        }

        for item in white_list:
            certificate_generated = generated_certificates.get(
                item.user.id, '')
            result.append({
                'id':
                item.id,
                'user_id':
                item.user.id,
                'user_name':
                unicode(item.user.username),
                'user_email':
                unicode(item.user.email),
                'course_id':
                unicode(item.course_id),
                'created':
                item.created.strftime(u"%B %d, %Y"),
                'certificate_generated':
                certificate_generated
                and certificate_generated.strftime(u"%B %d, %Y"),
                'notes':
                unicode(item.notes or ''),
            })
        return result
コード例 #27
0
class Migration(migrations.Migration):

    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
    ]

    operations = [
        migrations.CreateModel(
            name='BadgeAssertion',
            fields=[
                ('id',
                 models.AutoField(verbose_name='ID',
                                  serialize=False,
                                  auto_created=True,
                                  primary_key=True)),
                ('course_id',
                 CourseKeyField(default=None, max_length=255, blank=True)),
                ('mode', models.CharField(max_length=100)),
                ('data', jsonfield.fields.JSONField()),
                ('user',
                 models.ForeignKey(to=settings.AUTH_USER_MODEL,
                                   on_delete=models.CASCADE)),
            ],
        ),
        migrations.CreateModel(
            name='BadgeImageConfiguration',
            fields=[
                ('id',
                 models.AutoField(verbose_name='ID',
                                  serialize=False,
                                  auto_created=True,
                                  primary_key=True)),
                ('mode',
                 models.CharField(
                     help_text=
                     'The course mode for this badge image. For example, "verified" or "honor".',
                     unique=True,
                     max_length=125)),
                ('icon',
                 models.ImageField(
                     help_text=
                     'Badge images must be square PNG files. The file size should be under 250KB.',
                     upload_to='badges',
                     validators=[validate_badge_image])),
                ('default',
                 models.BooleanField(
                     default=False,
                     help_text=
                     'Set this value to True if you want this image to be the default image for any course modes that do not have a specified badge image. You can have only one default image.'
                 )),
            ],
        ),
        migrations.CreateModel(
            name='CertificateGenerationConfiguration',
            fields=[
                ('id',
                 models.AutoField(verbose_name='ID',
                                  serialize=False,
                                  auto_created=True,
                                  primary_key=True)),
                ('change_date',
                 models.DateTimeField(auto_now_add=True,
                                      verbose_name='Change date')),
                ('enabled',
                 models.BooleanField(default=False, verbose_name='Enabled')),
                ('changed_by',
                 models.ForeignKey(on_delete=django.db.models.deletion.PROTECT,
                                   editable=False,
                                   to=settings.AUTH_USER_MODEL,
                                   null=True,
                                   verbose_name='Changed by')),
            ],
            options={
                'ordering': ('-change_date', ),
                'abstract': False,
            },
        ),
        migrations.CreateModel(
            name='CertificateGenerationCourseSetting',
            fields=[
                ('id',
                 models.AutoField(verbose_name='ID',
                                  serialize=False,
                                  auto_created=True,
                                  primary_key=True)),
                ('created',
                 model_utils.fields.AutoCreatedField(
                     default=django.utils.timezone.now,
                     verbose_name='created',
                     editable=False)),
                ('modified',
                 model_utils.fields.AutoLastModifiedField(
                     default=django.utils.timezone.now,
                     verbose_name='modified',
                     editable=False)),
                ('course_key', CourseKeyField(max_length=255, db_index=True)),
                ('enabled', models.BooleanField(default=False)),
            ],
            options={
                'get_latest_by': 'created',
            },
        ),
        migrations.CreateModel(
            name='CertificateHtmlViewConfiguration',
            fields=[
                ('id',
                 models.AutoField(verbose_name='ID',
                                  serialize=False,
                                  auto_created=True,
                                  primary_key=True)),
                ('change_date',
                 models.DateTimeField(auto_now_add=True,
                                      verbose_name='Change date')),
                ('enabled',
                 models.BooleanField(default=False, verbose_name='Enabled')),
                ('configuration',
                 models.TextField(
                     help_text=u'Certificate HTML View Parameters (JSON)')),
                ('changed_by',
                 models.ForeignKey(on_delete=django.db.models.deletion.PROTECT,
                                   editable=False,
                                   to=settings.AUTH_USER_MODEL,
                                   null=True,
                                   verbose_name='Changed by')),
            ],
            options={
                'ordering': ('-change_date', ),
                'abstract': False,
            },
        ),
        migrations.CreateModel(
            name='CertificateTemplate',
            fields=[
                ('id',
                 models.AutoField(verbose_name='ID',
                                  serialize=False,
                                  auto_created=True,
                                  primary_key=True)),
                ('created',
                 model_utils.fields.AutoCreatedField(
                     default=django.utils.timezone.now,
                     verbose_name='created',
                     editable=False)),
                ('modified',
                 model_utils.fields.AutoLastModifiedField(
                     default=django.utils.timezone.now,
                     verbose_name='modified',
                     editable=False)),
                ('name',
                 models.CharField(help_text='Name of template.',
                                  max_length=255)),
                ('description',
                 models.CharField(help_text='Description and/or admin notes.',
                                  max_length=255,
                                  null=True,
                                  blank=True)),
                ('template',
                 models.TextField(help_text='Django template HTML.')),
                ('organization_id',
                 models.IntegerField(help_text='Organization of template.',
                                     null=True,
                                     db_index=True,
                                     blank=True)),
                ('course_key',
                 CourseKeyField(db_index=True,
                                max_length=255,
                                null=True,
                                blank=True)),
                ('mode',
                 models.CharField(
                     default=b'honor',
                     choices=[(b'verified', b'verified'), (b'honor', b'honor'),
                              (b'audit', b'audit'),
                              (b'professional', b'professional'),
                              (b'no-id-professional', b'no-id-professional')],
                     max_length=125,
                     blank=True,
                     help_text='The course mode for this template.',
                     null=True)),
                ('is_active',
                 models.BooleanField(default=False,
                                     help_text='On/Off switch.')),
            ],
            options={
                'get_latest_by': 'created',
            },
        ),
        migrations.CreateModel(
            name='CertificateTemplateAsset',
            fields=[
                ('id',
                 models.AutoField(verbose_name='ID',
                                  serialize=False,
                                  auto_created=True,
                                  primary_key=True)),
                ('created',
                 model_utils.fields.AutoCreatedField(
                     default=django.utils.timezone.now,
                     verbose_name='created',
                     editable=False)),
                ('modified',
                 model_utils.fields.AutoLastModifiedField(
                     default=django.utils.timezone.now,
                     verbose_name='modified',
                     editable=False)),
                ('description',
                 models.CharField(help_text='Description of the asset.',
                                  max_length=255,
                                  null=True,
                                  blank=True)),
                ('asset',
                 models.FileField(
                     help_text='Asset file. It could be an image or css file.',
                     max_length=255,
                     upload_to=cert_models.template_assets_path)),
            ],
            options={
                'get_latest_by': 'created',
            },
        ),
        migrations.CreateModel(
            name='CertificateWhitelist',
            fields=[
                ('id',
                 models.AutoField(verbose_name='ID',
                                  serialize=False,
                                  auto_created=True,
                                  primary_key=True)),
                ('course_id',
                 CourseKeyField(default=None, max_length=255, blank=True)),
                ('whitelist', models.BooleanField(default=0)),
                ('created',
                 model_utils.fields.AutoCreatedField(
                     default=django.utils.timezone.now,
                     verbose_name='created',
                     editable=False)),
                ('notes', models.TextField(default=None, null=True)),
                ('user',
                 models.ForeignKey(to=settings.AUTH_USER_MODEL,
                                   on_delete=models.CASCADE)),
            ],
        ),
        migrations.CreateModel(
            name='ExampleCertificate',
            fields=[
                ('id',
                 models.AutoField(verbose_name='ID',
                                  serialize=False,
                                  auto_created=True,
                                  primary_key=True)),
                ('created',
                 model_utils.fields.AutoCreatedField(
                     default=django.utils.timezone.now,
                     verbose_name='created',
                     editable=False)),
                ('modified',
                 model_utils.fields.AutoLastModifiedField(
                     default=django.utils.timezone.now,
                     verbose_name='modified',
                     editable=False)),
                ('description',
                 models.CharField(
                     help_text=
                     "A human-readable description of the example certificate.  For example, 'verified' or 'honor' to differentiate between two types of certificates.",
                     max_length=255)),
                ('uuid',
                 models.CharField(
                     default=cert_models._make_uuid,
                     help_text=
                     'A unique identifier for the example certificate.  This is used when we receive a response from the queue to determine which example certificate was processed.',
                     unique=True,
                     max_length=255,
                     db_index=True)),
                ('access_key',
                 models.CharField(
                     default=cert_models._make_uuid,
                     help_text=
                     'An access key for the example certificate.  This is used when we receive a response from the queue to validate that the sender is the same entity we asked to generate the certificate.',
                     max_length=255,
                     db_index=True)),
                ('full_name',
                 models.CharField(
                     default='John Do\xeb',
                     help_text=
                     'The full name that will appear on the certificate.',
                     max_length=255)),
                ('template',
                 models.CharField(
                     help_text=
                     'The template file to use when generating the certificate.',
                     max_length=255)),
                ('status',
                 models.CharField(
                     default=u'started',
                     help_text='The status of the example certificate.',
                     max_length=255,
                     choices=[(u'started', u'Started'),
                              (u'success', u'Success'),
                              (u'error', u'Error')])),
                ('error_reason',
                 models.TextField(
                     default=None,
                     help_text=
                     'The reason an error occurred during certificate generation.',
                     null=True)),
                ('download_url',
                 models.CharField(
                     default=None,
                     max_length=255,
                     null=True,
                     help_text='The download URL for the generated certificate.'
                 )),
            ],
        ),
        migrations.CreateModel(
            name='ExampleCertificateSet',
            fields=[
                ('id',
                 models.AutoField(verbose_name='ID',
                                  serialize=False,
                                  auto_created=True,
                                  primary_key=True)),
                ('created',
                 model_utils.fields.AutoCreatedField(
                     default=django.utils.timezone.now,
                     verbose_name='created',
                     editable=False)),
                ('modified',
                 model_utils.fields.AutoLastModifiedField(
                     default=django.utils.timezone.now,
                     verbose_name='modified',
                     editable=False)),
                ('course_key', CourseKeyField(max_length=255, db_index=True)),
            ],
            options={
                'get_latest_by': 'created',
            },
        ),
        migrations.CreateModel(
            name='GeneratedCertificate',
            fields=[
                ('id',
                 models.AutoField(verbose_name='ID',
                                  serialize=False,
                                  auto_created=True,
                                  primary_key=True)),
                ('course_id',
                 CourseKeyField(default=None, max_length=255, blank=True)),
                ('verify_uuid',
                 models.CharField(default=u'', max_length=32, blank=True)),
                ('download_uuid',
                 models.CharField(default=u'', max_length=32, blank=True)),
                ('download_url',
                 models.CharField(default=u'', max_length=128, blank=True)),
                ('grade',
                 models.CharField(default=u'', max_length=5, blank=True)),
                ('key', models.CharField(default=u'',
                                         max_length=32,
                                         blank=True)),
                ('distinction', models.BooleanField(default=False)),
                ('status',
                 models.CharField(default=u'unavailable', max_length=32)),
                ('mode',
                 models.CharField(default=u'honor',
                                  max_length=32,
                                  choices=[(u'verified', u'verified'),
                                           (u'honor', u'honor'),
                                           (u'audit', u'audit'),
                                           (u'professional', u'professional'),
                                           (u'no-id-professional',
                                            u'no-id-professional')])),
                ('name', models.CharField(max_length=255, blank=True)),
                ('created_date', models.DateTimeField(auto_now_add=True)),
                ('modified_date', models.DateTimeField(auto_now=True)),
                ('error_reason',
                 models.CharField(default=u'', max_length=512, blank=True)),
                ('user',
                 models.ForeignKey(to=settings.AUTH_USER_MODEL,
                                   on_delete=models.CASCADE)),
            ],
        ),
        migrations.AddField(
            model_name='examplecertificate',
            name='example_cert_set',
            field=models.ForeignKey(to='certificates.ExampleCertificateSet',
                                    on_delete=models.CASCADE),
        ),
        migrations.AlterUniqueTogether(
            name='certificatetemplate',
            unique_together=set([('organization_id', 'course_key', 'mode')]),
        ),
        migrations.AlterUniqueTogether(
            name='generatedcertificate',
            unique_together=set([('user', 'course_id')]),
        ),
        migrations.AlterUniqueTogether(
            name='badgeassertion',
            unique_together=set([('course_id', 'user', 'mode')]),
        ),
    ]
コード例 #28
0
ファイル: models.py プロジェクト: fdns/eol-edx
class GeneratedCertificate(models.Model):
    """
    Base model for generated certificates

    .. pii: PII can exist in the generated certificate linked to in this model. Certificate data is currently retained.
    .. pii_types: name, username
    .. pii_retirement: retained
    """
    # Import here instead of top of file since this module gets imported before
    # the course_modes app is loaded, resulting in a Django deprecation warning.
    from course_modes.models import CourseMode

    # Only returns eligible certificates. This should be used in
    # preference to the default `objects` manager in most cases.
    eligible_certificates = EligibleCertificateManager()

    # Only returns eligible certificates for courses that have an
    # associated CourseOverview
    eligible_available_certificates = EligibleAvailableCertificateManager()

    # Normal object manager, which should only be used when ineligible
    # certificates (i.e. new audit certs) should be included in the
    # results. Django requires us to explicitly declare this.
    objects = models.Manager()

    MODES = Choices('verified', 'honor', 'audit', 'professional',
                    'no-id-professional', 'masters')

    VERIFIED_CERTS_MODES = [
        CourseMode.VERIFIED, CourseMode.CREDIT_MODE, CourseMode.MASTERS
    ]

    user = models.ForeignKey(User, on_delete=models.CASCADE)
    course_id = CourseKeyField(max_length=255, blank=True, default=None)
    verify_uuid = models.CharField(max_length=32,
                                   blank=True,
                                   default='',
                                   db_index=True)
    download_uuid = models.CharField(max_length=32, blank=True, default='')
    download_url = models.CharField(max_length=128, blank=True, default='')
    grade = models.CharField(max_length=5, blank=True, default='')
    key = models.CharField(max_length=32, blank=True, default='')
    distinction = models.BooleanField(default=False)
    status = models.CharField(max_length=32, default='unavailable')
    mode = models.CharField(max_length=32, choices=MODES, default=MODES.honor)
    name = models.CharField(blank=True, max_length=255)
    created_date = models.DateTimeField(auto_now_add=True)
    modified_date = models.DateTimeField(auto_now=True)
    error_reason = models.CharField(max_length=512, blank=True, default='')

    class Meta(object):
        unique_together = (('user', 'course_id'), )
        app_label = "certificates"

    @classmethod
    def certificate_for_student(cls, student, course_id):
        """
        This returns the certificate for a student for a particular course
        or None if no such certificate exits.
        """
        try:
            return cls.objects.get(user=student, course_id=course_id)
        except cls.DoesNotExist:
            pass

        return None

    @classmethod
    def course_ids_with_certs_for_user(cls, user):
        """
        Return a set of CourseKeys for which the user has certificates.

        Sometimes we just want to check if a user has already been issued a
        certificate for a given course (e.g. to test refund elibigility).
        Instead of checking if `certificate_for_student` returns `None` on each
        course_id individually, we instead just return a set of all CourseKeys
        for which this student has certificates all at once.
        """
        return {
            cert.course_id
            for cert in cls.objects.filter(user=user).only('course_id')  # pylint: disable=no-member
        }

    @classmethod
    def get_unique_statuses(cls, course_key=None, flat=False):
        """
        1 - Return unique statuses as a list of dictionaries containing the following key value pairs
            [
            {'status': 'status value from db', 'count': 'occurrence count of the status'},
            {...},
            ..., ]

        2 - if flat is 'True' then return unique statuses as a list
        3 - if course_key is given then return unique statuses associated with the given course

        :param course_key: Course Key identifier
        :param flat: boolean showing whether to return statuses as a list of values or a list of dictionaries.
        """
        query = cls.objects

        if course_key:
            query = query.filter(course_id=course_key)

        if flat:
            return query.values_list('status', flat=True).distinct()
        else:
            return query.values('status').annotate(count=Count('status'))

    def __repr__(self):
        return "<GeneratedCertificate: {course_id}, user={user}>".format(
            course_id=self.course_id, user=self.user)

    def invalidate(self):
        """
        Invalidate Generated Certificate by  marking it 'unavailable'.

        Following is the list of fields with their defaults
            1 - verify_uuid = '',
            2 - download_uuid = '',
            3 - download_url = '',
            4 - grade = ''
            5 - status = 'unavailable'
        """
        self.verify_uuid = ''
        self.download_uuid = ''
        self.download_url = ''
        self.grade = ''
        self.status = CertificateStatuses.unavailable

        self.save()

    def mark_notpassing(self, grade):
        """
        Invalidates a Generated Certificate by marking it as not passing
        """
        self.verify_uuid = ''
        self.download_uuid = ''
        self.download_url = ''
        self.grade = grade
        self.status = CertificateStatuses.notpassing
        self.save()

    def is_valid(self):
        """
        Return True if certificate is valid else return False.
        """
        return self.status == CertificateStatuses.downloadable

    def save(self, *args, **kwargs):
        """
        After the base save() method finishes, fire the COURSE_CERT_AWARDED
        signal iff we are saving a record of a learner passing the course.
        As well as the COURSE_CERT_CHANGED for any save event.
        """
        super(GeneratedCertificate, self).save(*args, **kwargs)
        COURSE_CERT_CHANGED.send_robust(
            sender=self.__class__,
            user=self.user,
            course_key=self.course_id,
            mode=self.mode,
            status=self.status,
        )
        if CertificateStatuses.is_passing_status(self.status):
            COURSE_CERT_AWARDED.send_robust(
                sender=self.__class__,
                user=self.user,
                course_key=self.course_id,
                mode=self.mode,
                status=self.status,
            )
コード例 #29
0
ファイル: models.py プロジェクト: tartansandal/edx-platform
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)

    .. no_pii:
    """

    class Meta(object):
        app_label = 'course_overviews'

    # IMPORTANT: Bump this whenever you modify this model and/or add a migration.
    VERSION = 7

    # 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=u'outdated_entry')
    display_name = TextField(null=True)
    display_number_with_default = TextField()
    display_org_with_default = TextField()

    # Start/end dates
    # TODO Remove 'start' & 'end' in removing field in column renaming, DE-1822
    start = DateTimeField(null=True)
    end = DateTimeField(null=True)
    start_date = DateTimeField(null=True)
    end_date = 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()
    certificate_available_date = DateTimeField(default=None, null=True)

    # 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)
    marketing_url = TextField(null=True)
    eligible_for_financial_aid = BooleanField(default=True)

    language = TextField(null=True)

    history = HistoricalRecords()

    @classmethod
    def _create_or_update(cls, course):
        """
        Creates or updates 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: created or updated 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

        # 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

        course_overview = cls.objects.filter(id=course.id)
        if course_overview.exists():
            log.info(u'Updating course overview for %s.', six.text_type(course.id))
            course_overview = course_overview.first()
        else:
            log.info(u'Creating course overview for %s.', six.text_type(course.id))
            course_overview = cls()

        course_overview.version = cls.VERSION
        course_overview.id = course.id
        course_overview._location = course.location
        course_overview.org = course.location.org
        course_overview.display_name = display_name
        course_overview.display_number_with_default = course.display_number_with_default
        course_overview.display_org_with_default = course.display_org_with_default

        course_overview.start = start
        course_overview.end = end
        course_overview.advertised_start = course.advertised_start
        course_overview.announcement = course.announcement

        course_overview.course_image_url = course_image_url(course)
        course_overview.social_sharing_url = course.social_sharing_url

        course_overview.certificates_display_behavior = course.certificates_display_behavior
        course_overview.certificates_show_before_end = course.certificates_show_before_end
        course_overview.cert_html_view_enabled = course.cert_html_view_enabled
        course_overview.has_any_active_web_certificate = (get_active_web_certificate(course) is not None)
        course_overview.cert_name_short = course.cert_name_short
        course_overview.cert_name_long = course.cert_name_long
        course_overview.certificate_available_date = course.certificate_available_date
        course_overview.lowest_passing_grade = lowest_passing_grade
        course_overview.end_of_course_survey_url = course.end_of_course_survey_url

        course_overview.days_early_for_beta = course.days_early_for_beta
        course_overview.mobile_available = course.mobile_available
        course_overview.visible_to_staff_only = course.visible_to_staff_only
        course_overview._pre_requisite_courses_json = json.dumps(course.pre_requisite_courses)

        course_overview.enrollment_start = course.enrollment_start
        course_overview.enrollment_end = course.enrollment_end
        course_overview.enrollment_domain = course.enrollment_domain
        course_overview.invitation_only = course.invitation_only
        course_overview.max_student_enrollments_allowed = max_student_enrollments_allowed

        course_overview.catalog_visibility = course.catalog_visibility
        course_overview.short_description = CourseDetails.fetch_about_attribute(course.id, 'short_description')
        course_overview.effort = CourseDetails.fetch_about_attribute(course.id, 'effort')
        course_overview.course_video_url = CourseDetails.fetch_video_url(course.id)
        course_overview.self_paced = course.self_paced

        if not CatalogIntegration.is_enabled():
            course_overview.language = course.language

        return course_overview

    @classmethod
    def load_from_module_store(cls, course_id):
        """
        Load a CourseDescriptor, create or update a 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.
        """
        log.info(
            "Attempting to load CourseOverview for course %s from modulestore.",
            course_id,
        )
        store = modulestore()
        with store.bulk_operations(course_id):
            course = store.get_course(course_id)
            if isinstance(course, CourseDescriptor):
                course_overview = cls._create_or_update(course)
                try:
                    with transaction.atomic():
                        course_overview.save()
                        # Remove and recreate all the course tabs
                        CourseOverviewTab.objects.filter(course_overview=course_overview).delete()
                        CourseOverviewTab.objects.bulk_create([
                            CourseOverviewTab(tab_id=tab.tab_id, course_overview=course_overview)
                            for tab in course.tabs
                        ])
                        # Remove and recreate course images
                        CourseOverviewImageSet.objects.filter(course_overview=course_overview).delete()
                        CourseOverviewImageSet.create(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).
                    log.info(
                        "Multiple CourseOverviews for course %s requested "
                        "simultaneously; will only save one.",
                        course_id,
                    )
                except Exception:
                    log.exception(
                        "Saving CourseOverview for course %s failed with "
                        "unexpected exception!",
                        course_id,
                    )
                    raise

                return course_overview
            elif course is not None:
                raise IOError(
                    "Error while loading CourseOverview for course {} "
                    "from the module store: {}",
                    six.text_type(course_id),
                    course.error_msg if isinstance(course, ErrorDescriptor) else six.text_type(course)
                )
            else:
                log.info(
                    "Could not create CourseOverview for non-existent course: %s",
                    course_id,
                )
                raise cls.DoesNotExist()

    @classmethod
    def course_exists(cls, course_id):
        """
        Check whether a course run exists (in CourseOverviews _or_ modulestore).

        Checks the CourseOverview table first.
        If it is not there, check the modulestore.
        Equivalent to, but more efficient than:
            bool(CourseOverview.get_from_id(course_id))

        Arguments:
            course_id (CourseKey)

        Returns: bool
        """
        if cls.objects.filter(id=course_id).exists():
            return True
        return modulestore().has_course(course_id)

    @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:
                # Reload the overview from the modulestore to update the version
                course_overview = cls.load_from_module_store(course_id)
        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(course_overview)

        return course_overview or cls.load_from_module_store(course_id)

    @classmethod
    def get_from_ids(cls, course_ids):
        """
        Return a dict mapping course_ids to CourseOverviews.

        Tries to select all CourseOverviews in one query,
        then fetches remaining (uncached) overviews from the modulestore.

        Course IDs for non-existant courses will map to None.

        Arguments:
            course_ids (iterable[CourseKey])

        Returns: dict[CourseKey, CourseOverview|None]
        """
        overviews = {
            overview.id: overview
            for overview in cls.objects.select_related('image_set').filter(
                id__in=course_ids,
                version__gte=cls.VERSION
            )
        }
        for course_id in course_ids:
            if course_id not in overviews:
                try:
                    overviews[course_id] = cls.load_from_module_store(course_id)
                except CourseOverview.DoesNotExist:
                    overviews[course_id] = None
        return overviews

    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 block_metadata_utils.url_name_for_block(self)

    @property
    def display_name_with_default(self):
        """
        Return reasonable display name for the course.
        """
        return block_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.
        """
        # pylint: disable=line-too-long
        return block_metadata_utils.display_name_with_default_escaped(self)  # xss-lint: disable=python-deprecated-display-name

    @property
    def dashboard_start_display(self):
        """
         Return start date to diplay on learner's dashboard, preferably `Course Advertised Start`
        """
        return self.advertised_start or self.start

    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 has_marketing_url(self):
        """
        Returns whether the course has marketing url.
        """
        return settings.FEATURES.get('ENABLE_MKTG_SITE') and bool(self.marketing_url)

    def has_social_sharing_url(self):
        """
        Returns whether the course has social sharing url.
        """
        is_social_sharing_enabled = getattr(settings, 'SOCIAL_SHARING_SETTINGS', {}).get('CUSTOM_COURSE_URLS')
        return is_social_sharing_enabled and bool(self.social_sharing_url)

    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)

    @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,
        )

    @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(),
            self.certificate_available_date,
            self.self_paced
        )

    @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)

    @pre_requisite_courses.setter
    def pre_requisite_courses(self, value):
        """
        Django requires there be a setter for this, but it is not
        necessary for the way we currently use it. Due to the way
        CourseOverviews are constructed raising errors here will
        cause a lot of issues. These should not be mutable after
        construction, so for now we just eat this.
        """
        pass

    @classmethod
    def update_select_courses(cls, course_keys, force_update=False):
        """
        A side-effecting method that updates CourseOverview objects for
        the given course_keys.

        Arguments:
            course_keys (list[CourseKey]): Identifies for which courses to
                return CourseOverview objects.
            force_update (boolean): Optional parameter that indicates
                whether the requested CourseOverview objects should be
                forcefully updated (i.e., re-synched with the modulestore).
        """
        log.info(u'Generating course overview for %d courses.', len(course_keys))
        log.debug(u'Generating course overview(s) for the following courses: %s', course_keys)

        action = CourseOverview.load_from_module_store if force_update else CourseOverview.get_from_id

        for course_key in course_keys:
            try:
                action(course_key)
            except Exception as ex:  # pylint: disable=broad-except
                log.exception(
                    u'An error occurred while generating course overview for %s: %s',
                    six.text_type(course_key),
                    text_type(ex),
                )

        log.info('Finished generating course overviews.')

    @classmethod
    def get_all_courses(cls, orgs=None, filter_=None):
        """
        Return a queryset containing all CourseOverview objects in the database.

        Arguments:
            orgs (list[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 orgs:
            # 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 matching allows us to deal with this kind of dirty data.
            org_filter = Q()  # Avoiding the `reduce()` for more readability, so a no-op filter starter is needed.
            for org in orgs:
                org_filter |= Q(org__iexact=org)
            course_overviews = course_overviews.filter(org_filter)

        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 CourseOverview.objects.values_list('id', flat=True)

    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 self.apply_cdn_to_urls(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'

    @property
    def closest_released_language(self):
        """
        Returns the language code that most closely matches this course' language and is fully
        supported by the LMS, or None if there are no fully supported languages that
        match the target.
        """
        return get_closest_released_language(self.language) if self.language else None

    def apply_cdn_to_urls(self, image_urls):
        """
        Given a dict of resolutions -> urls, return a copy with CDN applied.

        If CDN does not exist or is disabled, just returns the original. The
        URLs that we store in CourseOverviewImageSet are all already top level
        paths, so we don't need to go through the /static remapping magic that
        happens with other course assets. We just need to add the CDN server if
        appropriate.
        """
        cdn_config = AssetBaseUrlConfig.current()
        if not cdn_config.enabled:
            return image_urls

        base_url = cdn_config.base_url

        return {
            resolution: self._apply_cdn_to_url(url, base_url)
            for resolution, url in image_urls.items()
        }

    def _apply_cdn_to_url(self, url, base_url):
        """
        Applies a new CDN/base URL to the given URL.

        If a URL is absolute, we skip switching the host since it could
        be a hostname that isn't behind our CDN, and we could unintentionally
        break the URL overall.
        """

        # The URL can't be empty.
        if not url:
            return url

        _, netloc, path, params, query, fragment = urlparse(url)

        # If this is an absolute URL, just return it as is.  It could be a domain
        # that isn't ours, and thus CDNing it would actually break it.
        if netloc:
            return url

        return urlunparse(('', base_url, path, params, query, fragment))

    def __str__(self):
        """Represent ourselves with the course key."""
        return six.text_type(self.id)
コード例 #30
0
class PersistentSubsectionGrade(TimeStampedModel):
    """
    A django model tracking persistent grades at the subsection level.

    .. no_pii:
    """
    class Meta:
        app_label = "grades"
        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'),
        ]
        # Allows querying in the following ways:
        # (modified): find all the grades updated within a certain timespan
        # (modified, course_id): find all the grades updated within a timespan for a certain course
        # (modified, course_id, usage_key): find all the grades updated within a timespan for a subsection
        #   in a course
        # (first_attempted, course_id, user_id): find all attempted subsections in a course for a user
        # (first_attempted, course_id): find all attempted subsections in a course for all users
        index_together = [('modified', 'course_id', 'usage_key'),
                          ('first_attempted', 'course_id', 'user_id')]

    # primary key will need to be large for this table
    id = UnsignedBigIntAutoField(primary_key=True)  # pylint: disable=invalid-name

    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=True, null=True)
    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)

    # timestamp for the learner's first attempt at content in
    # this subsection. If null, indicates no attempt
    # has yet been made.
    first_attempted = models.DateTimeField(null=True, blank=True)

    # track which blocks were visible at the time of grade calculation
    visible_blocks = models.ForeignKey(VisibleBlocks,
                                       db_column='visible_blocks_hash',
                                       to_field='hashed',
                                       on_delete=models.CASCADE)

    _CACHE_NAMESPACE = 'grades.models.PersistentSubsectionGrade'

    @property
    def full_usage_key(self):
        """
        Returns the "correct" usage key value with the run filled in.
        """
        if self.usage_key.run is None:  # lint-amnesty, pylint: disable=no-member
            # pylint: disable=unexpected-keyword-arg,no-value-for-parameter
            return self.usage_key.replace(course_key=self.course_id)
        else:
            return self.usage_key

    def __str__(self):
        """
        Returns a string representation of this model.
        """
        return (
            "{} user: {}, course version: {}, subsection: {} ({}). {}/{} graded, {}/{} all, first_attempted: {}"
        ).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,
            self.first_attempted,
        )

    @classmethod
    def prefetch(cls, course_key, users):
        """
        Prefetches grades for the given users in the given course.
        """
        cache_key = cls._cache_key(course_key)
        get_cache(cls._CACHE_NAMESPACE)[cache_key] = defaultdict(list)
        cached_grades = get_cache(cls._CACHE_NAMESPACE)[cache_key]
        queryset = cls.objects.select_related(
            'visible_blocks', 'override').filter(
                user_id__in=[user.id for user in users],
                course_id=course_key,
            )
        for record in queryset:
            cached_grades[record.user_id].append(record)

    @classmethod
    def clear_prefetched_data(cls, course_key):
        """
        Clears prefetched grades for this course from the RequestCache.
        """
        get_cache(cls._CACHE_NAMESPACE).pop(cls._cache_key(course_key), None)

    @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', 'override').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
        """
        try:
            prefetched_grades = get_cache(
                cls._CACHE_NAMESPACE)[cls._cache_key(course_key)]
            try:
                return prefetched_grades[user_id]
            except KeyError:
                # The user's grade is not in the cached dict of subsection grades,
                # so return an empty list.
                return []
        except KeyError:
            # subsection grades were not prefetched for the course, so get them from the DB
            return cls.objects.select_related('visible_blocks',
                                              'override').filter(
                                                  user_id=user_id,
                                                  course_id=course_key,
                                              )

    @classmethod
    def update_or_create_grade(cls, **params):
        """
        Wrapper for objects.update_or_create.
        """
        cls._prepare_params(params)
        VisibleBlocks.cached_get_or_create(params['user_id'],
                                           params['visible_blocks'])
        cls._prepare_params_visible_blocks_id(params)

        # TODO: do we NEED to pop these?
        first_attempted = params.pop('first_attempted')
        user_id = params.pop('user_id')
        usage_key = params.pop('usage_key')

        grade, _ = cls.objects.update_or_create(
            user_id=user_id,
            course_id=usage_key.course_key,
            usage_key=usage_key,
            defaults=params,
        )

        # TODO: Remove as part of EDUCATOR-4602.
        if str(usage_key.course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019':
            log.info(
                'Created/updated grade ***{}*** for user ***{}*** in course ***{}***'
                'for subsection ***{}*** with default params ***{}***'.format(
                    grade, user_id, usage_key.course_key, usage_key, params))

        grade.override = PersistentSubsectionGradeOverride.get_override(
            user_id, usage_key)
        if first_attempted is not None and grade.first_attempted is None:
            grade.first_attempted = first_attempted
            grade.save()

        cls._emit_grade_calculated_event(grade)
        return grade

    @classmethod
    def bulk_create_grades(cls, grade_params_iter, user_id, course_key):
        """
        Bulk creation of grades.
        """
        if not grade_params_iter:
            return

        PersistentSubsectionGradeOverride.prefetch(user_id, course_key)

        list(map(cls._prepare_params, grade_params_iter))
        VisibleBlocks.bulk_get_or_create(
            user_id, course_key,
            [params['visible_blocks'] for params in grade_params_iter])
        list(map(cls._prepare_params_visible_blocks_id, grade_params_iter))

        grades = [
            PersistentSubsectionGrade(**params) for params in grade_params_iter
        ]
        grades = cls.objects.bulk_create(grades)
        for grade in grades:
            cls._emit_grade_calculated_event(grade)
        return grades

    @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']

    @staticmethod
    def _emit_grade_calculated_event(grade):
        events.subsection_grade_calculated(grade)

    @classmethod
    def _cache_key(cls, course_id):
        return f"subsection_grades_cache.{course_id}"