Skip to content

Commit

Permalink
Merge pull request #492 from ucsd-ets/alisalman/ucsd-courses
Browse files Browse the repository at this point in the history
Restrict courses to ucsd students only
  • Loading branch information
Ali-Salman29 authored Dec 22, 2022
2 parents a9df544 + 2cb737d commit a8e57d2
Show file tree
Hide file tree
Showing 11 changed files with 66 additions and 31 deletions.
1 change: 1 addition & 0 deletions cms/djangoapps/contentstore/courseware_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,7 @@ def index_about_information(cls, modulestore, course):
'course': course_run_key,
'content': {},
'image_url': course_image_url(course),
'only_allow_ucsd_students': course.only_allow_ucsd_students,
}

# load data for all of the 'about' modules for this course into a dictionary
Expand Down
14 changes: 6 additions & 8 deletions common/lib/xmodule/xmodule/course_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -1028,17 +1028,15 @@ class CourseFields(object):
),
scope=Scope.settings
)
restricted_organizations = List(
display_name=_("Restrict Course"),
only_allow_ucsd_students = Boolean(
display_name=_("Restrict course to UCSD students"),
help=_(
"Restrict course only to certain organizations, "
"the default values is ['*'], it means all organization can access this course. "
"To only allow ucsd emails, replace list with [\"ucsd.\"]. This allow all domains of ucsd i.e [email protected], [email protected]."
"To further restric top level domains you can specify [domain].[top-level-domaain] i.e [\"ucsd.com\", \"ucsd.ca\", \"gmail.com\"]. "
"Similary, you can add as many domains in the list to allow specific organization students to access this course"
"Restrict course only to the ucsd students. "
"If the value is true, It only allow students logedin with ucsd emails. "
"Note that, the studnet's emails should contain @ucsd in their domain name. "
),
scope=Scope.settings,
default=['*'],
default=False,
)

class CourseModule(CourseFields, SequenceModule): # pylint: disable=abstract-method
Expand Down
17 changes: 1 addition & 16 deletions lms/djangoapps/courseware/access_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Simple utility functions for computing access.
It allows us to share code between access.py and block transformers.
"""
import re


from datetime import datetime, timedelta
from logging import getLogger
Expand Down Expand Up @@ -167,18 +167,3 @@ def check_public_access(course, visibilities):
return ACCESS_GRANTED

return ACCESS_DENIED

def check_organization_access(user, course):
"""
Checks if the urers belogs to certain organization
"""
organizations_regexes = course.restricted_organizations
if '*' in organizations_regexes:
return ACCESS_GRANTED

email = user.email if user.is_authenticated else ''
for regex in organizations_regexes:
if re.match(f'^[a-zA-Z0-9_.+-]+@{regex}', email):
return ACCESS_GRANTED

return ACCESS_DENIED
4 changes: 2 additions & 2 deletions lms/djangoapps/courseware/courses.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from fs.errors import ResourceNotFound
from opaque_keys.edx.keys import UsageKey
from openedx.features.course_experience import COURSE_ENABLE_UNENROLLED_ACCESS_FLAG
from openedx.features.ucsd_features.access_utils import check_organization_access
from path import Path as path
from six import text_type

Expand Down Expand Up @@ -49,7 +50,6 @@
from lms.djangoapps.courseware.access_utils import (
check_authentication,
check_enrollment,
check_organization_access,
)
from lms.djangoapps.courseware.courseware_access_exception import CoursewareAccessException
from lms.djangoapps.courseware.exceptions import CourseAccessRedirect
Expand Down Expand Up @@ -696,7 +696,7 @@ def get_courses(user, org=None, filter_=None):
)

return LazySequence(
(c for c in courses if has_access(user, permission_name, c) and check_organization_access(user, get_course_by_id(c.id))),
(c for c in courses if has_access(user, permission_name, c)),
est_len=courses.count()
)

Expand Down
3 changes: 3 additions & 0 deletions lms/djangoapps/courseware/views/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
from openedx.features.course_experience.waffle import ENABLE_COURSE_ABOUT_SIDEBAR_HTML
from openedx.features.course_experience.waffle import waffle as course_experience_waffle
from openedx.features.enterprise_support.api import data_sharing_consent_required
from openedx.features.ucsd_features.utils import check_ucsd_email, get_course_id
from common.djangoapps.student.models import CourseEnrollment, UserTestGroup
from common.djangoapps.track import segment
from common.djangoapps.util.cache import cache, cache_if_anonymous
Expand Down Expand Up @@ -255,6 +256,7 @@ def courses(request):
Render "find courses" page. The course selection work is done in courseware.courses.
"""
courses_list = []
has_ucsd_courses_access = bool(has_access(request.user, 'staff', 'global')) or check_ucsd_email(request.user)
course_discovery_meanings = getattr(settings, 'COURSE_DISCOVERY_MEANINGS', {})
if not settings.FEATURES.get('ENABLE_COURSE_DISCOVERY'):
courses_list = get_courses(request.user)
Expand All @@ -272,6 +274,7 @@ def courses(request):
"courseware/courses.html",
{
'courses': courses_list,
'has_ucsd_courses_access': has_ucsd_courses_access,
'course_discovery_meanings': course_discovery_meanings,
'programs_list': programs_list,
}
Expand Down
4 changes: 2 additions & 2 deletions lms/static/js/discovery/discovery_factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
'js/discovery/views/search_form', 'js/discovery/views/courses_listing',
'js/discovery/views/filter_bar', 'js/discovery/views/refine_sidebar'],
function(Backbone, SearchState, Filters, SearchForm, CoursesListing, FilterBar, RefineSidebar) {
return function(meanings, searchQuery, userLanguage, userTimezone) {
return function(meanings, searchQuery, userLanguage, userTimezone, hasUCSDCoursesAccess) {
var dispatcher = _.extend({}, Backbone.Events);
var search = new SearchState();
var search = new SearchState(hasUCSDCoursesAccess);
var filters = new Filters();
var form = new SearchForm();
var filterBar = new FilterBar({collection: filters});
Expand Down
14 changes: 13 additions & 1 deletion lms/static/js/discovery/models/course_discovery.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,25 @@
latestCount: 0
},

initialize: function() {
initialize: function(hasUCSDCoursesAccess) {
this.hasUCSDCoursesAccess = hasUCSDCoursesAccess;
this.courseCards = new Backbone.Collection([], {model: CourseCard});
this.facetOptions = new Backbone.Collection([], {model: FacetOption});
},

ucsd_course_filter: function (course) {
if (course.data.only_allow_ucsd_students){
if (this.hasUCSDCoursesAccess) {
return true;
}
return false;
}
return true;
},

parse: function(response) {
var courses = response.results || [];
courses = courses.filter(course=>this.ucsd_course_filter(course));
var facets = response.facets || {};
this.courseCards.add(_.pluck(courses, 'data'));
this.set({
Expand Down
4 changes: 2 additions & 2 deletions lms/static/js/discovery/models/search_state.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
terms: {},
jqhxr: null,

initialize: function() {
this.discovery = new CourseDiscovery();
initialize: function(hasUCSDCoursesAccess) {
this.discovery = new CourseDiscovery(hasUCSDCoursesAccess);
this.listenTo(this.discovery, 'sync', this.onSync, this);
this.listenTo(this.discovery, 'error', this.onError, this);
},
Expand Down
18 changes: 18 additions & 0 deletions openedx/features/ucsd_features/access_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""
Simple utility functions for computing access.
"""
from lms.djangoapps.courseware.access_response import AccessResponse
from openedx.features.ucsd_features.utils import check_ucsd_email

ACCESS_GRANTED = AccessResponse(True)
ACCESS_DENIED = AccessResponse(False)

def check_organization_access(user, course):
"""
Checks if the urers belogs to ucsd organization
"""
if course.only_allow_ucsd_students:
if check_ucsd_email(user):
return ACCESS_GRANTED
return ACCESS_DENIED
return ACCESS_GRANTED
10 changes: 10 additions & 0 deletions openedx/features/ucsd_features/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,3 +189,13 @@ def get_course_id(course):
a string course id in this format edX+CS123
"""
return f'{course.org}+{course.number}'.upper()

def check_ucsd_email(user):
"""
Checks if user email's domain is ucsd
"""
if user.is_authenticated and user.email:
email = user.email
domain = email[email.index('@') + 1 : ]
return domain.startswith('ucsd.')
return False
8 changes: 8 additions & 0 deletions openedx/features/ucsd_features/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
from lms.djangoapps.courseware.access_utils import check_public_access
from lms.djangoapps.courseware.courses import (
can_self_enroll_in_course,
check_course_organization_access_with_redirect,
get_course_by_id,
get_course_with_access,
get_permission_for_course_about
)
Expand Down Expand Up @@ -50,9 +52,15 @@ def course_page(request, course_id):
raise Http404(u"Course key not found on discovery")

course_key = course.get('key')
course_canonical_key = course.get('canonical_course_run_key')
course_runs = course.get('course_runs')
course_runs_context = []
course_price = 0

if course_canonical_key:
module_course = get_course_by_id(CourseKey.from_string(course.get('canonical_course_run_key')))
module_course and check_course_organization_access_with_redirect(module_course, request.user)

# extract course run context for each course run
for course_run in course_runs:
# extract price from course runs
Expand Down

0 comments on commit a8e57d2

Please sign in to comment.