refactor: Refactor upstream links summary to add the top-level parent logic [FC-0097] (#37208)

- Refactor upstream links summary to add the top-level parent logic
- Update the `filter_links` function to annotate each result with `ready_to_sync_from_children`
This commit is contained in:
Chris Chávez
2025-08-18 19:01:53 -05:00
committed by GitHub
parent bc76a865f4
commit a1195efc20
5 changed files with 351 additions and 145 deletions

View File

@@ -7,8 +7,8 @@ from itertools import chain
from config_models.models import ConfigurationModel
from django.db import models
from django.db.models import Count, F, Q, QuerySet, Max
from django.db.models.fields import IntegerField, TextField
from django.db.models import QuerySet, OuterRef, Case, When, Exists, Value, ExpressionWrapper
from django.db.models.fields import IntegerField, TextField, BooleanField
from django.db.models.functions import Coalesce
from django.db.models.lookups import GreaterThan
from django.utils.translation import gettext_lazy as _
@@ -111,6 +111,20 @@ class EntityLinkBase(models.Model):
created = manual_date_time_field()
updated = manual_date_time_field()
@property
def upstream_context_title(self) -> str:
"""
Returns upstream context title.
"""
raise NotImplementedError
@property
def published_at(self) -> str | None:
"""
Returns the published date of the entity
"""
raise NotImplementedError
class Meta:
abstract = True
@@ -157,6 +171,15 @@ class ComponentLink(EntityLinkBase):
"""
return self.upstream_block.publishable_entity.learning_package.title
@property
def published_at(self) -> str | None:
"""
Returns the published date of the component
"""
if self.upstream_block.publishable_entity.published is None:
raise AttributeError(_("The component must be published to access `published_at`"))
return self.upstream_block.publishable_entity.published.publish_log_record.publish_log.published_at
@classmethod
def filter_links(
cls,
@@ -189,7 +212,9 @@ class ComponentLink(EntityLinkBase):
Coalesce("upstream_block__publishable_entity__published__version__version_num", 0),
Coalesce("version_declined", 0)
)
)
),
# This is alwys False, the components doens't have children
ready_to_sync_from_children=Value(False, output_field=BooleanField())
)
if ready_to_sync is not None:
result = result.filter(ready_to_sync=ready_to_sync)
@@ -216,40 +241,6 @@ class ComponentLink(EntityLinkBase):
return result
@classmethod
def summarize_by_downstream_context(cls, downstream_context_key: CourseKey) -> QuerySet:
"""
Returns a summary of links by upstream context for given downstream_context_key.
Example:
[
{
"upstream_context_title": "CS problems 3",
"upstream_context_key": "lib:OpenedX:CSPROB3",
"ready_to_sync_count": 11,
"total_count": 14,
"last_published_at": "2025-05-02T20:20:44.989042Z"
},
{
"upstream_context_title": "CS problems 2",
"upstream_context_key": "lib:OpenedX:CSPROB2",
"ready_to_sync_count": 15,
"total_count": 24,
"last_published_at": "2025-05-03T21:20:44.989042Z"
},
]
"""
result = cls.filter_links(downstream_context_key=downstream_context_key).values(
"upstream_context_key",
upstream_context_title=F("upstream_block__publishable_entity__learning_package__title"),
).annotate(
ready_to_sync_count=Count("id", Q(ready_to_sync=True)),
total_count=Count("id"),
last_published_at=Max(
"upstream_block__publishable_entity__published__publish_log_record__publish_log__published_at"
)
)
return result
@classmethod
def update_or_create(
cls,
@@ -351,6 +342,15 @@ class ContainerLink(EntityLinkBase):
"""
return self.upstream_container.publishable_entity.learning_package.title
@property
def published_at(self) -> str | None:
"""
Returns the published date of the container
"""
if self.upstream_container.publishable_entity.published is None:
raise AttributeError(_("The container must be published to access `published_at`"))
return self.upstream_container.publishable_entity.published.publish_log_record.publish_log.published_at
@classmethod
def filter_links(
cls,
@@ -402,6 +402,54 @@ class ContainerLink(EntityLinkBase):
@classmethod
def _annotate_query_with_ready_to_sync(cls, query_set: QuerySet["EntityLinkBase"]) -> QuerySet["EntityLinkBase"]:
"""
Adds ready to sync related values to the query set:
* `ready_to_sync`: When the container is ready to sync.
* `ready_to_sync_from_children`: When any children is ready to sync.
"""
# SubQuery to verify if some container children (associated with top-level parent)
# needs sync.
subq_container = cls.objects.filter(
top_level_parent=OuterRef('pk')
).annotate(
child_ready=Case(
When(
GreaterThan(
Coalesce("upstream_container__publishable_entity__published__version__version_num", 0),
Coalesce("version_synced", 0)
) & GreaterThan(
Coalesce("upstream_container__publishable_entity__published__version__version_num", 0),
Coalesce("version_declined", 0)
),
then=1
),
default=0,
output_field=models.IntegerField()
)
).filter(child_ready=1)
# SubQuery to verify if some component children (assisiated with top-level parent)
# needs sync.
subq_components = ComponentLink.objects.filter(
top_level_parent=OuterRef('pk')
).annotate(
child_ready=Case(
When(
GreaterThan(
Coalesce("upstream_block__publishable_entity__published__version__version_num", 0),
Coalesce("version_synced", 0)
) & GreaterThan(
Coalesce("upstream_block__publishable_entity__published__version__version_num", 0),
Coalesce("version_declined", 0)
),
then=1
),
default=0,
output_field=models.IntegerField()
)
).filter(child_ready=1)
# TODO: is there a way to run `subq_container` or `subq_components` depending on the container type?
return query_set.annotate(
ready_to_sync=(
GreaterThan(
@@ -411,43 +459,13 @@ class ContainerLink(EntityLinkBase):
Coalesce("upstream_container__publishable_entity__published__version__version_num", 0),
Coalesce("version_declined", 0)
)
)
),
ready_to_sync_from_children=ExpressionWrapper(
Exists(subq_container) | Exists(subq_components),
output_field=BooleanField(),
),
)
@classmethod
def summarize_by_downstream_context(cls, downstream_context_key: CourseKey) -> QuerySet:
"""
Returns a summary of links by upstream context for given downstream_context_key.
Example:
[
{
"upstream_context_title": "CS problems 3",
"upstream_context_key": "lib:OpenedX:CSPROB3",
"ready_to_sync_count": 11,
"total_count": 14,
"last_published_at": "2025-05-02T20:20:44.989042Z"
},
{
"upstream_context_title": "CS problems 2",
"upstream_context_key": "lib:OpenedX:CSPROB2",
"ready_to_sync_count": 15,
"total_count": 24,
"last_published_at": "2025-05-03T21:20:44.989042Z"
},
]
"""
result = cls.filter_links(downstream_context_key=downstream_context_key).values(
"upstream_context_key",
upstream_context_title=F("upstream_container__publishable_entity__learning_package__title"),
).annotate(
ready_to_sync_count=Count("id", Q(ready_to_sync=True)),
total_count=Count('id'),
last_published_at=Max(
"upstream_container__publishable_entity__published__publish_log_record__publish_log__published_at"
)
)
return result
@classmethod
def update_or_create(
cls,

View File

@@ -14,6 +14,7 @@ class ComponentLinksSerializer(serializers.ModelSerializer):
upstream_context_title = serializers.CharField(read_only=True)
upstream_version = serializers.IntegerField(read_only=True, source="upstream_version_num")
ready_to_sync = serializers.BooleanField()
ready_to_sync_from_children = serializers.BooleanField()
top_level_parent_usage_key = serializers.CharField(
source='top_level_parent.downstream_usage_key',
read_only=True,
@@ -43,6 +44,7 @@ class ContainerLinksSerializer(serializers.ModelSerializer):
upstream_context_title = serializers.CharField(read_only=True)
upstream_version = serializers.IntegerField(read_only=True, source="upstream_version_num")
ready_to_sync = serializers.BooleanField()
ready_to_sync_from_children = serializers.BooleanField()
top_level_parent_usage_key = serializers.CharField(
source='top_level_parent.downstream_usage_key',
read_only=True,

View File

@@ -239,6 +239,8 @@ class DownstreamListView(DeveloperErrorViewMixin, APIView):
raise ValidationError(detail=f"Malformed key: {upstream_key}") from exc
links: list[EntityLinkBase] | QuerySet[EntityLinkBase] = []
if item_type is None or item_type == 'all':
# itertools.chain() efficiently concatenates multiple iterables into one iterator,
# yielding items from each in sequence without creating intermediate lists.
links = list(chain(
ComponentLink.filter_links(**link_filter),
ContainerLink.filter_links(**link_filter)
@@ -346,34 +348,84 @@ class DownstreamSummaryView(DeveloperErrorViewMixin, APIView):
course_key = CourseKey.from_string(course_key_string)
except InvalidKeyError as exc:
raise ValidationError(detail=f"Malformed course key: {course_key_string}") from exc
component_links = ComponentLink.summarize_by_downstream_context(downstream_context_key=course_key)
container_links = ContainerLink.summarize_by_downstream_context(downstream_context_key=course_key)
merged = {}
if not has_studio_read_access(request.user, course_key):
raise PermissionDenied
def process_list(lst):
"""
Process a list to merge it with values in `merged`
"""
for item in lst:
key = item["upstream_context_key"]
if key not in merged:
merged[key] = item.copy()
else:
merged[key]["ready_to_sync_count"] += item["ready_to_sync_count"]
merged[key]["total_count"] += item["total_count"]
if item["last_published_at"] > merged[key]["last_published_at"]:
merged[key]["last_published_at"] = item["last_published_at"]
# Gets all links of the Course, using the
# top-level parents filter (see `filter_links()` for more info about top-level parents).
# `itertools.chain()` efficiently concatenates multiple iterables into one iterator,
# yielding items from each in sequence without creating intermediate lists.
links = list(chain(
ComponentLink.filter_links(
downstream_context_key=course_key,
use_top_level_parents=True,
),
ContainerLink.filter_links(
downstream_context_key=course_key,
use_top_level_parents=True,
),
))
# Merge `component_links` and `container_links` by adding the values of
# `ready_to_sync_count` and `total_count` of each library.
process_list(component_links)
process_list(container_links)
# Delete duplicates. From `ComponentLink` and `ContainerLink`
# repeated containers may come in this case:
# If we have a `Unit A` and a `Component B`, if you update and publish
# both, form `ComponentLink` and `ContainerLink` you get the same `Unit A`.
links = self._remove_duplicates(links)
result = {}
links = list(merged.values())
serializer = PublishableEntityLinksSummarySerializer(links, many=True)
for link in links:
# We iterate each list to do the counting by Library (`context_key`)
context_key = link.upstream_context_key
if context_key not in result:
result[context_key] = {
"upstream_context_key": context_key,
"upstream_context_title": link.upstream_context_title,
"ready_to_sync_count": 0,
"total_count": 0,
"last_published_at": None,
}
# Total count
result[context_key]["total_count"] += 1
# Ready to sync count, it also checks if the container has
# descendants that need sync (`ready_to_sync_from_children`).
if link.ready_to_sync or link.ready_to_sync_from_children: # type: ignore[attr-defined]
result[context_key]["ready_to_sync_count"] += 1
# The Max `published_at` value
# An AttributeError may be thrown if copied/pasted an unpublished item from library to course.
# That case breaks all the course library sync page.
# TODO: Delete this `try` after avoid copy/paster unpublished items.
try:
published_at = link.published_at
except AttributeError:
published_at = None
if published_at is not None and (
result[context_key]["last_published_at"] is None
or result[context_key]["last_published_at"] < published_at
):
result[context_key]["last_published_at"] = published_at
serializer = PublishableEntityLinksSummarySerializer(list(result.values()), many=True)
return Response(serializer.data)
def _remove_duplicates(self, links: list[EntityLinkBase]) -> list[EntityLinkBase]:
"""
Remove duplicates based on `EntityLinkBase.downstream_usage_key`
"""
seen_keys = set()
unique_links = []
for link in links:
if link.downstream_usage_key not in seen_keys:
seen_keys.add(link.downstream_usage_key)
unique_links.append(link)
return unique_links
@view_auth_classes(is_authenticated=True)
class DownstreamView(DeveloperErrorViewMixin, APIView):

View File

@@ -340,6 +340,7 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
'upstream_context_title': self.library_title,
'upstream_version': 2,
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'upstream_context_key': self.library_id,
'downstream_usage_key': downstream_html1,
'downstream_context_key': str(self.course.id),
@@ -356,6 +357,7 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
'upstream_context_title': self.library_title,
'upstream_version': 2,
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'upstream_context_key': self.library_id,
'downstream_usage_key': downstream_problem1,
'downstream_context_key': str(self.course.id),
@@ -372,6 +374,7 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
'upstream_context_title': self.library_title,
'upstream_version': 2,
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'upstream_context_key': self.library_id,
'downstream_usage_key': downstream_problem2,
'downstream_context_key': str(self.course.id),
@@ -388,6 +391,7 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
'upstream_context_title': self.library_title,
'upstream_version': 2,
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'upstream_context_key': self.library_id,
'downstream_usage_key': downstream_unit["locator"],
'downstream_context_key': str(self.course.id),
@@ -484,6 +488,7 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
'upstream_context_title': self.library_title,
'upstream_version': 2,
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'upstream_context_key': self.library_id,
'downstream_usage_key': downstream_html1,
'downstream_context_key': str(self.course.id),
@@ -500,6 +505,7 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
'upstream_context_title': self.library_title,
'upstream_version': 3, # <--- updated
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'upstream_context_key': self.library_id,
'downstream_usage_key': downstream_problem1,
'downstream_context_key': str(self.course.id),
@@ -516,6 +522,7 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
'upstream_context_title': self.library_title,
'upstream_version': 2,
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'upstream_context_key': self.library_id,
'downstream_usage_key': downstream_problem2,
'downstream_context_key': str(self.course.id),
@@ -532,6 +539,7 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
'upstream_context_title': self.library_title,
'upstream_version': 2, # <--- not updated since we didn't directly modify the unit
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'upstream_context_key': self.library_id,
'downstream_usage_key': downstream_unit["locator"],
'downstream_context_key': str(self.course.id),
@@ -626,6 +634,7 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
'upstream_context_title': self.library_title,
'upstream_version': 2,
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'upstream_context_key': self.library_id,
'downstream_usage_key': downstream_html1,
'downstream_context_key': str(self.course.id),
@@ -642,6 +651,7 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
'upstream_context_title': self.library_title,
'upstream_version': 3,
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'upstream_context_key': self.library_id,
'downstream_usage_key': downstream_problem1,
'downstream_context_key': str(self.course.id),
@@ -658,6 +668,7 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
'upstream_context_title': self.library_title,
'upstream_version': 2,
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'upstream_context_key': self.library_id,
'downstream_usage_key': downstream_problem3,
'downstream_context_key': str(self.course.id),
@@ -674,6 +685,7 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
'upstream_context_title': self.library_title,
'upstream_version': 4, # <--- updated
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'upstream_context_key': self.library_id,
'downstream_usage_key': downstream_unit["locator"],
'downstream_context_key': str(self.course.id),
@@ -750,6 +762,7 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
'upstream_context_title': self.library_title,
'upstream_version': 2,
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'upstream_context_key': self.library_id,
'downstream_usage_key': downstream_html1,
'downstream_context_key': str(self.course.id),
@@ -766,6 +779,7 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
'upstream_context_title': self.library_title,
'upstream_version': 3,
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'upstream_context_key': self.library_id,
'downstream_usage_key': downstream_problem1,
'downstream_context_key': str(self.course.id),
@@ -782,6 +796,7 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
'upstream_context_title': self.library_title,
'upstream_version': 2,
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'upstream_context_key': self.library_id,
'downstream_usage_key': downstream_problem3,
'downstream_context_key': str(self.course.id),
@@ -798,6 +813,7 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
'upstream_context_title': self.library_title,
'upstream_version': 5, # <--- updated
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'upstream_context_key': self.library_id,
'downstream_usage_key': downstream_unit["locator"],
'downstream_context_key': str(self.course.id),

View File

@@ -96,6 +96,7 @@ class _BaseDownstreamViewTestMixin:
# Creating container to test the top-level parent
self.top_level_unit_id = self._create_container(self.library_id, "unit", "unit-2", "Unit 2")["id"]
self.top_level_unit_id_2 = self._create_container(self.library_id, "unit", "unit-3", "Unit 3")["id"]
self.top_level_subsection_id = self._create_container(
self.library_id,
"subsection",
@@ -114,6 +115,7 @@ class _BaseDownstreamViewTestMixin:
self._publish_container(self.subsection_id)
self._publish_container(self.section_id)
self._publish_container(self.top_level_unit_id)
self._publish_container(self.top_level_unit_id_2)
self._publish_container(self.top_level_subsection_id)
self._publish_container(self.top_level_section_id)
self.mock_upstream_link = f"{settings.COURSE_AUTHORING_MICROFRONTEND_URL}/library/{self.library_id}/components?usageKey={self.video_lib_id}" # pylint: disable=line-too-long # noqa: E501
@@ -140,6 +142,24 @@ class _BaseDownstreamViewTestMixin:
).usage_key
# Creating Blocks with top-level-parents
# Unit created as a top-level parent
self.top_level_downstream_unit = BlockFactory.create(
category='vertical',
parent=sequential,
upstream=self.top_level_unit_id,
upstream_version=1,
)
self.top_level_downstream_html_key = BlockFactory.create(
category='html',
parent=self.top_level_downstream_unit,
upstream=self.html_lib_id_2,
upstream_version=1,
top_level_downstream_parent_key=get_block_key_dict(
self.top_level_downstream_unit.usage_key,
)
).usage_key
# Section created as a top-level parent
self.top_level_downstream_chapter = BlockFactory.create(
category='chapter', parent=self.course, upstream=self.top_level_section_id, upstream_version=1,
)
@@ -152,27 +172,18 @@ class _BaseDownstreamViewTestMixin:
self.top_level_downstream_chapter.usage_key,
),
)
self.top_level_downstream_unit = BlockFactory.create(
self.top_level_downstream_unit_2 = BlockFactory.create(
category='vertical',
parent=self.top_level_downstream_sequential,
upstream=self.top_level_unit_id,
upstream=self.top_level_unit_id_2,
upstream_version=1,
top_level_downstream_parent_key=get_block_key_dict(
self.top_level_downstream_sequential.usage_key,
)
self.top_level_downstream_chapter.usage_key,
),
)
self.top_level_downstream_html_key = BlockFactory.create(
category='html',
parent=self.top_level_downstream_unit,
upstream=self.html_lib_id_2,
upstream_version=1,
top_level_downstream_parent_key=get_block_key_dict(
self.top_level_downstream_unit.usage_key,
)
).usage_key
self.top_level_downstream_video_key = BlockFactory.create(
category='video',
parent=self.top_level_downstream_unit,
parent=self.top_level_downstream_unit_2,
upstream=self.video_lib_id_2,
upstream_version=1,
top_level_downstream_parent_key=get_block_key_dict(
@@ -589,6 +600,7 @@ class GetUpstreamViewTest(
'downstream_usage_key': str(self.downstream_video_key),
'id': 1,
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'updated': date_format,
'upstream_context_key': self.library_id,
'upstream_context_title': self.library_title,
@@ -605,6 +617,7 @@ class GetUpstreamViewTest(
'downstream_usage_key': str(self.downstream_html_key),
'id': 2,
'ready_to_sync': True,
'ready_to_sync_from_children': False,
'updated': date_format,
'upstream_context_key': self.library_id,
'upstream_context_title': self.library_title,
@@ -621,6 +634,7 @@ class GetUpstreamViewTest(
'downstream_usage_key': str(self.top_level_downstream_html_key),
'id': 3,
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'updated': date_format,
'upstream_context_key': self.library_id,
'upstream_context_title': self.library_title,
@@ -637,6 +651,7 @@ class GetUpstreamViewTest(
'downstream_usage_key': str(self.top_level_downstream_video_key),
'id': 4,
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'updated': date_format,
'upstream_context_key': self.library_id,
'upstream_context_title': self.library_title,
@@ -653,6 +668,7 @@ class GetUpstreamViewTest(
'downstream_usage_key': str(self.downstream_chapter_key),
'id': 1,
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'updated': date_format,
'upstream_context_key': self.library_id,
'upstream_context_title': self.library_title,
@@ -669,6 +685,7 @@ class GetUpstreamViewTest(
'downstream_usage_key': str(self.downstream_sequential_key),
'id': 2,
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'updated': date_format,
'upstream_context_key': self.library_id,
'upstream_context_title': self.library_title,
@@ -685,6 +702,7 @@ class GetUpstreamViewTest(
'downstream_usage_key': str(self.downstream_unit_key),
'id': 3,
'ready_to_sync': True,
'ready_to_sync_from_children': False,
'updated': date_format,
'upstream_context_key': self.library_id,
'upstream_context_title': self.library_title,
@@ -698,9 +716,27 @@ class GetUpstreamViewTest(
{
'created': date_format,
'downstream_context_key': str(self.course.id),
'downstream_usage_key': str(self.top_level_downstream_chapter.usage_key),
'downstream_usage_key': str(self.top_level_downstream_unit.usage_key),
'id': 4,
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'updated': date_format,
'upstream_context_key': self.library_id,
'upstream_context_title': self.library_title,
'upstream_key': self.top_level_unit_id,
'upstream_type': 'container',
'upstream_version': 1,
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': None,
},
{
'created': date_format,
'downstream_context_key': str(self.course.id),
'downstream_usage_key': str(self.top_level_downstream_chapter.usage_key),
'id': 5,
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'updated': date_format,
'upstream_context_key': self.library_id,
'upstream_context_title': self.library_title,
@@ -715,8 +751,9 @@ class GetUpstreamViewTest(
'created': date_format,
'downstream_context_key': str(self.course.id),
'downstream_usage_key': str(self.top_level_downstream_sequential.usage_key),
'id': 5,
'id': 6,
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'updated': date_format,
'upstream_context_key': self.library_id,
'upstream_context_title': self.library_title,
@@ -730,22 +767,23 @@ class GetUpstreamViewTest(
{
'created': date_format,
'downstream_context_key': str(self.course.id),
'downstream_usage_key': str(self.top_level_downstream_unit.usage_key),
'id': 6,
'downstream_usage_key': str(self.top_level_downstream_unit_2.usage_key),
'id': 7,
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'updated': date_format,
'upstream_context_key': self.library_id,
'upstream_context_title': self.library_title,
'upstream_key': self.top_level_unit_id,
'upstream_key': self.top_level_unit_id_2,
'upstream_type': 'container',
'upstream_version': 1,
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': str(self.top_level_downstream_sequential.usage_key),
'top_level_parent_usage_key': str(self.top_level_downstream_chapter.usage_key),
},
]
self.assertListEqual(data["results"], expected)
self.assertEqual(data["count"], 10)
self.assertEqual(data["count"], 11)
def test_permission_denied_with_course_filter(self):
self.client.login(username="simple_user", password="password")
@@ -771,6 +809,7 @@ class GetUpstreamViewTest(
'downstream_usage_key': str(self.downstream_video_key),
'id': 1,
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'updated': date_format,
'upstream_context_key': self.library_id,
'upstream_context_title': self.library_title,
@@ -787,6 +826,7 @@ class GetUpstreamViewTest(
'downstream_usage_key': str(self.downstream_html_key),
'id': 2,
'ready_to_sync': True,
'ready_to_sync_from_children': False,
'updated': date_format,
'upstream_context_key': self.library_id,
'upstream_context_title': self.library_title,
@@ -803,6 +843,7 @@ class GetUpstreamViewTest(
'downstream_usage_key': str(self.top_level_downstream_html_key),
'id': 3,
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'updated': date_format,
'upstream_context_key': self.library_id,
'upstream_context_title': self.library_title,
@@ -819,6 +860,7 @@ class GetUpstreamViewTest(
'downstream_usage_key': str(self.top_level_downstream_video_key),
'id': 4,
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'updated': date_format,
'upstream_context_key': self.library_id,
'upstream_context_title': self.library_title,
@@ -852,6 +894,7 @@ class GetUpstreamViewTest(
'downstream_usage_key': str(self.downstream_chapter_key),
'id': 1,
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'updated': date_format,
'upstream_context_key': self.library_id,
'upstream_context_title': self.library_title,
@@ -868,6 +911,7 @@ class GetUpstreamViewTest(
'downstream_usage_key': str(self.downstream_sequential_key),
'id': 2,
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'updated': date_format,
'upstream_context_key': self.library_id,
'upstream_context_title': self.library_title,
@@ -884,6 +928,7 @@ class GetUpstreamViewTest(
'downstream_usage_key': str(self.downstream_unit_key),
'id': 3,
'ready_to_sync': True,
'ready_to_sync_from_children': False,
'updated': date_format,
'upstream_context_key': self.library_id,
'upstream_context_title': self.library_title,
@@ -897,9 +942,27 @@ class GetUpstreamViewTest(
{
'created': date_format,
'downstream_context_key': str(self.course.id),
'downstream_usage_key': str(self.top_level_downstream_chapter.usage_key),
'downstream_usage_key': str(self.top_level_downstream_unit.usage_key),
'id': 4,
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'updated': date_format,
'upstream_context_key': self.library_id,
'upstream_context_title': self.library_title,
'upstream_key': self.top_level_unit_id,
'upstream_type': 'container',
'upstream_version': 1,
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': None,
},
{
'created': date_format,
'downstream_context_key': str(self.course.id),
'downstream_usage_key': str(self.top_level_downstream_chapter.usage_key),
'id': 5,
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'updated': date_format,
'upstream_context_key': self.library_id,
'upstream_context_title': self.library_title,
@@ -914,8 +977,9 @@ class GetUpstreamViewTest(
'created': date_format,
'downstream_context_key': str(self.course.id),
'downstream_usage_key': str(self.top_level_downstream_sequential.usage_key),
'id': 5,
'id': 6,
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'updated': date_format,
'upstream_context_key': self.library_id,
'upstream_context_title': self.library_title,
@@ -929,22 +993,23 @@ class GetUpstreamViewTest(
{
'created': date_format,
'downstream_context_key': str(self.course.id),
'downstream_usage_key': str(self.top_level_downstream_unit.usage_key),
'id': 6,
'downstream_usage_key': str(self.top_level_downstream_unit_2.usage_key),
'id': 7,
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'updated': date_format,
'upstream_context_key': self.library_id,
'upstream_context_title': self.library_title,
'upstream_key': self.top_level_unit_id,
'upstream_key': self.top_level_unit_id_2,
'upstream_type': 'container',
'upstream_version': 1,
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': str(self.top_level_downstream_sequential.usage_key),
'top_level_parent_usage_key': str(self.top_level_downstream_chapter.usage_key),
},
]
self.assertListEqual(data["results"], expected)
self.assertEqual(data["count"], 6)
self.assertEqual(data["count"], 7)
@ddt.data(
('all', 2),
@@ -1027,9 +1092,27 @@ class GetUpstreamViewTest(
{
'created': date_format,
'downstream_context_key': str(self.course.id),
'downstream_usage_key': str(self.top_level_downstream_chapter.usage_key),
'downstream_usage_key': str(self.top_level_downstream_unit.usage_key),
'id': 4,
'ready_to_sync': False,
'ready_to_sync': False, # <-- It's False because the container doesn't have changes
'ready_to_sync_from_children': True, # <-- It's True because a child has changes
'updated': date_format,
'upstream_context_key': self.library_id,
'upstream_context_title': self.library_title,
'upstream_key': self.top_level_unit_id,
'upstream_type': 'container',
'upstream_version': 1,
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': None,
},
{
'created': date_format,
'downstream_context_key': str(self.course.id),
'downstream_usage_key': str(self.top_level_downstream_chapter.usage_key),
'id': 5,
'ready_to_sync': False, # <-- It's False because the container doesn't have changes
'ready_to_sync_from_children': True, # <-- It's True because a child has changes
'updated': date_format,
'upstream_context_key': self.library_id,
'upstream_context_title': self.library_title,
@@ -1040,28 +1123,13 @@ class GetUpstreamViewTest(
'version_synced': 1,
'top_level_parent_usage_key': None,
},
{
'created': date_format,
'downstream_context_key': str(self.course.id),
'downstream_usage_key': str(self.top_level_downstream_unit.usage_key),
'id': 6,
'ready_to_sync': False,
'updated': date_format,
'upstream_context_key': self.library_id,
'upstream_context_title': self.library_title,
'upstream_key': self.top_level_unit_id,
'upstream_type': 'container',
'upstream_version': 1,
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': str(self.top_level_downstream_sequential.usage_key),
},
{
'created': date_format,
'downstream_context_key': str(self.course.id),
'downstream_usage_key': str(self.downstream_html_key),
'id': 2,
'ready_to_sync': True,
'ready_to_sync_from_children': False,
'updated': date_format,
'upstream_context_key': self.library_id,
'upstream_context_title': self.library_title,
@@ -1078,6 +1146,7 @@ class GetUpstreamViewTest(
'downstream_usage_key': str(self.downstream_unit_key),
'id': 3,
'ready_to_sync': True,
'ready_to_sync_from_children': False,
'updated': date_format,
'upstream_context_key': self.library_id,
'upstream_context_title': self.library_title,
@@ -1089,6 +1158,8 @@ class GetUpstreamViewTest(
'top_level_parent_usage_key': None,
},
]
print(data["results"])
print(expected)
self.assertListEqual(data["results"], expected)
def test_200_get_ready_to_sync_top_level_parents_with_containers(self):
@@ -1121,6 +1192,7 @@ class GetUpstreamViewTest(
'downstream_usage_key': str(self.downstream_html_key),
'id': 2,
'ready_to_sync': True,
'ready_to_sync_from_children': False,
'updated': date_format,
'upstream_context_key': self.library_id,
'upstream_context_title': self.library_title,
@@ -1137,6 +1209,7 @@ class GetUpstreamViewTest(
'downstream_usage_key': str(self.downstream_unit_key),
'id': 3,
'ready_to_sync': True,
'ready_to_sync_from_children': False,
'updated': date_format,
'upstream_context_key': self.library_id,
'upstream_context_title': self.library_title,
@@ -1151,8 +1224,9 @@ class GetUpstreamViewTest(
'created': date_format,
'downstream_context_key': str(self.course.id),
'downstream_usage_key': str(self.top_level_downstream_chapter.usage_key),
'id': 4,
'ready_to_sync': False,
'id': 5,
'ready_to_sync': False, # <-- It's False because the container doesn't have changes
'ready_to_sync_from_children': True, # <-- It's True because a child has changes
'updated': date_format,
'upstream_context_key': self.library_id,
'upstream_context_title': self.library_title,
@@ -1203,8 +1277,9 @@ class GetUpstreamViewTest(
'created': date_format,
'downstream_context_key': str(self.course.id),
'downstream_usage_key': str(self.top_level_downstream_chapter.usage_key),
'id': 4,
'ready_to_sync': True,
'id': 5,
'ready_to_sync': True, # <-- It's True because the section has changes
'ready_to_sync_from_children': True, # <-- It's True because a child has changes
'updated': date_format,
'upstream_context_key': self.library_id,
'upstream_context_title': self.library_title,
@@ -1221,6 +1296,7 @@ class GetUpstreamViewTest(
'downstream_usage_key': str(self.downstream_html_key),
'id': 2,
'ready_to_sync': True,
'ready_to_sync_from_children': False,
'updated': date_format,
'upstream_context_key': self.library_id,
'upstream_context_title': self.library_title,
@@ -1237,6 +1313,7 @@ class GetUpstreamViewTest(
'downstream_usage_key': str(self.downstream_unit_key),
'id': 3,
'ready_to_sync': True,
'ready_to_sync_from_children': False,
'updated': date_format,
'upstream_context_key': self.library_id,
'upstream_context_title': self.library_title,
@@ -1281,11 +1358,52 @@ class GetDownstreamSummaryViewTest(
response = self.call_api(str(self.course.id))
assert response.status_code == 200
data = response.json()
# The `total_count` is 7 because the top-level logic:
# * The `section-2`, that is the top-level parent of `subsection-2`, `unit-3`, `html-baz-2`
# * The `unit-2`, that is the top-level parent of `video-baz-2`
# * The `section-1`
# * The `subsection-1`
# * The `unit-1`
# * The `html-baz-1`
# * The `video-baz-1`
expected = [{
'upstream_context_title': 'Test Library 1',
'upstream_context_key': self.library_id,
'ready_to_sync_count': 2,
'total_count': 10,
'total_count': 7,
'last_published_at': self.now.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
}]
self.assertListEqual(data, expected)
# Publish Subsection
self._update_container(self.top_level_subsection_id, display_name="Subsection 3")
self._publish_container(self.top_level_subsection_id)
response = self.call_api(str(self.course.id))
assert response.status_code == 200
data = response.json()
expected = [{
'upstream_context_title': 'Test Library 1',
'upstream_context_key': self.library_id,
'ready_to_sync_count': 3, # <-- + the section (top-level parent of subsection)
'total_count': 7,
'last_published_at': self.now.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
}]
self.assertListEqual(data, expected)
# Publish Section
self._update_container(self.top_level_section_id, display_name="Section 3")
self._publish_container(self.top_level_section_id)
response = self.call_api(str(self.course.id))
assert response.status_code == 200
data = response.json()
expected = [{
'upstream_context_title': 'Test Library 1',
'upstream_context_key': self.library_id,
'ready_to_sync_count': 3, # <-- is the same value because the section is the top-level parent
'total_count': 7,
'last_published_at': self.now.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
}]
self.assertListEqual(data, expected)