diff --git a/langcorrect/posts/helpers.py b/langcorrect/posts/helpers.py index f8a77b98..bc9338ea 100644 --- a/langcorrect/posts/helpers.py +++ b/langcorrect/posts/helpers.py @@ -1,5 +1,8 @@ from django.conf import settings +from django.db.models import Count, Q +from django.db.models.query import QuerySet +from langcorrect.languages.models import Language, LevelChoices from langcorrect.posts.models import Post @@ -13,6 +16,43 @@ def get_post_counts_by_language(languages, corrected=False): return data +def get_post_counts_by_author_native_language( + native_languages: QuerySet[Language], selected_lang_code: str | None = None, corrected: bool = False +) -> dict[Language, int]: + """ + Returns a dictionary containing the count of posts based on the post author's native language. + + :param native_languages: A QuerySet of Language objects representing authors' native languages + :param selected_lang_code: Optional. If provided, filters posts based on the language code. + :param corrected: Optional. If True, considers only corrected posts; otherwise, includes all posts. + :return: A dictionary where keys are Language objects and values are the corresponding post counts. + """ + data = {} + + if selected_lang_code is not None and selected_lang_code != "all": + selected_lang_code_filter = Q(language__code=selected_lang_code) + else: + selected_lang_code_filter = Q() + + qs = ( + Post.available_objects.filter( + selected_lang_code_filter, + is_corrected=corrected, + user__languagelevel__level=LevelChoices.NATIVE, + user__languagelevel__language__in=native_languages, + ) + .values("user__languagelevel__language") + .annotate(count=Count("id")) + ) + + # Build Language to count dictionary from language_id to count + language_id_to_count = {item["user__languagelevel__language"]: item["count"] for item in qs} + for language in native_languages: + data[language] = language_id_to_count.get(language.id, 0) + + return data + + def check_can_create_post(user): """ Checks if the user can create a new post based on their correction ratio. diff --git a/langcorrect/posts/tests/test_views.py b/langcorrect/posts/tests/test_views.py index dcf2235e..ef826c36 100644 --- a/langcorrect/posts/tests/test_views.py +++ b/langcorrect/posts/tests/test_views.py @@ -5,7 +5,7 @@ from django.urls import reverse from langcorrect.contributions.models import Contribution -from langcorrect.languages.models import Language +from langcorrect.languages.models import Language, LevelChoices from langcorrect.posts.models import Post, PostRow, PostVisibility from langcorrect.posts.tests.factories import LANGUAGE_TO_FAKER_LOCALE, PostFactory from langcorrect.posts.tests.utils import generate_text, generate_title @@ -218,6 +218,7 @@ def generate_posts_by_code(cls, lang_code, is_corrected, permission, amount): def setUpTestData(cls): cls.en_ja_user = UserFactory(native_languages=["en"], studying_languages=["ja"]) cls.enko_ja_user = UserFactory(native_languages=["en", "ko"], studying_languages=["ja"]) + cls.ja_ko_user = UserFactory(native_languages=["ja"], studying_languages=["en"]) UserFactory.create_batch(5) @@ -228,6 +229,8 @@ def setUpTestData(cls): cls.generate_posts_by_code("ko", False, PostVisibility.MEMBER, 2) cls.generate_posts_by_code("ko", True, PostVisibility.PUBLIC, 3) + PostFactory(user=cls.ja_ko_user) + def test_anonymous_queryset(self): response = self.client.get(reverse("posts:list")) posts = response.context["object_list"] @@ -282,3 +285,23 @@ def test_filter_queryset_by_language_code(self): posts = response.context["object_list"] expected_posts = Post.available_objects.filter(language__code=korean_code).order_by("is_corrected", "-created") self.assertQuerySetEqual(posts, expected_posts) + + def test_filter_queryset_by_author_native_language_code(self): + """ + English/Korean native speaker is learning Japanese, + so when teaching, wants to find English/Korean posts + written by Japanese users. + """ + self.client.force_login(self.enko_ja_user) + korean_code = "ko" + japanese_code = "ja" + params = {"lang_code": korean_code, "author_native_lang_code": japanese_code} + url = f"{reverse('posts:list')}?{urlencode(params)}" + response = self.client.get(url) + posts = response.context["object_list"] + expected_posts = Post.available_objects.filter( + language__code=korean_code, + user__languagelevel__level=LevelChoices.NATIVE, + user__languagelevel__language__code=japanese_code, + ).order_by("is_corrected", "-created") + self.assertQuerySetEqual(posts, expected_posts) diff --git a/langcorrect/posts/views.py b/langcorrect/posts/views.py index efca015c..2d5def80 100644 --- a/langcorrect/posts/views.py +++ b/langcorrect/posts/views.py @@ -12,9 +12,13 @@ from langcorrect.contributions.helpers import update_user_writing_streak from langcorrect.corrections.helpers import get_popular_correctors, populate_user_corrections from langcorrect.corrections.models import CorrectedRow, OverallFeedback, PerfectRow -from langcorrect.languages.models import LanguageLevel +from langcorrect.languages.models import LanguageLevel, LevelChoices from langcorrect.posts.forms import CustomPostForm -from langcorrect.posts.helpers import check_can_create_post, get_post_counts_by_language +from langcorrect.posts.helpers import ( + check_can_create_post, + get_post_counts_by_author_native_language, + get_post_counts_by_language, +) from langcorrect.posts.models import Post, PostImage, PostReply, PostVisibility from langcorrect.prompts.models import Prompt from langcorrect.users.models import User @@ -38,7 +42,10 @@ def get_mode(self): return self.request.GET.get("mode", "teach") def get_lang_code(self): - return self.request.GET.get("lang_code", None) + return self.request.GET.get("lang_code", "all") + + def get_author_native_lang_code(self): + return self.request.GET.get("author_native_lang_code", "all") def get_queryset(self): qs = super().get_queryset() @@ -50,6 +57,7 @@ def get_queryset(self): mode = self.get_mode() lang_code = self.get_lang_code() + author_native_lang_code = self.get_author_native_lang_code() if mode == "following": qs = qs.filter(user__in=current_user.get_following_users_ids) @@ -61,6 +69,12 @@ def get_queryset(self): if lang_code and lang_code != "all": qs = qs.filter(language__code=lang_code).order_by("is_corrected", "-created") + if author_native_lang_code and author_native_lang_code != "all": + qs = qs.filter( + user__languagelevel__language__code=author_native_lang_code, + user__languagelevel__level=LevelChoices.NATIVE, + ).order_by("is_corrected", "-created") + return qs def get_context_data(self, **kwargs): @@ -69,18 +83,27 @@ def get_context_data(self, **kwargs): mode = self.get_mode() selected_lang_code = self.get_lang_code() + selected_author_native_lang_code = self.get_author_native_lang_code() language_filter_choices = None + author_native_language_filter_choices = None if mode == "learn": language_filter_choices = get_post_counts_by_language(current_user.studying_languages, corrected=True) elif mode == "teach" and current_user.is_authenticated: language_filter_choices = get_post_counts_by_language(current_user.native_languages) + if current_user.is_authenticated: + author_native_language_filter_choices = get_post_counts_by_author_native_language( + current_user.studying_languages, selected_lang_code + ) + context.update( { "mode": mode, "language_filters": language_filter_choices, + "author_native_language_filters": author_native_language_filter_choices, + "selected_author_native_lang_code": selected_author_native_lang_code, "selected_lang_code": selected_lang_code, } ) diff --git a/langcorrect/static/js/post_list.js b/langcorrect/static/js/post_list.js index 96aaa2b4..50dfe7e4 100644 --- a/langcorrect/static/js/post_list.js +++ b/langcorrect/static/js/post_list.js @@ -3,8 +3,23 @@ window.addEventListener('DOMContentLoaded', () => { const languageSelect = document.getElementById('language-select'); languageSelect.addEventListener('change', function (evt) { - const selectedOption = evt.target.options[evt.target.selectedIndex]; - const link = selectedOption.dataset.link; - window.location = link; + updateUrl(evt.target, 'lang_code'); }); + + const authorNativeLanguageSelect = document.getElementById( + 'author-native-language-select', + ); + authorNativeLanguageSelect.addEventListener('change', function (evt) { + updateUrl(evt.target, 'author_native_lang_code'); + }); + + function updateUrl(selectElement, queryParamName) { + const selectedOption = + selectElement.options[selectElement.options.selectedIndex]; + const url = new URL(window.location.href); + url.searchParams.delete('page'); + url.searchParams.set('mode', mode); + url.searchParams.set(queryParamName, selectedOption.value); + window.location = url.toString(); + } }); diff --git a/langcorrect/templates/posts/post_list.html b/langcorrect/templates/posts/post_list.html index 67eedf9d..7f32af07 100644 --- a/langcorrect/templates/posts/post_list.html +++ b/langcorrect/templates/posts/post_list.html @@ -37,16 +37,26 @@

{% translate "Recently corrected public entries" class="link-secondary text-decoration-none {% if mode == 'following' %}link-dark fw-bold{% endif %}">{% translate "Following" %}
+ +