Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create XBlock permissions for can_view_answer, can_view_correctness #33325

Open
ormsbee opened this issue Sep 22, 2023 · 2 comments · May be fixed by #34320
Open

Create XBlock permissions for can_view_answer, can_view_correctness #33325

ormsbee opened this issue Sep 22, 2023 · 2 comments · May be fixed by #34320

Comments

@ormsbee
Copy link
Contributor

ormsbee commented Sep 22, 2023

The ProblemBlock has some sophisticated logic for determining whether the user should be allowed to see the answer (and also whether they were correct or not):

def answer_available(self):
"""
Is the user allowed to see an answer?
"""
user_is_staff = self.runtime.service(self, 'user').get_current_user().opt_attrs.get(ATTR_KEY_USER_IS_STAFF)
if not self.correctness_available():
# If correctness is being withheld, then don't show answers either.
return False
elif self.showanswer == '':
return False
elif self.showanswer == SHOWANSWER.NEVER:
return False
elif user_is_staff:
# This is after the 'never' check because admins can see the answer
# unless the problem explicitly prevents it
return True
elif self.showanswer == SHOWANSWER.ATTEMPTED:
return self.is_attempted() or self.is_past_due()
elif self.showanswer == SHOWANSWER.ANSWERED:
# NOTE: this is slightly different from 'attempted' -- resetting the problems
# makes lcp.done False, but leaves attempts unchanged.
return self.is_correct()
elif self.showanswer == SHOWANSWER.CLOSED:
return self.closed()
elif self.showanswer == SHOWANSWER.FINISHED:
return self.closed() or self.is_correct()
elif self.showanswer == SHOWANSWER.CORRECT_OR_PAST_DUE:
return self.is_correct() or self.is_past_due()
elif self.showanswer == SHOWANSWER.PAST_DUE:
return self.is_past_due()
elif self.showanswer == SHOWANSWER.AFTER_SOME_NUMBER_OF_ATTEMPTS:
required_attempts = self.attempts_before_showanswer_button
if self.max_attempts and required_attempts >= self.max_attempts:
required_attempts = self.max_attempts
return self.attempts >= required_attempts
elif self.showanswer == SHOWANSWER.ALWAYS:
return True
elif self.showanswer == SHOWANSWER.AFTER_ALL_ATTEMPTS:
return self.used_all_attempts()
elif self.showanswer == SHOWANSWER.AFTER_ALL_ATTEMPTS_OR_CORRECT:
return self.used_all_attempts() or self.is_correct()
elif self.showanswer == SHOWANSWER.ATTEMPTED_NO_PAST_DUE:
return self.is_attempted()
return False
def correctness_available(self):
"""
Is the user allowed to see whether she's answered correctly?
Limits access to the correct/incorrect flags, messages, and problem score.
"""
user_is_staff = self.runtime.service(self, 'user').get_current_user().opt_attrs.get(ATTR_KEY_USER_IS_STAFF)
return ShowCorrectness.correctness_available(
show_correctness=self.show_correctness,
due_date=self.close_date,
has_staff_access=user_is_staff,
)

This has been partially re-implemented in other XBlocks over time. We should refactor this into a runtime service that will return a simple True/False for this question, so that we can get consistent behavior without other XBlock authors having to think about it.

@steff456
Copy link

steff456 commented Oct 6, 2023

Hi @ormsbee

I'm currently working in this issue and wanted to double check with you if the idea is to create a service as the ones described in openedx/core/djangoapps/xblock/runtime/runtime.py on line 231.

If not, can you please explain to me how should I create the new runtime service?

Thanks a lot!

@ormsbee
Copy link
Contributor Author

ormsbee commented Oct 9, 2023

@steff456: Yes, those are what I meant. Another place to find a list of example services is in the LMS rendering code:

services = {
'fs': FSService(),
'mako': mako_service,
'user': DjangoXBlockUserService(
user,
user_is_beta_tester=CourseBetaTesterRole(course_id).has_user(user),
user_is_staff=user_is_staff,
user_is_global_staff=bool(has_access(user, 'staff', 'global')),
user_role=get_user_role(user, course_id),
anonymous_user_id=anonymous_id_for_user(user, course_id),
# See the docstring of `DjangoXBlockUserService`.
deprecated_anonymous_user_id=anonymous_id_for_user(user, None),
request_country_code=user_location,
),
'verification': XBlockVerificationService(),
'proctoring': ProctoringService(),
'milestones': milestones_helpers.get_service(),
'credit': CreditService(),
'bookmarks': BookmarksService(user=user),
'gating': GatingService(),
'grade_utils': GradesUtilService(course_id=course_id),
'user_state': UserStateService(),
'content_type_gating': ContentTypeGatingService(),
'cache': CacheService(cache),
'sandbox': SandboxService(contentstore=contentstore, course_id=course_id),
'replace_urls': replace_url_service,
# Rebind module service to deal with noauth modules getting attached to users.
'rebind_user': RebindUserService(
user,
course_id,
track_function=track_function,
position=position,
wrap_xblock_display=wrap_xblock_display,
grade_bucket_type=grade_bucket_type,
static_asset_path=static_asset_path,
user_location=user_location,
request_token=request_token,
will_recheck_access=will_recheck_access,
),
'completion': CompletionService(user=user, context_key=course_id) if user and user.is_authenticated else None,
'i18n': XBlockI18nService,
'library_tools': LibraryToolsService(store, user_id=user.id if user else None),
'partitions': PartitionService(course_id=course_id, cache=DEFAULT_REQUEST_CACHE.data),
'settings': SettingsService(),
'user_tags': UserTagsService(user=user, course_id=course_id),
'badging': BadgingService(course_id=course_id, modulestore=store) if badges_enabled() else None,
'teams': TeamsService(),
'teams_configuration': TeamsConfigurationService(),
'call_to_action': CallToActionService(),
'publish': EventPublishingService(user, course_id, track_function),
}

Just a word of warning that this service would need to be instantiated in multiple places, like:

  • def load_services_for_studio(runtime, user):
    """
    Function to set some required services used for XBlock edits and studio_view.
    (i.e. whenever we're not loading _prepare_runtime_for_preview.) This is required to make information
    about the current user (especially permissions) available via services as needed.
    """
    services = {
    "user": DjangoXBlockUserService(user),
    "studio_user_permissions": StudioPermissionsService(user),
    "mako": MakoService(),
    "settings": SettingsService(),
    "lti-configuration": ConfigurationService(CourseAllowPIISharingInLTIFlag),
    "teams_configuration": TeamsConfigurationService(),
    "library_tools": LibraryToolsService(modulestore(), user.id),
    }
  • services = {
    "user": DjangoXBlockUserService(user),
    "studio_user_permissions": StudioPermissionsService(user),
    "mako": MakoService(),
    "settings": SettingsService(),
    "lti-configuration": ConfigurationService(CourseAllowPIISharingInLTIFlag),
    "teams_configuration": TeamsConfigurationService(),
    "library_tools": LibraryToolsService(modulestore(), user.id)
    }

(It might be useful to look for instantiations of DjangoXBlockUserService to see different places where the runtime can get initialized.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants