Merge branch 'master' into kireiev/AXM-549/feat/upstream_PR_active_inactive_courses_API
This commit is contained in:
@@ -10,6 +10,8 @@ metadata:
|
||||
- url: "https://docs.openedx.org"
|
||||
title: "Documentation"
|
||||
icon: "Web"
|
||||
annotations:
|
||||
openedx.org/release: "master"
|
||||
spec:
|
||||
owner: group:wg-maintenance-edx-platform
|
||||
type: 'service'
|
||||
|
||||
@@ -974,3 +974,18 @@ class CourseUpdateNotificationTests(ModuleStoreTestCase):
|
||||
assert Notification.objects.all().count() == 1
|
||||
notification = Notification.objects.first()
|
||||
assert notification.content == "<p><strong>content Sub content heading</strong></p>"
|
||||
|
||||
def test_if_html_unescapes(self):
|
||||
"""
|
||||
Tests if html unescapes when creating content of course update notification
|
||||
"""
|
||||
user = UserFactory()
|
||||
CourseEnrollment.enroll(user=user, course_key=self.course.id)
|
||||
assert Notification.objects.all().count() == 0
|
||||
content = "<p><p> &nbsp;</p><br />"\
|
||||
"<p>abcd</p><br />"\
|
||||
"<p>&nbsp;</p><br /></p>"
|
||||
send_course_update_notification(self.course.id, content, self.user)
|
||||
assert Notification.objects.all().count() == 1
|
||||
notification = Notification.objects.first()
|
||||
assert notification.content == "<p><strong>abcd</strong></p>"
|
||||
|
||||
@@ -3,6 +3,7 @@ Common utility functions useful throughout the contentstore
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import configparser
|
||||
import html
|
||||
import logging
|
||||
import re
|
||||
from collections import defaultdict
|
||||
@@ -2258,6 +2259,7 @@ def clean_html_body(html_body):
|
||||
"""
|
||||
Get html body, remove tags and limit to 500 characters
|
||||
"""
|
||||
html_body = html.unescape(html_body).strip()
|
||||
html_body = BeautifulSoup(Truncator(html_body).chars(500, html=True), 'html.parser')
|
||||
text_content = html_body.get_text(separator=" ").strip()
|
||||
text_content = text_content.replace('\n', '').replace('\r', '')
|
||||
|
||||
@@ -408,7 +408,6 @@ class ClipboardPasteFromV2LibraryTestCase(ModuleStoreTestCase):
|
||||
self.store = modulestore()
|
||||
|
||||
self.library = library_api.create_library(
|
||||
library_type=library_api.COMPLEX,
|
||||
org=Organization.objects.create(name="Test Org", short_name="CL-TEST"),
|
||||
slug="lib",
|
||||
title="Library",
|
||||
|
||||
@@ -558,6 +558,7 @@ def _create_block(request):
|
||||
"locator": str(created_xblock.location),
|
||||
"courseKey": str(created_xblock.location.course_key),
|
||||
"static_file_notices": asdict(notices),
|
||||
"upstreamRef": str(created_xblock.upstream),
|
||||
})
|
||||
|
||||
category = request.json["category"]
|
||||
|
||||
@@ -3,7 +3,7 @@ define([
|
||||
], function($, CourseDetailsModel, MainView) {
|
||||
'use strict';
|
||||
|
||||
return function(detailsUrl, showMinGradeWarning, showCertificateAvailableDate, upgradeDeadline, useV2CertDisplaySettings) {
|
||||
return function(detailsUrl, showMinGradeWarning, showCertificateAvailableDate, upgradeDeadline) {
|
||||
var model;
|
||||
// highlighting labels when fields are focused in
|
||||
$('form :input')
|
||||
@@ -23,7 +23,6 @@ define([
|
||||
model = new CourseDetailsModel();
|
||||
model.urlRoot = detailsUrl;
|
||||
model.showCertificateAvailableDate = showCertificateAvailableDate;
|
||||
model.useV2CertDisplaySettings = useV2CertDisplaySettings;
|
||||
model.set('upgrade_deadline', upgradeDeadline);
|
||||
model.fetch({
|
||||
// eslint-disable-next-line no-shadow
|
||||
@@ -33,7 +32,6 @@ define([
|
||||
model: model,
|
||||
showMinGradeWarning: showMinGradeWarning
|
||||
});
|
||||
editor.useV2CertDisplaySettings = useV2CertDisplaySettings;
|
||||
editor.render();
|
||||
},
|
||||
reset: true,
|
||||
|
||||
@@ -84,35 +84,33 @@ function(Backbone, _, gettext, ValidationHelpers, DateUtils, StringUtils) {
|
||||
);
|
||||
}
|
||||
|
||||
if (this.useV2CertDisplaySettings) {
|
||||
if (
|
||||
newattrs.certificates_display_behavior
|
||||
&& !(Object.values(CERTIFICATES_DISPLAY_BEHAVIOR_OPTIONS).includes(newattrs.certificates_display_behavior))
|
||||
) {
|
||||
errors.certificates_display_behavior = StringUtils.interpolate(
|
||||
gettext(
|
||||
'The certificate display behavior must be one of: {behavior_options}'
|
||||
),
|
||||
{
|
||||
behavior_options: Object.values(CERTIFICATES_DISPLAY_BEHAVIOR_OPTIONS).join(', ')
|
||||
}
|
||||
);
|
||||
}
|
||||
if (
|
||||
newattrs.certificates_display_behavior
|
||||
&& !(Object.values(CERTIFICATES_DISPLAY_BEHAVIOR_OPTIONS).includes(newattrs.certificates_display_behavior))
|
||||
) {
|
||||
errors.certificates_display_behavior = StringUtils.interpolate(
|
||||
gettext(
|
||||
'The certificate display behavior must be one of: {behavior_options}'
|
||||
),
|
||||
{
|
||||
behavior_options: Object.values(CERTIFICATES_DISPLAY_BEHAVIOR_OPTIONS).join(', ')
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Throw error if there's a value for certificate_available_date
|
||||
if (
|
||||
(newattrs.certificate_available_date && newattrs.certificates_display_behavior != CERTIFICATES_DISPLAY_BEHAVIOR_OPTIONS.END_WITH_DATE)
|
||||
|| (!newattrs.certificate_available_date && newattrs.certificates_display_behavior == CERTIFICATES_DISPLAY_BEHAVIOR_OPTIONS.END_WITH_DATE)
|
||||
) {
|
||||
errors.certificates_display_behavior = StringUtils.interpolate(
|
||||
gettext(
|
||||
'The certificates display behavior must be {valid_option} if certificate available date is set.'
|
||||
),
|
||||
{
|
||||
valid_option: CERTIFICATES_DISPLAY_BEHAVIOR_OPTIONS.END_WITH_DATE
|
||||
}
|
||||
);
|
||||
}
|
||||
// Throw error if there's a value for certificate_available_date
|
||||
if (
|
||||
(newattrs.certificate_available_date && newattrs.certificates_display_behavior != CERTIFICATES_DISPLAY_BEHAVIOR_OPTIONS.END_WITH_DATE)
|
||||
|| (!newattrs.certificate_available_date && newattrs.certificates_display_behavior == CERTIFICATES_DISPLAY_BEHAVIOR_OPTIONS.END_WITH_DATE)
|
||||
) {
|
||||
errors.certificates_display_behavior = StringUtils.interpolate(
|
||||
gettext(
|
||||
'The certificates display behavior must be {valid_option} if certificate available date is set.'
|
||||
),
|
||||
{
|
||||
valid_option: CERTIFICATES_DISPLAY_BEHAVIOR_OPTIONS.END_WITH_DATE
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (newattrs.intro_video && newattrs.intro_video !== this.get('intro_video')) {
|
||||
|
||||
@@ -388,9 +388,6 @@ function(ValidatingView, CodeMirror, _, $, ui, DateUtils, FileUploadModel,
|
||||
Hides and clears the certificate available date field if a display behavior that doesn't use it is
|
||||
chosen. Because we are clearing it, toggling back to "end_with_date" will require re-entering the date
|
||||
*/
|
||||
if (!this.useV2CertDisplaySettings) {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line prefer-const
|
||||
let showDatepicker = this.model.get('certificates_display_behavior') == 'end_with_date';
|
||||
// eslint-disable-next-line prefer-const
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Tests for util.db module."""
|
||||
|
||||
from io import StringIO
|
||||
import unittest
|
||||
|
||||
import ddt
|
||||
from django.core.management import call_command
|
||||
@@ -121,7 +120,6 @@ class MigrationTests(TestCase):
|
||||
Tests for migrations.
|
||||
"""
|
||||
|
||||
@unittest.skip('Skipping temporarily to drop column in table')
|
||||
@override_settings(MIGRATION_MODULES={})
|
||||
def test_migrations_are_in_sync(self):
|
||||
"""
|
||||
|
||||
@@ -4,7 +4,7 @@ Basically the LMS devstack settings plus a few items needed to successfully
|
||||
import all the Studio code.
|
||||
"""
|
||||
|
||||
|
||||
from textwrap import dedent
|
||||
import os
|
||||
|
||||
from openedx.core.lib.derived import derive_settings
|
||||
@@ -27,18 +27,71 @@ for key, value in FEATURES.items():
|
||||
FEATURES[key] = True
|
||||
|
||||
# Settings that will fail if we enable them, and we don't need them for docs anyway.
|
||||
FEATURES['RUN_AS_ANALYTICS_SERVER_ENABLED'] = False
|
||||
FEATURES['ENABLE_SOFTWARE_SECURE_FAKE'] = False
|
||||
FEATURES['ENABLE_MKTG_SITE'] = False
|
||||
FEATURES["RUN_AS_ANALYTICS_SERVER_ENABLED"] = False
|
||||
FEATURES["ENABLE_SOFTWARE_SECURE_FAKE"] = False
|
||||
FEATURES["ENABLE_MKTG_SITE"] = False
|
||||
|
||||
INSTALLED_APPS.extend([
|
||||
'cms.djangoapps.contentstore.apps.ContentstoreConfig',
|
||||
'cms.djangoapps.course_creators',
|
||||
'cms.djangoapps.xblock_config.apps.XBlockConfig',
|
||||
'lms.djangoapps.lti_provider',
|
||||
])
|
||||
INSTALLED_APPS.extend(
|
||||
[
|
||||
"cms.djangoapps.contentstore.apps.ContentstoreConfig",
|
||||
"cms.djangoapps.course_creators",
|
||||
"cms.djangoapps.xblock_config.apps.XBlockConfig",
|
||||
"lms.djangoapps.lti_provider",
|
||||
]
|
||||
)
|
||||
|
||||
# Swagger generation details
|
||||
openapi_security_info_basic = (
|
||||
"Obtain with a `POST` request to `/user/v1/account/login_session/`. "
|
||||
"If needed, copy the cookies from the response to your new call."
|
||||
)
|
||||
openapi_security_info_jwt = dedent(
|
||||
"""
|
||||
Obtain by making a `POST` request to `/oauth2/v1/access_token`.
|
||||
|
||||
You will need to be logged in and have a client ID and secret already created.
|
||||
|
||||
Your request should have the headers
|
||||
|
||||
```
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
```
|
||||
|
||||
Your request should have the data payload
|
||||
|
||||
```
|
||||
'grant_type': 'client_credentials'
|
||||
'client_id': [your client ID]
|
||||
'client_secret': [your client secret]
|
||||
'token_type': 'jwt'
|
||||
```
|
||||
|
||||
Your JWT will be returned in the response as `access_token`. Prefix with `JWT ` in your header.
|
||||
"""
|
||||
)
|
||||
openapi_security_info_csrf = (
|
||||
"Obtain by making a `GET` request to `/csrf/api/v1/token`. The token will be in the response cookie `csrftoken`."
|
||||
)
|
||||
SWAGGER_SETTINGS["SECURITY_DEFINITIONS"] = {
|
||||
"Basic": {
|
||||
"type": "basic",
|
||||
"description": openapi_security_info_basic,
|
||||
},
|
||||
"jwt": {
|
||||
"type": "apiKey",
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": openapi_security_info_jwt,
|
||||
},
|
||||
"csrf": {
|
||||
"type": "apiKey",
|
||||
"name": "X-CSRFToken",
|
||||
"in": "header",
|
||||
"description": openapi_security_info_csrf,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
COMMON_TEST_DATA_ROOT = ''
|
||||
COMMON_TEST_DATA_ROOT = ""
|
||||
|
||||
derive_settings(__name__)
|
||||
|
||||
@@ -13,8 +13,44 @@ produces:
|
||||
securityDefinitions:
|
||||
Basic:
|
||||
type: basic
|
||||
description: Obtain with a `POST` request to `/user/v1/account/login_session/`. If
|
||||
needed, copy the cookies from the response to your new call.
|
||||
jwt:
|
||||
type: apiKey
|
||||
name: Authorization
|
||||
in: header
|
||||
description: |2
|
||||
|
||||
Obtain by making a `POST` request to `/oauth2/v1/access_token`.
|
||||
|
||||
You will need to be logged in and have a client ID and secret already created.
|
||||
|
||||
Your request should have the headers
|
||||
|
||||
```
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
```
|
||||
|
||||
Your request should have the data payload
|
||||
|
||||
```
|
||||
'grant_type': 'client_credentials'
|
||||
'client_id': [your client ID]
|
||||
'client_secret': [your client secret]
|
||||
'token_type': 'jwt'
|
||||
```
|
||||
|
||||
Your JWT will be returned in the response as `access_token`. Prefix with `JWT ` in your header.
|
||||
csrf:
|
||||
type: apiKey
|
||||
name: X-CSRFToken
|
||||
in: header
|
||||
description: Obtain by making a `GET` request to `/csrf/api/v1/token`. The token
|
||||
will be in the response cookie `csrftoken`.
|
||||
security:
|
||||
- Basic: []
|
||||
- csrf: []
|
||||
- jwt: []
|
||||
paths:
|
||||
/agreements/v1/integrity_signature/{course_id}:
|
||||
get:
|
||||
@@ -3975,6 +4011,7 @@ paths:
|
||||
"profile_name": "Jon Doe"
|
||||
"verification_attempt_id": (Optional)
|
||||
"proctored_exam_attempt_id": (Optional)
|
||||
"platform_verification_attempt_id": (Optional)
|
||||
"status": (Optional)
|
||||
}
|
||||
parameters:
|
||||
@@ -4130,6 +4167,7 @@ paths:
|
||||
"profile_name": "Jon Doe"
|
||||
"verification_attempt_id": (Optional)
|
||||
"proctored_exam_attempt_id": (Optional)
|
||||
"platform_verification_attempt_id": (Optional)
|
||||
"status": (Optional)
|
||||
}
|
||||
parameters:
|
||||
@@ -6788,6 +6826,59 @@ paths:
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
/mobile/{api_version}/notifications/create-token/:
|
||||
post:
|
||||
operationId: mobile_notifications_create-token_create
|
||||
summary: |-
|
||||
**Use Case**
|
||||
This endpoint allows clients to register a device for push notifications.
|
||||
description: |-
|
||||
If the device is already registered, the existing registration will be updated.
|
||||
If setting PUSH_NOTIFICATIONS_SETTINGS is not configured, the endpoint will return a 501 error.
|
||||
|
||||
**Example Request**
|
||||
POST /api/mobile/{version}/notifications/create-token/
|
||||
**POST Parameters**
|
||||
The body of the POST request can include the following parameters.
|
||||
* name (optional) - A name of the device.
|
||||
* registration_id (required) - The device token of the device.
|
||||
* device_id (optional) - ANDROID_ID / TelephonyManager.getDeviceId() (always as hex)
|
||||
* active (optional) - Whether the device is active, default is True.
|
||||
If False, the device will not receive notifications.
|
||||
* cloud_message_type (required) - You should choose FCM or GCM. Currently, only FCM is supported.
|
||||
* application_id (optional) - Opaque application identity, should be filled in for multiple
|
||||
key/certificate access. Should be equal settings.FCM_APP_NAME.
|
||||
**Example Response**
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"name": "My Device",
|
||||
"registration_id": "fj3j4",
|
||||
"device_id": 1234,
|
||||
"active": true,
|
||||
"date_created": "2024-04-18T07:39:37.132787Z",
|
||||
"cloud_message_type": "FCM",
|
||||
"application_id": "my_app_id"
|
||||
}
|
||||
```
|
||||
parameters:
|
||||
- name: data
|
||||
in: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/GCMDevice'
|
||||
responses:
|
||||
'201':
|
||||
description: ''
|
||||
schema:
|
||||
$ref: '#/definitions/GCMDevice'
|
||||
tags:
|
||||
- mobile
|
||||
parameters:
|
||||
- name: api_version
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
/mobile/{api_version}/users/{username}:
|
||||
get:
|
||||
operationId: mobile_users_read
|
||||
@@ -8849,22 +8940,6 @@ paths:
|
||||
tags:
|
||||
- user
|
||||
parameters: []
|
||||
/user/v1/accounts/verifications/{attempt_id}/:
|
||||
get:
|
||||
operationId: user_v1_accounts_verifications_read
|
||||
description: Get IDV attempt details by attempt_id. Only accessible by global
|
||||
staff.
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
tags:
|
||||
- user
|
||||
parameters:
|
||||
- name: attempt_id
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
/user/v1/accounts/{username}:
|
||||
get:
|
||||
operationId: user_v1_accounts_read
|
||||
@@ -9423,22 +9498,57 @@ paths:
|
||||
- user
|
||||
post:
|
||||
operationId: user_account_login_session_create
|
||||
summary: Log in a user.
|
||||
description: |-
|
||||
See `login_user` for details.
|
||||
|
||||
Example Usage:
|
||||
|
||||
POST /api/user/v1/login_session
|
||||
with POST params `email`, `password`.
|
||||
|
||||
200 {'success': true}
|
||||
parameters: []
|
||||
summary: POST /user/{api_version}/account/login_session/
|
||||
description: Returns 200 on success, and a detailed error message otherwise.
|
||||
parameters:
|
||||
- name: data
|
||||
in: body
|
||||
required: true
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
password:
|
||||
type: string
|
||||
responses:
|
||||
'201':
|
||||
'200':
|
||||
description: ''
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
value:
|
||||
type: string
|
||||
error_code:
|
||||
type: string
|
||||
'400':
|
||||
description: ''
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
value:
|
||||
type: string
|
||||
error_code:
|
||||
type: string
|
||||
'403':
|
||||
description: ''
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
value:
|
||||
type: string
|
||||
error_code:
|
||||
type: string
|
||||
tags:
|
||||
- user
|
||||
security:
|
||||
- csrf: []
|
||||
parameters:
|
||||
- name: api_version
|
||||
in: path
|
||||
@@ -10047,6 +10157,7 @@ definitions:
|
||||
required:
|
||||
- celebrations
|
||||
- course_access
|
||||
- studio_access
|
||||
- course_id
|
||||
- is_enrolled
|
||||
- is_self_paced
|
||||
@@ -10084,6 +10195,9 @@ definitions:
|
||||
additionalProperties:
|
||||
type: string
|
||||
x-nullable: true
|
||||
studio_access:
|
||||
title: Studio access
|
||||
type: boolean
|
||||
course_id:
|
||||
title: Course id
|
||||
type: string
|
||||
@@ -11237,10 +11351,24 @@ definitions:
|
||||
title: Verification attempt id
|
||||
type: integer
|
||||
x-nullable: true
|
||||
verification_attempt_status:
|
||||
title: Verification attempt status
|
||||
type: string
|
||||
minLength: 1
|
||||
x-nullable: true
|
||||
proctored_exam_attempt_id:
|
||||
title: Proctored exam attempt id
|
||||
type: integer
|
||||
x-nullable: true
|
||||
platform_verification_attempt_id:
|
||||
title: Platform verification attempt id
|
||||
type: integer
|
||||
x-nullable: true
|
||||
platform_verification_attempt_status:
|
||||
title: Platform verification attempt status
|
||||
type: string
|
||||
minLength: 1
|
||||
x-nullable: true
|
||||
status:
|
||||
title: Status
|
||||
type: string
|
||||
@@ -11277,10 +11405,24 @@ definitions:
|
||||
title: Verification attempt id
|
||||
type: integer
|
||||
x-nullable: true
|
||||
verification_attempt_status:
|
||||
title: Verification attempt status
|
||||
type: string
|
||||
minLength: 1
|
||||
x-nullable: true
|
||||
proctored_exam_attempt_id:
|
||||
title: Proctored exam attempt id
|
||||
type: integer
|
||||
x-nullable: true
|
||||
platform_verification_attempt_id:
|
||||
title: Platform verification attempt id
|
||||
type: integer
|
||||
x-nullable: true
|
||||
platform_verification_attempt_status:
|
||||
title: Platform verification attempt status
|
||||
type: string
|
||||
minLength: 1
|
||||
x-nullable: true
|
||||
status:
|
||||
title: Status
|
||||
type: string
|
||||
@@ -11710,6 +11852,52 @@ definitions:
|
||||
title: Enddatetime
|
||||
type: string
|
||||
format: date-time
|
||||
GCMDevice:
|
||||
required:
|
||||
- registration_id
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
title: ID
|
||||
type: integer
|
||||
name:
|
||||
title: Name
|
||||
type: string
|
||||
maxLength: 255
|
||||
x-nullable: true
|
||||
registration_id:
|
||||
title: Registration ID
|
||||
type: string
|
||||
minLength: 1
|
||||
device_id:
|
||||
title: Device id
|
||||
description: 'ANDROID_ID / TelephonyManager.getDeviceId() (e.g: 0x01)'
|
||||
type: integer
|
||||
x-nullable: true
|
||||
active:
|
||||
title: Is active
|
||||
description: Inactive devices will not be sent notifications
|
||||
type: boolean
|
||||
date_created:
|
||||
title: Creation date
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
x-nullable: true
|
||||
cloud_message_type:
|
||||
title: Cloud Message Type
|
||||
description: You should choose FCM, GCM is deprecated
|
||||
type: string
|
||||
enum:
|
||||
- FCM
|
||||
- GCM
|
||||
application_id:
|
||||
title: Application ID
|
||||
description: Opaque application identity, should be filled in for multiple
|
||||
key/certificate access
|
||||
type: string
|
||||
maxLength: 64
|
||||
x-nullable: true
|
||||
mobile_api.User:
|
||||
required:
|
||||
- username
|
||||
|
||||
@@ -537,6 +537,11 @@ def _send_course_email(entry_id, email_id, to_list, global_email_context, subtas
|
||||
email_context['course_id'] = str(course_email.course_id)
|
||||
email_context['unsubscribe_link'] = get_unsubscribed_link(current_recipient['username'],
|
||||
str(course_email.course_id))
|
||||
email_context['unsubscribe_text'] = 'Unsubscribe from course updates for this course'
|
||||
email_context['disclaimer'] = (
|
||||
"You are receiving this email because you are enrolled in the "
|
||||
f"{email_context['platform_name']} course {email_context['course_title']}"
|
||||
)
|
||||
|
||||
if is_bulk_email_edx_ace_enabled():
|
||||
message = ACEEmail(site, email_context)
|
||||
|
||||
@@ -3814,6 +3814,26 @@ class TestCoursewareMFESearchAPI(SharedModuleStoreTestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(body, {'enabled': False})
|
||||
|
||||
@patch.dict('django.conf.settings.FEATURES', {'COURSEWARE_SEARCH_INCLUSION_DATE': '2020'})
|
||||
@ddt.data(
|
||||
(datetime(2013, 9, 18, 11, 30, 00), False),
|
||||
(None, False),
|
||||
(datetime(2024, 9, 18, 11, 30, 00), True),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_inclusion_date_greater_than_course_start(self, start_date, expected_enabled):
|
||||
course_with_start = CourseFactory.create(start=start_date)
|
||||
api_url = reverse('courseware_search_enabled_view', kwargs={'course_id': str(course_with_start.id)})
|
||||
|
||||
user_staff = UserFactory(is_staff=True)
|
||||
|
||||
self.client.login(username=user_staff.username, password=TEST_PASSWORD)
|
||||
response = self.client.get(api_url, content_type='application/json')
|
||||
body = json.loads(response.content.decode('utf-8'))
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(body, {'enabled': expected_enabled})
|
||||
|
||||
|
||||
class TestCoursewareMFENavigationSidebarTogglesAPI(SharedModuleStoreTestCase):
|
||||
"""
|
||||
|
||||
@@ -2317,6 +2317,13 @@ def courseware_mfe_search_enabled(request, course_id=None):
|
||||
else:
|
||||
enabled = True
|
||||
|
||||
inclusion_date = settings.FEATURES.get('COURSEWARE_SEARCH_INCLUSION_DATE')
|
||||
start_date = CourseOverview.get_from_id(course_key).start
|
||||
|
||||
# only include courses that have a start date later than the setting-defined inclusion date
|
||||
if inclusion_date:
|
||||
enabled = enabled and (start_date and start_date.strftime('%Y-%m-%d') > inclusion_date)
|
||||
|
||||
payload = {"enabled": courseware_mfe_search_is_enabled(course_key) if enabled else False}
|
||||
return JsonResponse(payload)
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ from path import Path as path
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from enterprise.constants import (
|
||||
ENTERPRISE_ADMIN_ROLE,
|
||||
ENTERPRISE_LEARNER_ROLE,
|
||||
ENTERPRISE_CATALOG_ADMIN_ROLE,
|
||||
ENTERPRISE_DASHBOARD_ADMIN_ROLE,
|
||||
ENTERPRISE_ENROLLMENT_API_ADMIN_ROLE,
|
||||
@@ -60,6 +61,7 @@ from enterprise.constants import (
|
||||
SYSTEM_ENTERPRISE_PROVISIONING_ADMIN_ROLE,
|
||||
PROVISIONING_ENTERPRISE_CUSTOMER_ADMIN_ROLE,
|
||||
PROVISIONING_PENDING_ENTERPRISE_CUSTOMER_ADMIN_ROLE,
|
||||
DEFAULT_ENTERPRISE_ENROLLMENT_INTENTIONS_ROLE,
|
||||
)
|
||||
|
||||
from openedx.core.constants import COURSE_KEY_REGEX, COURSE_KEY_PATTERN, COURSE_ID_PATTERN
|
||||
@@ -4722,11 +4724,15 @@ ENTERPRISE_READONLY_ACCOUNT_FIELDS = [
|
||||
ENTERPRISE_CUSTOMER_COOKIE_NAME = 'enterprise_customer_uuid'
|
||||
BASE_COOKIE_DOMAIN = 'localhost'
|
||||
SYSTEM_TO_FEATURE_ROLE_MAPPING = {
|
||||
ENTERPRISE_LEARNER_ROLE: [
|
||||
DEFAULT_ENTERPRISE_ENROLLMENT_INTENTIONS_ROLE,
|
||||
],
|
||||
ENTERPRISE_ADMIN_ROLE: [
|
||||
ENTERPRISE_DASHBOARD_ADMIN_ROLE,
|
||||
ENTERPRISE_CATALOG_ADMIN_ROLE,
|
||||
ENTERPRISE_ENROLLMENT_API_ADMIN_ROLE,
|
||||
ENTERPRISE_REPORTING_CONFIG_ADMIN_ROLE,
|
||||
DEFAULT_ENTERPRISE_ENROLLMENT_INTENTIONS_ROLE,
|
||||
],
|
||||
ENTERPRISE_OPERATOR_ROLE: [
|
||||
ENTERPRISE_DASHBOARD_ADMIN_ROLE,
|
||||
@@ -4735,6 +4741,7 @@ SYSTEM_TO_FEATURE_ROLE_MAPPING = {
|
||||
ENTERPRISE_REPORTING_CONFIG_ADMIN_ROLE,
|
||||
ENTERPRISE_FULFILLMENT_OPERATOR_ROLE,
|
||||
ENTERPRISE_SSO_ORCHESTRATOR_OPERATOR_ROLE,
|
||||
DEFAULT_ENTERPRISE_ENROLLMENT_INTENTIONS_ROLE,
|
||||
],
|
||||
SYSTEM_ENTERPRISE_PROVISIONING_ADMIN_ROLE: [
|
||||
PROVISIONING_ENTERPRISE_CUSTOMER_ADMIN_ROLE,
|
||||
|
||||
14
openedx.yaml
14
openedx.yaml
@@ -1,14 +0,0 @@
|
||||
# This file describes this Open edX repo, as described in OEP-2:
|
||||
# https://open-edx-proposals.readthedocs.io/en/latest/oep-0002-bp-repo-metadata.html#specification
|
||||
|
||||
nick: edx
|
||||
openedx-release: {ref: "2u/release"}
|
||||
|
||||
oeps:
|
||||
oep-2: true
|
||||
oep-18: true
|
||||
oep-7: true
|
||||
|
||||
tags:
|
||||
- core
|
||||
- webservice
|
||||
@@ -115,7 +115,6 @@ class TestSearchApi(ModuleStoreTestCase):
|
||||
|
||||
# Create a content library:
|
||||
self.library = library_api.create_library(
|
||||
library_type=library_api.COMPLEX,
|
||||
org=OrganizationFactory.create(short_name="org1"),
|
||||
slug="lib",
|
||||
title="Library",
|
||||
|
||||
@@ -114,7 +114,7 @@ from openedx.core.lib.xblock_serializer.api import serialize_modulestore_block_f
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
from . import permissions, tasks
|
||||
from .constants import ALL_RIGHTS_RESERVED, COMPLEX
|
||||
from .constants import ALL_RIGHTS_RESERVED
|
||||
from .models import ContentLibrary, ContentLibraryPermission, ContentLibraryBlockImportTask
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -176,7 +176,6 @@ class ContentLibraryMetadata:
|
||||
description = attr.ib("")
|
||||
num_blocks = attr.ib(0)
|
||||
version = attr.ib(0)
|
||||
type = attr.ib(default=COMPLEX)
|
||||
last_published = attr.ib(default=None, type=datetime)
|
||||
last_draft_created = attr.ib(default=None, type=datetime)
|
||||
last_draft_created_by = attr.ib(default=None, type=datetime)
|
||||
@@ -306,15 +305,13 @@ class LibraryXBlockType:
|
||||
# ============
|
||||
|
||||
|
||||
def get_libraries_for_user(user, org=None, library_type=None, text_search=None, order=None):
|
||||
def get_libraries_for_user(user, org=None, text_search=None, order=None):
|
||||
"""
|
||||
Return content libraries that the user has permission to view.
|
||||
"""
|
||||
filter_kwargs = {}
|
||||
if org:
|
||||
filter_kwargs['org__short_name'] = org
|
||||
if library_type:
|
||||
filter_kwargs['type'] = library_type
|
||||
qs = ContentLibrary.objects.filter(**filter_kwargs) \
|
||||
.select_related('learning_package', 'org') \
|
||||
.order_by('org__short_name', 'slug')
|
||||
@@ -361,7 +358,6 @@ def get_metadata(queryset, text_search=None):
|
||||
ContentLibraryMetadata(
|
||||
key=lib.library_key,
|
||||
title=lib.learning_package.title if lib.learning_package else "",
|
||||
type=lib.type,
|
||||
description="",
|
||||
version=0,
|
||||
allow_public_learning=lib.allow_public_learning,
|
||||
@@ -446,7 +442,6 @@ def get_library(library_key):
|
||||
return ContentLibraryMetadata(
|
||||
key=library_key,
|
||||
title=learning_package.title,
|
||||
type=ref.type,
|
||||
description=ref.learning_package.description,
|
||||
num_blocks=num_blocks,
|
||||
version=version,
|
||||
@@ -474,7 +469,6 @@ def create_library(
|
||||
allow_public_learning=False,
|
||||
allow_public_read=False,
|
||||
library_license=ALL_RIGHTS_RESERVED,
|
||||
library_type=COMPLEX,
|
||||
):
|
||||
"""
|
||||
Create a new content library.
|
||||
@@ -491,8 +485,6 @@ def create_library(
|
||||
|
||||
allow_public_read: Allow anyone to view blocks (including source) in Studio?
|
||||
|
||||
library_type: Deprecated parameter, not really used. Set to COMPLEX.
|
||||
|
||||
Returns a ContentLibraryMetadata instance.
|
||||
"""
|
||||
assert isinstance(org, Organization)
|
||||
@@ -502,7 +494,6 @@ def create_library(
|
||||
ref = ContentLibrary.objects.create(
|
||||
org=org,
|
||||
slug=slug,
|
||||
type=library_type,
|
||||
allow_public_learning=allow_public_learning,
|
||||
allow_public_read=allow_public_read,
|
||||
license=library_license,
|
||||
@@ -526,7 +517,6 @@ def create_library(
|
||||
return ContentLibraryMetadata(
|
||||
key=ref.library_key,
|
||||
title=title,
|
||||
type=library_type,
|
||||
description=description,
|
||||
num_blocks=0,
|
||||
version=0,
|
||||
@@ -611,7 +601,6 @@ def update_library(
|
||||
description=None,
|
||||
allow_public_learning=None,
|
||||
allow_public_read=None,
|
||||
library_type=None,
|
||||
library_license=None,
|
||||
):
|
||||
"""
|
||||
@@ -621,7 +610,7 @@ def update_library(
|
||||
A value of None means "don't change".
|
||||
"""
|
||||
lib_obj_fields = [
|
||||
allow_public_learning, allow_public_read, library_type, library_license
|
||||
allow_public_learning, allow_public_read, library_license
|
||||
]
|
||||
lib_obj_changed = any(field is not None for field in lib_obj_fields)
|
||||
learning_pkg_changed = any(field is not None for field in [title, description])
|
||||
@@ -640,10 +629,6 @@ def update_library(
|
||||
content_lib.allow_public_learning = allow_public_learning
|
||||
if allow_public_read is not None:
|
||||
content_lib.allow_public_read = allow_public_read
|
||||
if library_type is not None:
|
||||
# TODO: Get rid of this field entirely, and remove library_type
|
||||
# from any functions that take it as an argument.
|
||||
content_lib.library_type = library_type
|
||||
if library_license is not None:
|
||||
content_lib.library_license = library_license
|
||||
content_lib.save()
|
||||
@@ -856,13 +841,6 @@ def validate_can_add_block_to_library(
|
||||
"""
|
||||
assert isinstance(library_key, LibraryLocatorV2)
|
||||
content_library = ContentLibrary.objects.get_by_key(library_key) # type: ignore[attr-defined]
|
||||
if content_library.type != COMPLEX:
|
||||
if block_type != content_library.type:
|
||||
raise IncompatibleTypesError(
|
||||
_('Block type "{block_type}" is not compatible with library type "{library_type}".').format(
|
||||
block_type=block_type, library_type=content_library.type,
|
||||
)
|
||||
)
|
||||
|
||||
# If adding a component would take us over our max, return an error.
|
||||
component_count = authoring_api.get_all_drafts(content_library.learning_package.id).count()
|
||||
@@ -1288,10 +1266,7 @@ def get_allowed_block_types(library_key): # pylint: disable=unused-argument
|
||||
# TODO: return support status and template options
|
||||
# See cms/djangoapps/contentstore/views/component.py
|
||||
block_types = sorted(name for name, class_ in XBlock.load_classes())
|
||||
lib = get_library(library_key)
|
||||
if lib.type != COMPLEX:
|
||||
# Problem and Video libraries only permit XBlocks of the same name.
|
||||
block_types = (name for name in block_types if name == lib.type)
|
||||
|
||||
info = []
|
||||
for block_type in block_types:
|
||||
# TODO: unify the contentstore helper with the xblock.api version of
|
||||
|
||||
@@ -2,16 +2,6 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
VIDEO = 'video'
|
||||
COMPLEX = 'complex'
|
||||
PROBLEM = 'problem'
|
||||
|
||||
LIBRARY_TYPES = (
|
||||
(VIDEO, _('Video')),
|
||||
(COMPLEX, _('Complex')),
|
||||
(PROBLEM, _('Problem')),
|
||||
)
|
||||
|
||||
# These are all the licenses we support so far.
|
||||
ALL_RIGHTS_RESERVED = ''
|
||||
CC_4_BY = 'CC:4.0:BY'
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 4.2.16 on 2024-10-24 20:21
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('content_libraries', '0010_contentlibrary_learning_package_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='contentlibrary',
|
||||
name='bundle_uuid',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='contentlibrary',
|
||||
name='type',
|
||||
),
|
||||
]
|
||||
@@ -54,8 +54,7 @@ from pylti1p3.grade import Grade
|
||||
|
||||
from opaque_keys.edx.django.models import UsageKeyField
|
||||
from openedx.core.djangoapps.content_libraries.constants import (
|
||||
LIBRARY_TYPES, COMPLEX, LICENSE_OPTIONS,
|
||||
ALL_RIGHTS_RESERVED,
|
||||
LICENSE_OPTIONS, ALL_RIGHTS_RESERVED,
|
||||
)
|
||||
from openedx_learning.api.authoring_models import LearningPackage
|
||||
from organizations.models import Organization # lint-amnesty, pylint: disable=wrong-import-order
|
||||
@@ -101,18 +100,6 @@ class ContentLibrary(models.Model):
|
||||
org = models.ForeignKey(Organization, on_delete=models.PROTECT, null=False)
|
||||
slug = models.SlugField(allow_unicode=True)
|
||||
|
||||
# We no longer use the ``bundle_uuid`` and ``type`` fields, but we'll leave
|
||||
# them in the model until after the Redwood release, just in case someone
|
||||
# out there was using v2 libraries. We don't expect this, since it wasn't in
|
||||
# a usable state, but there's always a chance someone managed to do it and
|
||||
# is still using it. By keeping the schema backwards compatible, the thought
|
||||
# is that they would update to the latest version, notice their libraries
|
||||
# aren't working correctly, and still have the ability to recover their data
|
||||
# if the code was rolled back.
|
||||
# TODO: Remove these fields after the Redwood release is cut.
|
||||
bundle_uuid = models.UUIDField(unique=True, null=True, default=None)
|
||||
type = models.CharField(max_length=25, default=COMPLEX, choices=LIBRARY_TYPES)
|
||||
|
||||
license = models.CharField(max_length=25, default=ALL_RIGHTS_RESERVED, choices=LICENSE_OPTIONS)
|
||||
learning_package = models.OneToOneField(
|
||||
LearningPackage,
|
||||
|
||||
@@ -11,8 +11,6 @@ from opaque_keys import InvalidKeyError
|
||||
|
||||
from openedx_learning.api.authoring_models import Collection
|
||||
from openedx.core.djangoapps.content_libraries.constants import (
|
||||
LIBRARY_TYPES,
|
||||
COMPLEX,
|
||||
ALL_RIGHTS_RESERVED,
|
||||
LICENSE_OPTIONS,
|
||||
)
|
||||
@@ -37,10 +35,8 @@ class ContentLibraryMetadataSerializer(serializers.Serializer):
|
||||
# begins with 'lib:'. (The numeric ID of the ContentLibrary object in MySQL
|
||||
# is not exposed via this API.)
|
||||
id = serializers.CharField(source="key", read_only=True)
|
||||
type = serializers.ChoiceField(choices=LIBRARY_TYPES, default=COMPLEX)
|
||||
org = serializers.SlugField(source="key.org")
|
||||
slug = serializers.CharField(source="key.slug", validators=(validate_unicode_slug, ))
|
||||
bundle_uuid = serializers.UUIDField(format='hex_verbose', read_only=True)
|
||||
title = serializers.CharField()
|
||||
description = serializers.CharField(allow_blank=True)
|
||||
num_blocks = serializers.IntegerField(read_only=True)
|
||||
@@ -86,7 +82,6 @@ class ContentLibraryUpdateSerializer(serializers.Serializer):
|
||||
description = serializers.CharField()
|
||||
allow_public_learning = serializers.BooleanField()
|
||||
allow_public_read = serializers.BooleanField()
|
||||
type = serializers.ChoiceField(choices=LIBRARY_TYPES)
|
||||
license = serializers.ChoiceField(choices=LICENSE_OPTIONS)
|
||||
|
||||
|
||||
@@ -118,7 +113,7 @@ class ContentLibraryPermissionSerializer(ContentLibraryPermissionLevelSerializer
|
||||
group_name = serializers.CharField(source="group.name", allow_null=True, allow_blank=False, default=None)
|
||||
|
||||
|
||||
class BaseFilterSerializer(serializers.Serializer):
|
||||
class ContentLibraryFilterSerializer(serializers.Serializer):
|
||||
"""
|
||||
Base serializer for filtering listings on the content library APIs.
|
||||
"""
|
||||
@@ -127,13 +122,6 @@ class BaseFilterSerializer(serializers.Serializer):
|
||||
order = serializers.CharField(default=None, required=False)
|
||||
|
||||
|
||||
class ContentLibraryFilterSerializer(BaseFilterSerializer):
|
||||
"""
|
||||
Serializer for filtering library listings.
|
||||
"""
|
||||
type = serializers.ChoiceField(choices=LIBRARY_TYPES, default=None, required=False)
|
||||
|
||||
|
||||
class CollectionMetadataSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for CollectionMetadata
|
||||
|
||||
@@ -9,7 +9,7 @@ from organizations.models import Organization
|
||||
from rest_framework.test import APITransactionTestCase, APIClient
|
||||
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from openedx.core.djangoapps.content_libraries.constants import COMPLEX, ALL_RIGHTS_RESERVED
|
||||
from openedx.core.djangoapps.content_libraries.constants import ALL_RIGHTS_RESERVED
|
||||
from openedx.core.djangolib.testing.utils import skip_unless_cms
|
||||
|
||||
# Define the URLs here - don't use reverse() because we want to detect
|
||||
@@ -124,7 +124,7 @@ class ContentLibrariesRestApiTest(APITransactionTestCase):
|
||||
self.client = old_client # pylint: disable=attribute-defined-outside-init
|
||||
|
||||
def _create_library(
|
||||
self, slug, title, description="", org=None, library_type=COMPLEX,
|
||||
self, slug, title, description="", org=None,
|
||||
license_type=ALL_RIGHTS_RESERVED, expect_response=200,
|
||||
):
|
||||
""" Create a library """
|
||||
@@ -135,7 +135,6 @@ class ContentLibrariesRestApiTest(APITransactionTestCase):
|
||||
"slug": slug,
|
||||
"title": title,
|
||||
"description": description,
|
||||
"type": library_type,
|
||||
"license": license_type,
|
||||
}, expect_response)
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ from organizations.models import Organization
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from openedx.core.djangoapps.content_libraries.constants import CC_4_BY, COMPLEX, PROBLEM, VIDEO
|
||||
from openedx.core.djangoapps.content_libraries.constants import CC_4_BY
|
||||
from openedx.core.djangoapps.content_libraries.tests.base import (
|
||||
URL_BLOCK_GET_HANDLER_URL,
|
||||
URL_BLOCK_METADATA_URL,
|
||||
@@ -100,7 +100,6 @@ class ContentLibrariesTestCase(ContentLibrariesRestApiTest, OpenEdxEventsTestMix
|
||||
"title": "A Tést Lꜟطrary",
|
||||
"description": "Just Téstꜟng",
|
||||
"version": 0,
|
||||
"type": COMPLEX,
|
||||
"license": CC_4_BY,
|
||||
"has_unpublished_changes": False,
|
||||
"has_unpublished_deletes": False,
|
||||
@@ -199,13 +198,13 @@ class ContentLibrariesTestCase(ContentLibrariesRestApiTest, OpenEdxEventsTestMix
|
||||
Test the filters in the list libraries API
|
||||
"""
|
||||
self._create_library(
|
||||
slug="test-lib-filter-1", title="Fob", description="Bar", library_type=VIDEO,
|
||||
slug="test-lib-filter-1", title="Fob", description="Bar",
|
||||
)
|
||||
self._create_library(
|
||||
slug="test-lib-filter-2", title="Library-Title-2", description="Bar-2",
|
||||
)
|
||||
self._create_library(
|
||||
slug="l3", title="Library-Title-3", description="Description", library_type=VIDEO,
|
||||
slug="l3", title="Library-Title-3", description="Description",
|
||||
)
|
||||
|
||||
Organization.objects.get_or_create(
|
||||
@@ -215,7 +214,6 @@ class ContentLibrariesTestCase(ContentLibrariesRestApiTest, OpenEdxEventsTestMix
|
||||
self._create_library(
|
||||
slug="l4", title="Library-Title-4",
|
||||
description="Library-Description", org='org-test',
|
||||
library_type=VIDEO,
|
||||
)
|
||||
self._create_library(
|
||||
slug="l5", title="Library-Title-5", description="Library-Description",
|
||||
@@ -225,14 +223,11 @@ class ContentLibrariesTestCase(ContentLibrariesRestApiTest, OpenEdxEventsTestMix
|
||||
assert len(self._list_libraries()) == 5
|
||||
assert len(self._list_libraries({'org': 'org-test'})) == 2
|
||||
assert len(self._list_libraries({'text_search': 'test-lib-filter'})) == 2
|
||||
assert len(self._list_libraries({'text_search': 'test-lib-filter', 'type': VIDEO})) == 1
|
||||
assert len(self._list_libraries({'text_search': 'library-title'})) == 4
|
||||
assert len(self._list_libraries({'text_search': 'library-title', 'type': VIDEO})) == 2
|
||||
assert len(self._list_libraries({'text_search': 'bar'})) == 2
|
||||
assert len(self._list_libraries({'text_search': 'org-test'})) == 2
|
||||
assert len(self._list_libraries({'org': 'org-test',
|
||||
'text_search': 'library-title-4'})) == 1
|
||||
assert len(self._list_libraries({'type': VIDEO})) == 3
|
||||
|
||||
self.assertOrderEqual(
|
||||
self._list_libraries({'order': 'title'}),
|
||||
@@ -496,27 +491,6 @@ class ContentLibrariesTestCase(ContentLibrariesRestApiTest, OpenEdxEventsTestMix
|
||||
assert len(self._get_library_blocks(lib['id'], {'block_type': 'problem'})['results']) == 3
|
||||
assert len(self._get_library_blocks(lib['id'], {'block_type': 'squirrel'})['results']) == 0
|
||||
|
||||
@ddt.data(
|
||||
('video-problem', VIDEO, 'problem', 400),
|
||||
('video-video', VIDEO, 'video', 200),
|
||||
('problem-problem', PROBLEM, 'problem', 200),
|
||||
('problem-video', PROBLEM, 'video', 400),
|
||||
('complex-video', COMPLEX, 'video', 200),
|
||||
('complex-problem', COMPLEX, 'problem', 200),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_library_blocks_type_constrained(self, slug, library_type, block_type, expect_response):
|
||||
"""
|
||||
Test that type-constrained libraries enforce their constraint when adding an XBlock.
|
||||
"""
|
||||
lib = self._create_library(
|
||||
slug=slug, title="A Test Library", description="Testing XBlocks", library_type=library_type,
|
||||
)
|
||||
lib_id = lib["id"]
|
||||
|
||||
# Add a 'problem' XBlock to the library:
|
||||
self._add_block_to_library(lib_id, block_type, 'test-block', expect_response=expect_response)
|
||||
|
||||
def test_library_not_found(self):
|
||||
"""Test that requests fail with 404 when the library does not exist"""
|
||||
valid_not_found_key = 'lb:valid:key:video:1'
|
||||
@@ -755,24 +729,6 @@ class ContentLibrariesTestCase(ContentLibrariesRestApiTest, OpenEdxEventsTestMix
|
||||
# Second block should throw error
|
||||
self._add_block_to_library(lib_id, "problem", "problem1", expect_response=400)
|
||||
|
||||
@ddt.data(
|
||||
('complex-types', COMPLEX, False),
|
||||
('video-types', VIDEO, True),
|
||||
('problem-types', PROBLEM, True),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_block_types(self, slug, library_type, constrained):
|
||||
"""
|
||||
Test that the permitted block types listing for a library change based on type.
|
||||
"""
|
||||
lib = self._create_library(slug=slug, title='Test Block Types', library_type=library_type)
|
||||
types = self._get_library_block_types(lib['id'])
|
||||
if constrained:
|
||||
assert len(types) == 1
|
||||
assert types[0]['block_type'] == library_type
|
||||
else:
|
||||
assert len(types) > 1
|
||||
|
||||
def test_content_library_create_event(self):
|
||||
"""
|
||||
Check that CONTENT_LIBRARY_CREATED event is sent when a content library is created.
|
||||
|
||||
@@ -15,7 +15,6 @@ from organizations.models import Organization
|
||||
from opaque_keys.edx.locator import LibraryLocatorV2, LibraryUsageLocatorV2
|
||||
|
||||
from ..models import ALL_RIGHTS_RESERVED
|
||||
from ..models import COMPLEX
|
||||
from ..models import ContentLibrary
|
||||
from ..models import LtiGradedResource
|
||||
from ..models import LtiProfile
|
||||
@@ -35,7 +34,6 @@ class ContentLibraryTest(TestCase):
|
||||
return ContentLibrary.objects.create(
|
||||
org=org,
|
||||
slug='foobar',
|
||||
type=COMPLEX,
|
||||
allow_public_learning=False,
|
||||
allow_public_read=False,
|
||||
license=ALL_RIGHTS_RESERVED,
|
||||
|
||||
@@ -21,7 +21,7 @@ from openedx.core.djangoapps.content_libraries.tests.base import (
|
||||
URL_BLOCK_FIELDS_URL,
|
||||
)
|
||||
from openedx.core.djangoapps.content_libraries.tests.user_state_block import UserStateTestBlock
|
||||
from openedx.core.djangoapps.content_libraries.constants import COMPLEX, ALL_RIGHTS_RESERVED
|
||||
from openedx.core.djangoapps.content_libraries.constants import ALL_RIGHTS_RESERVED
|
||||
from openedx.core.djangoapps.dark_lang.models import DarkLangConfig
|
||||
from openedx.core.djangoapps.xblock import api as xblock_api
|
||||
from openedx.core.djangolib.testing.utils import skip_unless_lms, skip_unless_cms
|
||||
@@ -50,7 +50,6 @@ class ContentLibraryContentTestMixin:
|
||||
_, slug = self.id().rsplit('.', 1)
|
||||
with transaction.atomic():
|
||||
self.library = library_api.create_library(
|
||||
library_type=COMPLEX,
|
||||
org=self.organization,
|
||||
slug=slugify(slug),
|
||||
title=(f"{slug} Test Lib"),
|
||||
|
||||
@@ -5,8 +5,6 @@ Tests for LTI views.
|
||||
from django.conf import settings
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from openedx.core.djangoapps.content_libraries.constants import PROBLEM
|
||||
|
||||
from .base import (
|
||||
ContentLibrariesRestApiTest,
|
||||
URL_LIB_LTI_JWKS,
|
||||
@@ -60,10 +58,10 @@ class LibraryBlockLtiUrlViewTestMixin:
|
||||
"""
|
||||
|
||||
library = self._create_library(
|
||||
slug="libgg", title="A Test Library", description="Testing library", library_type=PROBLEM,
|
||||
slug="libgg", title="A Test Library", description="Testing library",
|
||||
)
|
||||
|
||||
block = self._add_block_to_library(library['id'], PROBLEM, PROBLEM)
|
||||
block = self._add_block_to_library(library['id'], 'problem', 'problem')
|
||||
usage_key = str(block['id'])
|
||||
|
||||
url = f'/api/libraries/v2/blocks/{usage_key}/lti/'
|
||||
|
||||
@@ -227,14 +227,12 @@ class LibraryRootView(GenericAPIView):
|
||||
serializer = ContentLibraryFilterSerializer(data=request.query_params)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
org = serializer.validated_data['org']
|
||||
library_type = serializer.validated_data['type']
|
||||
text_search = serializer.validated_data['text_search']
|
||||
order = serializer.validated_data['order']
|
||||
|
||||
queryset = api.get_libraries_for_user(
|
||||
request.user,
|
||||
org=org,
|
||||
library_type=library_type,
|
||||
text_search=text_search,
|
||||
order=order,
|
||||
)
|
||||
@@ -259,7 +257,6 @@ class LibraryRootView(GenericAPIView):
|
||||
data = dict(serializer.validated_data)
|
||||
# Converting this over because using the reserved names 'type' and 'license' would shadow the built-in
|
||||
# definitions elsewhere.
|
||||
data['library_type'] = data.pop('type')
|
||||
data['library_license'] = data.pop('license')
|
||||
key_data = data.pop("key")
|
||||
# Move "slug" out of the "key.slug" pseudo-field that the serializer added:
|
||||
@@ -313,8 +310,6 @@ class LibraryDetailsView(APIView):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
data = dict(serializer.validated_data)
|
||||
# Prevent ourselves from shadowing global names.
|
||||
if 'type' in data:
|
||||
data['library_type'] = data.pop('type')
|
||||
if 'license' in data:
|
||||
data['library_license'] = data.pop('license')
|
||||
try:
|
||||
|
||||
@@ -16,7 +16,6 @@ from openedx.core.djangoapps.notifications.base_notification import (
|
||||
COURSE_NOTIFICATION_TYPES,
|
||||
)
|
||||
from openedx.core.djangoapps.notifications.config.waffle import ENABLE_EMAIL_NOTIFICATIONS
|
||||
from openedx.core.djangoapps.notifications.email_notifications import EmailCadence
|
||||
from openedx.core.djangoapps.notifications.models import CourseNotificationPreference, Notification
|
||||
from openedx.core.djangoapps.notifications.email.utils import (
|
||||
add_additional_attributes_to_notifications,
|
||||
@@ -320,9 +319,7 @@ class TestUpdatePreferenceFromPatch(ModuleStoreTestCase):
|
||||
if channel == param_channel:
|
||||
assert type_prefs[channel] == new_value
|
||||
if channel == 'email':
|
||||
cadence_value = EmailCadence.NEVER
|
||||
if new_value:
|
||||
cadence_value = self.get_default_cadence_value(app_name, noti_type)
|
||||
cadence_value = self.get_default_cadence_value(app_name, noti_type)
|
||||
assert type_prefs['email_cadence'] == cadence_value
|
||||
else:
|
||||
default_app_json = self.default_json[app_name]
|
||||
@@ -381,9 +378,7 @@ class TestUpdatePreferenceFromPatch(ModuleStoreTestCase):
|
||||
if app_name == param_app_name:
|
||||
assert type_prefs[channel] == new_value
|
||||
if channel == 'email':
|
||||
cadence_value = EmailCadence.NEVER
|
||||
if new_value:
|
||||
cadence_value = self.get_default_cadence_value(app_name, noti_type)
|
||||
cadence_value = self.get_default_cadence_value(app_name, noti_type)
|
||||
assert type_prefs['email_cadence'] == cadence_value
|
||||
else:
|
||||
default_app_json = self.default_json[app_name]
|
||||
@@ -415,9 +410,7 @@ class TestUpdatePreferenceFromPatch(ModuleStoreTestCase):
|
||||
if noti_type == param_notification_type:
|
||||
assert type_prefs[channel] == new_value
|
||||
if channel == 'email':
|
||||
cadence_value = EmailCadence.NEVER
|
||||
if new_value:
|
||||
cadence_value = self.get_default_cadence_value(app_name, noti_type)
|
||||
cadence_value = self.get_default_cadence_value(app_name, noti_type)
|
||||
assert type_prefs['email_cadence'] == cadence_value
|
||||
else:
|
||||
default_app_json = self.default_json[app_name]
|
||||
|
||||
@@ -406,9 +406,7 @@ def update_user_preferences_from_patch(encrypted_username, encrypted_patch):
|
||||
continue
|
||||
if is_editable(app_name, noti_type, channel):
|
||||
type_prefs[channel] = pref_value
|
||||
if channel == 'email':
|
||||
cadence_value = get_default_cadence_value(app_name, noti_type)\
|
||||
if pref_value else EmailCadence.NEVER
|
||||
type_prefs['email_cadence'] = cadence_value
|
||||
if channel == 'email' and pref_value and type_prefs.get('email_cadence') == EmailCadence.NEVER:
|
||||
type_prefs['email_cadence'] = get_default_cadence_value(app_name, noti_type)
|
||||
preference.save()
|
||||
notification_preference_unsubscribe_event(user)
|
||||
|
||||
@@ -28,8 +28,9 @@
|
||||
style="max-height: 28px; max-width: 28px; margin: 0.75rem 1rem 0.75rem 0"
|
||||
/>
|
||||
</td>
|
||||
<td class="notification-content" width="100%" align="left" valign="top" style=" padding: 1rem 1rem 1rem 0.5rem">
|
||||
<td class="notification-content" width="100%" align="left" valign="top" style="padding: 1rem 1rem 1rem 0.5rem;">
|
||||
<blockquote style="font-size: 0.875rem; font-weight:400; line-height:24px; color:#454545; margin: 0 0 0.5rem 0;">
|
||||
<style> strong {color: #00262B; font-weight:500} </style>
|
||||
{{ notification.email_content | truncatechars_html:600 | safe }}
|
||||
</blockquote>
|
||||
{% if notification.details %}
|
||||
@@ -37,7 +38,7 @@
|
||||
{{ notification.details | safe }}
|
||||
</blockquote>
|
||||
{% endif %}
|
||||
<blockquote style="color:#707070; margin: 0">
|
||||
<blockquote style="color:#707070; margin: 0; font-size: 14px; font-style: normal; font-weight: 400; line-height: 24px;">
|
||||
<span style="float: left">
|
||||
<span>{{ notification.course_name }}</span>
|
||||
<span style="padding: 0 0.375rem">{{ "·"|safe }}</span>
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
<div style="margin:0; padding:0; min-width: 100%; background-color:#C9C9C9">
|
||||
<head>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap'" rel="stylesheet" type="text/css">
|
||||
</head>
|
||||
|
||||
<div style="margin:0; padding:0; min-width: 100%; background-color:#C9C9C9; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; font-smooth: never;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="line-height:1.5; max-width:600px; font-family:Inter">
|
||||
<tbody style="background-color:#f5f5f5">
|
||||
<tr>
|
||||
|
||||
@@ -27,7 +27,6 @@ from openedx.core.djangoapps.django_comment_common.models import (
|
||||
FORUM_ROLE_MODERATOR
|
||||
)
|
||||
from openedx.core.djangoapps.notifications.config.waffle import ENABLE_NOTIFICATIONS
|
||||
from openedx.core.djangoapps.notifications.email_notifications import EmailCadence
|
||||
from openedx.core.djangoapps.notifications.models import (
|
||||
CourseNotificationPreference,
|
||||
Notification,
|
||||
@@ -936,7 +935,6 @@ class UpdatePreferenceFromEncryptedDataView(ModuleStoreTestCase):
|
||||
for app_name, app_prefs in config.items():
|
||||
for type_prefs in app_prefs['notification_types'].values():
|
||||
assert type_prefs['email'] is False
|
||||
assert type_prefs['email_cadence'] == EmailCadence.NEVER
|
||||
|
||||
def test_if_config_version_is_updated(self):
|
||||
"""
|
||||
|
||||
@@ -23,11 +23,14 @@ from django.views.decorators.csrf import csrf_exempt, csrf_protect, ensure_csrf_
|
||||
from django.views.decorators.debug import sensitive_post_parameters
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django_ratelimit.decorators import ratelimit
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from edx_django_utils.monitoring import set_custom_attribute
|
||||
from eventtracking import tracker
|
||||
from openedx_events.learning.data import UserData, UserPersonalData
|
||||
from openedx_events.learning.signals import SESSION_LOGIN_COMPLETED
|
||||
from openedx_filters.learning.filters import StudentLoginRequested
|
||||
from rest_framework import status
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from common.djangoapps import third_party_auth
|
||||
@@ -49,7 +52,7 @@ from openedx.core.djangoapps.user_authn.exceptions import AuthFailedError, Vulne
|
||||
from openedx.core.djangoapps.user_authn.tasks import check_pwned_password_and_send_track_event
|
||||
from openedx.core.djangoapps.user_authn.toggles import (
|
||||
is_require_third_party_auth_enabled,
|
||||
should_redirect_to_authn_microfrontend
|
||||
should_redirect_to_authn_microfrontend,
|
||||
)
|
||||
from openedx.core.djangoapps.user_authn.views.login_form import get_login_session_form
|
||||
from openedx.core.djangoapps.user_authn.views.password_reset import send_password_reset_email_for_user
|
||||
@@ -62,7 +65,7 @@ from openedx.features.enterprise_support.api import activate_learner_enterprise,
|
||||
log = logging.getLogger("edx.student")
|
||||
AUDIT_LOG = logging.getLogger("audit")
|
||||
USER_MODEL = get_user_model()
|
||||
PASSWORD_RESET_INITIATED = 'edx.user.passwordreset.initiated'
|
||||
PASSWORD_RESET_INITIATED = "edx.user.passwordreset.initiated"
|
||||
|
||||
|
||||
def _do_third_party_auth(request):
|
||||
@@ -70,9 +73,9 @@ def _do_third_party_auth(request):
|
||||
User is already authenticated via 3rd party, now try to find and return their associated Django user.
|
||||
"""
|
||||
running_pipeline = pipeline.get(request)
|
||||
username = running_pipeline['kwargs'].get('username')
|
||||
backend_name = running_pipeline['backend']
|
||||
third_party_uid = running_pipeline['kwargs']['uid']
|
||||
username = running_pipeline["kwargs"].get("username")
|
||||
backend_name = running_pipeline["backend"]
|
||||
third_party_uid = running_pipeline["kwargs"]["uid"]
|
||||
requested_provider = provider.Registry.get_from_pipeline(running_pipeline)
|
||||
platform_name = configuration_helpers.get_value("platform_name", settings.PLATFORM_NAME)
|
||||
|
||||
@@ -81,26 +84,25 @@ def _do_third_party_auth(request):
|
||||
except USER_MODEL.DoesNotExist:
|
||||
AUDIT_LOG.info(
|
||||
"Login failed - user with username {username} has no social auth "
|
||||
"with backend_name {backend_name}".format(
|
||||
username=username, backend_name=backend_name)
|
||||
"with backend_name {backend_name}".format(username=username, backend_name=backend_name)
|
||||
)
|
||||
message = Text(_(
|
||||
"You've successfully signed in to your {provider_name} account, "
|
||||
"but this account isn't linked with your {platform_name} account yet. {blank_lines}"
|
||||
"Use your {platform_name} username and password to sign in to {platform_name} below, "
|
||||
"and then link your {platform_name} account with {provider_name} from your dashboard. {blank_lines}"
|
||||
"If you don't have an account on {platform_name} yet, "
|
||||
"click {register_label_strong} at the top of the page."
|
||||
)).format(
|
||||
blank_lines=HTML('<br/><br/>'),
|
||||
message = Text(
|
||||
_(
|
||||
"You've successfully signed in to your {provider_name} account, "
|
||||
"but this account isn't linked with your {platform_name} account yet. {blank_lines}"
|
||||
"Use your {platform_name} username and password to sign in to {platform_name} below, "
|
||||
"and then link your {platform_name} account with {provider_name} from your dashboard. {blank_lines}"
|
||||
"If you don't have an account on {platform_name} yet, "
|
||||
"click {register_label_strong} at the top of the page."
|
||||
)
|
||||
).format(
|
||||
blank_lines=HTML("<br/><br/>"),
|
||||
platform_name=platform_name,
|
||||
provider_name=requested_provider.name,
|
||||
register_label_strong=HTML('<strong>{register_text}</strong>').format(
|
||||
register_text=_('Register')
|
||||
)
|
||||
register_label_strong=HTML("<strong>{register_text}</strong>").format(register_text=_("Register")),
|
||||
)
|
||||
|
||||
raise AuthFailedError(message, error_code='third-party-auth-with-no-linked-account') # lint-amnesty, pylint: disable=raise-missing-from
|
||||
raise AuthFailedError(message, error_code="third-party-auth-with-no-linked-account") # lint-amnesty, pylint: disable=raise-missing-from
|
||||
|
||||
|
||||
def _get_user_by_email(email):
|
||||
@@ -128,14 +130,14 @@ def _get_user_by_email_or_username(request, api_version):
|
||||
Finds a user object in the database based on the given request, ignores all fields except for email and username.
|
||||
"""
|
||||
is_api_v2 = api_version != API_V1
|
||||
login_fields = ['email', 'password']
|
||||
login_fields = ["email", "password"]
|
||||
if is_api_v2:
|
||||
login_fields = ['email_or_username', 'password']
|
||||
login_fields = ["email_or_username", "password"]
|
||||
|
||||
if any(f not in request.POST.keys() for f in login_fields):
|
||||
raise AuthFailedError(_('There was an error receiving your login information. Please email us.'))
|
||||
raise AuthFailedError(_("There was an error receiving your login information. Please email us."))
|
||||
|
||||
email_or_username = request.POST.get('email', None) or request.POST.get('email_or_username', None)
|
||||
email_or_username = request.POST.get("email", None) or request.POST.get("email_or_username", None)
|
||||
user = _get_user_by_email(email_or_username)
|
||||
|
||||
if not user and is_api_v2:
|
||||
@@ -143,7 +145,7 @@ def _get_user_by_email_or_username(request, api_version):
|
||||
user = _get_user_by_username(email_or_username)
|
||||
|
||||
if not user:
|
||||
digest = hashlib.shake_128(email_or_username.encode('utf-8')).hexdigest(16)
|
||||
digest = hashlib.shake_128(email_or_username.encode("utf-8")).hexdigest(16)
|
||||
AUDIT_LOG.warning(f"Login failed - Unknown user email or username {digest}")
|
||||
|
||||
return user
|
||||
@@ -165,27 +167,30 @@ def _generate_locked_out_error_message():
|
||||
"""
|
||||
|
||||
locked_out_period_in_sec = settings.MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS
|
||||
error_message = Text(_('To protect your account, it’s been temporarily '
|
||||
'locked. Try again in {locked_out_period} minutes.'
|
||||
'{li_start}To be on the safe side, you can reset your '
|
||||
'password {link_start}here{link_end} before you try again.')).format(
|
||||
link_start=HTML('<a http="#login" class="form-toggle" data-type="password-reset">'),
|
||||
link_end=HTML('</a>'),
|
||||
li_start=HTML('<li>'),
|
||||
li_end=HTML('</li>'),
|
||||
locked_out_period=int(locked_out_period_in_sec / 60))
|
||||
error_message = Text(
|
||||
_(
|
||||
"To protect your account, it’s been temporarily "
|
||||
"locked. Try again in {locked_out_period} minutes."
|
||||
"{li_start}To be on the safe side, you can reset your "
|
||||
"password {link_start}here{link_end} before you try again."
|
||||
)
|
||||
).format(
|
||||
link_start=HTML('<a http="#login" class="form-toggle" data-type="password-reset">'),
|
||||
link_end=HTML("</a>"),
|
||||
li_start=HTML("<li>"),
|
||||
li_end=HTML("</li>"),
|
||||
locked_out_period=int(locked_out_period_in_sec / 60),
|
||||
)
|
||||
raise AuthFailedError(
|
||||
error_message,
|
||||
error_code='account-locked-out',
|
||||
context={
|
||||
'locked_out_period': int(locked_out_period_in_sec / 60)
|
||||
}
|
||||
error_code="account-locked-out",
|
||||
context={"locked_out_period": int(locked_out_period_in_sec / 60)},
|
||||
)
|
||||
|
||||
|
||||
def _enforce_password_policy_compliance(request, user): # lint-amnesty, pylint: disable=missing-function-docstring
|
||||
try:
|
||||
password_policy_compliance.enforce_compliance_on_login(user, request.POST.get('password'))
|
||||
password_policy_compliance.enforce_compliance_on_login(user, request.POST.get("password"))
|
||||
except password_policy_compliance.NonCompliantPasswordWarning as e:
|
||||
# Allow login, but warn the user that they will be required to reset their password soon.
|
||||
PageLevelMessages.register_warning_message(request, HTML(str(e)))
|
||||
@@ -201,7 +206,7 @@ def _enforce_password_policy_compliance(request, user): # lint-amnesty, pylint:
|
||||
{
|
||||
"user_id": user.id,
|
||||
"source": "Policy Compliance",
|
||||
}
|
||||
},
|
||||
)
|
||||
send_password_reset_email_for_user(user, request)
|
||||
|
||||
@@ -214,19 +219,17 @@ def _log_and_raise_inactive_user_auth_error(unauthenticated_user):
|
||||
Depending on Django version we can get here a couple of ways, but this takes care of logging an auth attempt
|
||||
by an inactive user, re-sending the activation email, and raising an error with the correct message.
|
||||
"""
|
||||
AUDIT_LOG.warning(
|
||||
f"Login failed - Account not active for user.id: {unauthenticated_user.id}, resending activation"
|
||||
)
|
||||
AUDIT_LOG.warning(f"Login failed - Account not active for user.id: {unauthenticated_user.id}, resending activation")
|
||||
|
||||
profile = UserProfile.objects.get(user=unauthenticated_user)
|
||||
compose_and_send_activation_email(unauthenticated_user, profile)
|
||||
|
||||
raise AuthFailedError(
|
||||
error_code='inactive-user',
|
||||
error_code="inactive-user",
|
||||
context={
|
||||
'platformName': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME),
|
||||
'supportLink': configuration_helpers.get_value('SUPPORT_SITE_LINK', settings.SUPPORT_SITE_LINK)
|
||||
}
|
||||
"platformName": configuration_helpers.get_value("PLATFORM_NAME", settings.PLATFORM_NAME),
|
||||
"supportLink": configuration_helpers.get_value("SUPPORT_SITE_LINK", settings.SUPPORT_SITE_LINK),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -234,9 +237,11 @@ def _authenticate_first_party(request, unauthenticated_user, third_party_auth_re
|
||||
"""
|
||||
Use Django authentication on the given request, using rate limiting if configured
|
||||
"""
|
||||
should_be_rate_limited = getattr(request, 'limited', False)
|
||||
should_be_rate_limited = getattr(request, "limited", False)
|
||||
if should_be_rate_limited:
|
||||
raise AuthFailedError(_('Too many failed login attempts. Try again later.')) # lint-amnesty, pylint: disable=raise-missing-from
|
||||
raise AuthFailedError(
|
||||
_("Too many failed login attempts. Try again later.")
|
||||
) # lint-amnesty, pylint: disable=raise-missing-from
|
||||
|
||||
# If the user doesn't exist, we want to set the username to an invalid username so that authentication is guaranteed
|
||||
# to fail and we can take advantage of the ratelimited backend
|
||||
@@ -248,12 +253,8 @@ def _authenticate_first_party(request, unauthenticated_user, third_party_auth_re
|
||||
if not third_party_auth_requested:
|
||||
_check_user_auth_flow(request.site, unauthenticated_user)
|
||||
|
||||
password = normalize_password(request.POST['password'])
|
||||
return authenticate(
|
||||
username=username,
|
||||
password=password,
|
||||
request=request
|
||||
)
|
||||
password = normalize_password(request.POST["password"])
|
||||
return authenticate(username=username, password=password, request=request)
|
||||
|
||||
|
||||
def _handle_failed_authentication(user, authenticated_user):
|
||||
@@ -279,34 +280,37 @@ def _handle_failed_authentication(user, authenticated_user):
|
||||
if not LoginFailures.is_user_locked_out(user):
|
||||
max_failures_allowed = settings.MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED
|
||||
remaining_attempts = max_failures_allowed - failure_count
|
||||
error_message = Text(_('Email or password is incorrect.'
|
||||
'{li_start}You have {remaining_attempts} more sign-in '
|
||||
'attempts before your account is temporarily locked.{li_end}'
|
||||
'{li_start}If you\'ve forgotten your password, click '
|
||||
'{link_start}here{link_end} to reset.{li_end}')).format(
|
||||
link_start=HTML(
|
||||
'<a http="#login" class="form-toggle" data-type="password-reset">'
|
||||
),
|
||||
link_end=HTML('</a>'),
|
||||
li_start=HTML('<li>'),
|
||||
li_end=HTML('</li>'),
|
||||
remaining_attempts=remaining_attempts)
|
||||
error_message = Text(
|
||||
_(
|
||||
"Email or password is incorrect."
|
||||
"{li_start}You have {remaining_attempts} more sign-in "
|
||||
"attempts before your account is temporarily locked.{li_end}"
|
||||
"{li_start}If you've forgotten your password, click "
|
||||
"{link_start}here{link_end} to reset.{li_end}"
|
||||
)
|
||||
).format(
|
||||
link_start=HTML('<a http="#login" class="form-toggle" data-type="password-reset">'),
|
||||
link_end=HTML("</a>"),
|
||||
li_start=HTML("<li>"),
|
||||
li_end=HTML("</li>"),
|
||||
remaining_attempts=remaining_attempts,
|
||||
)
|
||||
raise AuthFailedError(
|
||||
error_message,
|
||||
error_code='failed-login-attempt',
|
||||
error_code="failed-login-attempt",
|
||||
context={
|
||||
'remaining_attempts': remaining_attempts,
|
||||
'allowed_failure_attempts': max_failures_allowed,
|
||||
'failure_count': failure_count,
|
||||
}
|
||||
"remaining_attempts": remaining_attempts,
|
||||
"allowed_failure_attempts": max_failures_allowed,
|
||||
"failure_count": failure_count,
|
||||
},
|
||||
)
|
||||
|
||||
_generate_locked_out_error_message()
|
||||
|
||||
raise AuthFailedError(
|
||||
_('Email or password is incorrect.'),
|
||||
error_code='incorrect-email-or-password',
|
||||
context={'failure_count': failure_count},
|
||||
_("Email or password is incorrect."),
|
||||
error_code="incorrect-email-or-password",
|
||||
context={"failure_count": failure_count},
|
||||
)
|
||||
|
||||
|
||||
@@ -352,25 +356,18 @@ def _track_user_login(user, request):
|
||||
# .. pii_retirement: third_party
|
||||
segment.identify(
|
||||
user.id,
|
||||
{
|
||||
'email': user.email,
|
||||
'username': user.username
|
||||
},
|
||||
{"email": user.email, "username": user.username},
|
||||
{
|
||||
# Disable MailChimp because we don't want to update the user's email
|
||||
# and username in MailChimp on every page load. We only need to capture
|
||||
# this data on registration/activation.
|
||||
'MailChimp': False
|
||||
}
|
||||
"MailChimp": False
|
||||
},
|
||||
)
|
||||
segment.track(
|
||||
user.id,
|
||||
"edx.bi.user.account.authenticated",
|
||||
{
|
||||
'category': "conversion",
|
||||
'label': request.POST.get('course_id'),
|
||||
'provider': None
|
||||
},
|
||||
{"category": "conversion", "label": request.POST.get("course_id"), "provider": None},
|
||||
)
|
||||
|
||||
|
||||
@@ -380,20 +377,22 @@ def _create_message(site, root_url, allowed_domain):
|
||||
to an allowed domain and not whitelisted then ask such users to login
|
||||
through allowed domain SSO provider.
|
||||
"""
|
||||
msg = Text(_(
|
||||
'As {allowed_domain} user, You must login with your {allowed_domain} '
|
||||
'{link_start}{provider} account{link_end}.'
|
||||
)).format(
|
||||
msg = Text(
|
||||
_(
|
||||
"As {allowed_domain} user, You must login with your {allowed_domain} "
|
||||
"{link_start}{provider} account{link_end}."
|
||||
)
|
||||
).format(
|
||||
allowed_domain=allowed_domain,
|
||||
link_start=HTML("<a href='{root_url}{tpa_provider_link}'>").format(
|
||||
root_url=root_url if root_url else '',
|
||||
tpa_provider_link='{dashboard_url}?tpa_hint={tpa_hint}'.format(
|
||||
dashboard_url=reverse('dashboard'),
|
||||
tpa_hint=site.configuration.get_value('THIRD_PARTY_AUTH_ONLY_HINT'),
|
||||
)
|
||||
root_url=root_url if root_url else "",
|
||||
tpa_provider_link="{dashboard_url}?tpa_hint={tpa_hint}".format(
|
||||
dashboard_url=reverse("dashboard"),
|
||||
tpa_hint=site.configuration.get_value("THIRD_PARTY_AUTH_ONLY_HINT"),
|
||||
),
|
||||
),
|
||||
provider=site.configuration.get_value('THIRD_PARTY_AUTH_ONLY_PROVIDER'),
|
||||
link_end=HTML("</a>")
|
||||
provider=site.configuration.get_value("THIRD_PARTY_AUTH_ONLY_PROVIDER"),
|
||||
link_end=HTML("</a>"),
|
||||
)
|
||||
return msg
|
||||
|
||||
@@ -404,13 +403,13 @@ def _check_user_auth_flow(site, user):
|
||||
then ask user to login through allowed domain SSO provider.
|
||||
"""
|
||||
if user and ENABLE_LOGIN_USING_THIRDPARTY_AUTH_ONLY.is_enabled():
|
||||
allowed_domain = site.configuration.get_value('THIRD_PARTY_AUTH_ONLY_DOMAIN', '').lower()
|
||||
email_parts = user.email.split('@')
|
||||
allowed_domain = site.configuration.get_value("THIRD_PARTY_AUTH_ONLY_DOMAIN", "").lower()
|
||||
email_parts = user.email.split("@")
|
||||
if len(email_parts) != 2:
|
||||
# User has a nonstandard email so we record their id.
|
||||
# we don't record their e-mail in case there is sensitive info accidentally
|
||||
# in there.
|
||||
set_custom_attribute('login_tpa_domain_shortcircuit_user_id', user.id)
|
||||
set_custom_attribute("login_tpa_domain_shortcircuit_user_id", user.id)
|
||||
log.warning("User %s has nonstandard e-mail. Shortcircuiting THIRD_PART_AUTH_ONLY_DOMAIN check.", user.id)
|
||||
return
|
||||
user_domain = email_parts[1].strip().lower()
|
||||
@@ -422,19 +421,19 @@ def _check_user_auth_flow(site, user):
|
||||
raise AuthFailedError(msg)
|
||||
|
||||
raise AuthFailedError(
|
||||
error_code='allowed-domain-login-error',
|
||||
error_code="allowed-domain-login-error",
|
||||
context={
|
||||
'allowed_domain': allowed_domain,
|
||||
'provider': site.configuration.get_value('THIRD_PARTY_AUTH_ONLY_PROVIDER'),
|
||||
'tpa_hint': site.configuration.get_value('THIRD_PARTY_AUTH_ONLY_HINT'),
|
||||
}
|
||||
"allowed_domain": allowed_domain,
|
||||
"provider": site.configuration.get_value("THIRD_PARTY_AUTH_ONLY_PROVIDER"),
|
||||
"tpa_hint": site.configuration.get_value("THIRD_PARTY_AUTH_ONLY_HINT"),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(['GET'])
|
||||
@require_http_methods(["GET"])
|
||||
def finish_auth(request):
|
||||
""" Following logistration (1st or 3rd party), handle any special query string params.
|
||||
"""Following logistration (1st or 3rd party), handle any special query string params.
|
||||
|
||||
See FinishAuthView.js for details on the query string params.
|
||||
|
||||
@@ -459,10 +458,13 @@ def finish_auth(request):
|
||||
GET /account/finish_auth/?course_id=course-v1:blah&enrollment_action=enroll
|
||||
|
||||
"""
|
||||
return render_to_response('student_account/finish_auth.html', {
|
||||
'disable_courseware_js': True,
|
||||
'disable_footer': True,
|
||||
})
|
||||
return render_to_response(
|
||||
"student_account/finish_auth.html",
|
||||
{
|
||||
"disable_courseware_js": True,
|
||||
"disable_footer": True,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def enterprise_selection_page(request, user, next_url):
|
||||
@@ -478,14 +480,14 @@ def enterprise_selection_page(request, user, next_url):
|
||||
|
||||
response = get_enterprise_learner_data_from_api(user)
|
||||
if response and len(response) > 1:
|
||||
redirect_url = reverse('enterprise_select_active') + '/?success_url=' + urllib.parse.quote(next_url)
|
||||
redirect_url = reverse("enterprise_select_active") + "/?success_url=" + urllib.parse.quote(next_url)
|
||||
|
||||
# Check to see if next url has an enterprise in it. In this case if user is associated with
|
||||
# that enterprise, activate that enterprise and bypass the selection page.
|
||||
if re.match(ENTERPRISE_ENROLLMENT_URL_REGEX, urllib.parse.unquote(next_url)):
|
||||
enterprise_in_url = re.search(UUID4_REGEX, next_url).group(0)
|
||||
for enterprise in response:
|
||||
if enterprise_in_url == str(enterprise['enterprise_customer']['uuid']):
|
||||
if enterprise_in_url == str(enterprise["enterprise_customer"]["uuid"]):
|
||||
is_activated_successfully = activate_learner_enterprise(request, user, enterprise_in_url)
|
||||
if is_activated_successfully:
|
||||
redirect_url = next_url
|
||||
@@ -495,20 +497,20 @@ def enterprise_selection_page(request, user, next_url):
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@require_http_methods(['POST'])
|
||||
@require_http_methods(["POST"])
|
||||
@ratelimit(
|
||||
key='openedx.core.djangoapps.util.ratelimit.request_post_email_or_username',
|
||||
key="openedx.core.djangoapps.util.ratelimit.request_post_email_or_username",
|
||||
rate=settings.LOGISTRATION_PER_EMAIL_RATELIMIT_RATE,
|
||||
method='POST',
|
||||
method="POST",
|
||||
block=False,
|
||||
) # lint-amnesty, pylint: disable=too-many-statements
|
||||
@ratelimit(
|
||||
key='openedx.core.djangoapps.util.ratelimit.real_ip',
|
||||
key="openedx.core.djangoapps.util.ratelimit.real_ip",
|
||||
rate=settings.LOGISTRATION_RATELIMIT_RATE,
|
||||
method='POST',
|
||||
method="POST",
|
||||
block=False,
|
||||
) # lint-amnesty, pylint: disable=too-many-statements
|
||||
def login_user(request, api_version='v1'): # pylint: disable=too-many-statements
|
||||
def login_user(request, api_version="v1"): # pylint: disable=too-many-statements
|
||||
"""
|
||||
AJAX request to log in the user.
|
||||
|
||||
@@ -542,10 +544,10 @@ def login_user(request, api_version='v1'): # pylint: disable=too-many-statement
|
||||
_parse_analytics_param_for_course_id(request)
|
||||
|
||||
third_party_auth_requested = third_party_auth.is_enabled() and pipeline.running(request)
|
||||
first_party_auth_requested = any(bool(request.POST.get(p)) for p in ['email', 'email_or_username', 'password'])
|
||||
first_party_auth_requested = any(bool(request.POST.get(p)) for p in ["email", "email_or_username", "password"])
|
||||
is_user_third_party_authenticated = False
|
||||
|
||||
set_custom_attribute('login_user_course_id', request.POST.get('course_id'))
|
||||
set_custom_attribute("login_user_course_id", request.POST.get("course_id"))
|
||||
|
||||
if is_require_third_party_auth_enabled() and not third_party_auth_requested:
|
||||
return HttpResponseForbidden(
|
||||
@@ -564,12 +566,12 @@ def login_user(request, api_version='v1'): # pylint: disable=too-many-statement
|
||||
try:
|
||||
user = _do_third_party_auth(request)
|
||||
is_user_third_party_authenticated = True
|
||||
set_custom_attribute('login_user_tpa_success', True)
|
||||
set_custom_attribute("login_user_tpa_success", True)
|
||||
except AuthFailedError as e:
|
||||
set_custom_attribute('login_user_tpa_success', False)
|
||||
set_custom_attribute('login_user_tpa_failure_msg', e.value)
|
||||
set_custom_attribute("login_user_tpa_success", False)
|
||||
set_custom_attribute("login_user_tpa_failure_msg", e.value)
|
||||
if e.error_code:
|
||||
set_custom_attribute('login_error_code', e.error_code)
|
||||
set_custom_attribute("login_error_code", e.error_code)
|
||||
|
||||
# user successfully authenticated with a third party provider, but has no linked Open edX account
|
||||
response_content = e.get_response()
|
||||
@@ -585,7 +587,10 @@ def login_user(request, api_version='v1'): # pylint: disable=too-many-statement
|
||||
possibly_authenticated_user = StudentLoginRequested.run_filter(user=possibly_authenticated_user)
|
||||
except StudentLoginRequested.PreventLogin as exc:
|
||||
raise AuthFailedError(
|
||||
str(exc), redirect_url=exc.redirect_to, error_code=exc.error_code, context=exc.context,
|
||||
str(exc),
|
||||
redirect_url=exc.redirect_to,
|
||||
error_code=exc.error_code,
|
||||
context=exc.context,
|
||||
) from exc
|
||||
|
||||
if not is_user_third_party_authenticated:
|
||||
@@ -599,82 +604,82 @@ def login_user(request, api_version='v1'): # pylint: disable=too-many-statement
|
||||
):
|
||||
_handle_failed_authentication(user, possibly_authenticated_user)
|
||||
|
||||
pwned_properties = check_pwned_password_and_send_track_event(
|
||||
user_id=user.id,
|
||||
password=request.POST.get('password'),
|
||||
internal_user=user.is_staff,
|
||||
request_page='login'
|
||||
) if not is_user_third_party_authenticated else {}
|
||||
# Set default for third party login
|
||||
password_frequency = pwned_properties.get('frequency', -1)
|
||||
if (
|
||||
settings.ENABLE_AUTHN_LOGIN_BLOCK_HIBP_POLICY and
|
||||
password_frequency >= settings.HIBP_LOGIN_BLOCK_PASSWORD_FREQUENCY_THRESHOLD
|
||||
):
|
||||
raise VulnerablePasswordError(
|
||||
accounts.AUTHN_LOGIN_BLOCK_HIBP_POLICY_MSG,
|
||||
'require-password-change'
|
||||
pwned_properties = (
|
||||
check_pwned_password_and_send_track_event(
|
||||
user_id=user.id,
|
||||
password=request.POST.get("password"),
|
||||
internal_user=user.is_staff,
|
||||
request_page="login",
|
||||
)
|
||||
if not is_user_third_party_authenticated
|
||||
else {}
|
||||
)
|
||||
# Set default for third party login
|
||||
password_frequency = pwned_properties.get("frequency", -1)
|
||||
if (
|
||||
settings.ENABLE_AUTHN_LOGIN_BLOCK_HIBP_POLICY
|
||||
and password_frequency >= settings.HIBP_LOGIN_BLOCK_PASSWORD_FREQUENCY_THRESHOLD
|
||||
):
|
||||
raise VulnerablePasswordError(accounts.AUTHN_LOGIN_BLOCK_HIBP_POLICY_MSG, "require-password-change")
|
||||
|
||||
_handle_successful_authentication_and_login(possibly_authenticated_user, request)
|
||||
|
||||
# The AJAX method calling should know the default destination upon success
|
||||
redirect_url, finish_auth_url = None, ''
|
||||
redirect_url, finish_auth_url = None, ""
|
||||
|
||||
if third_party_auth_requested:
|
||||
running_pipeline = pipeline.get(request)
|
||||
finish_auth_url = pipeline.get_complete_url(backend_name=running_pipeline['backend'])
|
||||
finish_auth_url = pipeline.get_complete_url(backend_name=running_pipeline["backend"])
|
||||
|
||||
if is_user_third_party_authenticated:
|
||||
redirect_url = finish_auth_url
|
||||
elif should_redirect_to_authn_microfrontend():
|
||||
next_url, root_url = get_next_url_for_login_page(request, include_host=True)
|
||||
redirect_url = get_redirect_url_with_host(
|
||||
root_url,
|
||||
enterprise_selection_page(request, possibly_authenticated_user, finish_auth_url or next_url)
|
||||
root_url, enterprise_selection_page(request, possibly_authenticated_user, finish_auth_url or next_url)
|
||||
)
|
||||
|
||||
if (
|
||||
settings.ENABLE_AUTHN_LOGIN_NUDGE_HIBP_POLICY and
|
||||
0 <= password_frequency <= settings.HIBP_LOGIN_NUDGE_PASSWORD_FREQUENCY_THRESHOLD
|
||||
settings.ENABLE_AUTHN_LOGIN_NUDGE_HIBP_POLICY
|
||||
and 0 <= password_frequency <= settings.HIBP_LOGIN_NUDGE_PASSWORD_FREQUENCY_THRESHOLD
|
||||
):
|
||||
raise VulnerablePasswordError(
|
||||
accounts.AUTHN_LOGIN_NUDGE_HIBP_POLICY_MSG,
|
||||
'nudge-password-change',
|
||||
redirect_url
|
||||
accounts.AUTHN_LOGIN_NUDGE_HIBP_POLICY_MSG, "nudge-password-change", redirect_url
|
||||
)
|
||||
|
||||
response = JsonResponse({
|
||||
'success': True,
|
||||
'redirect_url': redirect_url,
|
||||
})
|
||||
response = JsonResponse(
|
||||
{
|
||||
"success": True,
|
||||
"redirect_url": redirect_url,
|
||||
}
|
||||
)
|
||||
|
||||
# Ensure that the external marketing site can
|
||||
# detect that the user is logged in.
|
||||
response = set_logged_in_cookies(request, response, possibly_authenticated_user)
|
||||
set_custom_attribute('login_user_auth_failed_error', False)
|
||||
set_custom_attribute('login_user_response_status', response.status_code)
|
||||
set_custom_attribute('login_user_redirect_url', redirect_url)
|
||||
set_custom_attribute("login_user_auth_failed_error", False)
|
||||
set_custom_attribute("login_user_response_status", response.status_code)
|
||||
set_custom_attribute("login_user_redirect_url", redirect_url)
|
||||
mark_user_change_as_expected(user.id)
|
||||
return response
|
||||
except AuthFailedError as error:
|
||||
response_content = error.get_response()
|
||||
log.exception(response_content)
|
||||
|
||||
error_code = response_content.get('error_code')
|
||||
error_code = response_content.get("error_code")
|
||||
if error_code:
|
||||
set_custom_attribute('login_error_code', error_code)
|
||||
email_or_username_key = 'email' if api_version == API_V1 else 'email_or_username'
|
||||
set_custom_attribute("login_error_code", error_code)
|
||||
email_or_username_key = "email" if api_version == API_V1 else "email_or_username"
|
||||
email_or_username = request.POST.get(email_or_username_key, None)
|
||||
email_or_username = possibly_authenticated_user.email if possibly_authenticated_user else email_or_username
|
||||
response_content['email'] = email_or_username
|
||||
response_content["email"] = email_or_username
|
||||
except VulnerablePasswordError as error:
|
||||
response_content = error.get_response()
|
||||
log.exception(response_content)
|
||||
|
||||
response = JsonResponse(response_content, status=400)
|
||||
set_custom_attribute('login_user_auth_failed_error', True)
|
||||
set_custom_attribute('login_user_response_status', response.status_code)
|
||||
set_custom_attribute("login_user_auth_failed_error", True)
|
||||
set_custom_attribute("login_user_response_status", response.status_code)
|
||||
return response
|
||||
|
||||
|
||||
@@ -683,10 +688,10 @@ def login_user(request, api_version='v1'): # pylint: disable=too-many-statement
|
||||
# to get a CSRF token before we need to refresh adds too much
|
||||
# complexity.
|
||||
@csrf_exempt
|
||||
@require_http_methods(['POST'])
|
||||
@require_http_methods(["POST"])
|
||||
def login_refresh(request): # lint-amnesty, pylint: disable=missing-function-docstring
|
||||
if not request.user.is_authenticated or request.user.is_anonymous:
|
||||
return JsonResponse('Unauthorized', status=401)
|
||||
return JsonResponse("Unauthorized", status=401)
|
||||
|
||||
try:
|
||||
return get_response_with_refreshed_jwt_cookies(request, request.user)
|
||||
@@ -700,33 +705,57 @@ def redirect_to_lms_login(request):
|
||||
This view redirect the admin/login url to the site's login page if
|
||||
waffle switch is on otherwise returns the admin site's login view.
|
||||
"""
|
||||
return redirect('/login?next=/admin')
|
||||
return redirect("/login?next=/admin")
|
||||
|
||||
|
||||
login_user_schema = openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
"email": openapi.Schema(type=openapi.TYPE_STRING),
|
||||
"password": openapi.Schema(type=openapi.TYPE_STRING),
|
||||
},
|
||||
)
|
||||
|
||||
login_user_return_schema = openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
"success": openapi.Schema(type=openapi.TYPE_BOOLEAN),
|
||||
"value": openapi.Schema(type=openapi.TYPE_STRING),
|
||||
"error_code": openapi.Schema(type=openapi.TYPE_STRING),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class LoginSessionView(APIView):
|
||||
"""HTTP end-points for logging in users. """
|
||||
"""HTTP end-points for logging in users."""
|
||||
|
||||
# This end-point is available to anonymous users,
|
||||
# so do not require authentication.
|
||||
authentication_classes = []
|
||||
|
||||
login_user_responses = {
|
||||
status.HTTP_200_OK: login_user_return_schema,
|
||||
status.HTTP_400_BAD_REQUEST: login_user_return_schema,
|
||||
status.HTTP_403_FORBIDDEN: login_user_return_schema,
|
||||
}
|
||||
|
||||
@method_decorator(ensure_csrf_cookie)
|
||||
def get(self, request, *args, **kwargs):
|
||||
return HttpResponse(get_login_session_form(request).to_json(), content_type="application/json") # lint-amnesty, pylint: disable=http-response-with-content-type-json
|
||||
|
||||
@swagger_auto_schema(
|
||||
request_body=login_user_schema,
|
||||
responses=login_user_responses,
|
||||
security=[
|
||||
{"csrf": []},
|
||||
],
|
||||
)
|
||||
@method_decorator(csrf_protect)
|
||||
def post(self, request, api_version):
|
||||
"""Log in a user.
|
||||
|
||||
See `login_user` for details.
|
||||
|
||||
Example Usage:
|
||||
|
||||
POST /api/user/v1/login_session
|
||||
with POST params `email`, `password`.
|
||||
|
||||
200 {'success': true}
|
||||
"""
|
||||
POST /user/{api_version}/account/login_session/
|
||||
|
||||
Returns 200 on success, and a detailed error message otherwise.
|
||||
"""
|
||||
return login_user(request, api_version)
|
||||
|
||||
@@ -736,19 +765,19 @@ class LoginSessionView(APIView):
|
||||
|
||||
|
||||
def _parse_analytics_param_for_course_id(request):
|
||||
""" If analytics request param is found, parse and add course id as a new request param. """
|
||||
"""If analytics request param is found, parse and add course id as a new request param."""
|
||||
# Make a copy of the current POST request to modify.
|
||||
modified_request = request.POST.copy()
|
||||
if isinstance(request, HttpRequest):
|
||||
# Works for an HttpRequest but not a rest_framework.request.Request.
|
||||
# Note: This case seems to be used for tests only.
|
||||
request.POST = modified_request
|
||||
set_custom_attribute('login_user_request_type', 'django')
|
||||
set_custom_attribute("login_user_request_type", "django")
|
||||
else:
|
||||
# The request must be a rest_framework.request.Request.
|
||||
# Note: Only DRF seems to be used in Production.
|
||||
request._data = modified_request # pylint: disable=protected-access
|
||||
set_custom_attribute('login_user_request_type', 'drf')
|
||||
set_custom_attribute("login_user_request_type", "drf")
|
||||
|
||||
# Include the course ID if it's specified in the analytics info
|
||||
# so it can be included in analytics events.
|
||||
@@ -758,9 +787,5 @@ def _parse_analytics_param_for_course_id(request):
|
||||
if "enroll_course_id" in analytics:
|
||||
modified_request["course_id"] = analytics.get("enroll_course_id")
|
||||
except (ValueError, TypeError):
|
||||
set_custom_attribute('shim_analytics_course_id', 'parse-error')
|
||||
log.error(
|
||||
"Could not parse analytics object sent to user API: {analytics}".format(
|
||||
analytics=analytics
|
||||
)
|
||||
)
|
||||
set_custom_attribute("shim_analytics_course_id", "parse-error")
|
||||
log.error("Could not parse analytics object sent to user API: {analytics}".format(analytics=analytics))
|
||||
|
||||
@@ -32,3 +32,7 @@ elasticsearch<7.14.0
|
||||
# This can be unpinned once https://github.com/openedx/edx-platform/issues/34586
|
||||
# has been resolved and edx-platform is running with pymongo>=4.4.0
|
||||
|
||||
|
||||
# Cause: https://github.com/openedx/edx-lint/issues/458
|
||||
# This can be unpinned once https://github.com/openedx/edx-lint/issues/459 has been resolved.
|
||||
pip<24.3
|
||||
|
||||
@@ -82,7 +82,7 @@ django-storages<1.14.4
|
||||
# The team that owns this package will manually bump this package rather than having it pulled in automatically.
|
||||
# This is to allow them to better control its deployment and to do it in a process that works better
|
||||
# for them.
|
||||
edx-enterprise==4.28.3
|
||||
edx-enterprise==4.30.0
|
||||
|
||||
# Date: 2024-05-09
|
||||
# This has to be constrained as well because newer versions of edx-i18n-tools need the
|
||||
|
||||
@@ -86,5 +86,5 @@ sympy==1.13.3
|
||||
# via
|
||||
# -r requirements/edx-sandbox/base.in
|
||||
# openedx-calc
|
||||
tqdm==4.66.5
|
||||
tqdm==4.66.6
|
||||
# via nltk
|
||||
|
||||
@@ -70,13 +70,13 @@ bleach[css]==6.1.0
|
||||
# xblock-poll
|
||||
boto==2.49.0
|
||||
# via -r requirements/edx/kernel.in
|
||||
boto3==1.35.46
|
||||
boto3==1.35.50
|
||||
# via
|
||||
# -r requirements/edx/kernel.in
|
||||
# django-ses
|
||||
# fs-s3fs
|
||||
# ora2
|
||||
botocore==1.35.46
|
||||
botocore==1.35.50
|
||||
# via
|
||||
# -r requirements/edx/kernel.in
|
||||
# boto3
|
||||
@@ -144,7 +144,7 @@ code-annotations==1.8.0
|
||||
# edx-toggles
|
||||
codejail-includes==1.0.0
|
||||
# via -r requirements/edx/kernel.in
|
||||
crowdsourcehinter-xblock==0.7
|
||||
crowdsourcehinter-xblock==0.8
|
||||
# via -r requirements/edx/bundled.in
|
||||
cryptography==43.0.3
|
||||
# via
|
||||
@@ -455,7 +455,7 @@ edx-django-utils==7.0.0
|
||||
# openedx-events
|
||||
# ora2
|
||||
# super-csv
|
||||
edx-drf-extensions==10.4.0
|
||||
edx-drf-extensions==10.5.0
|
||||
# via
|
||||
# -r requirements/edx/kernel.in
|
||||
# edx-completion
|
||||
@@ -467,11 +467,11 @@ edx-drf-extensions==10.4.0
|
||||
# edx-when
|
||||
# edxval
|
||||
# openedx-learning
|
||||
edx-enterprise==4.28.3
|
||||
edx-enterprise==4.30.0
|
||||
# via
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# -r requirements/edx/kernel.in
|
||||
edx-event-bus-kafka==5.8.1
|
||||
edx-event-bus-kafka==6.0.0
|
||||
# via -r requirements/edx/kernel.in
|
||||
edx-event-bus-redis==0.5.1
|
||||
# via -r requirements/edx/kernel.in
|
||||
@@ -584,7 +584,7 @@ geoip2==4.8.0
|
||||
# via -r requirements/edx/kernel.in
|
||||
glob2==0.7
|
||||
# via -r requirements/edx/kernel.in
|
||||
google-api-core[grpc]==2.21.0
|
||||
google-api-core[grpc]==2.22.0
|
||||
# via
|
||||
# firebase-admin
|
||||
# google-api-python-client
|
||||
@@ -747,7 +747,7 @@ markupsafe==3.0.2
|
||||
# xblock
|
||||
maxminddb==2.6.2
|
||||
# via geoip2
|
||||
meilisearch==0.31.5
|
||||
meilisearch==0.31.6
|
||||
# via
|
||||
# -r requirements/edx/kernel.in
|
||||
# edx-search
|
||||
@@ -1038,9 +1038,9 @@ pyyaml==6.0.2
|
||||
# xblock
|
||||
random2==1.0.2
|
||||
# via -r requirements/edx/kernel.in
|
||||
recommender-xblock==2.2.1
|
||||
recommender-xblock==3.0.0
|
||||
# via -r requirements/edx/bundled.in
|
||||
redis==5.1.1
|
||||
redis==5.2.0
|
||||
# via
|
||||
# -r requirements/edx/kernel.in
|
||||
# walrus
|
||||
@@ -1144,7 +1144,7 @@ slumber==0.7.1
|
||||
# -r requirements/edx/kernel.in
|
||||
# edx-bulk-grades
|
||||
# edx-enterprise
|
||||
snowflake-connector-python==3.12.2
|
||||
snowflake-connector-python==3.12.3
|
||||
# via edx-enterprise
|
||||
social-auth-app-django==5.4.1
|
||||
# via
|
||||
@@ -1191,7 +1191,7 @@ tinycss2==1.2.1
|
||||
# via bleach
|
||||
tomlkit==0.13.2
|
||||
# via snowflake-connector-python
|
||||
tqdm==4.66.5
|
||||
tqdm==4.66.6
|
||||
# via
|
||||
# nltk
|
||||
# openai
|
||||
@@ -1254,7 +1254,7 @@ webencodings==0.5.1
|
||||
# bleach
|
||||
# html5lib
|
||||
# tinycss2
|
||||
webob==1.8.8
|
||||
webob==1.8.9
|
||||
# via
|
||||
# -r requirements/edx/kernel.in
|
||||
# xblock
|
||||
@@ -1291,7 +1291,7 @@ xmlsec==1.3.13
|
||||
# python3-saml
|
||||
xss-utils==0.6.0
|
||||
# via -r requirements/edx/kernel.in
|
||||
yarl==1.16.0
|
||||
yarl==1.17.0
|
||||
# via aiohttp
|
||||
zipp==3.20.2
|
||||
# via importlib-metadata
|
||||
|
||||
@@ -140,14 +140,14 @@ boto==2.49.0
|
||||
# via
|
||||
# -r requirements/edx/doc.txt
|
||||
# -r requirements/edx/testing.txt
|
||||
boto3==1.35.46
|
||||
boto3==1.35.50
|
||||
# via
|
||||
# -r requirements/edx/doc.txt
|
||||
# -r requirements/edx/testing.txt
|
||||
# django-ses
|
||||
# fs-s3fs
|
||||
# ora2
|
||||
botocore==1.35.46
|
||||
botocore==1.35.50
|
||||
# via
|
||||
# -r requirements/edx/doc.txt
|
||||
# -r requirements/edx/testing.txt
|
||||
@@ -282,7 +282,7 @@ coverage[toml]==7.6.4
|
||||
# via
|
||||
# -r requirements/edx/testing.txt
|
||||
# pytest-cov
|
||||
crowdsourcehinter-xblock==0.7
|
||||
crowdsourcehinter-xblock==0.8
|
||||
# via
|
||||
# -r requirements/edx/doc.txt
|
||||
# -r requirements/edx/testing.txt
|
||||
@@ -576,7 +576,7 @@ django-stubs==1.16.0
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# -r requirements/edx/development.in
|
||||
# djangorestframework-stubs
|
||||
django-stubs-ext==5.1.0
|
||||
django-stubs-ext==5.1.1
|
||||
# via django-stubs
|
||||
django-user-tasks==3.2.0
|
||||
# via
|
||||
@@ -728,7 +728,7 @@ edx-django-utils==7.0.0
|
||||
# openedx-events
|
||||
# ora2
|
||||
# super-csv
|
||||
edx-drf-extensions==10.4.0
|
||||
edx-drf-extensions==10.5.0
|
||||
# via
|
||||
# -r requirements/edx/doc.txt
|
||||
# -r requirements/edx/testing.txt
|
||||
@@ -741,12 +741,12 @@ edx-drf-extensions==10.4.0
|
||||
# edx-when
|
||||
# edxval
|
||||
# openedx-learning
|
||||
edx-enterprise==4.28.3
|
||||
edx-enterprise==4.30.0
|
||||
# via
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# -r requirements/edx/doc.txt
|
||||
# -r requirements/edx/testing.txt
|
||||
edx-event-bus-kafka==5.8.1
|
||||
edx-event-bus-kafka==6.0.0
|
||||
# via
|
||||
# -r requirements/edx/doc.txt
|
||||
# -r requirements/edx/testing.txt
|
||||
@@ -760,7 +760,7 @@ edx-i18n-tools==1.5.0
|
||||
# -r requirements/edx/doc.txt
|
||||
# -r requirements/edx/testing.txt
|
||||
# ora2
|
||||
edx-lint==5.4.0
|
||||
edx-lint==5.4.1
|
||||
# via -r requirements/edx/testing.txt
|
||||
edx-milestones==0.6.0
|
||||
# via
|
||||
@@ -879,11 +879,11 @@ execnet==2.1.1
|
||||
# pytest-xdist
|
||||
factory-boy==3.3.1
|
||||
# via -r requirements/edx/testing.txt
|
||||
faker==30.8.0
|
||||
faker==30.8.1
|
||||
# via
|
||||
# -r requirements/edx/testing.txt
|
||||
# factory-boy
|
||||
fastapi==0.115.3
|
||||
fastapi==0.115.4
|
||||
# via
|
||||
# -r requirements/edx/testing.txt
|
||||
# pact-python
|
||||
@@ -943,7 +943,7 @@ glob2==0.7
|
||||
# via
|
||||
# -r requirements/edx/doc.txt
|
||||
# -r requirements/edx/testing.txt
|
||||
google-api-core[grpc]==2.21.0
|
||||
google-api-core[grpc]==2.22.0
|
||||
# via
|
||||
# -r requirements/edx/doc.txt
|
||||
# -r requirements/edx/testing.txt
|
||||
@@ -1236,7 +1236,7 @@ mccabe==0.7.0
|
||||
# via
|
||||
# -r requirements/edx/testing.txt
|
||||
# pylint
|
||||
meilisearch==0.31.5
|
||||
meilisearch==0.31.6
|
||||
# via
|
||||
# -r requirements/edx/doc.txt
|
||||
# -r requirements/edx/testing.txt
|
||||
@@ -1712,7 +1712,7 @@ pytest-metadata==1.8.0
|
||||
# via
|
||||
# -r requirements/edx/testing.txt
|
||||
# pytest-json-report
|
||||
pytest-randomly==3.15.0
|
||||
pytest-randomly==3.16.0
|
||||
# via -r requirements/edx/testing.txt
|
||||
pytest-xdist[psutil]==3.6.1
|
||||
# via -r requirements/edx/testing.txt
|
||||
@@ -1800,11 +1800,11 @@ random2==1.0.2
|
||||
# via
|
||||
# -r requirements/edx/doc.txt
|
||||
# -r requirements/edx/testing.txt
|
||||
recommender-xblock==2.2.1
|
||||
recommender-xblock==3.0.0
|
||||
# via
|
||||
# -r requirements/edx/doc.txt
|
||||
# -r requirements/edx/testing.txt
|
||||
redis==5.1.1
|
||||
redis==5.2.0
|
||||
# via
|
||||
# -r requirements/edx/doc.txt
|
||||
# -r requirements/edx/testing.txt
|
||||
@@ -1958,7 +1958,7 @@ snowballstemmer==2.2.0
|
||||
# via
|
||||
# -r requirements/edx/doc.txt
|
||||
# sphinx
|
||||
snowflake-connector-python==3.12.2
|
||||
snowflake-connector-python==3.12.3
|
||||
# via
|
||||
# -r requirements/edx/doc.txt
|
||||
# -r requirements/edx/testing.txt
|
||||
@@ -2053,7 +2053,7 @@ staff-graded-xblock==2.3.0
|
||||
# via
|
||||
# -r requirements/edx/doc.txt
|
||||
# -r requirements/edx/testing.txt
|
||||
starlette==0.41.0
|
||||
starlette==0.41.2
|
||||
# via
|
||||
# -r requirements/edx/testing.txt
|
||||
# fastapi
|
||||
@@ -2101,7 +2101,7 @@ tomlkit==0.13.2
|
||||
# snowflake-connector-python
|
||||
tox==4.23.2
|
||||
# via -r requirements/edx/testing.txt
|
||||
tqdm==4.66.5
|
||||
tqdm==4.66.6
|
||||
# via
|
||||
# -r requirements/edx/doc.txt
|
||||
# -r requirements/edx/testing.txt
|
||||
@@ -2180,7 +2180,7 @@ vine==5.1.0
|
||||
# amqp
|
||||
# celery
|
||||
# kombu
|
||||
virtualenv==20.27.0
|
||||
virtualenv==20.27.1
|
||||
# via
|
||||
# -r requirements/edx/testing.txt
|
||||
# tox
|
||||
@@ -2222,7 +2222,7 @@ webencodings==0.5.1
|
||||
# bleach
|
||||
# html5lib
|
||||
# tinycss2
|
||||
webob==1.8.8
|
||||
webob==1.8.9
|
||||
# via
|
||||
# -r requirements/edx/doc.txt
|
||||
# -r requirements/edx/testing.txt
|
||||
@@ -2280,7 +2280,7 @@ xss-utils==0.6.0
|
||||
# via
|
||||
# -r requirements/edx/doc.txt
|
||||
# -r requirements/edx/testing.txt
|
||||
yarl==1.16.0
|
||||
yarl==1.17.0
|
||||
# via
|
||||
# -r requirements/edx/doc.txt
|
||||
# -r requirements/edx/testing.txt
|
||||
|
||||
@@ -102,13 +102,13 @@ bleach[css]==6.1.0
|
||||
# xblock-poll
|
||||
boto==2.49.0
|
||||
# via -r requirements/edx/base.txt
|
||||
boto3==1.35.46
|
||||
boto3==1.35.50
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# django-ses
|
||||
# fs-s3fs
|
||||
# ora2
|
||||
botocore==1.35.46
|
||||
botocore==1.35.50
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# boto3
|
||||
@@ -194,7 +194,7 @@ code-annotations==1.8.0
|
||||
# edx-toggles
|
||||
codejail-includes==1.0.0
|
||||
# via -r requirements/edx/base.txt
|
||||
crowdsourcehinter-xblock==0.7
|
||||
crowdsourcehinter-xblock==0.8
|
||||
# via -r requirements/edx/base.txt
|
||||
cryptography==43.0.3
|
||||
# via
|
||||
@@ -535,7 +535,7 @@ edx-django-utils==7.0.0
|
||||
# openedx-events
|
||||
# ora2
|
||||
# super-csv
|
||||
edx-drf-extensions==10.4.0
|
||||
edx-drf-extensions==10.5.0
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# edx-completion
|
||||
@@ -547,11 +547,11 @@ edx-drf-extensions==10.4.0
|
||||
# edx-when
|
||||
# edxval
|
||||
# openedx-learning
|
||||
edx-enterprise==4.28.3
|
||||
edx-enterprise==4.30.0
|
||||
# via
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# -r requirements/edx/base.txt
|
||||
edx-event-bus-kafka==5.8.1
|
||||
edx-event-bus-kafka==6.0.0
|
||||
# via -r requirements/edx/base.txt
|
||||
edx-event-bus-redis==0.5.1
|
||||
# via -r requirements/edx/base.txt
|
||||
@@ -683,7 +683,7 @@ gitpython==3.1.43
|
||||
# via -r requirements/edx/doc.in
|
||||
glob2==0.7
|
||||
# via -r requirements/edx/base.txt
|
||||
google-api-core[grpc]==2.21.0
|
||||
google-api-core[grpc]==2.22.0
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# firebase-admin
|
||||
@@ -891,7 +891,7 @@ maxminddb==2.6.2
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# geoip2
|
||||
meilisearch==0.31.5
|
||||
meilisearch==0.31.6
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# edx-search
|
||||
@@ -1247,9 +1247,9 @@ pyyaml==6.0.2
|
||||
# xblock
|
||||
random2==1.0.2
|
||||
# via -r requirements/edx/base.txt
|
||||
recommender-xblock==2.2.1
|
||||
recommender-xblock==3.0.0
|
||||
# via -r requirements/edx/base.txt
|
||||
redis==5.1.1
|
||||
redis==5.2.0
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# walrus
|
||||
@@ -1371,7 +1371,7 @@ smmap==5.0.1
|
||||
# via gitdb
|
||||
snowballstemmer==2.2.0
|
||||
# via sphinx
|
||||
snowflake-connector-python==3.12.2
|
||||
snowflake-connector-python==3.12.3
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# edx-enterprise
|
||||
@@ -1472,7 +1472,7 @@ tomlkit==0.13.2
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# snowflake-connector-python
|
||||
tqdm==4.66.5
|
||||
tqdm==4.66.6
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# nltk
|
||||
@@ -1547,7 +1547,7 @@ webencodings==0.5.1
|
||||
# bleach
|
||||
# html5lib
|
||||
# tinycss2
|
||||
webob==1.8.8
|
||||
webob==1.8.9
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# xblock
|
||||
@@ -1586,7 +1586,7 @@ xmlsec==1.3.13
|
||||
# python3-saml
|
||||
xss-utils==0.6.0
|
||||
# via -r requirements/edx/base.txt
|
||||
yarl==1.16.0
|
||||
yarl==1.17.0
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# aiohttp
|
||||
|
||||
@@ -116,7 +116,7 @@ ruamel-yaml==0.17.40
|
||||
# via semgrep
|
||||
ruamel-yaml-clib==0.2.12
|
||||
# via ruamel-yaml
|
||||
semgrep==1.92.0
|
||||
semgrep==1.93.0
|
||||
# via -r requirements/edx/semgrep.in
|
||||
tomli==2.0.2
|
||||
# via semgrep
|
||||
|
||||
@@ -102,13 +102,13 @@ bleach[css]==6.1.0
|
||||
# xblock-poll
|
||||
boto==2.49.0
|
||||
# via -r requirements/edx/base.txt
|
||||
boto3==1.35.46
|
||||
boto3==1.35.50
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# django-ses
|
||||
# fs-s3fs
|
||||
# ora2
|
||||
botocore==1.35.46
|
||||
botocore==1.35.50
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# boto3
|
||||
@@ -213,7 +213,7 @@ coverage[toml]==7.6.4
|
||||
# via
|
||||
# -r requirements/edx/coverage.txt
|
||||
# pytest-cov
|
||||
crowdsourcehinter-xblock==0.7
|
||||
crowdsourcehinter-xblock==0.8
|
||||
# via -r requirements/edx/base.txt
|
||||
cryptography==43.0.3
|
||||
# via
|
||||
@@ -559,7 +559,7 @@ edx-django-utils==7.0.0
|
||||
# openedx-events
|
||||
# ora2
|
||||
# super-csv
|
||||
edx-drf-extensions==10.4.0
|
||||
edx-drf-extensions==10.5.0
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# edx-completion
|
||||
@@ -571,11 +571,11 @@ edx-drf-extensions==10.4.0
|
||||
# edx-when
|
||||
# edxval
|
||||
# openedx-learning
|
||||
edx-enterprise==4.28.3
|
||||
edx-enterprise==4.30.0
|
||||
# via
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# -r requirements/edx/base.txt
|
||||
edx-event-bus-kafka==5.8.1
|
||||
edx-event-bus-kafka==6.0.0
|
||||
# via -r requirements/edx/base.txt
|
||||
edx-event-bus-redis==0.5.1
|
||||
# via -r requirements/edx/base.txt
|
||||
@@ -584,7 +584,7 @@ edx-i18n-tools==1.5.0
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# -r requirements/edx/base.txt
|
||||
# ora2
|
||||
edx-lint==5.4.0
|
||||
edx-lint==5.4.1
|
||||
# via -r requirements/edx/testing.in
|
||||
edx-milestones==0.6.0
|
||||
# via -r requirements/edx/base.txt
|
||||
@@ -674,9 +674,9 @@ execnet==2.1.1
|
||||
# via pytest-xdist
|
||||
factory-boy==3.3.1
|
||||
# via -r requirements/edx/testing.in
|
||||
faker==30.8.0
|
||||
faker==30.8.1
|
||||
# via factory-boy
|
||||
fastapi==0.115.3
|
||||
fastapi==0.115.4
|
||||
# via pact-python
|
||||
fastavro==1.9.7
|
||||
# via
|
||||
@@ -717,7 +717,7 @@ geoip2==4.8.0
|
||||
# via -r requirements/edx/base.txt
|
||||
glob2==0.7
|
||||
# via -r requirements/edx/base.txt
|
||||
google-api-core[grpc]==2.21.0
|
||||
google-api-core[grpc]==2.22.0
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# firebase-admin
|
||||
@@ -944,7 +944,7 @@ maxminddb==2.6.2
|
||||
# geoip2
|
||||
mccabe==0.7.0
|
||||
# via pylint
|
||||
meilisearch==0.31.5
|
||||
meilisearch==0.31.6
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# edx-search
|
||||
@@ -1295,7 +1295,7 @@ pytest-metadata==1.8.0
|
||||
# via
|
||||
# -r requirements/edx/testing.in
|
||||
# pytest-json-report
|
||||
pytest-randomly==3.15.0
|
||||
pytest-randomly==3.16.0
|
||||
# via -r requirements/edx/testing.in
|
||||
pytest-xdist[psutil]==3.6.1
|
||||
# via -r requirements/edx/testing.in
|
||||
@@ -1365,9 +1365,9 @@ pyyaml==6.0.2
|
||||
# xblock
|
||||
random2==1.0.2
|
||||
# via -r requirements/edx/base.txt
|
||||
recommender-xblock==2.2.1
|
||||
recommender-xblock==3.0.0
|
||||
# via -r requirements/edx/base.txt
|
||||
redis==5.1.1
|
||||
redis==5.2.0
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# walrus
|
||||
@@ -1490,7 +1490,7 @@ slumber==0.7.1
|
||||
# edx-enterprise
|
||||
sniffio==1.3.1
|
||||
# via anyio
|
||||
snowflake-connector-python==3.12.2
|
||||
snowflake-connector-python==3.12.3
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# edx-enterprise
|
||||
@@ -1522,7 +1522,7 @@ sqlparse==0.5.1
|
||||
# django
|
||||
staff-graded-xblock==2.3.0
|
||||
# via -r requirements/edx/base.txt
|
||||
starlette==0.41.0
|
||||
starlette==0.41.2
|
||||
# via fastapi
|
||||
stevedore==5.3.0
|
||||
# via
|
||||
@@ -1560,7 +1560,7 @@ tomlkit==0.13.2
|
||||
# snowflake-connector-python
|
||||
tox==4.23.2
|
||||
# via -r requirements/edx/testing.in
|
||||
tqdm==4.66.5
|
||||
tqdm==4.66.6
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# nltk
|
||||
@@ -1614,7 +1614,7 @@ vine==5.1.0
|
||||
# amqp
|
||||
# celery
|
||||
# kombu
|
||||
virtualenv==20.27.0
|
||||
virtualenv==20.27.1
|
||||
# via tox
|
||||
voluptuous==0.15.2
|
||||
# via
|
||||
@@ -1644,7 +1644,7 @@ webencodings==0.5.1
|
||||
# bleach
|
||||
# html5lib
|
||||
# tinycss2
|
||||
webob==1.8.8
|
||||
webob==1.8.9
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# xblock
|
||||
@@ -1685,7 +1685,7 @@ xmlsec==1.3.13
|
||||
# python3-saml
|
||||
xss-utils==0.6.0
|
||||
# via -r requirements/edx/base.txt
|
||||
yarl==1.16.0
|
||||
yarl==1.17.0
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# aiohttp
|
||||
|
||||
@@ -9,6 +9,8 @@ wheel==0.44.0
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
pip==24.2
|
||||
# via -r requirements/pip.in
|
||||
# via
|
||||
# -c requirements/common_constraints.txt
|
||||
# -r requirements/pip.in
|
||||
setuptools==75.2.0
|
||||
# via -r requirements/pip.in
|
||||
|
||||
@@ -10,9 +10,9 @@ attrs==24.2.0
|
||||
# via zeep
|
||||
backoff==2.2.1
|
||||
# via -r scripts/user_retirement/requirements/base.in
|
||||
boto3==1.35.46
|
||||
boto3==1.35.50
|
||||
# via -r scripts/user_retirement/requirements/base.in
|
||||
botocore==1.35.46
|
||||
botocore==1.35.50
|
||||
# via
|
||||
# boto3
|
||||
# s3transfer
|
||||
@@ -50,7 +50,7 @@ edx-django-utils==7.0.0
|
||||
# via edx-rest-api-client
|
||||
edx-rest-api-client==6.0.0
|
||||
# via -r scripts/user_retirement/requirements/base.in
|
||||
google-api-core==2.21.0
|
||||
google-api-core==2.22.0
|
||||
# via google-api-python-client
|
||||
google-api-python-client==2.149.0
|
||||
# via -r scripts/user_retirement/requirements/base.in
|
||||
|
||||
@@ -14,11 +14,11 @@ attrs==24.2.0
|
||||
# zeep
|
||||
backoff==2.2.1
|
||||
# via -r scripts/user_retirement/requirements/base.txt
|
||||
boto3==1.35.46
|
||||
boto3==1.35.50
|
||||
# via
|
||||
# -r scripts/user_retirement/requirements/base.txt
|
||||
# moto
|
||||
botocore==1.35.46
|
||||
botocore==1.35.50
|
||||
# via
|
||||
# -r scripts/user_retirement/requirements/base.txt
|
||||
# boto3
|
||||
@@ -72,7 +72,7 @@ edx-django-utils==7.0.0
|
||||
# edx-rest-api-client
|
||||
edx-rest-api-client==6.0.0
|
||||
# via -r scripts/user_retirement/requirements/base.txt
|
||||
google-api-core==2.21.0
|
||||
google-api-core==2.22.0
|
||||
# via
|
||||
# -r scripts/user_retirement/requirements/base.txt
|
||||
# google-api-python-client
|
||||
@@ -272,7 +272,7 @@ urllib3==1.26.20
|
||||
# botocore
|
||||
# requests
|
||||
# responses
|
||||
werkzeug==3.0.4
|
||||
werkzeug==3.0.6
|
||||
# via moto
|
||||
xmltodict==0.14.2
|
||||
# via moto
|
||||
|
||||
@@ -11,7 +11,7 @@ from xblock.fields import Scope, String
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
from xmodule.editing_block import EditingMixin
|
||||
from xmodule.raw_block import RawMixin
|
||||
from xmodule.util.builtin_assets import add_webpack_js_to_fragment, add_sass_to_fragment
|
||||
from xmodule.util.builtin_assets import add_webpack_js_to_fragment, add_css_to_fragment
|
||||
from xmodule.xml_block import XmlMixin
|
||||
from xmodule.x_module import (
|
||||
ResourceTemplates,
|
||||
@@ -180,7 +180,7 @@ class AnnotatableBlock(
|
||||
"""
|
||||
fragment = Fragment()
|
||||
fragment.add_content(self.get_html())
|
||||
add_sass_to_fragment(fragment, 'AnnotatableBlockDisplay.scss')
|
||||
add_css_to_fragment(fragment, 'AnnotatableBlockDisplay.css')
|
||||
add_webpack_js_to_fragment(fragment, 'AnnotatableBlockDisplay')
|
||||
shim_xmodule_js(fragment, 'Annotatable')
|
||||
|
||||
@@ -193,7 +193,7 @@ class AnnotatableBlock(
|
||||
fragment = Fragment(
|
||||
self.runtime.service(self, 'mako').render_cms_template(self.mako_template, self.get_context())
|
||||
)
|
||||
add_sass_to_fragment(fragment, 'AnnotatableBlockEditor.scss')
|
||||
add_css_to_fragment(fragment, 'AnnotatableBlockEditor.css')
|
||||
add_webpack_js_to_fragment(fragment, 'AnnotatableBlockEditor')
|
||||
shim_xmodule_js(fragment, self.studio_js_module_name)
|
||||
return fragment
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.xmodule_display.xmodule_AnnotatableBlock {
|
||||
@import "annotatable/display.scss";
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
.xmodule_edit.xmodule_AnnotatableBlock {
|
||||
@import "codemirror/codemirror.scss";
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
.xmodule_display.xmodule_PollBlock {
|
||||
@import "poll/display.scss";
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
.xmodule_display.xmodule_WordCloudBlock {
|
||||
@import "word_cloud/display.scss";
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
/* TODO: move top-level variables to a common _variables.scss.
|
||||
* NOTE: These variables were only added here because when this was integrated with the CMS,
|
||||
* SASS compilation errors were triggered because the CMS didn't have the same variables defined
|
||||
* that the LMS did, so the quick fix was to localize the LMS variables not shared by the CMS.
|
||||
* -Abarrett and Vshnayder
|
||||
*/
|
||||
@import 'bourbon/bourbon';
|
||||
@import 'lms/theme/variables';
|
||||
@import 'bootstrap/scss/variables';
|
||||
@import 'lms/theme/variables-v1';
|
||||
|
||||
$annotatable--border-color: var(--gray-l3);
|
||||
$annotatable--body-font-size: em(14);
|
||||
|
||||
.annotatable-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.annotatable-header {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.annotatable-section {
|
||||
position: relative;
|
||||
padding: 0.5em 1em;
|
||||
border: 1px solid $annotatable--border-color;
|
||||
border-radius: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
|
||||
&.shaded { background-color: #ededed; }
|
||||
|
||||
.annotatable-section-title {
|
||||
font-weight: bold;
|
||||
a { font-weight: normal; }
|
||||
}
|
||||
|
||||
.annotatable-section-body {
|
||||
border-top: 1px solid $annotatable--border-color;
|
||||
margin-top: 0.5em;
|
||||
padding-top: 0.5em;
|
||||
|
||||
@include clearfix();
|
||||
}
|
||||
|
||||
ul.instructions-template {
|
||||
list-style: disc;
|
||||
margin-left: 4em;
|
||||
b { font-weight: bold; }
|
||||
i { font-style: italic; }
|
||||
|
||||
code {
|
||||
display: inline;
|
||||
white-space: pre;
|
||||
font-family: Courier New, monospace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.annotatable-toggle {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
margin: 2px 1em 2px 0;
|
||||
&.expanded::after { content: " \2191"; }
|
||||
&.collapsed::after { content: " \2193"; }
|
||||
}
|
||||
|
||||
.annotatable-span {
|
||||
@extend %ui-fake-link;
|
||||
|
||||
display: inline;
|
||||
|
||||
$highlight_index: 0;
|
||||
|
||||
@each $highlight in (
|
||||
(yellow rgba(255, 255,10, 0.3) rgba(255, 255,10, 0.9)),
|
||||
(red rgba(178,19,16,0.3) rgba(178,19,16,0.9)),
|
||||
(orange rgba(255,165,0, 0.3) rgba(255,165,0, 0.9)),
|
||||
(green rgba(25,255,132,0.3) rgba(25,255,132,0.9)),
|
||||
(blue rgba(35,163,255,0.3) rgba(35,163,255,0.9)),
|
||||
(purple rgba(115,9,178,0.3) rgba(115,9,178,0.9))) {
|
||||
|
||||
$highlight_index: $highlight_index + 1;
|
||||
$marker: nth($highlight, 1);
|
||||
$color: nth($highlight, 2);
|
||||
$selected_color: nth($highlight, 3);
|
||||
|
||||
@if $highlight_index == 1 {
|
||||
&.highlight {
|
||||
background-color: $color;
|
||||
&.selected { background-color: $selected_color; }
|
||||
}
|
||||
}
|
||||
|
||||
&.highlight-#{$marker} {
|
||||
background-color: $color;
|
||||
&.selected { background-color: $selected_color; }
|
||||
}
|
||||
}
|
||||
|
||||
&.hide {
|
||||
cursor: none;
|
||||
background-color: inherit;
|
||||
|
||||
.annotatable-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.annotatable-comment {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-tooltip.qtip.ui-tooltip {
|
||||
font-size: $annotatable--body-font-size;
|
||||
border: 1px solid #333;
|
||||
border-radius: 1em;
|
||||
background-color: rgba(0, 0, 0, 0.85);
|
||||
color: var(--white);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
.ui-tooltip-titlebar {
|
||||
font-size: em(16);
|
||||
color: inherit;
|
||||
background-color: transparent;
|
||||
padding: calc((var(--baseline)/4)) calc((var(--baseline)/2));
|
||||
border: none;
|
||||
|
||||
.ui-tooltip-title {
|
||||
padding: calc((var(--baseline)/4)) 0;
|
||||
border-bottom: 2px solid #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ui-tooltip-icon {
|
||||
right: 10px;
|
||||
background: #333;
|
||||
}
|
||||
|
||||
.ui-state-hover {
|
||||
color: inherit;
|
||||
border: 1px solid var(--gray-l3);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-tooltip-content {
|
||||
color: inherit;
|
||||
font-size: em(14);
|
||||
text-align: left;
|
||||
font-weight: 400;
|
||||
padding: 0 calc((var(--baseline)/2)) calc((var(--baseline)/2)) calc((var(--baseline)/2));
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
p {
|
||||
color: inherit;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-tooltip.qtip.ui-tooltip-annotatable {
|
||||
max-width: 375px;
|
||||
|
||||
.ui-tooltip-content {
|
||||
padding: 0 calc((var(--baseline)/2));
|
||||
|
||||
.annotatable-comment {
|
||||
display: block;
|
||||
margin: 0 0 calc((var(--baseline)/2)) 0;
|
||||
max-height: 225px;
|
||||
overflow: auto;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.annotatable-reply {
|
||||
display: block;
|
||||
border-top: 2px solid #333;
|
||||
padding: calc((var(--baseline)/4)) 0;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
bottom: -20px;
|
||||
left: 50%;
|
||||
height: 0;
|
||||
width: 0;
|
||||
margin-left: calc(-1 * (var(--baseline) / 4));
|
||||
border: 10px solid transparent;
|
||||
border-top-color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
}
|
||||
@@ -1,226 +0,0 @@
|
||||
@import 'bourbon/bourbon';
|
||||
@import 'lms/theme/variables';
|
||||
@import 'bootstrap/scss/variables';
|
||||
@import 'lms/theme/variables-v1';
|
||||
|
||||
div.poll_question {
|
||||
@media print {
|
||||
display: block;
|
||||
width: auto;
|
||||
padding: 0;
|
||||
|
||||
canvas, img {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: ($baseline*0.75);
|
||||
color: #fe57a1;
|
||||
font-size: 1.9em;
|
||||
|
||||
&.problem-header {
|
||||
div.staff {
|
||||
margin-top: ($baseline*1.5);
|
||||
font-size: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
display: block;
|
||||
width: auto;
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
text-align: justify;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.poll_answer {
|
||||
margin-bottom: $baseline;
|
||||
|
||||
&.short {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.question {
|
||||
height: auto;
|
||||
clear: both;
|
||||
min-height: 30px;
|
||||
|
||||
&.short {
|
||||
clear: none;
|
||||
width: 30%;
|
||||
display: inline;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.button {
|
||||
@extend %ui-fake-link;
|
||||
|
||||
-webkit-appearance: none;
|
||||
-webkit-background-clip: padding-box;
|
||||
-webkit-border-image: none;
|
||||
-webkit-box-align: center;
|
||||
-webkit-box-shadow: rgb(255, 255, 255) 0px 1px 0px 0px inset;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-rtl-ordering: logical;
|
||||
-webkit-user-select: text;
|
||||
-webkit-writing-mode: horizontal-tb;
|
||||
background-clip: padding-box;
|
||||
background-color: rgb(238, 238, 238);
|
||||
background-image: -webkit-linear-gradient(top, rgb(238, 238, 238), rgb(210, 210, 210));
|
||||
border-bottom-color: rgb(202, 202, 202);
|
||||
border-bottom-left-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-width: 1px;
|
||||
border-left-color: rgb(202, 202, 202);
|
||||
border-left-style: solid;
|
||||
border-left-width: 1px;
|
||||
border-right-color: rgb(202, 202, 202);
|
||||
border-right-style: solid;
|
||||
border-right-width: 1px;
|
||||
border-top-color: rgb(202, 202, 202);
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
border-top-style: solid;
|
||||
border-top-width: 1px;
|
||||
box-shadow: rgb(255, 255, 255) 0px 1px 0px 0px inset;
|
||||
box-sizing: border-box;
|
||||
color: rgb(51, 51, 51);
|
||||
|
||||
/* display: inline-block; */
|
||||
display: inline;
|
||||
float: left;
|
||||
font-family: 'Open Sans', Verdana, Geneva, sans-serif;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
font-weight: bold;
|
||||
letter-spacing: normal;
|
||||
line-height: 25.59375px;
|
||||
margin-bottom: ($baseline*0.75);
|
||||
margin: 0;
|
||||
padding: 0px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
text-indent: 0px;
|
||||
text-shadow: rgb(248, 248, 248) 0px 1px 0px;
|
||||
text-transform: none;
|
||||
vertical-align: top;
|
||||
white-space: pre-line;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
word-spacing: 0px;
|
||||
writing-mode: lr-tb;
|
||||
}
|
||||
|
||||
.button.answered {
|
||||
-webkit-box-shadow: rgb(97, 184, 225) 0px 1px 0px 0px inset;
|
||||
background-color: rgb(29, 157, 217);
|
||||
background-image: -webkit-linear-gradient(top, rgb(29, 157, 217), rgb(14, 124, 176));
|
||||
border-bottom-color: rgb(13, 114, 162);
|
||||
border-left-color: rgb(13, 114, 162);
|
||||
border-right-color: rgb(13, 114, 162);
|
||||
border-top-color: rgb(13, 114, 162);
|
||||
box-shadow: rgb(97, 184, 225) 0px 1px 0px 0px inset;
|
||||
color: rgb(255, 255, 255);
|
||||
text-shadow: rgb(7, 103, 148) 0px 1px 0px;
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
.text {
|
||||
@extend %ui-fake-link;
|
||||
|
||||
display: inline;
|
||||
float: left;
|
||||
width: 80%;
|
||||
text-align: left;
|
||||
min-height: 30px;
|
||||
margin-left: $baseline;
|
||||
height: auto;
|
||||
margin-bottom: $baseline;
|
||||
|
||||
&.short {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stats {
|
||||
min-height: 40px;
|
||||
margin-top: $baseline;
|
||||
clear: both;
|
||||
|
||||
&.short {
|
||||
margin-top: 0;
|
||||
clear: none;
|
||||
display: inline;
|
||||
float: right;
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.bar {
|
||||
width: 75%;
|
||||
height: 20px;
|
||||
border: 1px solid black;
|
||||
display: inline;
|
||||
float: left;
|
||||
margin-right: ($baseline/2);
|
||||
|
||||
&.short {
|
||||
width: 65%;
|
||||
height: 20px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.percent {
|
||||
background-color: gray;
|
||||
width: 0;
|
||||
height: 20px;
|
||||
|
||||
&.short { }
|
||||
}
|
||||
}
|
||||
|
||||
.number {
|
||||
width: 80px;
|
||||
display: inline;
|
||||
float: right;
|
||||
height: 28px;
|
||||
text-align: right;
|
||||
|
||||
&.short {
|
||||
width: 120px;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.poll_answer.answered {
|
||||
-webkit-box-shadow: rgb(97, 184, 225) 0 1px 0 0 inset;
|
||||
background-color: rgb(29, 157, 217);
|
||||
background-image: -webkit-linear-gradient(top, rgb(29, 157, 217), rgb(14, 124, 176));
|
||||
border-bottom-color: rgb(13, 114, 162);
|
||||
border-left-color: rgb(13, 114, 162);
|
||||
border-right-color: rgb(13, 114, 162);
|
||||
border-top-color: rgb(13, 114, 162);
|
||||
box-shadow: rgb(97, 184, 225) 0 1px 0 0 inset;
|
||||
color: rgb(255, 255, 255);
|
||||
text-shadow: rgb(7, 103, 148) 0 1px 0;
|
||||
}
|
||||
|
||||
.button.reset-button {
|
||||
clear: both;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
@import 'bourbon/bourbon';
|
||||
@import 'lms/theme/variables';
|
||||
@import 'bootstrap/scss/variables';
|
||||
@import 'lms/theme/variables-v1';
|
||||
|
||||
.input-cloud {
|
||||
margin: calc((var(--baseline)/4));
|
||||
}
|
||||
|
||||
.result_cloud_section {
|
||||
display: none;
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
.result_cloud_section.active {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
margin-top: 1em;
|
||||
|
||||
h3 {
|
||||
font-size: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.your_words{
|
||||
font-size: 0.85em;
|
||||
display: block;
|
||||
}
|
||||
@@ -21,7 +21,7 @@ from xblock.fields import Boolean, Dict, List, Scope, String # lint-amnesty, py
|
||||
from openedx.core.djangolib.markup import Text, HTML
|
||||
from xmodule.mako_block import MakoTemplateBlockBase
|
||||
from xmodule.stringify import stringify_children
|
||||
from xmodule.util.builtin_assets import add_webpack_js_to_fragment, add_sass_to_fragment
|
||||
from xmodule.util.builtin_assets import add_webpack_js_to_fragment, add_css_to_fragment
|
||||
from xmodule.x_module import (
|
||||
ResourceTemplates,
|
||||
shim_xmodule_js,
|
||||
@@ -136,7 +136,7 @@ class PollBlock(
|
||||
'configuration_json': self.dump_poll(),
|
||||
}
|
||||
fragment.add_content(self.runtime.service(self, 'mako').render_lms_template('poll.html', params))
|
||||
add_sass_to_fragment(fragment, 'PollBlockDisplay.scss')
|
||||
add_css_to_fragment(fragment, 'PollBlockDisplay.css')
|
||||
add_webpack_js_to_fragment(fragment, 'PollBlockDisplay')
|
||||
shim_xmodule_js(fragment, 'Poll')
|
||||
return fragment
|
||||
|
||||
243
xmodule/static/css-builtin-blocks/AnnotatableBlockDisplay.css
Normal file
243
xmodule/static/css-builtin-blocks/AnnotatableBlockDisplay.css
Normal file
@@ -0,0 +1,243 @@
|
||||
@import url("https://fonts.googleapis.com/css?family=Open+Sans:300,400,400i,600,700");
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock {
|
||||
/* TODO: move top-level variables to a common _variables.scss.
|
||||
* NOTE: These variables were only added here because when this was integrated with the CMS,
|
||||
* SASS compilation errors were triggered because the CMS didn't have the same variables defined
|
||||
* that the LMS did, so the quick fix was to localize the LMS variables not shared by the CMS.
|
||||
* -Abarrett and Vshnayder
|
||||
*/
|
||||
/* stylelint-disable-line */
|
||||
/* stylelint-disable-line */
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .annotatable-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .annotatable-header {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .annotatable-section {
|
||||
position: relative;
|
||||
padding: 0.5em 1em;
|
||||
border: 1px solid var(--gray-l3);
|
||||
border-radius: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .annotatable-section.shaded {
|
||||
background-color: #ededed;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .annotatable-section .annotatable-section-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .annotatable-section .annotatable-section-title a {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .annotatable-section .annotatable-section-body {
|
||||
border-top: 1px solid var(--gray-l3);
|
||||
margin-top: 0.5em;
|
||||
padding-top: 0.5em;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .annotatable-section .annotatable-section-body:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .annotatable-section ul.instructions-template {
|
||||
list-style: disc;
|
||||
margin-left: 4em;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .annotatable-section ul.instructions-template b {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .annotatable-section ul.instructions-template i {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .annotatable-section ul.instructions-template code {
|
||||
display: inline;
|
||||
white-space: pre;
|
||||
font-family: Courier New, monospace;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .annotatable-toggle {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
margin: 2px 1em 2px 0;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .annotatable-toggle.expanded::after {
|
||||
content: " \2191";
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .annotatable-toggle.collapsed::after {
|
||||
content: " \2193";
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .annotatable-span {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight {
|
||||
background-color: rgba(255, 255, 10, 0.3);
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight.selected {
|
||||
background-color: rgba(255, 255, 10, 0.9);
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-yellow {
|
||||
background-color: rgba(255, 255, 10, 0.3);
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-yellow.selected {
|
||||
background-color: rgba(255, 255, 10, 0.9);
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-red {
|
||||
background-color: rgba(178, 19, 16, 0.3);
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-red.selected {
|
||||
background-color: rgba(178, 19, 16, 0.9);
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-orange {
|
||||
background-color: rgba(255, 165, 0, 0.3);
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-orange.selected {
|
||||
background-color: rgba(255, 165, 0, 0.9);
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-green {
|
||||
background-color: rgba(25, 255, 132, 0.3);
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-green.selected {
|
||||
background-color: rgba(25, 255, 132, 0.9);
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-blue {
|
||||
background-color: rgba(35, 163, 255, 0.3);
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-blue.selected {
|
||||
background-color: rgba(35, 163, 255, 0.9);
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-purple {
|
||||
background-color: rgba(115, 9, 178, 0.3);
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-purple.selected {
|
||||
background-color: rgba(115, 9, 178, 0.9);
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.hide {
|
||||
cursor: none;
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.hide .annotatable-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .annotatable-span .annotatable-comment {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip {
|
||||
font-size: 0.875em;
|
||||
border: 1px solid #333;
|
||||
border-radius: 1em;
|
||||
background-color: rgba(0, 0, 0, 0.85);
|
||||
color: var(--white);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip .ui-tooltip-titlebar {
|
||||
font-size: 1em;
|
||||
color: inherit;
|
||||
background-color: transparent;
|
||||
padding: calc((var(--baseline) / 4)) calc((var(--baseline) / 2));
|
||||
border: none;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip .ui-tooltip-titlebar .ui-tooltip-title {
|
||||
padding: calc((var(--baseline) / 4)) 0;
|
||||
border-bottom: 2px solid #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip .ui-tooltip-titlebar .ui-tooltip-icon {
|
||||
right: 10px;
|
||||
background: #333;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip .ui-tooltip-titlebar .ui-state-hover {
|
||||
color: inherit;
|
||||
border: 1px solid var(--gray-l3);
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip .ui-tooltip-content {
|
||||
color: inherit;
|
||||
font-size: 0.875em;
|
||||
text-align: left;
|
||||
font-weight: 400;
|
||||
padding: 0 calc((var(--baseline) / 2)) calc((var(--baseline) / 2)) calc((var(--baseline) / 2));
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip p {
|
||||
color: inherit;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip-annotatable {
|
||||
max-width: 375px;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip-annotatable .ui-tooltip-content {
|
||||
padding: 0 calc((var(--baseline) / 2));
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip-annotatable .ui-tooltip-content .annotatable-comment {
|
||||
display: block;
|
||||
margin: 0 0 calc((var(--baseline) / 2)) 0;
|
||||
max-height: 225px;
|
||||
overflow: auto;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip-annotatable .ui-tooltip-content .annotatable-reply {
|
||||
display: block;
|
||||
border-top: 2px solid #333;
|
||||
padding: calc((var(--baseline) / 4)) 0;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip-annotatable::after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
bottom: -20px;
|
||||
left: 50%;
|
||||
height: 0;
|
||||
width: 0;
|
||||
margin-left: calc(-1 * (var(--baseline) / 4));
|
||||
border: 10px solid transparent;
|
||||
border-top-color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.xmodule_edit.xmodule_AnnotatableBlock .CodeMirror {
|
||||
background: #fff;
|
||||
font-size: 13px;
|
||||
color: #3c3c3c;
|
||||
}
|
||||
221
xmodule/static/css-builtin-blocks/PollBlockDisplay.css
Normal file
221
xmodule/static/css-builtin-blocks/PollBlockDisplay.css
Normal file
@@ -0,0 +1,221 @@
|
||||
@import url("https://fonts.googleapis.com/css?family=Open+Sans:300,400,400i,600,700");
|
||||
|
||||
.xmodule_display.xmodule_PollBlock {
|
||||
/* stylelint-disable-line */
|
||||
/* stylelint-disable-line */
|
||||
}
|
||||
|
||||
@media print {
|
||||
.xmodule_display.xmodule_PollBlock div.poll_question {
|
||||
display: block;
|
||||
width: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_PollBlock div.poll_question canvas, .xmodule_display.xmodule_PollBlock div.poll_question img {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_PollBlock div.poll_question .inline {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_PollBlock div.poll_question h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: calc((var(--baseline) * 0.75));
|
||||
color: #fe57a1;
|
||||
font-size: 1.9em;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_PollBlock div.poll_question h3.problem-header div.staff {
|
||||
margin-top: calc((var(--baseline) * 1.5));
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.xmodule_display.xmodule_PollBlock div.poll_question h3 {
|
||||
display: block;
|
||||
width: auto;
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_PollBlock div.poll_question p {
|
||||
text-align: justify;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_PollBlock div.poll_question .poll_answer {
|
||||
margin-bottom: var(--baseline);
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_PollBlock div.poll_question .poll_answer.short {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_PollBlock div.poll_question .poll_answer .question {
|
||||
height: auto;
|
||||
clear: both;
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_PollBlock div.poll_question .poll_answer .question.short {
|
||||
clear: none;
|
||||
width: 30%;
|
||||
display: inline;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_PollBlock div.poll_question .poll_answer .question .button {
|
||||
-webkit-appearance: none;
|
||||
-webkit-background-clip: padding-box;
|
||||
-webkit-border-image: none;
|
||||
-webkit-box-align: center;
|
||||
-webkit-box-shadow: white 0px 1px 0px 0px inset;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-rtl-ordering: logical;
|
||||
-webkit-user-select: text;
|
||||
-webkit-writing-mode: horizontal-tb;
|
||||
background-clip: padding-box;
|
||||
background-color: #eeeeee;
|
||||
background-image: -webkit-linear-gradient(top, #eeeeee, #d2d2d2);
|
||||
border-bottom-color: #cacaca;
|
||||
border-bottom-left-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-width: 1px;
|
||||
border-left-color: #cacaca;
|
||||
border-left-style: solid;
|
||||
border-left-width: 1px;
|
||||
border-right-color: #cacaca;
|
||||
border-right-style: solid;
|
||||
border-right-width: 1px;
|
||||
border-top-color: #cacaca;
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
border-top-style: solid;
|
||||
border-top-width: 1px;
|
||||
box-shadow: white 0px 1px 0px 0px inset;
|
||||
box-sizing: border-box;
|
||||
color: #333333;
|
||||
/* display: inline-block; */
|
||||
display: inline;
|
||||
float: left;
|
||||
font-family: 'Open Sans', Verdana, Geneva, sans-serif;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
font-weight: bold;
|
||||
letter-spacing: normal;
|
||||
line-height: 25.59375px;
|
||||
margin-bottom: calc((var(--baseline) * 0.75));
|
||||
margin: 0;
|
||||
padding: 0px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
text-indent: 0px;
|
||||
text-shadow: #f8f8f8 0px 1px 0px;
|
||||
text-transform: none;
|
||||
vertical-align: top;
|
||||
white-space: pre-line;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
word-spacing: 0px;
|
||||
writing-mode: lr-tb;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_PollBlock div.poll_question .poll_answer .question .button.answered {
|
||||
-webkit-box-shadow: #61b8e1 0px 1px 0px 0px inset;
|
||||
background-color: #1d9dd9;
|
||||
background-image: -webkit-linear-gradient(top, #1d9dd9, #0e7cb0);
|
||||
border-bottom-color: #0d72a2;
|
||||
border-left-color: #0d72a2;
|
||||
border-right-color: #0d72a2;
|
||||
border-top-color: #0d72a2;
|
||||
box-shadow: #61b8e1 0px 1px 0px 0px inset;
|
||||
color: white;
|
||||
text-shadow: #076794 0px 1px 0px;
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_PollBlock div.poll_question .poll_answer .question .text {
|
||||
display: inline;
|
||||
float: left;
|
||||
width: 80%;
|
||||
text-align: left;
|
||||
min-height: 30px;
|
||||
margin-left: var(--baseline);
|
||||
height: auto;
|
||||
margin-bottom: var(--baseline);
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_PollBlock div.poll_question .poll_answer .question .text.short {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_PollBlock div.poll_question .poll_answer .stats {
|
||||
min-height: 40px;
|
||||
margin-top: var(--baseline);
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_PollBlock div.poll_question .poll_answer .stats.short {
|
||||
margin-top: 0;
|
||||
clear: none;
|
||||
display: inline;
|
||||
float: right;
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_PollBlock div.poll_question .poll_answer .stats .bar {
|
||||
width: 75%;
|
||||
height: 20px;
|
||||
border: 1px solid black;
|
||||
display: inline;
|
||||
float: left;
|
||||
margin-right: calc((var(--baseline) / 2));
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_PollBlock div.poll_question .poll_answer .stats .bar.short {
|
||||
width: 65%;
|
||||
height: 20px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_PollBlock div.poll_question .poll_answer .stats .bar .percent {
|
||||
background-color: gray;
|
||||
width: 0;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_PollBlock div.poll_question .poll_answer .stats .number {
|
||||
width: 80px;
|
||||
display: inline;
|
||||
float: right;
|
||||
height: 28px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_PollBlock div.poll_question .poll_answer .stats .number.short {
|
||||
width: 120px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_PollBlock div.poll_question .poll_answer.answered {
|
||||
-webkit-box-shadow: #61b8e1 0 1px 0 0 inset;
|
||||
background-color: #1d9dd9;
|
||||
background-image: -webkit-linear-gradient(top, #1d9dd9, #0e7cb0);
|
||||
border-bottom-color: #0d72a2;
|
||||
border-left-color: #0d72a2;
|
||||
border-right-color: #0d72a2;
|
||||
border-top-color: #0d72a2;
|
||||
box-shadow: #61b8e1 0 1px 0 0 inset;
|
||||
color: white;
|
||||
text-shadow: #076794 0 1px 0;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_PollBlock div.poll_question .button.reset-button {
|
||||
clear: both;
|
||||
float: right;
|
||||
}
|
||||
32
xmodule/static/css-builtin-blocks/WordCloudBlockDisplay.css
Normal file
32
xmodule/static/css-builtin-blocks/WordCloudBlockDisplay.css
Normal file
@@ -0,0 +1,32 @@
|
||||
@import url("https://fonts.googleapis.com/css?family=Open+Sans:300,400,400i,600,700");
|
||||
|
||||
.xmodule_display.xmodule_WordCloudBlock {
|
||||
/* stylelint-disable-line */
|
||||
/* stylelint-disable-line */
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_WordCloudBlock .input-cloud {
|
||||
margin: calc((var(--baseline) / 4));
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_WordCloudBlock .result_cloud_section {
|
||||
display: none;
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_WordCloudBlock .result_cloud_section.active {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_WordCloudBlock .result_cloud_section.active h3 {
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
.xmodule_display.xmodule_WordCloudBlock .your_words {
|
||||
font-size: 0.85em;
|
||||
display: block;
|
||||
}
|
||||
@@ -15,7 +15,7 @@ from xblock.core import XBlock
|
||||
from xblock.fields import Boolean, Dict, Integer, List, Scope, String
|
||||
from xmodule.editing_block import EditingMixin
|
||||
from xmodule.raw_block import EmptyDataRawMixin
|
||||
from xmodule.util.builtin_assets import add_webpack_js_to_fragment, add_sass_to_fragment
|
||||
from xmodule.util.builtin_assets import add_webpack_js_to_fragment, add_css_to_fragment
|
||||
from xmodule.xml_block import XmlMixin
|
||||
from xmodule.x_module import (
|
||||
ResourceTemplates,
|
||||
@@ -262,7 +262,7 @@ class WordCloudBlock( # pylint: disable=abstract-method
|
||||
'num_inputs': self.num_inputs,
|
||||
'submitted': self.submitted,
|
||||
}))
|
||||
add_sass_to_fragment(fragment, 'WordCloudBlockDisplay.scss')
|
||||
add_css_to_fragment(fragment, 'WordCloudBlockDisplay.css')
|
||||
add_webpack_js_to_fragment(fragment, 'WordCloudBlockDisplay')
|
||||
shim_xmodule_js(fragment, 'WordCloud')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user