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

Delete participations of long inactive users #2329

Open
wants to merge 50 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
517af89
Add PARTICIPATION_DELETION_AFTER_INACTIVE_MONTHS to settings.py with …
Redflashx12 Oct 21, 2024
b36e41e
Add bulk delete of inactive users from their evaluations
Redflashx12 Oct 21, 2024
6c9f948
Update to use timedelta and just mentioning months instead of convert…
Redflashx12 Oct 21, 2024
bdfb350
Filter users which do not have any evaluations in which they are inac…
Redflashx12 Oct 21, 2024
582aba5
change unit to days since months is not supported and add small comment
Redflashx12 Oct 21, 2024
cc3e1b2
fix typo
Redflashx12 Oct 21, 2024
b4cf95c
fix correct date subtraction
Redflashx12 Oct 21, 2024
8c1a755
Add default value for test_run
Redflashx12 Oct 21, 2024
4e1fcf5
WIP test for inactivity
Redflashx12 Oct 21, 2024
7dcfa62
Refactor functionality to dedicated function
Redflashx12 Nov 4, 2024
a2db34e
Fix test by archiving the semester and using settings value in test f…
Redflashx12 Nov 4, 2024
0b810cf
Remove unneeded +1 and brackets, add evaluation needed to not be mark…
Redflashx12 Nov 4, 2024
937acad
refactor and split into two tests
Redflashx12 Nov 4, 2024
7852f2e
Fix indentation
Redflashx12 Nov 4, 2024
60301ad
Forgot to remove factor 30 for months since it's already included, ne…
Redflashx12 Nov 4, 2024
a4475e0
black formatting
Redflashx12 Nov 4, 2024
0a33410
isort formatting
Redflashx12 Nov 4, 2024
5f10109
increase test coverage by adding test case for test run
Redflashx12 Nov 4, 2024
6cc1cf4
Fix django import of settings
Redflashx12 Nov 11, 2024
87bbf6d
fix to use pythonic way of checking len
Redflashx12 Nov 11, 2024
b198732
Use timedelta instead of numeric value for days
Redflashx12 Nov 11, 2024
d5fa9c6
Fix logic
Redflashx12 Nov 11, 2024
c9f3a0a
Add inactive users to be removed
Redflashx12 Nov 11, 2024
51b5f99
Change django magic query to look and check latest vote_end_date
Redflashx12 Nov 11, 2024
2f5711c
Refactor test to use django's setUpTestData
Redflashx12 Nov 11, 2024
74e9076
"Fix" type hint to use | None instead of Optional
Redflashx12 Nov 11, 2024
1dc1fb7
reorder imports
Redflashx12 Nov 11, 2024
36a5a00
linting
Redflashx12 Nov 11, 2024
01d60a2
Add more edge cases to tests
Redflashx12 Nov 11, 2024
03622d6
Remove assignment of never used variable
Redflashx12 Nov 11, 2024
c526f84
Merge branch 'main' into main
Redflashx12 Nov 11, 2024
810a957
refactor `remove_inactive_participations`
niklasmohrin Nov 18, 2024
2ab9cff
Rename variable
Redflashx12 Nov 18, 2024
6715f77
Implement feedback
Redflashx12 Nov 18, 2024
351fbaa
Fix logic error and wrong type
Redflashx12 Nov 18, 2024
08983c3
Fix missing rename of variable in @override_settings
Redflashx12 Nov 18, 2024
eebef2b
Extend to test for user.evaluations_participating_in.exists() and rep…
Redflashx12 Nov 18, 2024
cd63d80
Use mock.patch decorator instead of creating new Evaluations with use…
Redflashx12 Nov 18, 2024
3db7709
Remove unused import
Redflashx12 Nov 18, 2024
32a4c79
ruff fix
Redflashx12 Nov 18, 2024
3364e5c
Use proper plural formatting
Redflashx12 Jan 6, 2025
0b863c6
Use proper plural formatting
Redflashx12 Jan 6, 2025
58d239d
Fix missing import of ngettext
Redflashx12 Jan 6, 2025
fc9a393
Update assert in tests to use the new phrasing
Redflashx12 Jan 20, 2025
83754ea
Implement feedback from Richard
Redflashx12 Jan 20, 2025
6a7f0de
Remove last elif check which is unnecessary
Redflashx12 Jan 20, 2025
194e3cd
lint
Redflashx12 Jan 20, 2025
2a4553e
format
Redflashx12 Jan 20, 2025
0efe340
Implement feedback: more explicit test and renamed function
Redflashx12 Jan 27, 2025
876f619
Fix wrong rename
Redflashx12 Jan 27, 2025
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
2 changes: 2 additions & 0 deletions evap/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import logging
import os
import sys
from datetime import timedelta
from fractions import Fraction
from typing import Any

Expand All @@ -36,6 +37,7 @@
VOTER_COUNT_NEEDED_FOR_PUBLISHING_RATING_RESULTS = 2
VOTER_PERCENTAGE_NEEDED_FOR_PUBLISHING_AVERAGE_GRADE = 0.2
SMALL_COURSE_SIZE = 5 # up to which number of participants the evaluation gets additional warnings about anonymity
PARTICIPATION_DELETION_AFTER_INACTIVE_TIME = timedelta(days=18 * 30)

# a warning is shown next to results where less than RESULTS_WARNING_COUNT answers were given
# or the number of answers is less than RESULTS_WARNING_PERCENTAGE times the median number of answers (for this question in this evaluation)
Expand Down
67 changes: 67 additions & 0 deletions evap/staff/tests/test_tools.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from datetime import datetime, timedelta
from io import BytesIO
from itertools import cycle, repeat
from unittest.mock import MagicMock, patch

from django.contrib.auth.models import Group
from django.test import override_settings
from django.utils.html import escape
from model_bakery import baker
from openpyxl import load_workbook
Expand All @@ -19,6 +21,7 @@
from evap.staff.tools import (
conditional_escape,
merge_users,
remove_participations_if_inactive,
remove_user_from_represented_and_ccing_users,
user_edit_link,
)
Expand Down Expand Up @@ -217,6 +220,70 @@ def test_do_nothing_if_test_run(self):
self.assertEqual(len(messages), 4)


class RemoveParticipationDueToInactivityTest(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = baker.make(UserProfile)
cls.evaluation = baker.make(
Evaluation,
state=Evaluation.State.PUBLISHED,
vote_start_datetime=datetime.today() - timedelta(days=200),
vote_end_date=(datetime.today() - timedelta(days=180)).date(),
participants=[cls.user],
)
cls.evaluation.course.semester.archive()

@override_settings(PARTICIPATION_DELETION_AFTER_INACTIVE_TIME=timedelta(6 * 30))
def test_remove_user_due_to_inactivity(self):
self.assertEqual(list(self.user.evaluations_participating_in.all()), [self.evaluation])

messages = remove_participations_if_inactive(self.user)

self.assertFalse(self.user.evaluations_participating_in.exists())
self.assertTrue(self.user.can_be_marked_inactive_by_manager)
self.assertEqual(messages, [f"1 participation of {self.user.full_name} was removed due to inactivity."])

messages = remove_participations_if_inactive(self.user)

self.assertEqual(messages, [])

@patch("evap.evaluation.models.UserProfile.is_active", True)
@patch("evap.evaluation.models.UserProfile.can_be_marked_inactive_by_manager", True)
@override_settings(PARTICIPATION_DELETION_AFTER_INACTIVE_TIME=timedelta(360))
def test_do_not_remove_user_due_to_inactivity_with_recently_archived_evaluation(self):
niklasmohrin marked this conversation as resolved.
Show resolved Hide resolved
self.assertEqual(list(self.user.evaluations_participating_in.all()), [self.evaluation])

messages = remove_participations_if_inactive(self.user)

self.assertEqual(list(self.user.evaluations_participating_in.all()), [self.evaluation])
self.assertEqual(messages, [])

@patch("evap.evaluation.models.UserProfile.is_active", True)
@patch("evap.evaluation.models.UserProfile.can_be_marked_inactive_by_manager", False)
@override_settings(PARTICIPATION_DELETION_AFTER_INACTIVE_TIME=timedelta(360))
def test_do_not_remove_user_due_to_inactivity_with_active_evaluation(self):
niklasmohrin marked this conversation as resolved.
Show resolved Hide resolved
self.assertEqual(list(self.user.evaluations_participating_in.all()), [self.evaluation])

messages = remove_participations_if_inactive(self.user)

self.assertEqual(list(self.user.evaluations_participating_in.all()), [self.evaluation])
self.assertEqual(messages, [])
Comment on lines +269 to +270
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
self.assertEqual(list(self.user.evaluations_participating_in.all()), [self.evaluation])
self.assertEqual(messages, [])
self.assertQuerySetEqual(self.user.evaluations_participating_in.all(), [self.evaluation])
self.assertEqual(messages, [])

and also above?


@override_settings(PARTICIPATION_DELETION_AFTER_INACTIVE_TIME=timedelta(6 * 30))
def test_do_nothing_if_test_run(self):
self.assertTrue(self.user.evaluations_participating_in.exists())

messages = remove_participations_if_inactive(self.user, test_run=True)
pre_count = self.user.evaluations_participating_in.count()

self.assertEqual(list(self.user.evaluations_participating_in.all()), [self.evaluation])
self.assertTrue(self.user.can_be_marked_inactive_by_manager)
self.assertEqual(messages, [f"1 participation of {self.user.full_name} would be removed due to inactivity."])

post_count = self.user.evaluations_participating_in.count()
self.assertEqual(pre_count, post_count)


class UserEditLinkTest(TestCase):
def test_user_edit_link(self):
user = baker.make(UserProfile)
Expand Down
38 changes: 36 additions & 2 deletions evap/staff/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@
from django.contrib.auth.models import Group
from django.core.exceptions import SuspiciousOperation
from django.db import transaction
from django.db.models import Count
from django.db.models import Count, Max
from django.urls import reverse
from django.utils.html import escape, format_html, format_html_join
from django.utils.safestring import SafeString
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ngettext

from evap.evaluation.models import Contribution, Course, Evaluation, TextAnswer, UserProfile
from evap.evaluation.models_logging import LogEntry
from evap.evaluation.tools import clean_email, is_external_email
from evap.evaluation.tools import StrOrPromise, clean_email, is_external_email
from evap.grades.models import GradeDocument
from evap.results.tools import STATES_WITH_RESULTS_CACHING, cache_results

Expand Down Expand Up @@ -195,6 +196,9 @@
user, deletable_users + users_to_mark_inactive, test_run
):
messages.warning(request, message)
for user in users_to_mark_inactive:
for message in remove_participations_if_inactive(user, test_run):
messages.warning(request, message)

Check warning on line 201 in evap/staff/tools.py

View check run for this annotation

Codecov / codecov/patch

evap/staff/tools.py#L201

Added line #L201 was not covered by tests
Copy link
Member

@niklasmohrin niklasmohrin Jan 27, 2025

Choose a reason for hiding this comment

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

This line is not covered by tests, because we only test the implementation of remove_inactive_participations - should we modify TestUserBulkUpdateView to include this? @richardebeling @Kakadus

It would boil down to setting up a situation where a user is marked as inactive and checking that the message appears.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm not sure whether you want me to implement this and if so, where can I find the TestUserBuildUpdateView? I didn't find it when searching through the project files.

Copy link
Member

Choose a reason for hiding this comment

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

Typo, it's supposed to be bulk update (TestUserBulkUpdateView). Since @richardebeling seems to agree, I think we would want a short test in that class so that some of the warnings you added are shown

if test_run:
messages.info(request, _("No data was changed in this test run."))
else:
Expand All @@ -203,6 +207,7 @@
for user in users_to_mark_inactive:
user.is_active = False
user.save()

for user, email in users_to_be_updated:
user.email = email
user.save()
Expand Down Expand Up @@ -375,6 +380,35 @@
return remove_messages


def remove_participations_if_inactive(user: UserProfile, test_run=False) -> list[StrOrPromise]:
if user.is_active and not user.can_be_marked_inactive_by_manager:
return []
last_participation = user.evaluations_participating_in.aggregate(Max("vote_end_date"))["vote_end_date__max"]
if (
last_participation is None
or (date.today() - last_participation) < settings.PARTICIPATION_DELETION_AFTER_INACTIVE_TIME
):
return []

evaluation_count = user.evaluations_participating_in.count()
if test_run:
return [
ngettext(
"{} participation of {} would be removed due to inactivity.",
"{} participations of {} would be removed due to inactivity.",
evaluation_count,
).format(evaluation_count, user.full_name)
]
user.evaluations_participating_in.clear()
return [
ngettext(
"{} participation of {} was removed due to inactivity.",
"{} participations of {} were removed due to inactivity.",
evaluation_count,
).format(evaluation_count, user.full_name)
]


def user_edit_link(user_id):
return format_html(
'<a href="{}" target=_blank><span class="fas fa-user-pen"></span> {}</a>',
Expand Down
Loading