Merge branch 'master' into edx-depr31

This commit is contained in:
Yagnesh1998
2023-08-04 10:28:54 +05:30
committed by GitHub
37 changed files with 731 additions and 947 deletions

View File

@@ -0,0 +1,27 @@
# Generated by Django 3.2.20 on 2023-08-02 13:59
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import opaque_keys.edx.django.models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('agreements', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='LTIPIISignature',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('course_key', opaque_keys.edx.django.models.CourseKeyField(db_index=True, max_length=255)),
('lti_tools', models.JSONField()),
('lti_tools_hash', models.IntegerField()),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@@ -0,0 +1,14 @@
# Generated by Django 3.2.20 on 2023-08-03 13:18
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('agreements', '0002_ltipiisignature'),
('agreements', '0002_ltipiitool'),
]
operations = [
]

View File

@@ -0,0 +1,26 @@
# Generated by Django 3.2.20 on 2023-08-03 16:09
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import opaque_keys.edx.django.models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('agreements', '0003_merge_0002_ltipiisignature_0002_ltipiitool'),
]
operations = [
migrations.CreateModel(
name='ProctoringPIISignature',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('course_key', opaque_keys.edx.django.models.CourseKeyField(db_index=True, max_length=255)),
('proctoring_provider', models.CharField(max_length=255)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@@ -34,3 +34,33 @@ class LTIPIITool(models.Model):
class Meta:
app_label = 'agreements'
class LTIPIISignature(models.Model):
"""
This model stores a user's acknowledgement to share PII via LTI tools in a particular course.
"""
user = models.ForeignKey(User, db_index=True, on_delete=models.CASCADE)
course_key = CourseKeyField(max_length=255, db_index=True)
lti_tools = models.JSONField()
# lti_tools_hash represents the hash of the list of LTI tools receiving
# PII acknowledged by the user. The hash is used to compare user
# acknowledgments - which reduces response time and decreases any impact
# on unit rendering time.
lti_tools_hash = models.IntegerField()
class Meta:
app_label = 'agreements'
class ProctoringPIISignature(models.Model):
"""
This model stores a user's acknowledgment to share PII via proctoring in a particular course.
"""
user = models.ForeignKey(User, db_index=True, on_delete=models.CASCADE)
course_key = CourseKeyField(max_length=255, db_index=True)
proctoring_provider = models.CharField(max_length=255)
class Meta:
app_label = 'agreements'

View File

@@ -42,20 +42,17 @@ class ClipboardTestCase(ModuleStoreTestCase):
client.login(username=self.user.username, password=self.user_password)
response = client.get(CLIPBOARD_ENDPOINT)
# We don't consider this a 404 error, it's a 200 with an empty response
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {
assert response.status_code == 200
assert response.json() == {
"content": None,
"source_usage_key": "",
"source_context_title": "",
"source_edit_url": "",
})
}
## The Python method for getting the API response should be identical:
self.assertEqual(
response.json(),
python_api.get_user_clipboard_json(self.user.id, response.wsgi_request),
)
assert response.json() == python_api.get_user_clipboard_json(self.user.id, response.wsgi_request)
# And the pure python API should return None
self.assertEqual(python_api.get_user_clipboard(self.user.id), None)
assert python_api.get_user_clipboard(self.user.id) is None
def _setup_course(self):
""" Set up the "Toy Course" and an APIClient for testing clipboard functionality. """
@@ -66,8 +63,8 @@ class ClipboardTestCase(ModuleStoreTestCase):
# Initial conditions: clipboard is empty:
response = client.get(CLIPBOARD_ENDPOINT)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["content"], None)
assert response.status_code == 200
assert response.json()["content"] is None
return (course_key, client)
@@ -82,27 +79,27 @@ class ClipboardTestCase(ModuleStoreTestCase):
response = client.post(CLIPBOARD_ENDPOINT, {"usage_key": str(video_key)}, format="json")
# Validate the response:
self.assertEqual(response.status_code, 200)
assert response.status_code == 200
response_data = response.json()
self.assertEqual(response_data["source_usage_key"], str(video_key))
self.assertEqual(response_data["source_context_title"], "Toy Course")
self.assertEqual(response_data["content"], {**response_data["content"], **{
assert response_data["source_usage_key"] == str(video_key)
assert response_data["source_context_title"] == "Toy Course"
assert response_data["content"] == {**response_data["content"], **{
"block_type": "video",
"block_type_display": "Video",
# To ensure API stability, we are hard-coding these expected values:
"purpose": "clipboard",
"status": "ready",
"display_name": "default", # Weird name but that's what defined in the toy course
}})
}}
# Test the actual OLX in the clipboard:
olx_url = response_data["content"]["olx_url"]
olx_response = client.get(olx_url)
self.assertEqual(olx_response.status_code, 200)
self.assertEqual(olx_response.get("Content-Type"), "application/vnd.openedx.xblock.v1.video+xml")
assert olx_response.status_code == 200
assert olx_response.get("Content-Type") == "application/vnd.openedx.xblock.v1.video+xml"
self.assertXmlEqual(olx_response.content.decode(), SAMPLE_VIDEO_OLX)
# Now if we GET the clipboard again, the GET response should exactly equal the last POST response:
self.assertEqual(client.get(CLIPBOARD_ENDPOINT).json(), response_data)
assert client.get(CLIPBOARD_ENDPOINT).json() == response_data
def test_copy_video_python_get(self):
"""
@@ -113,25 +110,25 @@ class ClipboardTestCase(ModuleStoreTestCase):
# Copy the video
video_key = course_key.make_usage_key("video", "sample_video")
response = client.post(CLIPBOARD_ENDPOINT, {"usage_key": str(video_key)}, format="json")
self.assertEqual(response.status_code, 200)
assert response.status_code == 200
# Get the clipboard status using python:
clipboard_data = python_api.get_user_clipboard(self.user.id)
self.assertIsNotNone(clipboard_data)
self.assertEqual(clipboard_data.source_usage_key, video_key)
assert clipboard_data is not None
assert clipboard_data.source_usage_key == video_key
# source_context_title is not in the python API because it's easy to retrieve a course's name from python code.
self.assertEqual(clipboard_data.content.block_type, "video")
assert clipboard_data.content.block_type == "video"
# To ensure API stability, we are hard-coding these expected values:
self.assertEqual(clipboard_data.content.purpose, "clipboard")
self.assertEqual(clipboard_data.content.status, "ready")
self.assertEqual(clipboard_data.content.display_name, "default")
assert clipboard_data.content.purpose == "clipboard"
assert clipboard_data.content.status == "ready"
assert clipboard_data.content.display_name == "default"
# Test the actual OLX in the clipboard:
olx_data = python_api.get_staged_content_olx(clipboard_data.content.id)
self.assertXmlEqual(olx_data, SAMPLE_VIDEO_OLX)
def test_copy_html(self):
"""
Test copying an HTML from the course
Test copying an HTML XBlock from the course
"""
course_key, client = self._setup_course()
@@ -140,32 +137,104 @@ class ClipboardTestCase(ModuleStoreTestCase):
response = client.post(CLIPBOARD_ENDPOINT, {"usage_key": str(html_key)}, format="json")
# Validate the response:
self.assertEqual(response.status_code, 200)
assert response.status_code == 200
response_data = response.json()
self.assertEqual(response_data["source_usage_key"], str(html_key))
self.assertEqual(response_data["source_context_title"], "Toy Course")
self.assertEqual(response_data["content"], {**response_data["content"], **{
assert response_data["source_usage_key"] == str(html_key)
assert response_data["source_context_title"] == "Toy Course"
assert response_data["content"] == {**response_data["content"], **{
"block_type": "html",
# To ensure API stability, we are hard-coding these expected values:
"purpose": "clipboard",
"status": "ready",
"display_name": "Text", # Has no display_name set so we fallback to this default
}})
}}
# Test the actual OLX in the clipboard:
olx_url = response_data["content"]["olx_url"]
olx_response = client.get(olx_url)
self.assertEqual(olx_response.status_code, 200)
self.assertEqual(olx_response.get("Content-Type"), "application/vnd.openedx.xblock.v1.html+xml")
assert olx_response.status_code == 200
assert olx_response.get("Content-Type") == "application/vnd.openedx.xblock.v1.html+xml"
# For HTML, we really want to be sure that the OLX is serialized in this exact format (using CDATA), so we check
# the actual string directly rather than using assertXmlEqual():
self.assertEqual(olx_response.content.decode(), dedent("""
assert olx_response.content.decode() == dedent("""
<html url_name="toyhtml" display_name="Text"><![CDATA[
<a href='/static/handouts/sample_handout.txt'>Sample</a>
]]></html>
""").lstrip())
""").lstrip()
# Now if we GET the clipboard again, the GET response should exactly equal the last POST response:
self.assertEqual(client.get(CLIPBOARD_ENDPOINT).json(), response_data)
assert client.get(CLIPBOARD_ENDPOINT).json() == response_data
def test_copy_unit(self):
"""
Test copying a unit (vertical block) from the course
"""
course_key, client = self._setup_course()
# Copy the HTML
unit_key = course_key.make_usage_key("vertical", "vertical_test")
response = client.post(CLIPBOARD_ENDPOINT, {"usage_key": str(unit_key)}, format="json")
# Validate the response:
assert response.status_code == 200
response_data = response.json()
assert response_data["source_usage_key"] == str(unit_key)
assert response_data["source_context_title"] == "Toy Course"
assert response_data["content"] == {**response_data["content"], **{
"block_type": "vertical",
# To ensure API stability, we are hard-coding these expected values:
"purpose": "clipboard",
"status": "ready",
"display_name": "vertical test", # Has no display_name set so display_name_with_default falls back to this
}}
# Test the actual OLX in the clipboard:
olx_url = response_data["content"]["olx_url"]
olx_response = client.get(olx_url)
assert olx_response.status_code == 200
assert olx_response.get("Content-Type") == "application/vnd.openedx.xblock.v1.vertical+xml"
self.assertXmlEqual(olx_response.content.decode(), """
<vertical url_name="vertical_test">
<video
url_name="sample_video"
display_name="default"
youtube="0.75:JMD_ifUUfsU,1.00:OEoXaMPEzfM,1.25:AKqURZnYqpk,1.50:DYpADpL7jAY"
youtube_id_0_75="JMD_ifUUfsU"
youtube_id_1_0="OEoXaMPEzfM"
youtube_id_1_25="AKqURZnYqpk"
youtube_id_1_5="DYpADpL7jAY"
/>
<video
url_name="separate_file_video"
display_name="default"
youtube="0.75:JMD_ifUUfsU,1.00:OEoXaMPEzfM,1.25:AKqURZnYqpk,1.50:DYpADpL7jAY"
youtube_id_0_75="JMD_ifUUfsU"
youtube_id_1_0="OEoXaMPEzfM"
youtube_id_1_25="AKqURZnYqpk"
youtube_id_1_5="DYpADpL7jAY"
/>
<video
url_name="video_with_end_time"
display_name="default"
youtube="0.75:JMD_ifUUfsU,1.00:OEoXaMPEzfM,1.25:AKqURZnYqpk,1.50:DYpADpL7jAY"
end_time="00:00:10"
youtube_id_0_75="JMD_ifUUfsU"
youtube_id_1_0="OEoXaMPEzfM"
youtube_id_1_25="AKqURZnYqpk"
youtube_id_1_5="DYpADpL7jAY"
/>
<poll_question
url_name="T1_changemind_poll_foo_2"
display_name="Change your answer"
reset="false"
>
&lt;p&gt;Have you changed your mind?&lt;/p&gt;
<answer id="yes">Yes</answer>
<answer id="no">No</answer>
</poll_question>
</vertical>
""")
# Now if we GET the clipboard again, the GET response should exactly equal the last POST response:
assert client.get(CLIPBOARD_ENDPOINT).json() == response_data
def test_copy_several_things(self):
"""
@@ -176,29 +245,29 @@ class ClipboardTestCase(ModuleStoreTestCase):
# Copy the video and validate the response:
video_key = course_key.make_usage_key("video", "sample_video")
response = client.post(CLIPBOARD_ENDPOINT, {"usage_key": str(video_key)}, format="json")
self.assertEqual(response.status_code, 200)
assert response.status_code == 200
video_clip_data = response.json()
self.assertEqual(video_clip_data["source_usage_key"], str(video_key))
self.assertEqual(video_clip_data["content"]["block_type"], "video")
assert video_clip_data["source_usage_key"] == str(video_key)
assert video_clip_data["content"]["block_type"] == "video"
old_olx_url = video_clip_data["content"]["olx_url"]
self.assertEqual(client.get(old_olx_url).status_code, 200)
assert client.get(old_olx_url).status_code == 200
# Now copy some HTML:
html_key = course_key.make_usage_key("html", "toyhtml")
response = client.post(CLIPBOARD_ENDPOINT, {"usage_key": str(html_key)}, format="json")
self.assertEqual(response.status_code, 200)
assert response.status_code == 200
# Now check the clipboard:
response = client.get(CLIPBOARD_ENDPOINT)
html_clip_data = response.json()
self.assertEqual(html_clip_data["source_usage_key"], str(html_key))
self.assertEqual(html_clip_data["content"]["block_type"], "html")
self.assertEqual(html_clip_data["content"]["block_type_display"], "Text")
assert html_clip_data["source_usage_key"] == str(html_key)
assert html_clip_data["content"]["block_type"] == "html"
assert html_clip_data["content"]["block_type_display"] == "Text"
## The Python method for getting the API response should be identical:
self.assertEqual(html_clip_data, python_api.get_user_clipboard_json(self.user.id, response.wsgi_request))
assert html_clip_data == python_api.get_user_clipboard_json(self.user.id, response.wsgi_request)
# The OLX link from the video will no longer work:
self.assertEqual(client.get(old_olx_url).status_code, 404)
assert client.get(old_olx_url).status_code == 404
def test_copy_static_assets(self):
"""
@@ -218,7 +287,7 @@ class ClipboardTestCase(ModuleStoreTestCase):
response = client.post(CLIPBOARD_ENDPOINT, {"usage_key": str(html_key)}, format="json")
# Validate the response:
self.assertEqual(response.status_code, 200)
assert response.status_code == 200
response_data = response.json()
staged_content_id = response_data["content"]["id"]
olx_str = python_api.get_staged_content_olx(staged_content_id)
@@ -253,7 +322,7 @@ class ClipboardTestCase(ModuleStoreTestCase):
response = client.post(CLIPBOARD_ENDPOINT, {"usage_key": str(html_block.location)}, format="json")
# Validate the response:
self.assertEqual(response.status_code, 200)
assert response.status_code == 200
response_data = response.json()
staged_content_id = response_data["content"]["id"]
olx_str = python_api.get_staged_content_olx(staged_content_id)
@@ -274,9 +343,9 @@ class ClipboardTestCase(ModuleStoreTestCase):
html_key = course_key.make_usage_key("html", "toyhtml")
with self.allow_transaction_exception():
response = nonstaff_client.post(CLIPBOARD_ENDPOINT, {"usage_key": str(html_key)}, format="json")
self.assertEqual(response.status_code, 403)
assert response.status_code == 403
response = nonstaff_client.get(CLIPBOARD_ENDPOINT)
self.assertEqual(response.json()["content"], None)
assert response.json()["content"] is None
def test_no_stealing_clipboard_content(self):
"""
@@ -293,11 +362,10 @@ class ClipboardTestCase(ModuleStoreTestCase):
# Then another user tries to get the OLX:
olx_url = response.json()["content"]["olx_url"]
response = nonstaff_client.get(olx_url)
self.assertEqual(response.status_code, 403)
assert response.status_code == 403
def assertXmlEqual(self, xml_str_a: str, xml_str_b: str) -> bool:
""" Assert that the given XML strings are equal, ignoring attribute order and some whitespace variations. """
self.assertEqual(
ElementTree.canonicalize(xml_str_a, strip_text=True),
ElementTree.canonicalize(xml_str_b, strip_text=True),
)
a = ElementTree.canonicalize(xml_str_a, strip_text=True)
b = ElementTree.canonicalize(xml_str_b, strip_text=True)
assert a == b