Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions common/djangoapps/student/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,13 @@
from six import text_type
from slumber.exceptions import HttpClientError, HttpServerError
from user_util import user_util
from organizations.models import Organization, UserOrganizationMapping
from openedx.core.djangoapps.theming.helpers import get_current_site

import lms.lib.comment_client as cc
from student.signals import UNENROLL_DONE, ENROLL_STATUS_CHANGE, ENROLLMENT_TRACK_UPDATED
from lms.djangoapps.certificates.models import GeneratedCertificate
from lms.djangoapps.instructor.sites import get_organization_for_site
from course_modes.models import CourseMode
from courseware.models import (
CourseDynamicUpgradeDeadlineConfiguration,
Expand Down Expand Up @@ -1571,6 +1574,53 @@ def enroll_by_email(cls, email, course_id, mode=None, ignore_errors=True):
return None
raise

@classmethod
def enroll_by_email_in_organization(cls, email, course_id, mode=None, ignore_errors=True):
"""
Appsembler Specific: This method is a copy of enroll_by_email written
above. It does mostly the same, with the difference that looks for the
user by email, but inside the organization. If the user is registered
but in a different organization, it won't be enrolled.

Enroll a user in a course given their email and looking by the current
organization. This saves immediately.

Note that enrolling by email is generally done in big batches and the
error rate is high. For that reason, we supress User lookup errors by
default.

Returns a CourseEnrollment object. If the User does not exist and
`ignore_errors` is set to `True`, it will return None.

`email` Email address of the User to add to enroll in the course.

`course_id` is our usual course_id string (e.g. "edX/Test101/2013_Fall)

`mode` is a string specifying what kind of enrollment this is. The
default is the default course mode, 'audit'. Other options
include 'professional', 'verified', 'honor',
'no-id-professional' and 'credit'.
See CourseMode in common/djangoapps/course_modes/models.py.

`ignore_errors` is a boolean indicating whether we should suppress
`User.DoesNotExist` errors (returning None) or let it
bubble up.

It is expected that this method is called from a method which has already
verified the user authentication and access.
"""
try:
site = get_current_site()
organization = get_organization_for_site(site)
user = organization.userorganizationmapping_set.get(user__email=email).user
return cls.enroll(user, course_id, mode)
except UserOrganizationMapping.DoesNotExist:
err_msg = u"Tried to enroll email {} into course {}, but user not found"
log.error(err_msg.format(email, course_id))
if ignore_errors:
return None
raise

@classmethod
def unenroll(cls, user, course_id, skip_refund=False):
"""
Expand Down Expand Up @@ -2317,6 +2367,42 @@ def get_user_by_username_or_email(username_or_email):
return user


def get_user_by_username_or_email_inside_organization(username_or_email):
"""
Appsembler Specific: This funtion is a copy of
the get_user_by_username_or_email written above, it basically does the same
with the difference that the user search is done inside the organization,
making sure to not return users that exists in other organizations.

Return a User object by looking up a user against username_or_email but
inside a certain organization.

Raises:
User.DoesNotExist if no user object can be found, the user was
retired, or the user is in the process of being retired.

MultipleObjectsReturned if one user has same email as username of
second user

MultipleObjectsReturned if more than one user has same email or
username
"""
username_or_email = strip_if_string(username_or_email)
# there should be one user with either username or email equal to username_or_email
site = get_current_site()
organization = get_organization_for_site(site)
try:
user = organization.userorganizationmapping_set.get(Q(user__email=username_or_email) | Q(user__username=username_or_email)).user
except UserOrganizationMapping.DoesNotExist:
raise User.DoesNotExist

if user.username == username_or_email:
UserRetirementRequest = apps.get_model('user_api', 'UserRetirementRequest')
if UserRetirementRequest.has_user_requested_retirement(user):
raise User.DoesNotExist
return user


def get_user(email):
user = User.objects.get(email=email)
u_prof = UserProfile.objects.get(user=user)
Expand Down
21 changes: 18 additions & 3 deletions lms/djangoapps/instructor/enrollment.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,14 @@
from lms.djangoapps.grades.events import STATE_DELETED_EVENT_TYPE
from lms.djangoapps.grades.signals.handlers import disconnect_submissions_signal_receiver
from lms.djangoapps.grades.signals.signals import PROBLEM_RAW_SCORE_CHANGED
from lms.djangoapps.instructor.sites import (
user_exists_in_organization,
get_organization_for_site,
get_user_in_organization_by_email,
)
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.theming.helpers import get_current_site
Comment thread
melvinsoft marked this conversation as resolved.
from openedx.core.djangoapps.user_api.models import UserPreference
from student.models import (
CourseEnrollment,
Expand All @@ -52,9 +58,13 @@ def __init__(self, course_id, email):
# N.B. retired users are not a concern here because they should be
# handled at a higher level (i.e. in enroll_email). Besides, this
# class creates readonly objects.
exists_user = User.objects.filter(email=email).exists()
site = get_current_site()
organization = get_organization_for_site(site)
exists_user = user_exists_in_organization(email, organization)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@melvinsoft why do a user check instead of just a single call to try to get the user and use if user instead of if exists_user? Is seems to make an extra call that is not needed.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@johnbaldwin Thanks for asking this, this is edX code, no ours, we trying to fix the issue with the less code changes as possible, since makes future merges way more difficult. I don't like the design, but I choose to stick with the code as much as I can.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, Thanks

if exists_user:
user = User.objects.get(email=email)
# Appsembler Specific: We look for the user inside the organization
# to avoid leakage if the user belong to another organization.
user = get_user_in_organization_by_email(email, organization)
mode, is_active = CourseEnrollment.enrollment_mode_for_user(user, course_id)
# is_active is `None` if the user is not enrolled in the course
exists_ce = is_active is not None and is_active
Expand Down Expand Up @@ -144,7 +154,12 @@ def enroll_email(course_id, student_email, auto_enroll=False, email_students=Fal
if previous_state.enrollment:
course_mode = previous_state.mode

enrollment_obj = CourseEnrollment.enroll_by_email(student_email, course_id, course_mode)
# Appsembler Specific: We call our custom method instead the default one
if settings.FEATURES.get('TAHOE_MULTITENANT_BULK_ENROLLMENT', False):
enrollment_obj = CourseEnrollment.enroll_by_email_in_organization(student_email, course_id, course_mode)
else:
enrollment_obj = CourseEnrollment.enroll_by_email(student_email, course_id, course_mode)

if email_students:
email_params['message'] = 'enrolled_enroll'
email_params['email_address'] = student_email
Expand Down
46 changes: 46 additions & 0 deletions lms/djangoapps/instructor/sites.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""

@johnbaldwin johnbaldwin Aug 13, 2019

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@melvinsoft We may want to move this module out of instructor to a common place. Since our site checking depends on Appsembler's fork of organizations, how about moving it to openedx.core.djangoapps.appsembler.sites.api? The api module seems reasonable to me.

Another option is to just use the existing openedx.core.djangoapps.appsembler.api.sites module, for which I have already created unit tests

Heplers for site/org isolations functions.
"""
from openedx.core.djangoapps.theming.helpers import get_current_site

from organizations.models import Organization, UserOrganizationMapping


def user_exists_in_organization(user_email, organization):
"""
Look is a user exists inside an organization based on a given email

`user_email` is the user email
`organization` the organization object

returns True or False
Representing is the user exists or not inside the org
"""
return organization.userorganizationmapping_set.filter(user__email=user_email).exists()


def get_organization_for_site(site):
"""
Returns an organization based in a given site.

`site` is the Site object

returns an organization or None
"""
return get_current_site().organizations.first()


def get_user_in_organization_by_email(user_email, organization):
"""
Return a user inside an organization based on a given email

`user_email` is the user email
`organization` the organization object

returns the User object or UserOrganizationMapping.DoesNotExist
"""
try:
user = organization.userorganizationmapping_set.get(user__email=user_email).user
return user
except UserOrganizationMapping.DoesNotExist:
return None
15 changes: 13 additions & 2 deletions lms/djangoapps/instructor/views/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
UserProfile,
anonymous_id_for_user,
get_user_by_username_or_email,
get_user_by_username_or_email_inside_organization,
unique_id_for_user,
is_email_retired
)
Expand Down Expand Up @@ -3124,7 +3125,12 @@ def get_student(username_or_email, course_key):
:return: User object
"""
try:
student = get_user_by_username_or_email(username_or_email)
# Appsembler Specific: We call our custom method insted the default one,
# to make sure the user is get inside the org.
if settings.FEATURES.get('TAHOE_MULTITENANT_BULK_ENROLLMENT', False):
Comment thread
melvinsoft marked this conversation as resolved.
student = get_user_by_username_or_email_inside_organization(username_or_email)
else:
student = get_user_by_username_or_email(username_or_email)
except ObjectDoesNotExist:
raise ValueError(_("{user} does not exist in the LMS. Please check your spelling and retry.").format(
user=username_or_email
Expand Down Expand Up @@ -3239,7 +3245,12 @@ def build_row_errors(key, _user, row_count):

user = student[user_index]
try:
user = get_user_by_username_or_email(user)
# Appsembler Specific: We call our custom method insted the default one,
# to make sure the user is get inside the org.
if settings.FEATURES.get('TAHOE_MULTITENANT_BULK_ENROLLMENT', False):
user = get_user_by_username_or_email_inside_organization(user)
else:
user = get_user_by_username_or_email(user)
except ObjectDoesNotExist:
build_row_errors('user_not_exist', user, row_num)
log.info(u'student %s does not exist', user)
Expand Down
13 changes: 11 additions & 2 deletions lms/djangoapps/instructor/views/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@
import json

import dateutil
from django.conf import settings
from django.contrib.auth.models import User
from django.http import HttpResponseBadRequest
from pytz import UTC
from django.utils.translation import ugettext as _
from opaque_keys.edx.keys import UsageKey
from six import text_type, string_types

from student.models import get_user_by_username_or_email
from student.models import (
get_user_by_username_or_email,
get_user_by_username_or_email_inside_organization
)
from courseware.field_overrides import disable_overrides
from courseware.models import StudentFieldOverride
from courseware.student_field_overrides import clear_override_for_user, get_override_for_user, override_field_for_user
Expand Down Expand Up @@ -67,7 +71,12 @@ def get_student_from_identifier(unique_student_identifier):

DEPRECATED: use student.models.get_user_by_username_or_email instead.
"""
return get_user_by_username_or_email(unique_student_identifier)
# Appsembler Specific: We call our custom method insted the default one,
# to make sure the user is get inside the org.
if settings.FEATURES.get('TAHOE_MULTITENANT_BULK_ENROLLMENT', False):
return get_user_by_username_or_email_inside_organization(unique_student_identifier)
else:
return get_user_by_username_or_email(unique_student_identifier)


def require_student_from_identifier(unique_student_identifier):
Expand Down