diff --git a/api/tests/v1/share/test_api_sharing.py b/api/tests/v1/share/test_api_sharing.py index 1da0ce1b..8aff6819 100644 --- a/api/tests/v1/share/test_api_sharing.py +++ b/api/tests/v1/share/test_api_sharing.py @@ -4,7 +4,6 @@ from django.urls import reverse from konova.settings import DEFAULT_GROUP from konova.tests.test_views import BaseTestCase -from konova.utils.user_checks import is_default_group_only class BaseAPIV1TestCase(BaseTestCase): @@ -138,7 +137,7 @@ class APIV1SharingTestCase(BaseAPIV1TestCase): # Give the user only default group rights default_group = self.groups.get(name=DEFAULT_GROUP) self.superuser.groups.set([default_group]) - self.assertTrue(is_default_group_only(self.superuser)) + self.assertTrue(self.superuser.is_default_group_only()) # Add only him as shared_users an object self.intervention.users.set([self.superuser]) diff --git a/api/views/views.py b/api/views/views.py index f3a86bc6..72ee2779 100644 --- a/api/views/views.py +++ b/api/views/views.py @@ -18,7 +18,6 @@ from compensation.models import EcoAccount from ema.models import Ema from intervention.models import Intervention from konova.utils.message_templates import DATA_UNSHARED -from konova.utils.user_checks import is_default_group_only from user.models import User, Team @@ -321,7 +320,7 @@ class AbstractModelShareAPIView(AbstractAPIView): for team_name in new_teams: new_teams_objs.append(Team.objects.get(name=team_name)) - if is_default_group_only(self.user): + if self.user.is_default_group_only(): # Default only users are not allowed to remove other users from having access. They can only add new ones! new_users_to_be_added = User.objects.filter( username__in=new_users diff --git a/compensation/models/payment.py b/compensation/models/payment.py index 6f3f4c24..ec56910d 100644 --- a/compensation/models/payment.py +++ b/compensation/models/payment.py @@ -10,8 +10,6 @@ from django.db import models from intervention.models import Intervention from konova.models import BaseResource -from konova.utils.message_templates import PAYMENT_REMOVED -from user.models import UserActionLogEntry class Payment(BaseResource): diff --git a/compensation/views/compensation/compensation.py b/compensation/views/compensation/compensation.py index 1b4691c7..1a091b00 100644 --- a/compensation/views/compensation/compensation.py +++ b/compensation/views/compensation/compensation.py @@ -27,7 +27,6 @@ from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE, \ RECORDED_BLOCKS_EDIT, CHECK_STATE_RESET, FORM_INVALID, PARAMS_INVALID, IDENTIFIER_REPLACED, \ COMPENSATION_ADDED_TEMPLATE, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED -from konova.utils.user_checks import in_group @login_required @@ -265,9 +264,9 @@ def detail_view(request: HttpRequest, id: str): "sum_before_states": sum_before_states, "sum_after_states": sum_after_states, "diff_states": diff_states, - "is_default_member": in_group(_user, DEFAULT_GROUP), - "is_zb_member": in_group(_user, ZB_GROUP), - "is_ets_member": in_group(_user, ETS_GROUP), + "is_default_member": _user.in_group(DEFAULT_GROUP), + "is_zb_member": _user.in_group(ZB_GROUP), + "is_ets_member": _user.in_group(ETS_GROUP), "LANIS_LINK": comp.get_LANIS_link(), TAB_TITLE_IDENTIFIER: f"{comp.identifier} - {comp.title}", "has_finished_deadlines": comp.get_finished_deadlines().exists(), diff --git a/compensation/views/eco_account/eco_account.py b/compensation/views/eco_account/eco_account.py index 840a39c2..6798b647 100644 --- a/compensation/views/eco_account/eco_account.py +++ b/compensation/views/eco_account/eco_account.py @@ -23,7 +23,6 @@ from konova.settings import ETS_GROUP, DEFAULT_GROUP, ZB_GROUP from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.message_templates import CANCEL_ACC_RECORDED_OR_DEDUCTED, RECORDED_BLOCKS_EDIT, FORM_INVALID, \ IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED -from konova.utils.user_checks import in_group @login_required @@ -244,9 +243,9 @@ def detail_view(request: HttpRequest, id: str): "diff_states": diff_states, "available": available_relative, "available_total": available_total, - "is_default_member": in_group(_user, DEFAULT_GROUP), - "is_zb_member": in_group(_user, ZB_GROUP), - "is_ets_member": in_group(_user, ETS_GROUP), + "is_default_member": _user.in_group(DEFAULT_GROUP), + "is_zb_member": _user.in_group(ZB_GROUP), + "is_ets_member": _user.in_group(ETS_GROUP), "LANIS_LINK": acc.get_LANIS_link(), "deductions": deductions, "actions": actions, @@ -277,7 +276,7 @@ def remove_view(request: HttpRequest, id: str): # default group user if acc.recorded is not None or acc.deductions.exists(): user = request.user - if not in_group(user, ETS_GROUP): + if not user.in_group(ETS_GROUP): messages.info(request, CANCEL_ACC_RECORDED_OR_DEDUCTED) return redirect("compensation:acc:detail", id=id) diff --git a/ema/views/ema.py b/ema/views/ema.py index 0296b149..2a969fc8 100644 --- a/ema/views/ema.py +++ b/ema/views/ema.py @@ -24,7 +24,6 @@ from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, IDENTIFIER_REPLACED, FORM_INVALID, \ DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED -from konova.utils.user_checks import in_group @login_required @@ -172,9 +171,9 @@ def detail_view(request: HttpRequest, id: str): "sum_before_states": sum_before_states, "sum_after_states": sum_after_states, "diff_states": diff_states, - "is_default_member": in_group(_user, DEFAULT_GROUP), - "is_zb_member": in_group(_user, ZB_GROUP), - "is_ets_member": in_group(_user, ETS_GROUP), + "is_default_member": _user.in_group(DEFAULT_GROUP), + "is_zb_member": _user.in_group(ZB_GROUP), + "is_ets_member": _user.in_group(ETS_GROUP), "LANIS_LINK": ema.get_LANIS_link(), TAB_TITLE_IDENTIFIER: f"{ema.identifier} - {ema.title}", "has_finished_deadlines": ema.get_finished_deadlines().exists(), diff --git a/intervention/forms/modals/share.py b/intervention/forms/modals/share.py index 35c662d5..e24a13cf 100644 --- a/intervention/forms/modals/share.py +++ b/intervention/forms/modals/share.py @@ -12,7 +12,6 @@ from django.utils.translation import gettext_lazy as _ from intervention.inputs import TextToClipboardInput from konova.forms.modals import BaseModalForm from konova.utils.message_templates import ENTRY_REMOVE_MISSING_PERMISSION -from konova.utils.user_checks import is_default_group_only from user.models import Team, User @@ -80,7 +79,7 @@ class ShareModalForm(BaseModalForm): teams = self.cleaned_data.get("teams", Team.objects.none()) _is_valid = True - if is_default_group_only(self.user): + if self.user.is_default_group_only(): shared_users = self.instance.shared_users shared_teams = self.instance.shared_teams diff --git a/intervention/views/intervention.py b/intervention/views/intervention.py index 681fc3b6..e04f6281 100644 --- a/intervention/views/intervention.py +++ b/intervention/views/intervention.py @@ -23,7 +23,6 @@ from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.message_templates import DATA_CHECKED_PREVIOUSLY_TEMPLATE, RECORDED_BLOCKS_EDIT, \ CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED -from konova.utils.user_checks import in_group @login_required @@ -186,9 +185,9 @@ def detail_view(request: HttpRequest, id: str): "compensations": compensations, "has_access": is_data_shared, "geom_form": geom_form, - "is_default_member": in_group(_user, DEFAULT_GROUP), - "is_zb_member": in_group(_user, ZB_GROUP), - "is_ets_member": in_group(_user, ETS_GROUP), + "is_default_member": _user.in_group(DEFAULT_GROUP), + "is_zb_member": _user.in_group(ZB_GROUP), + "is_ets_member": _user.in_group(ETS_GROUP), "LANIS_LINK": intervention.get_LANIS_link(), "has_payment_without_document": has_payment_without_document, TAB_TITLE_IDENTIFIER: f"{intervention.identifier} - {intervention.title}", diff --git a/konova/models/object.py b/konova/models/object.py index 51817cb5..038b47f9 100644 --- a/konova/models/object.py +++ b/konova/models/object.py @@ -640,12 +640,11 @@ class ShareableObjectMixin(models.Model): Returns: """ - from konova.utils.user_checks import is_default_group_only users = self.shared_users cleaned_users = [] default_users = [] for user in users: - if not is_default_group_only(user): + if not user.is_default_group_only(): cleaned_users.append(user) else: default_users.append(user) diff --git a/konova/tests/unit/test_models.py b/konova/tests/unit/test_models.py index 8f0e881b..2ec7f0f3 100644 --- a/konova/tests/unit/test_models.py +++ b/konova/tests/unit/test_models.py @@ -6,12 +6,12 @@ Created on: 08.09.23 """ from django.test import RequestFactory +from django.utils.timezone import now from intervention.forms.modals.share import ShareModalForm -from konova.models import DeadlineType +from konova.models import DeadlineType, Resubmission from konova.settings import ZB_GROUP from konova.tests.test_views import BaseTestCase -from konova.utils.user_checks import is_default_group_only from user.models import UserAction @@ -171,8 +171,8 @@ class ShareableObjectMixinTestCase(BaseTestCase): self.intervention.share_with_user(self.user) self.intervention.share_with_user(self.superuser) - self.assertTrue(is_default_group_only(self.user)) - self.assertFalse(is_default_group_only(self.superuser)) + self.assertTrue(self.user.is_default_group_only()) + self.assertFalse(self.superuser.is_default_group_only()) self.assertTrue(self.intervention.is_shared_with(self.user)) self.assertTrue(self.intervention.is_shared_with(self.superuser)) @@ -180,3 +180,22 @@ class ShareableObjectMixinTestCase(BaseTestCase): self.intervention.unshare_with_default_users() self.assertFalse(self.intervention.is_shared_with(self.user)) self.assertTrue(self.intervention.is_shared_with(self.superuser)) + + +class ResubmissionTestCase(BaseTestCase): + def test_send_resubmission_mail(self): + resubmission = Resubmission.objects.create( + user=self.user, + resubmit_on=now().date(), + comment="Test", + ) + self.intervention.resubmissions.add(resubmission) + + self.assertFalse(resubmission.resubmission_sent) + resubmission.send_resubmission_mail( + self.intervention.identifier, + [ + "Test_municipal_1" + ], + ) + self.assertTrue(resubmission.resubmission_sent) diff --git a/konova/utils/user_checks.py b/konova/utils/user_checks.py deleted file mode 100644 index b4f65271..00000000 --- a/konova/utils/user_checks.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -Author: Michel Peltriaux -Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany -Contact: michel.peltriaux@sgdnord.rlp.de -Created on: 02.07.21 - -""" -from user.models import User - -from konova.settings import ETS_GROUP, ZB_GROUP - - -def in_group(user: User, group: str) -> bool: - """ Checks if the user is part of a group - - Args: - user (User): The user object - group (str): The group's name - - Returns: - bool - """ - return user.groups.filter( - name=group - ) - - -def is_default_group_only(user: User) -> bool: - """ Checks if the user is only part of the default group - - Args: - user (User): The user object - - Returns: - bool - """ - return not in_group(user, ZB_GROUP) and not in_group(user, ETS_GROUP) \ No newline at end of file diff --git a/user/models/user.py b/user/models/user.py index ad7a3eca..c01dbc48 100644 --- a/user/models/user.py +++ b/user/models/user.py @@ -60,6 +60,29 @@ class User(AbstractUser): name=ETS_GROUP ).exists() + def is_default_group_only(self) -> bool: + """ Checks if the user is only part of the default group + + Args: + + Returns: + bool + """ + return not self.in_group(ZB_GROUP) and not self.in_group(ETS_GROUP) + + def in_group(self, group: str) -> bool: + """ Checks if the user is part of a group + + Args: + group (str): The group's name + + Returns: + bool + """ + return self.groups.filter( + name=group + ) + def send_mail_shared_access_removed(self, obj_identifier, obj_title, municipals_names): """ Sends a mail to the user in case of removed shared access diff --git a/user/tests/unit/test_forms.py b/user/tests/unit/test_forms.py index 9aac1d13..48ecd5d9 100644 --- a/user/tests/unit/test_forms.py +++ b/user/tests/unit/test_forms.py @@ -5,13 +5,16 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 12.09.23 """ +from django.core.exceptions import ObjectDoesNotExist from django.test import RequestFactory from django.urls import reverse from django.utils.translation import gettext_lazy as _ +from api.models import APIUserToken from konova.tests.test_views import BaseTestCase -from user.forms.modals.team import NewTeamModalForm, EditTeamModalForm -from user.models import Team +from user.forms.modals.team import NewTeamModalForm, EditTeamModalForm, RemoveTeamModalForm, LeaveTeamModalForm +from user.forms.user import UserNotificationForm, UserAPITokenForm +from user.models import Team, UserAction, UserNotification class NewTeamModalFormTestCase(BaseTestCase): @@ -147,3 +150,137 @@ class EditTeamModalFormTestCase(NewTeamModalFormTestCase): self.assertEqual(obj.admins.count(), 1) self.assertEqual(obj.users.count(), 2) + +class RemoveTeamModalFormTestCase(BaseTestCase): + def setUp(self) -> None: + super().setUp() + self.request = RequestFactory().request() + self.request.user = self.user + + def test_init(self): + form = RemoveTeamModalForm( + request=self.request, + instance=self.team + ) + self.assertEqual(form.form_caption, str(_("ATTENTION!\n\nRemoving the team means all members will lose their access to data, based on this team! \n\nAre you sure to remove this team?"))) + self.assertEqual(form.user, self.request.user) + self.assertEqual(form.request, self.request) + + def test_save(self): + data = { + "confirm": True + } + form = RemoveTeamModalForm( + data, + request=self.request, + instance=self.team + ) + self.assertTrue(form.is_valid(), msg=form.errors) + form.save() + self.team.refresh_from_db() + + self.assertIsNotNone(self.team.deleted) + self.assertEqual(self.team.deleted.user, self.request.user) + self.assertEqual(self.team.deleted.action, UserAction.DELETED) + + +class LeaveTeamModalFormTestCase(BaseTestCase): + def setUp(self) -> None: + super().setUp() + self.request = RequestFactory().request() + self.request.user = self.user + + def test_init(self): + form = LeaveTeamModalForm( + request=self.request, + instance=self.team + ) + self.assertEqual(form.form_title, str(_("Leave team"))) + + def test_save(self): + self.team.users.add(self.user) + data = { + "confirm": True, + } + form = LeaveTeamModalForm( + data, + request=self.request, + instance=self.team + ) + self.assertTrue(form.is_valid(), msg=form.errors) + self.assertIn(self.request.user, self.team.users.all()) + form.save() + self.assertNotIn(self.request.user, self.team.users.all()) + + +class UserNotificationFormTestCase(BaseTestCase): + def setUp(self) -> None: + super().setUp() + + if not UserNotification.objects.all().exists(): + self.notifications = UserNotification.objects.bulk_create( + [ + UserNotification(id="notification_1", name="notification_1", is_active=True), + UserNotification(id="notification_2", name="notification_2", is_active=True), + UserNotification(id="notification_3", name="notification_3", is_active=True), + UserNotification(id="notification_4", name="notification_4", is_active=True), + ] + ) + + def test_init(self): + form = UserNotificationForm( + user=self.user + ) + self.assertEqual(form.form_title, str(_("Edit notifications"))) + self.assertEqual(form.form_caption, "") + self.assertEqual(form.action_url, reverse("user:notifications")) + self.assertEqual(form.cancel_redirect, reverse("user:index")) + + def test_save(self): + selected_notification = UserNotification.objects.first() + data = { + "notifications": [selected_notification.id,] + } + form = UserNotificationForm( + data=data, + user=self.user + ) + self.assertTrue(form.is_valid(), msg=form.errors) + self.assertEqual(self.user.notifications.count(), 0) + form.save() + self.assertEqual(self.user.notifications.count(), 1) + self.assertIn(selected_notification, self.user.notifications.all()) + + +class UserAPITokenFormTestCase(BaseTestCase): + def test_init(self): + form = UserAPITokenForm( + instance=self.user + ) + self.assertEqual(form.form_title, str(_("Create new token"))) + self.assertEqual(form.form_caption, str(_("A new token needs to be validated by an administrator!"))) + self.assertEqual(form.action_url, reverse("user:api-token")) + self.assertEqual(form.cancel_redirect, reverse("user:index")) + + self.assertIsNone(form.fields["token"].initial) + self.assertTrue(form.fields["token"].widget.attrs["readonly"]) + + def test_save(self): + data = { + "token": APIUserToken().token + } + form = UserAPITokenForm( + data, + instance=self.user + ) + self.assertTrue(form.is_valid(), msg=form.errors) + self.assertIsNone(self.user.api_token) + token = form.save() + self.assertEqual(self.user.api_token, token) + new_token = form.save() + self.assertEqual(self.user.api_token, new_token) + try: + token.refresh_from_db() + self.fail("Token should be deleted and not be fetchable anymore") + except ObjectDoesNotExist: + pass diff --git a/user/tests/unit/test_models.py b/user/tests/unit/test_models.py new file mode 100644 index 00000000..4c9338a5 --- /dev/null +++ b/user/tests/unit/test_models.py @@ -0,0 +1,61 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 13.09.23 + +""" +from konova.settings import ZB_GROUP, DEFAULT_GROUP, ETS_GROUP +from konova.tests.test_views import BaseTestCase +from user.enums import UserNotificationEnum +from user.models import UserNotification + + +class UserTestCase(BaseTestCase): + def test_is_notification_setting_set(self): + notification = UserNotification.objects.create( + id=UserNotificationEnum.NOTIFY_ON_DEDUCTION_CHANGES.name, + name=UserNotificationEnum.NOTIFY_ON_DEDUCTION_CHANGES.value, + ) + self.assertFalse(self.user.is_notification_setting_set(UserNotificationEnum.NOTIFY_ON_DEDUCTION_CHANGES)) + self.user.notifications.add(notification) + self.assertTrue(self.user.is_notification_setting_set(UserNotificationEnum.NOTIFY_ON_DEDUCTION_CHANGES)) + + def test_is_group_member(self): + zb_group = self.groups.get(name=ZB_GROUP) + ets_group = self.groups.get(name=ETS_GROUP) + default_group = self.groups.get(name=DEFAULT_GROUP) + + self.user.groups.set([]) + self.assertFalse(self.user.is_zb_user()) + self.assertFalse(self.user.is_ets_user()) + self.assertFalse(self.user.is_default_user()) + + self.user.groups.add(zb_group) + self.assertTrue(self.user.is_zb_user()) + + self.user.groups.add(ets_group) + self.assertTrue(self.user.is_ets_user()) + + self.user.groups.add(default_group) + self.assertTrue(self.user.is_default_user()) + + def test_get_API_token(self): + self.assertIsNone(self.user.api_token) + token = self.user.get_API_token() + self.assertIsNotNone(self.user.api_token) + self.assertEqual(self.user.api_token, token) + + # Make sure the same token is returned if command is called twice + token = self.user.get_API_token() + self.assertEqual(self.user.api_token, token) + + def test_shared_teams_property(self): + shared_teams = self.user.shared_teams + self.assertEqual(shared_teams.count(), 0) + + self.team.users.add(self.user) + shared_teams = self.user.shared_teams + self.assertEqual(shared_teams.count(), 1) + self.assertIn(self.team, shared_teams) +