Skip to content
Snippets Groups Projects
models.py 56 KiB
Newer Older
            log.error(
                u"Tried to unenroll email %s from course %s, but user not found",
                email,
                course_id
            )
    def is_enrolled(cls, user, course_key):
        """
        Returns True if the user is enrolled in the course (the entry must exist
        and it must have `is_active=True`). Otherwise, returns False.

        `user` is a Django User object. If it hasn't been saved yet (no `.id`
               attribute), this method will automatically save it before
               adding an enrollment for it.

        `course_id` is our usual course_id string (e.g. "edX/Test101/2013_Fall)
        """
        try:
            record = CourseEnrollment.objects.get(user=user, course_id=course_key)
            return record.is_active
        except cls.DoesNotExist:
            return False

    @classmethod
    def is_enrolled_by_partial(cls, user, course_id_partial):
        """
        Returns `True` if the user is enrolled in a course that starts with
        `course_id_partial`. Otherwise, returns False.

        Can be used to determine whether a student is enrolled in a course
        whose run name is unknown.

        `user` is a Django User object. If it hasn't been saved yet (no `.id`
               attribute), this method will automatically save it before
               adding an enrollment for it.

        `course_id_partial` (CourseKey) is missing the run component
        assert isinstance(course_id_partial, CourseKey)
        assert not course_id_partial.run  # None or empty string
        course_key = SlashSeparatedCourseKey(course_id_partial.org, course_id_partial.course, '')
        querystring = unicode(course_key.to_deprecated_string())
        try:
            return CourseEnrollment.objects.filter(
Julian Arni's avatar
Julian Arni committed
                user=user,
Julian Arni's avatar
Julian Arni committed
                is_active=1
            ).exists()
        except cls.DoesNotExist:
            return False

    @classmethod
    def enrollment_mode_for_user(cls, user, course_id):
        """
        Returns the enrollment mode for the given user for the given course

        `user` is a Django User object
        `course_id` is our usual course_id string (e.g. "edX/Test101/2013_Fall)
        Returns (mode, is_active) where mode is the enrollment mode of the student
            and is_active is whether the enrollment is active.
        Returns (None, None) if the courseenrollment record does not exist.
        """
        try:
            record = CourseEnrollment.objects.get(user=user, course_id=course_id)
            return (record.mode, record.is_active)
    @classmethod
    def enrollments_for_user(cls, user):
        return CourseEnrollment.objects.filter(user=user, is_active=1)

    @classmethod
    def users_enrolled_in(cls, course_id):
        """Return a queryset of User for every user enrolled in the course."""
        return User.objects.filter(
            courseenrollment__course_id=course_id,
            courseenrollment__is_active=True
        )

Julia Hansbrough's avatar
Julia Hansbrough committed
    @classmethod
Julia Hansbrough's avatar
Julia Hansbrough committed
    def enrollment_counts(cls, course_id):
Julia Hansbrough's avatar
Julia Hansbrough committed
        """
Julia Hansbrough's avatar
Julia Hansbrough committed
        Returns a dictionary that stores the total enrollment count for a course, as well as the
        enrollment count for each individual mode.
Julia Hansbrough's avatar
Julia Hansbrough committed
        """
Julia Hansbrough's avatar
Julia Hansbrough committed
        # Unfortunately, Django's "group by"-style queries look super-awkward
Julia Hansbrough's avatar
Julia Hansbrough committed
        query = use_read_replica_if_available(cls.objects.filter(course_id=course_id, is_active=True).values('mode').order_by().annotate(Count('mode')))
Julia Hansbrough's avatar
Julia Hansbrough committed
        total = 0
Sarina Canelake's avatar
Sarina Canelake committed
        enroll_dict = defaultdict(int)
Julia Hansbrough's avatar
Julia Hansbrough committed
        for item in query:
Sarina Canelake's avatar
Sarina Canelake committed
            enroll_dict[item['mode']] = item['mode__count']
Julia Hansbrough's avatar
Julia Hansbrough committed
            total += item['mode__count']
Sarina Canelake's avatar
Sarina Canelake committed
        enroll_dict['total'] = total
        return enroll_dict
Julia Hansbrough's avatar
Julia Hansbrough committed

    def is_paid_course(self):
        """
        Returns True, if course is paid
        """
        paid_course = CourseMode.objects.filter(Q(course_id=self.course_id) & Q(mode_slug='honor') &
                                                (Q(expiration_datetime__isnull=True) | Q(expiration_datetime__gte=datetime.now(pytz.UTC)))).exclude(min_price=0)
        if paid_course or self.mode == 'professional':
    def activate(self):
        """Makes this `CourseEnrollment` record active. Saves immediately."""
Julia Hansbrough's avatar
Julia Hansbrough committed
        self.update_enrollment(is_active=True)

    def deactivate(self):
        """Makes this `CourseEnrollment` record inactive. Saves immediately. An
        inactive record means that the student is not enrolled in this course.
        """
Julia Hansbrough's avatar
Julia Hansbrough committed
        self.update_enrollment(is_active=False)
        """Changes this `CourseEnrollment` record's mode to `mode`.  Saves immediately."""
Julia Hansbrough's avatar
Julia Hansbrough committed
        self.update_enrollment(mode=mode)
ichuang's avatar
ichuang committed

    def refundable(self):
        """
        For paid/verified certificates, students may receive a refund if they have
        a verified certificate and the deadline for refunds has not yet passed.
        """
        # In order to support manual refunds past the deadline, set can_refund on this object.
Sarina Canelake's avatar
Sarina Canelake committed
        # On unenrolling, the "UNENROLL_DONE" signal calls CertificateItem.refund_cert_callback(),
        # which calls this method to determine whether to refund the order.
        # This can't be set directly because refunds currently happen as a side-effect of unenrolling.
        # (side-effects are bad)
        if getattr(self, 'can_refund', None) is not None:
            return True

        # If the student has already been given a certificate they should not be refunded
        if GeneratedCertificate.certificate_for_student(self.user, self.course_id) is not None:
            return False

        #TODO - When Course administrators to define a refund period for paid courses then refundable will be supported. # pylint: disable=fixme
        course_mode = CourseMode.mode_for_course(self.course_id, 'verified')
        if course_mode is None:
            return False
        else:
            return True

    @property
    def username(self):
        return self.user.username

    @property
    def course(self):
        return modulestore().get_course(self.course_id)

class CourseEnrollmentAllowed(models.Model):
    """
    Table of users (specified by email address strings) who are allowed to enroll in a specified course.
    The user may or may not (yet) exist.  Enrollment by users listed in this table is allowed
    even if the enrollment time window is past.
    """
    email = models.CharField(max_length=255, db_index=True)
    course_id = CourseKeyField(max_length=255, db_index=True)
    auto_enroll = models.BooleanField(default=0)

    created = models.DateTimeField(auto_now_add=True, null=True, db_index=True)

Sarina Canelake's avatar
Sarina Canelake committed
    class Meta:  # pylint: disable=missing-docstring
        unique_together = (('email', 'course_id'),)
    def __unicode__(self):
        return "[CourseEnrollmentAllowed] %s: %s (%s)" % (self.email, self.course_id, self.created)

@total_ordering
class CourseAccessRole(models.Model):
    """
    Maps users to org, courses, and roles. Used by student.roles.CourseRole and OrgRole.
    To establish a user as having a specific role over all courses in the org, create an entry
    without a course_id.
    """

    objects = NoneToEmptyManager()

    user = models.ForeignKey(User)
    # blank org is for global group based roles such as course creator (may be deprecated)
    org = models.CharField(max_length=64, db_index=True, blank=True)
    # blank course_id implies org wide role
    course_id = CourseKeyField(max_length=255, db_index=True, blank=True)
    role = models.CharField(max_length=64, db_index=True)

Sarina Canelake's avatar
Sarina Canelake committed
    class Meta:  # pylint: disable=missing-docstring
        unique_together = ('user', 'org', 'course_id', 'role')

    @property
    def _key(self):
        """
        convenience function to make eq overrides easier and clearer. arbitrary decision
        that role is primary, followed by org, course, and then user
        """
        return (self.role, self.org, self.course_id, self.user_id)

    def __eq__(self, other):
        """
        Overriding eq b/c the django impl relies on the primary key which requires fetch. sometimes we
        just want to compare roles w/o doing another fetch.
        """
Sarina Canelake's avatar
Sarina Canelake committed
        return type(self) == type(other) and self._key == other._key  # pylint: disable=protected-access

    def __hash__(self):
        return hash(self._key)

    def __lt__(self, other):
        """
        Lexigraphic sort
        """
Sarina Canelake's avatar
Sarina Canelake committed
        return self._key < other._key  # pylint: disable=protected-access
    def __unicode__(self):
        return "[CourseAccessRole] user: {}   role: {}   org: {}   course: {}".format(self.user.username, self.role, self.org, self.course_id)

#### Helper methods for use from python manage.py shell and other classes.

Calen Pennington's avatar
Calen Pennington committed

def get_user_by_username_or_email(username_or_email):
    """
    Return a User object, looking up by email if username_or_email contains a
    '@', otherwise by username.

    Raises:
        User.DoesNotExist is lookup fails.
    """
    if '@' in username_or_email:
        return User.objects.get(email=username_or_email)
    else:
        return User.objects.get(username=username_or_email)
def get_user(email):
Sarina Canelake's avatar
Sarina Canelake committed
    user = User.objects.get(email=email)
    u_prof = UserProfile.objects.get(user=user)
    return user, u_prof

def user_info(email):
Sarina Canelake's avatar
Sarina Canelake committed
    user, u_prof = get_user(email)
    print "User id", user.id
    print "Username", user.username
    print "E-mail", user.email
    print "Name", u_prof.name
    print "Location", u_prof.location
    print "Language", u_prof.language
    return user, u_prof

def change_email(old_email, new_email):
Sarina Canelake's avatar
Sarina Canelake committed
    user = User.objects.get(email=old_email)
    user.email = new_email
    user.save()
def change_name(email, new_name):
Sarina Canelake's avatar
Sarina Canelake committed
    _user, u_prof = get_user(email)
    u_prof.name = new_name
    u_prof.save()
Piotr Mitros's avatar
Piotr Mitros committed
def user_count():
Piotr Mitros's avatar
Piotr Mitros committed
    print "All users", User.objects.all().count()
    print "Active users", User.objects.filter(is_active=True).count()
Piotr Mitros's avatar
Piotr Mitros committed
    return User.objects.all().count()

Piotr Mitros's avatar
Piotr Mitros committed
def active_user_count():
    return User.objects.filter(is_active=True).count()

Piotr Mitros's avatar
Piotr Mitros committed

Piotr Mitros's avatar
Piotr Mitros committed
def create_group(name, description):
    utg = UserTestGroup()
    utg.name = name
    utg.description = description
    utg.save()

def add_user_to_group(user, group):
    utg = UserTestGroup.objects.get(name=group)
    utg.users.add(User.objects.get(username=user))
Piotr Mitros's avatar
Piotr Mitros committed
    utg.save()
def remove_user_from_group(user, group):
    utg = UserTestGroup.objects.get(name=group)
    utg.users.remove(User.objects.get(username=user))
    utg.save()
Sarina Canelake's avatar
Sarina Canelake committed
DEFAULT_GROUPS = {
    'email_future_courses': 'Receive e-mails about future MITx courses',
    'email_helpers': 'Receive e-mails about how to help with MITx',
    'mitx_unenroll': 'Fully unenrolled -- no further communications',
    '6002x_unenroll': 'Took and dropped 6002x'
}

def add_user_to_default_group(user, group):
    try:
        utg = UserTestGroup.objects.get(name=group)
    except UserTestGroup.DoesNotExist:
        utg = UserTestGroup()
        utg.name = group
Sarina Canelake's avatar
Sarina Canelake committed
        utg.description = DEFAULT_GROUPS[group]
    utg.users.add(User.objects.get(username=user))
Piotr Mitros's avatar
Piotr Mitros committed
    utg.save()
ichuang's avatar
ichuang committed

def create_comments_service_user(user):
    if not settings.FEATURES['ENABLE_DISCUSSION_SERVICE']:
        # Don't try--it won't work, and it will fill the logs with lots of errors
        return
Rocky Duan's avatar
Rocky Duan committed
    try:
        cc_user = cc.User.from_django_user(user)
Sarina Canelake's avatar
Sarina Canelake committed
    except Exception:  # pylint: disable=broad-except
        log = logging.getLogger("edx.discussion")  # pylint: disable=redefined-outer-name
        log.error(
            "Could not create comments service user with id {}".format(user.id),
Sarina Canelake's avatar
Sarina Canelake committed
            exc_info=True
        )

# Define login and logout handlers here in the models file, instead of the views file,
# so that they are more likely to be loaded when a Studio user brings up the Studio admin
# page to login.  These are currently the only signals available, so we need to continue
# identifying and logging failures separately (in views).


@receiver(user_logged_in)
Sarina Canelake's avatar
Sarina Canelake committed
def log_successful_login(sender, request, user, **kwargs):  # pylint: disable=unused-argument
    """Handler to log when logins have occurred successfully."""
    if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
        AUDIT_LOG.info(u"Login success - user.id: {0}".format(user.id))
    else:
        AUDIT_LOG.info(u"Login success - {0} ({1})".format(user.username, user.email))
Sarina Canelake's avatar
Sarina Canelake committed
def log_successful_logout(sender, request, user, **kwargs):  # pylint: disable=unused-argument
    """Handler to log when logouts have occurred successfully."""
    if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
        AUDIT_LOG.info(u"Logout - user.id: {0}".format(request.user.id))
    else:
        AUDIT_LOG.info(u"Logout - {0}".format(request.user))


@receiver(user_logged_in)
@receiver(user_logged_out)
Sarina Canelake's avatar
Sarina Canelake committed
def enforce_single_login(sender, request, user, signal, **kwargs):    # pylint: disable=unused-argument
    """
    Sets the current session id in the user profile,
    to prevent concurrent logins.
    """
    if settings.FEATURES.get('PREVENT_CONCURRENT_LOGINS', False):
        if signal == user_logged_in:
            key = request.session.session_key
        else:
            key = None
        if user:
            user.profile.set_login_session(key)


class DashboardConfiguration(ConfigurationModel):
    """Dashboard Configuration settings.

    Includes configuration options for the dashboard, which impact behavior and rendering for the application.

    """
    recent_enrollment_time_delta = models.PositiveIntegerField(
        default=0,
        help_text="The number of seconds in which a new enrollment is considered 'recent'. "
                  "Used to display notifications."
    )

    @property
    def recent_enrollment_seconds(self):
        return self.recent_enrollment_time_delta


class LinkedInAddToProfileConfiguration(ConfigurationModel):
    """
    LinkedIn Add to Profile Configuration

    This configuration enables the "Add to Profile" LinkedIn
    button on the student dashboard.  The button appears when
    users have a certificate available; when clicked,
    users are sent to the LinkedIn site with a pre-filled
    form allowing them to add the certificate to their
    LinkedIn profile.

    MODE_TO_CERT_NAME = {
        "honor": ugettext_lazy(u"{platform_name} Honor Code Certificate for {course_name}"),
        "verified": ugettext_lazy(u"{platform_name} Verified Certificate for {course_name}"),
        "professional": ugettext_lazy(u"{platform_name} Professional Certificate for {course_name}"),
    }

    company_identifier = models.TextField(
            u"The company identifier for the LinkedIn Add-to-Profile button "
    # Deprecated
    dashboard_tracking_code = models.TextField(default="", blank=True)

    trk_partner_name = models.CharField(
        max_length=10,
        default="",
        blank=True,
        help_text=ugettext_lazy(
            u"Short identifier for the LinkedIn partner used in the tracking code.  "
            u"(Example: 'edx')  "
            u"If no value is provided, tracking codes will not be sent to LinkedIn."
        )
    )

    def add_to_profile_url(self, course_key, course_name, cert_mode, cert_url, source="o", target="dashboard"):
        """Construct the URL for the "add to profile" button.
            course_key (CourseKey): The identifier for the course.
            course_name (unicode): The display name of the course.
            cert_mode (str): The course mode of the user's certificate (e.g. "verified", "honor", "professional")
            cert_url (str): The download URL for the certificate.

        Keyword Arguments:
            source (str): Either "o" (for onsite/UI), "e" (for emails), or "m" (for mobile)
            target (str): An identifier for the occurrance of the button.

        """
        params = OrderedDict([
            ('_ed', self.company_identifier),
            ('pfCertificationName', self._cert_name(course_name, cert_mode).encode('utf-8')),
            ('pfCertificationUrl', cert_url),
            ('source', source)
        ])

        tracking_code = self._tracking_code(course_key, cert_mode, target)
        if tracking_code is not None:
            params['trk'] = tracking_code

        return u'http://www.linkedin.com/profile/add?{params}'.format(
            params=urlencode(params)
        )

    def _cert_name(self, course_name, cert_mode):
        """Name of the certification, for display on LinkedIn. """
        return self.MODE_TO_CERT_NAME.get(
            cert_mode,
            _(u"{platform_name} Certificate for {course_name}")
        ).format(
            platform_name=settings.PLATFORM_NAME,
            course_name=course_name
        )

    def _tracking_code(self, course_key, cert_mode, target):
        """Create a tracking code for the button.

        Tracking codes are used by LinkedIn to collect
        analytics about certifications users are adding
        to their profiles.

        The tracking code format is:
            &trk=[partner name]-[certificate type]-[date]-[target field]

        In our case, we're sending:
            &trk=edx-{COURSE ID}_{COURSE MODE}-{TARGET}

        If no partner code is configured, then this will
        return None, indicating that tracking codes are disabled.

        Arguments:

            course_key (CourseKey): The identifier for the course.
            cert_mode (str): The enrollment mode for the course.
            target (str): Identifier for where the button is located.

        Returns:
            unicode or None

        """
        return (
            u"{partner}-{course_key}_{cert_mode}-{target}".format(
                partner=self.trk_partner_name,
                course_key=unicode(course_key),
                cert_mode=cert_mode,
                target=target
            )
            if self.trk_partner_name else None
        )


class EntranceExamConfiguration(models.Model):
    """
    Represents a Student's entrance exam specific data for a single Course
    """

    user = models.ForeignKey(User, db_index=True)
    course_id = CourseKeyField(max_length=255, db_index=True)
    created = models.DateTimeField(auto_now_add=True, null=True, db_index=True)
    updated = models.DateTimeField(auto_now=True, db_index=True)

    # if skip_entrance_exam is True, then student can skip entrance exam
    # for the course
    skip_entrance_exam = models.BooleanField(default=True)

    class Meta(object):
        """
        Meta class to make user and course_id unique in the table
        """
        unique_together = (('user', 'course_id'), )

    def __unicode__(self):
        return "[EntranceExamConfiguration] %s: %s (%s) = %s" % (
            self.user, self.course_id, self.created, self.skip_entrance_exam
        )

    @classmethod
    def user_can_skip_entrance_exam(cls, user, course_key):
        """
        Return True if given user can skip entrance exam for given course otherwise False.
        """
        can_skip = False
        if settings.FEATURES.get('ENTRANCE_EXAMS', False):
            try:
                record = EntranceExamConfiguration.objects.get(user=user, course_id=course_key)
                can_skip = record.skip_entrance_exam
            except EntranceExamConfiguration.DoesNotExist:
                can_skip = False
        return can_skip