Skip to content

Commit

Permalink
Merge branch 'master' into NiedielnitsevIvan/FC-0047/feature/implemen…
Browse files Browse the repository at this point in the history
…t-push-notifications-chanel
  • Loading branch information
NiedielnitsevIvan authored Sep 12, 2024
2 parents c193d94 + 082350e commit 5fe7a2e
Show file tree
Hide file tree
Showing 33 changed files with 1,759 additions and 411 deletions.
10 changes: 9 additions & 1 deletion common/static/sass/_builtin-block-variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

:root {
--action-primary-active-bg: $action-primary-active-bg;
--all-text-inputs: $all-text-inputs;
--base-font-size: $base-font-size;
--base-line-height: $base-line-height;
--baseline: $baseline;
Expand All @@ -26,6 +25,7 @@
--blue-d1: $blue-d1;
--blue-d2: $blue-d2;
--blue-d4: $blue-d4;
--blue-s1: $blue-s1;
--body-color: $body-color;
--border-color: $border-color;
--bp-screen-lg: $bp-screen-lg;
Expand All @@ -34,6 +34,8 @@
--danger: $danger;
--darkGrey: $darkGrey;
--error-color: $error-color;
--error-color-dark: darken($error-color, 11%);
--error-color-light: lighten($error-color, 25%);
--font-bold: $font-bold;
--font-family-sans-serif: $font-family-sans-serif;
--general-color-accent: $general-color-accent;
Expand All @@ -44,6 +46,12 @@
--gray-l3: $gray-l3;
--gray-l4: $gray-l4;
--gray-l6: $gray-l6;
--icon-correct: url($static-path + '/images/correct-icon.png');
--icon-incorrect: url($static-path + '/images/incorrect-icon.png');
--icon-info: url($static-path + '/images/info-icon.png');
--icon-partially-correct: url($static-path + '/images/partially-correct-icon.png');
--icon-spinner: url($static-path + '/images/spinner.gif');
--icon-unanswered: url($static-path + '/images/unanswered-icon.png');
--incorrect: $incorrect;
--lightGrey: $lightGrey;
--lighter-base-font-color: $lighter-base-font-color;
Expand Down
18 changes: 15 additions & 3 deletions docs/hooks/events.rst
Original file line number Diff line number Diff line change
Expand Up @@ -233,17 +233,29 @@ Content Authoring Events
- 2023-07-20

* - `LIBRARY_BLOCK_CREATED <https://github.com/openedx/openedx-events/blob/c0eb4ba1a3d7d066d58e5c87920b8ccb0645f769/openedx_events/content_authoring/signals.py#L167>`_
- org.openedx.content_authoring.content_library.created.v1
- org.openedx.content_authoring.library_block.created.v1
- 2023-07-20

* - `LIBRARY_BLOCK_UPDATED <https://github.com/openedx/openedx-events/blob/c0eb4ba1a3d7d066d58e5c87920b8ccb0645f769/openedx_events/content_authoring/signals.py#L178>`_
- org.openedx.content_authoring.content_library.updated.v1
- org.openedx.content_authoring.library_block.updated.v1
- 2023-07-20

* - `LIBRARY_BLOCK_DELETED <https://github.com/openedx/openedx-events/blob/c0eb4ba1a3d7d066d58e5c87920b8ccb0645f769/openedx_events/content_authoring/signals.py#L189>`_
- org.openedx.content_authoring.content_library.deleted.v1
- org.openedx.content_authoring.library_block.deleted.v1
- 2023-07-20

* - `CONTENT_OBJECT_TAGS_CHANGED <https://github.com/openedx/openedx-events/blob/c0eb4ba1a3d7d066d58e5c87920b8ccb0645f769/openedx_events/content_authoring/signals.py#L207>`_
- org.openedx.content_authoring.content.object.tags.changed.v1
- 2024-03-31

* - `LIBRARY_COLLECTION_CREATED <https://github.com/openedx/openedx-events/blob/main/openedx_events/content_authoring/signals.py#L219>`_
- org.openedx.content_authoring.content_library.collection.created.v1
- 2024-08-23

* - `LIBRARY_COLLECTION_UPDATED <https://github.com/openedx/openedx-events/blob/main/openedx_events/content_authoring/signals.py#L230>`_
- org.openedx.content_authoring.content_library.collection.updated.v1
- 2024-08-23

* - `LIBRARY_COLLECTION_DELETED <https://github.com/openedx/openedx-events/blob/main/openedx_events/content_authoring/signals.py#L241>`_
- org.openedx.content_authoring.content_library.collection.deleted.v1
- 2024-08-23
131 changes: 125 additions & 6 deletions lms/djangoapps/instructor/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
UNENROLLED_TO_ALLOWEDTOENROLL,
UNENROLLED_TO_ENROLLED,
UNENROLLED_TO_UNENROLLED,
CourseAccessRole,
CourseEnrollment,
CourseEnrollmentAllowed,
ManualEnrollmentAudit,
Expand All @@ -60,12 +61,14 @@
CourseFinanceAdminRole,
CourseInstructorRole,
)
from common.djangoapps.student.tests.factories import BetaTesterFactory
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory
from common.djangoapps.student.tests.factories import GlobalStaffFactory
from common.djangoapps.student.tests.factories import InstructorFactory
from common.djangoapps.student.tests.factories import StaffFactory
from common.djangoapps.student.tests.factories import UserFactory
from common.djangoapps.student.tests.factories import (
BetaTesterFactory,
CourseEnrollmentFactory,
GlobalStaffFactory,
InstructorFactory,
StaffFactory,
UserFactory
)
from lms.djangoapps.bulk_email.models import BulkEmailFlag, CourseEmail, CourseEmailTemplate
from lms.djangoapps.certificates.data import CertificateStatuses
from lms.djangoapps.certificates.tests.factories import (
Expand Down Expand Up @@ -94,6 +97,9 @@
from openedx.core.djangoapps.course_groups.cohorts import set_course_cohorted
from openedx.core.djangoapps.django_comment_common.models import FORUM_ROLE_COMMUNITY_TA
from openedx.core.djangoapps.django_comment_common.utils import seed_permissions_roles
from openedx.core.djangoapps.oauth_dispatch import jwt as jwt_api
from openedx.core.djangoapps.oauth_dispatch.adapters import DOTAdapter
from openedx.core.djangoapps.oauth_dispatch.tests.factories import AccessTokenFactory, ApplicationFactory
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.site_configuration.tests.mixins import SiteMixin
from openedx.core.djangoapps.user_api.preferences.api import delete_user_preference
Expand Down Expand Up @@ -4675,3 +4681,116 @@ def test_get_certificate_for_user_no_certificate(self):
f"The student {self.user} does not have certificate for the course {self.course.id.course}. Kindly "
"verify student username/email and the selected course are correct and try again."
)


@patch.dict(settings.FEATURES, {'ALLOW_AUTOMATED_SIGNUPS': True})
class TestOauthInstructorAPILevelsAccess(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
"""
Test endpoints using Oauth2 authentication.
"""

@classmethod
def setUpClass(cls):
super().setUpClass()
cls.course = CourseFactory.create(
entrance_exam_id='i4x://{}/{}/chapter/Entrance_exam'.format('test_org', 'test_course')
)

def setUp(self):
super().setUp()

self.other_user = UserFactory()
dot_application = ApplicationFactory(user=self.other_user, authorization_grant_type='password')
access_token = AccessTokenFactory(user=self.other_user, application=dot_application)
oauth_adapter = DOTAdapter()
token_dict = {
'access_token': access_token,
'scope': 'email profile',
}
jwt_token = jwt_api.create_jwt_from_token(token_dict, oauth_adapter, use_asymmetric_key=True)

self.headers = {
'HTTP_AUTHORIZATION': 'JWT ' + jwt_token
}

# endpoints contains all urls with body and role.
self.endpoints = [
('list_course_role_members', {'rolename': 'staff'}, 'instructor'),
('register_and_enroll_students', {}, 'staff'),
('get_student_progress_url', {'course_id': str(self.course.id),
'unique_student_identifier': self.other_user.email
}, 'staff'
),
('list_entrance_exam_instructor_tasks', {'unique_student_identifier': self.other_user.email}, 'staff'),
('list_email_content', {}, 'staff'),
('show_student_extensions', {'student': self.other_user.email}, 'staff'),
('list_email_content', {}, 'staff'),
('list_report_downloads', {
"send-to": ["myself"],
"subject": "This is subject",
"message": "message"
}, 'data_researcher')
]

self.fake_jwt = ('wyJUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJjaGFuZ2UtbWUiLCJleHAiOjE3MjU4OTA2NzIsImdyY'
'W50X3R5cGUiOiJwYXNzd29yZCIsImlhdCI6MTcyNTg4NzA3MiwiaXNzIjoiaHR0cDovLzEyNy4wLjAuMTo4MDAwL29h'
'XNlcl9pZCI6MX0'
'.ec8neWp1YAuF40ye4oeK40obaapUvjfNPUQCycrsajwvcu58KcuLc96sf0JKmMMMn7DH9N98hg8W38iwbhKif1kLsCKr'
'tStl1u2XGvFkyMov8TtespbHit5LYRZpJwrhC1h50ru2buYj3isWrAElGPIDyAj0FAvSJnvJhWSMDtIwB2gxZI1DqOm'
'M6mzT7JbOU4QH2PNZrb2EZ11F6k9I-HrHnLQymr4s0vyjMlcBWllW3y19futNCgsFFRMXI4Z9zIbspsy5bq_Skub'
'dBpnl0P9x8vUJCAbFnJABAVPtF7F7nNsROQMKsZtQxaUUwdcYZi5qKL2GcgGfO0eTL4IbJA')

def assert_all_end_points(self, endpoint, body, role, add_role, use_jwt=True):
"""
Util method for verifying different end-points.
"""
if add_role:
role, _ = CourseAccessRole.objects.get_or_create(
course_id=self.course.id,
user=self.other_user,
role=role,
org=self.course.id.org
)

if use_jwt:
headers = self.headers
else:
headers = {
'HTTP_AUTHORIZATION': 'JWT ' + self.fake_jwt # this is fake jwt.
}

url = reverse(endpoint, kwargs={'course_id': str(self.course.id)})
response = self.client.post(
url,
data=body,
**headers
)
return response

def run_endpoint_tests(self, expected_status, add_role, use_jwt):
"""
Util method for running different end-points.
"""
for endpoint, body, role in self.endpoints:
with self.subTest(endpoint=endpoint, role=role, body=body):
response = self.assert_all_end_points(endpoint, body, role, add_role, use_jwt)
# JWT authentication works but it has no permissions.
assert response.status_code == expected_status, f"Failed for endpoint: {endpoint}"

def test_end_points_with_oauth_without_jwt(self):
"""
Verify the endpoint using invalid JWT returns 401.
"""
self.run_endpoint_tests(expected_status=401, add_role=False, use_jwt=False)

def test_end_points_with_oauth_without_permissions(self):
"""
Verify the endpoint using JWT authentication. But has no permissions.
"""
self.run_endpoint_tests(expected_status=403, add_role=False, use_jwt=True)

def test_end_points_with_oauth_with_permissions(self):
"""
Verify the endpoint using JWT authentication with permissions.
"""
self.run_endpoint_tests(expected_status=200, add_role=True, use_jwt=True)
2 changes: 1 addition & 1 deletion lms/djangoapps/instructor_task/tests/test_tasks_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ def test_query_counts(self):

with patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task'):
with check_mongo_calls(2):
with self.assertNumQueries(53):
with self.assertNumQueries(54):
CourseGradeReport.generate(None, None, course.id, {}, 'graded')

def test_inactive_enrollments(self):
Expand Down
5 changes: 5 additions & 0 deletions lms/djangoapps/verify_student/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1215,6 +1215,11 @@ class VerificationAttempt(TimeStampedModel):
blank=True,
)

@property
def updated_at(self):
"""Backwards compatibility with existing IDVerification models"""
return self.modified

@classmethod
def retire_user(cls, user_id):
"""
Expand Down
25 changes: 22 additions & 3 deletions lms/djangoapps/verify_student/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from lms.djangoapps.verify_student.utils import is_verification_expiring_soon
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers

from .models import ManualVerification, SoftwareSecurePhotoVerification, SSOVerification
from .models import ManualVerification, SoftwareSecurePhotoVerification, SSOVerification, VerificationAttempt
from .utils import most_recent_verification

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -75,7 +75,8 @@ def verifications_for_user(cls, user):
Return a list of all verifications associated with the given user.
"""
verifications = []
for verification in chain(SoftwareSecurePhotoVerification.objects.filter(user=user).order_by('-created_at'),
for verification in chain(VerificationAttempt.objects.filter(user=user).order_by('-created'),
SoftwareSecurePhotoVerification.objects.filter(user=user).order_by('-created_at'),
SSOVerification.objects.filter(user=user).order_by('-created_at'),
ManualVerification.objects.filter(user=user).order_by('-created_at')):
verifications.append(verification)
Expand All @@ -92,6 +93,11 @@ def get_verified_user_ids(cls, users):
'created_at__gt': now() - timedelta(days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"])
}
return chain(
VerificationAttempt.objects.filter(**{
'user__in': users,
'status': 'approved',
'created__gt': now() - timedelta(days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"])
}).values_list('user_id', flat=True),
SoftwareSecurePhotoVerification.objects.filter(**filter_kwargs).values_list('user_id', flat=True),
SSOVerification.objects.filter(**filter_kwargs).values_list('user_id', flat=True),
ManualVerification.objects.filter(**filter_kwargs).values_list('user_id', flat=True)
Expand All @@ -117,11 +123,14 @@ def get_expiration_datetime(cls, user, statuses):
'status__in': statuses,
}

id_verifications = VerificationAttempt.objects.filter(**filter_kwargs)
photo_id_verifications = SoftwareSecurePhotoVerification.objects.filter(**filter_kwargs)
sso_id_verifications = SSOVerification.objects.filter(**filter_kwargs)
manual_id_verifications = ManualVerification.objects.filter(**filter_kwargs)

attempt = most_recent_verification((photo_id_verifications, sso_id_verifications, manual_id_verifications))
attempt = most_recent_verification(
(photo_id_verifications, sso_id_verifications, manual_id_verifications, id_verifications)
)
return attempt and attempt.expiration_datetime

@classmethod
Expand Down Expand Up @@ -242,8 +251,18 @@ def get_verification_details_by_id(cls, attempt_id):
"""
Returns a verification attempt object by attempt_id
If the verification object cannot be found, returns None
This method does not take into account verifications stored in the
VerificationAttempt model used for pluggable IDV implementations.
As part of the work to implement pluggable IDV, this method's use
will be deprecated: https://openedx.atlassian.net/browse/OSPR-1011
"""
verification = None

# This does not look at the VerificationAttempt model since the provided id would become
# ambiguous between tables. The verification models in this list all inherit from the same
# base class and share the same id space.
verification_models = [
SoftwareSecurePhotoVerification,
SSOVerification,
Expand Down
Loading

0 comments on commit 5fe7a2e

Please sign in to comment.