Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds filtering by post author's native languages to post list (#140) #421

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions langcorrect/posts/helpers.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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.
Expand Down
25 changes: 24 additions & 1 deletion langcorrect/posts/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand All @@ -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"]
Expand Down Expand Up @@ -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)
29 changes: 26 additions & 3 deletions langcorrect/posts/views.py
Copy link
Contributor

@danielzeljko danielzeljko Jan 18, 2024

Choose a reason for hiding this comment

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

Recording.2024-01-18.150627.mp4

We have a couple of issues:

Posts do not filter correctly when lang_code is None

See video.

http://127.0.0.1:8000/journals/?mode=teach&author_native_lang_code=ja&lang_code=None

    def get_lang_code(self):
        return self.request.GET.get("lang_code", None)

What's happening here is that language code will evaluate to the string "None" which is a truthy value. So, we will either need to update the conditionals or explicitly check for this and return None:

    def get_lang_code(self):
       code = self.request.GET.get("lang_code", None)
       if code == "None"
         return None
       return code

I would also look into your get_author_native_lang_code method.

Incorrect post counts when both the native language and language codes are selected

See video. The count is displayed is 12, but the actual count is 14.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The issue comes from how we're setting the query params from the frontend. In get_context_data, we return the result of these getters, which in Python are None. These get passed back as selected_lang_code, which we use to create the url in post_list.html, and end up making the url look like ?mode=teach&author_native_lang_code=all&lang_code=None If we modify the getters to just default to the value "all", then we won't receive "None" in the query params.

    def get_lang_code(self):  
        return self.request.GET.get("lang_code", "all") 

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Need to look into incorrect post counts, should look for discrepancy btwn posts/views.py and posts/helpers.py

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also, right now the native language counts are a subset of the original language code (i.e it should always be less than or equal to the count of the original language code). This make it a little confusing if you try to set the original language code after first setting the native language code. Not sure what the best solution is -- maybe for consistency, the count should always reflect the posts that would be shown as a result of applying that filter? In which case we'd have to update the original one to take into account the native language filter. Do you agree?

Screenshot 2024-01-18 at 3 01 49 PM

Copy link
Contributor Author

Choose a reason for hiding this comment

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

From my observations, the native speaking language filter seems to always be off by one (lower)

Copy link
Contributor

Choose a reason for hiding this comment

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

... If we modify the getters to just default to the value "all", then we won't receive "None" in the query params.

I like your solution 💯

Not sure what the best solution is -- maybe for consistency, the count should always reflect the posts that would be shown as a result of applying that filter? In which case we'd have to update the original one to take into account the native language filter. Do you agree?

Good point -- let's make it consistent. I'm not sure where else that function is currently being used, so we would need to make sure it doesn't break elsewhere.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
danielzeljko marked this conversation as resolved.
Show resolved Hide resolved

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()
Expand All @@ -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)
Expand All @@ -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):
Expand All @@ -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,
}
)
Expand Down
21 changes: 18 additions & 3 deletions langcorrect/static/js/post_list.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
});
23 changes: 18 additions & 5 deletions langcorrect/templates/posts/post_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,26 @@ <h4 class="alert-heading fs-5">{% translate "Recently corrected public entries"
class="link-secondary text-decoration-none {% if mode == 'following' %}link-dark fw-bold{% endif %}">{% translate "Following" %}</a>
</div>
<div class="d-flex gap-3 align-items-center ">
<i class="fa-solid fa-comment text-muted"></i>
<select id="author-native-language-select"
class="form-select"
aria-label="Filter posts by their author's native languages">
<option value="all"
{% if selected_author_native_lang_code == "all" %}selected{% endif %}>All</option>
{% for language, count in author_native_language_filters.items %}
<option value="{{ language.code }}"
{% if selected_author_native_lang_code == language.code %}selected{% endif %}>
{% translate language.code %} ({{ count }})
</option>
{% endfor %}
</select>
<i class="fa-solid fa-globe text-muted"></i>
<select id="language-select"
class="form-select"
aria-label="Default select example">
<option value="{{ language.code }}"
data-link="?mode={{ mode }}&lang_code=all"
{% if selected_lang_code == "all" %}selected{% endif %}>All</option>
aria-label="Filter posts by your native languages">
<option value="all" {% if selected_lang_code == "all" %}selected{% endif %}>All</option>
{% for language, count in language_filters.items %}
<option value="{{ language.code }}"
data-link="?mode={{ mode }}&lang_code={{ language.code }}"
{% if selected_lang_code == language.code %}selected{% endif %}>
{% translate language.code %} ({{ count }})
</option>
Expand Down Expand Up @@ -92,5 +102,8 @@ <h4 class="alert-heading fs-5">{% translate "Recently corrected public entries"
</div>
{% endblock content %}
{% block inline_javascript %}
<script>
var mode = "{{ mode }}"
</script>
<script src="{% static 'js/post_list.js' %}"></script>
{% endblock inline_javascript %}