Skip to content
Merged
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
6 changes: 5 additions & 1 deletion kolibri/core/auth/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1052,7 +1052,11 @@ class LearnerGroupViewSet(ValuesViewset):
values = ("id", "name", "parent", "user_ids")

def annotate_queryset(self, queryset):
return annotate_array_aggregate(queryset, user_ids="membership__user__id")
return annotate_array_aggregate(
queryset,
filter=FacilityUser.get_is_active_q("membership"),
user_ids="membership__user__id",
)


class BaseSignUpViewSet(viewsets.GenericViewSet, CreateModelMixin):
Expand Down
23 changes: 23 additions & 0 deletions kolibri/core/auth/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,29 @@ def deserialize(cls, dict_model):

return super(FacilityUser, cls).deserialize(dict_model)

@classmethod
def get_is_active_q(cls, relation_prefix=""):
"""
Returns a Q object that can be used to filter related models by non-deleted users in an abstract way.

Example:
If you want to filter lesson assignments by active users, instead of doing:
LessonAssignment.objects.filter(collection__membership__user__date_deleted__isnull=True)
you can do:
LessonAssignment.objects.filter(FacilityUser.get_is_active_q(relation_prefix="collection__membership"))

For direct relations like `Role`, you can leave the `relation_prefix` blank:
Role.objects.filter(FacilityUser.get_is_active_q())

This is useful because it abstracts away the actual field name, so if we ever change it, we only need to change
it in one place.

"""
q = "user__date_deleted__isnull"
if relation_prefix:
q = f"{relation_prefix}__{q}"
return Q(**{q: True})

def calculate_partition(self):
return "{dataset_id}:user-ro:{user_id}".format(
dataset_id=self.dataset_id, user_id=self.ID_PLACEHOLDER
Expand Down
19 changes: 18 additions & 1 deletion kolibri/core/exams/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from kolibri.core.auth.api import KolibriAuthPermissions
from kolibri.core.auth.api import KolibriAuthPermissionsFilter
from kolibri.core.auth.constants.collection_kinds import ADHOCLEARNERSGROUP
from kolibri.core.auth.models import FacilityUser
from kolibri.core.content.models import ContentNode
from kolibri.core.content.utils.annotation import total_file_size
from kolibri.core.exams import models
Expand Down Expand Up @@ -103,6 +104,14 @@ def annotate_queryset(self, queryset):

def serialize_draft(self, queryset):
objects = queryset.values(*self.draft_values)

all_exam_learners_set = {
learner_id for obj in objects for learner_id in obj.get("learner_ids", [])
}
non_deleted_learners = FacilityUser.objects.filter(
id__in=all_exam_learners_set
).values_list("id", flat=True)

for item in objects:
# Set the draft flag to True
item["draft"] = True
Expand All @@ -111,6 +120,12 @@ def serialize_draft(self, queryset):
item["archive"] = False
item["date_archived"] = None
item["date_activated"] = None
# Filter out any deleted learners
item["learner_ids"] = [
learner_id
for learner_id in item.get("learner_ids", [])
if learner_id in non_deleted_learners
]
return objects

def filter_querysets(self, exam_queryset, draft_queryset):
Expand Down Expand Up @@ -231,7 +246,9 @@ def consolidate(self, items, queryset):
exam_id__in=exam_ids, collection__kind=ADHOCLEARNERSGROUP
)
adhoc_assignments = annotate_array_aggregate(
adhoc_assignments, learner_ids="collection__membership__user_id"
adhoc_assignments,
learner_ids="collection__membership__user_id",
filter=FacilityUser.get_is_active_q("collection__membership"),
)
adhoc_assignments = {
a["exam"]: a
Expand Down
5 changes: 4 additions & 1 deletion kolibri/core/lessons/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from kolibri.core.auth.api import KolibriAuthPermissions
from kolibri.core.auth.api import KolibriAuthPermissionsFilter
from kolibri.core.auth.constants.collection_kinds import ADHOCLEARNERSGROUP
from kolibri.core.auth.models import FacilityUser
from kolibri.core.content.models import ContentNode
from kolibri.core.content.utils.annotation import total_file_size
from kolibri.core.lessons.models import Lesson
Expand Down Expand Up @@ -78,7 +79,9 @@ def consolidate(self, items, queryset):
lesson_id__in=lesson_ids, collection__kind=ADHOCLEARNERSGROUP
)
adhoc_assignments = annotate_array_aggregate(
adhoc_assignments, learner_ids="collection__membership__user_id"
adhoc_assignments,
filter=FacilityUser.get_is_active_q("collection__membership"),
learner_ids="collection__membership__user_id",
)
adhoc_assignments = {
a["lesson"]: a
Expand Down
11 changes: 9 additions & 2 deletions kolibri/core/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,14 @@ def get_source_field(model, field_path):

def annotate_array_aggregate(queryset, **kwargs):
model = queryset.model
query_filter = kwargs.pop("filter", None)
if connection.vendor == "postgresql" and NotNullArrayAgg is not None:
return queryset.annotate(
**{
target: NotNullArrayAgg(
source, result_field=get_source_field(model, source)
source,
result_field=get_source_field(model, source),
filter=query_filter,
)
for target, source in kwargs.items()
}
Expand All @@ -92,7 +95,11 @@ def annotate_array_aggregate(queryset, **kwargs):
# is called by row and not across the entire queryset.
return queryset.values("pk").annotate(
**{
target: GroupConcat(source, result_field=get_source_field(model, source))
target: GroupConcat(
source,
result_field=get_source_field(model, source),
filter=query_filter,
)
for target, source in kwargs.items()
}
)
10 changes: 8 additions & 2 deletions kolibri/plugins/coach/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,10 @@ def retrieve(self, request, pk):
classroom_id = request.GET.get("classroom_id", None)
group_id = request.GET.get("group_id", None)
lesson_id = request.GET.get("lesson_id", None)
queryset = AttemptLog.objects.filter(masterylog__summarylog__content_id=pk)
queryset = AttemptLog.objects.filter(
FacilityUser.get_is_active_q("sessionlog"),
masterylog__summarylog__content_id=pk,
)
if lesson_id is not None:
collection_ids = Lesson.objects.get(
id=lesson_id
Expand Down Expand Up @@ -349,7 +352,10 @@ def retrieve(self, request, pk):
except Exam.DoesNotExist:
raise Http404
quiz_active = quiz["active"]
queryset = AttemptLog.objects.filter(sessionlog__content_id=pk)
queryset = AttemptLog.objects.filter(
FacilityUser.get_is_active_q("sessionlog"),
sessionlog__content_id=pk,
)
if quiz_active:
queryset = queryset.filter(masterylog__complete=True)
if group_id is not None:
Expand Down
6 changes: 5 additions & 1 deletion kolibri/plugins/coach/class_summary_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,11 @@ def serialize_coach_assigned_quiz_status(exam_data):


def serialize_groups(queryset):
queryset = annotate_array_aggregate(queryset, member_ids="membership__user__id")
queryset = annotate_array_aggregate(
queryset,
filter=FacilityUser.get_is_active_q("membership"),
member_ids="membership__user__id",
)
return list(queryset.values("id", "name", "member_ids"))


Expand Down