Skip to content

Community channels page#5770

Open
AlexVelezLl wants to merge 5 commits intolearningequality:unstablefrom
AlexVelezLl:community_channels_page
Open

Community channels page#5770
AlexVelezLl wants to merge 5 commits intolearningequality:unstablefrom
AlexVelezLl:community_channels_page

Conversation

@AlexVelezLl
Copy link
Member

@AlexVelezLl AlexVelezLl commented Mar 20, 2026

Summary

  • Adds new Community Channels List page.
  • Adds a new tab on the channelsList plugin.
  • Adapts response from public models to match the expected formats of existing components.
  • Updates public v2 channel metadata viewset to allow search and filter by languages, and page-based pagination. Also, returned channel version tokens, in case a specific public version has a token.
  • Updates useFilter composable to support multi-value select filters.
  • For now, I have just listed all possible values for countries, categories, and languages, but we can filter these down in a follow-up. Also, we will probably need some sort of "welcome page" or hide this tab until we have community library channels.
Grabacion.de.pantalla.2026-03-20.a.la.s.8.03.42.a.m.mov

Reviewer guidance

  • Make sure you have approved community channels.
  • On the main channels page, look at the new "Community Library" tab.
  • Play with filters and search.
  • Make sure that cards don't break with these new community channels.

AI usage

  • Used AI to set up the initial scaffold of the new router page, but ended up refactoring almost everything since it didn't do a good job following most up-to-date development patterns in our repo.

@marcellamaki
Copy link
Member

ended up refactoring almost everything since it didn't do a good job following most up-to-date development patterns in our repo

💀

@AlexVelezLl AlexVelezLl force-pushed the community_channels_page branch from b1bdaf4 to b22272c Compare March 20, 2026 14:35
@AlexVelezLl AlexVelezLl requested a review from rtibblesbot March 20, 2026 14:35
Copy link
Contributor

@rtibblesbot rtibblesbot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid new feature adding a Community Library browsing page with filters, search, pagination, and channel details. The architecture is clean — good use of provide/inject for filter state, backward-compatible pagination, and proper deduplication for M2M joins.

CI passing (relevant checks skipped due to path matching).

Suggestions:

  • Stray > after </script> in AboutCommunityLibraryModal.vue
  • Unused nResultsFound i18n string (duplicate of resultsText)
  • Debug print left in test
  • Country span renders with dot separator even when countries array is empty
  • Duplicate mapResponseChannel across two files

See inline comments for details.

Comments on lines not in diff:

contentcuration/kolibri_public/views.py:209praise: The existing deduplication via OrderedDict neatly handles potential duplicates from the M2M included_languages filter join. Good that this was already in place.


@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

How was this generated?

Reviewed the pull request diff checking for:

  • Correctness: bugs, edge cases, undocumented behavior, resource leaks, hardcoded values
  • Design: unnecessary complexity, naming, readability, comment accuracy, redundant state
  • Architecture: duplicated concerns, minimal interfaces, composition over inheritance
  • Testing: behavior-based assertions, mocks only at hard boundaries, accurate coverage
  • Completeness: missing dependencies, unupdated usages, i18n, accessibility, security
  • Principles: DRY (same reason to change), SRP, Rule of Three (no premature abstraction)
  • Checked CI status and linked issue acceptance criteria
  • For UI changes: inspected screenshots for layout, visual completeness, and consistency

} = communityChannelsStrings;

</script>
>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: There's a stray > character after the closing </script> tag. Vue SFC parsers will likely ignore it, but it should be removed to keep the file clean.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, good catch

searchLabel: {
message: 'Search',
context: 'Label for the search input',
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: nResultsFound appears to be an unused duplicate of resultsText (defined a few lines above). Only resultsText is referenced in the template. Consider removing nResultsFound to avoid confusion. They also use different ICU plural syntax (=1 vs one).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch, thanks.

response = self._list({"languages": "en,fr", "public": "false"})
self.assertEqual(response.status_code, 200, response.content)
ids = [UUID(item["id"]) for item in response.data]
print("ids", ids)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: Debug print("ids", ids) left in the test. Should be removed before merge.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

<span>
{{ language }}
</span>
<span v-if="channel.countries">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: v-if="channel.countries" is truthy for an empty array []. When the API returns countries: [] (which it does by default in consolidate), this span renders with empty text but still gets the dot separator from the CSS span:not(:last-child)::after rule. Consider v-if="channel.countries && channel.countries.length" or v-if="country" to avoid the visual artifact.

import SidePanelModal from 'shared/views/SidePanelModal';
import { commonStrings } from 'shared/strings/commonStrings';

function mapResponseChannel(channel) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: This mapResponseChannel function is nearly identical to the one in CommunityChannelDetailsModal.vue (which has additional fields like resource_count, resource_size, languages, licenses). Consider extracting a shared utility to avoid divergence over time. The details modal version is a superset — the list version could potentially reuse it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want to mess with it just yet, until we have a clear direction on whether we want to introduce info from the public models on more places.

@@ -0,0 +1,156 @@
import { computed, inject, provide } from 'vue';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: Clean use of provide/inject with Symbol keys to share filter state between the parent list component and the filters component without prop drilling. The composable pattern with both useCommunityChannelsFilters (provider) and injectCommunityChannelsFilters (consumer) is well-structured.



class ChannelMetadataListPagination(CachedListPagination):
page_size = None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: Good choice setting page_size = None — this makes pagination opt-in via query param, preserving backward compatibility for existing callers that expect a flat list response.

* @returns {UseFilterReturn}
*/
export function useFilter({ name, filterMap, defaultValue = null }) {
export function useFilter({ name, filterMap, multi, defaultValue = null }) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: The multi-select extension to useFilter is well-designed — comma-separated URL params, proper unifyMultiParams merging, and clean integration with existing single-select behavior.

Copy link
Contributor

@rtibblesbot rtibblesbot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prior findings

Resolved:

  • Stray > in AboutCommunityLibraryModal.vue (suggestion) — removed in 2720b83
  • Unused nResultsFound string (suggestion) — removed in 2720b83
  • Debug print in test (nitpick) — removed in 2720b83
  • Empty-array truthy check on channel.countries in StudioChannelCard (suggestion) — changed to v-if="country" in 2720b83

Acknowledged (not re-raised):

  • Duplicate mapResponseChannel (suggestion) — author replied: "I don't want to mess with it just yet"

5/5 prior findings resolved or acknowledged. 0 re-raised.


Clean delta — all prior findings addressed. One new suggestion on StudioDetailsPanel. CI passing.


@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

How was this generated?

Compared the current PR state against findings from a prior review:

  • Retrieved prior bot reviews deterministically via the GitHub API
  • Classified each prior finding as RESOLVED, UNADDRESSED, ACKNOWLEDGED, or CONTESTED
  • Only raised NEW findings for newly introduced code
  • Reviewed the pull request diff checking for correctness, design, architecture, testing, completeness, and adherence to DRY/SRP principles
  • Checked CI status and linked issue acceptance criteria

</StudioDetailsRow>

<StudioDetailsRow
v-if="_details.countries"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Same empty-array pattern that was just fixed in StudioChannelCard: _details.countries is [] (truthy) for community channels with no countries, so this row renders and ExpandableList receives items=null from the countries computed. Consider v-if="countries" (the computed property) to stay consistent with the StudioChannelCard fix.

self.assertEqual(response.status_code, 200, response.content)
ids = [UUID(item["id"]) for item in response.data]
self.assertIn(UUID(self.channel_en.id), ids)
self.assertNotIn(UUID(self.channel_multi.id), ids)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: Thorough test coverage — search, case insensitivity, combined filters, and the language M2M filter are all well-covered with clear setup and assertions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants