Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .course_index import CourseIndexSerializer
from .course_rerun import CourseRerunSerializer
from .course_team import CourseTeamSerializer
from .course_user_role import CourseUserRoleSerializer
Copy link
Collaborator

Choose a reason for hiding this comment

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

CourseUserRoleSerializer Do you need it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, needed. It's used by CourseUserRoleView to serialize the response data.

from .course_waffle_flags import CourseWaffleFlagsSerializer
from .grading import CourseGradingModelSerializer, CourseGradingSerializer
from .group_configurations import CourseGroupConfigurationsSerializer
Expand Down
6 changes: 6 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v1/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
CourseCertificatesView,
CourseDetailsView,
CourseTeamView,
CourseUserRoleView,
CourseTextbooksView,
CourseIndexView,
CourseGradingView,
Expand Down Expand Up @@ -92,6 +93,11 @@
CourseTeamView.as_view(),
name="course_team"
),
re_path(
fr'^course_user_role/{COURSE_ID_PATTERN}$',
CourseUserRoleView.as_view(),
name="course_user_role"
),
re_path(
fr'^course_grading/{COURSE_ID_PATTERN}$',
CourseGradingView.as_view(),
Expand Down
1 change: 1 addition & 0 deletions cms/djangoapps/contentstore/rest_api/v1/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .course_rerun import CourseRerunView
from .course_waffle_flags import CourseWaffleFlagsView
from .course_team import CourseTeamView
from .course_user_role import CourseUserRoleView
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do you use it? CourseUserRoleView

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, it's used. Registered in urls.py at the route course_user_role/{COURSE_ID_PATTERN}, making it an active API endpoint accessible at /api/contentstore/v1/course_user_role/{course_id}

from .grading import CourseGradingView
from .group_configurations import CourseGroupConfigurationsView
from .help_urls import HelpUrlsView
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
has_studio_read_access,
has_studio_write_access,
)
from common.djangoapps.student.models import CourseAccessRole
from common.djangoapps.student.roles import GlobalStaff
from common.djangoapps.util.date_utils import get_default_time_display
from common.djangoapps.util.json_request import JsonResponse, expect_json
from openedx.core.djangoapps.bookmarks import api as bookmarks_api
Expand Down Expand Up @@ -172,6 +174,43 @@ def handle_xblock(request, usage_key_string=None):
)
if not access_check(request.user, usage_key.course_key):
raise PermissionDenied()

# Debug logging to see what's in the request
log.info(f"=== XBLOCK REQUEST DEBUG === method={request.method}, user={request.user.username}, usage_key={usage_key}")
log.info(f"request.json exists: {hasattr(request, 'json')}, request.json value: {getattr(request, 'json', None)}")

# Check if user is trying to publish and if they have permission
if request.method in ("POST", "PUT", "PATCH"):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why GET not??

try:
publish_action = request.json.get("publish") if hasattr(request, 'json') and request.json else None
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
publish_action = request.json.get("publish") if hasattr(request, 'json') and request.json else None
publish_action = request.json.get("publish") if hasattr(request, "json") and request.json else None

single or double quotes

log.info(f"Publish action check: method={request.method}, publish={publish_action}, user={request.user.username}, is_superuser={request.user.is_superuser}")

if publish_action == "make_public":
# Check the user's course access role from database first
# This check applies to all users, including GlobalStaff
user_course_roles = list(CourseAccessRole.objects.filter(
user=request.user,
course_id=usage_key.course_key,
role__in=['instructor', 'staff']
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
role__in=['instructor', 'staff']
role__in=['instructor', 'staff'],

).values_list('role', flat=True))

is_global_staff = GlobalStaff().has_user(request.user)
log.info(f"Publish permission check: user={request.user.username}, roles={user_course_roles}, is_global_staff={is_global_staff}, course={usage_key.course_key}")

# If user is only staff (not instructor), deny publish permission
# This applies even to GlobalStaff users
if 'staff' in user_course_roles and 'instructor' not in user_course_roles:
log.warning(f"Publish DENIED for staff-only user: {request.user.username} (global_staff={is_global_staff})")
return JsonResponse(
{
"error": _("Only instructors can publish content. Staff members do not have publish permissions.")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why are you using _?

},
status=403,
)
else:
log.info(f"Publish ALLOWED for user: {request.user.username}, roles={user_course_roles}, global_staff={is_global_staff}")
except Exception as e:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Broad exception

log.error(f"Error checking publish permissions: {e}", exc_info=True)

if request.method == "GET":
accept_header = request.META.get("HTTP_ACCEPT", "application/json")
Expand Down
34 changes: 34 additions & 0 deletions common/djangoapps/student/roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -546,3 +546,37 @@ def courses_with_role(self):
* role (will be self.role--thus uninteresting)
"""
return CourseAccessRole.objects.filter(role__in=RoleCache.get_roles(self.role), user=self.user)


def get_user_course_role(user, course_key):
"""
Get the role for a user in a course from CourseAccessRole model.

First checks for course-specific roles (where course_id matches),
then falls back to org-level roles (where course_id is NULL/Empty).
If the user has multiple roles, returns the first one found.

Args:
user: Django User object
course_key: CourseKey for the course

Returns:
str: The exact role name from CourseAccessRole, or None if the user has no role
"""
# First, check for course-specific role
role_entry = CourseAccessRole.objects.filter(
user=user,
course_id=course_key
).first()

if role_entry:
return role_entry.role

# If no course-specific role, check for org-level role (course_id is Empty)
role_entry = CourseAccessRole.objects.filter(
user=user,
org=course_key.org,
course_id=CourseKeyField.Empty
).first()

return role_entry.role if role_entry else None
Loading