diff --git a/compensation/account_urls.py b/compensation/account_urls.py index 5d9d635f..97e5938e 100644 --- a/compensation/account_urls.py +++ b/compensation/account_urls.py @@ -20,6 +20,8 @@ urlpatterns = [ path('/remove', remove_view, name='acc-remove'), path('/state/new', state_new_view, name='acc-new-state'), path('/action/new', action_new_view, name='acc-new-action'), + path('/state//remove', state_remove_view, name='acc-state-remove'), + path('/action//remove', action_remove_view, name='acc-action-remove'), path('/deadline/new', deadline_new_view, name="acc-new-deadline"), path('/share/', share_view, name='share'), path('/share', create_share_view, name='share-create'), diff --git a/compensation/comp_urls.py b/compensation/comp_urls.py index a63a6903..8eb7c48d 100644 --- a/compensation/comp_urls.py +++ b/compensation/comp_urls.py @@ -20,6 +20,8 @@ urlpatterns = [ path('/remove', remove_view, name='remove'), path('/state/new', state_new_view, name='new-state'), path('/action/new', action_new_view, name='new-action'), + path('/state//remove', state_remove_view, name='state-remove'), + path('/action//remove', action_remove_view, name='action-remove'), path('/deadline/new', deadline_new_view, name="new-deadline"), path('/report', report_view, name='report'), @@ -28,10 +30,4 @@ urlpatterns = [ path('document/', get_document_view, name='get-doc'), path('document//remove/', remove_document_view, name='remove-doc'), - # Generic state routes - path('state//remove', state_remove_view, name='state-remove'), - - # Generic action routes - path('action//remove', action_remove_view, name='action-remove'), - ] \ No newline at end of file diff --git a/compensation/templates/compensation/detail/compensation/includes/actions.html b/compensation/templates/compensation/detail/compensation/includes/actions.html index 08930a9c..92cf45ff 100644 --- a/compensation/templates/compensation/detail/compensation/includes/actions.html +++ b/compensation/templates/compensation/detail/compensation/includes/actions.html @@ -50,7 +50,7 @@ {{ action.comment|default_if_none:"" }} {% if is_default_member and has_access %} - {% endif %} diff --git a/compensation/templates/compensation/detail/compensation/includes/states-after.html b/compensation/templates/compensation/detail/compensation/includes/states-after.html index 6b372974..f8dd00a4 100644 --- a/compensation/templates/compensation/detail/compensation/includes/states-after.html +++ b/compensation/templates/compensation/detail/compensation/includes/states-after.html @@ -51,7 +51,7 @@ {{ state.surface|floatformat:2 }} m² {% if is_default_member and has_access %} - {% endif %} diff --git a/compensation/templates/compensation/detail/compensation/includes/states-before.html b/compensation/templates/compensation/detail/compensation/includes/states-before.html index a0c541ae..39b5e038 100644 --- a/compensation/templates/compensation/detail/compensation/includes/states-before.html +++ b/compensation/templates/compensation/detail/compensation/includes/states-before.html @@ -51,7 +51,7 @@ {{ state.surface|floatformat:2 }} m² {% if is_default_member and has_access %} - {% endif %} diff --git a/compensation/templates/compensation/detail/eco_account/includes/actions.html b/compensation/templates/compensation/detail/eco_account/includes/actions.html index 868242a5..4e81e22b 100644 --- a/compensation/templates/compensation/detail/eco_account/includes/actions.html +++ b/compensation/templates/compensation/detail/eco_account/includes/actions.html @@ -50,7 +50,7 @@ {{ action.comment|default_if_none:"" }} {% if is_default_member and has_access %} - {% endif %} diff --git a/compensation/templates/compensation/detail/eco_account/includes/states-after.html b/compensation/templates/compensation/detail/eco_account/includes/states-after.html index bd71e25a..eab29ad6 100644 --- a/compensation/templates/compensation/detail/eco_account/includes/states-after.html +++ b/compensation/templates/compensation/detail/eco_account/includes/states-after.html @@ -51,7 +51,7 @@ {{ state.surface|floatformat:2 }} m² {% if is_default_member and has_access %} - {% endif %} diff --git a/compensation/templates/compensation/detail/eco_account/includes/states-before.html b/compensation/templates/compensation/detail/eco_account/includes/states-before.html index 8acb4865..ea3df844 100644 --- a/compensation/templates/compensation/detail/eco_account/includes/states-before.html +++ b/compensation/templates/compensation/detail/eco_account/includes/states-before.html @@ -51,7 +51,7 @@ {{ state.surface|floatformat:2 }} m² {% if is_default_member and has_access %} - {% endif %} diff --git a/compensation/tests/test_views.py b/compensation/tests/test_views.py index 92840281..5e0edd15 100644 --- a/compensation/tests/test_views.py +++ b/compensation/tests/test_views.py @@ -8,60 +8,76 @@ Created on: 27.10.21 from django.urls import reverse from django.test import Client -from compensation.models import CompensationState, CompensationAction from konova.settings import DEFAULT_GROUP from konova.tests.test_views import BaseViewTestCase -class ViewTestCase(BaseViewTestCase): +class CompensationViewTestCase(BaseViewTestCase): + """ + These tests focus on proper returned views depending on the user's groups privileges and login status + + """ comp_state = None comp_action = None - def setUp(self) -> None: - super().setUp() - self.create_dummy_states() - self.create_dummy_action() + @classmethod + def setUpTestData(cls) -> None: + super().setUpTestData() + state = cls.create_dummy_states() + cls.compensation.before_states.set([state]) + cls.compensation.after_states.set([state]) + + action = cls.create_dummy_action() + cls.compensation.actions.set([action]) # Prepare urls - self.index_url = reverse("compensation:index", args=()) - self.new_url = reverse("compensation:new", args=(self.intervention.id,)) - self.new_id_url = reverse("compensation:new-id", args=()) - self.detail_url = reverse("compensation:detail", args=(self.compensation.id,)) - self.log_url = reverse("compensation:log", args=(self.compensation.id,)) - self.edit_url = reverse("compensation:edit", args=(self.compensation.id,)) - self.remove_url = reverse("compensation:remove", args=(self.compensation.id,)) - self.report_url = reverse("compensation:report", args=(self.compensation.id,)) - self.state_new_url = reverse("compensation:new-state", args=(self.compensation.id,)) - self.action_new_url = reverse("compensation:new-action", args=(self.compensation.id,)) - self.deadline_new_url = reverse("compensation:new-deadline", args=(self.compensation.id,)) - self.new_doc_url = reverse("compensation:new-doc", args=(self.compensation.id,)) + cls.index_url = reverse("compensation:index", args=()) + cls.new_url = reverse("compensation:new", args=(cls.intervention.id,)) + cls.new_id_url = reverse("compensation:new-id", args=()) + cls.detail_url = reverse("compensation:detail", args=(cls.compensation.id,)) + cls.log_url = reverse("compensation:log", args=(cls.compensation.id,)) + cls.edit_url = reverse("compensation:edit", args=(cls.compensation.id,)) + cls.remove_url = reverse("compensation:remove", args=(cls.compensation.id,)) + cls.report_url = reverse("compensation:report", args=(cls.compensation.id,)) + cls.state_new_url = reverse("compensation:new-state", args=(cls.compensation.id,)) + cls.action_new_url = reverse("compensation:new-action", args=(cls.compensation.id,)) + cls.deadline_new_url = reverse("compensation:new-deadline", args=(cls.compensation.id,)) + cls.new_doc_url = reverse("compensation:new-doc", args=(cls.compensation.id,)) - self.state_remove_url = reverse("compensation:state-remove", args=(self.comp_state.id,)) - self.action_remove_url = reverse("compensation:action-remove", args=(self.comp_action.id,)) + cls.state_remove_url = reverse("compensation:state-remove", args=(cls.compensation.id, cls.comp_state.id,)) + cls.action_remove_url = reverse("compensation:action-remove", args=(cls.compensation.id, cls.comp_action.id,)) - def create_dummy_states(self): - """ Creates an intervention which can be used for tests + def test_anonymous_user(self): + """ Check correct status code for all requests + + Assumption: User not logged in Returns: """ - self.comp_state = CompensationState.objects.create( - surface=10.00, - biotope_type=None, - ) - self.compensation.before_states.set([self.comp_state]) - self.compensation.after_states.set([self.comp_state]) + client = Client() - def create_dummy_action(self): - """ Creates an intervention which can be used for tests + success_urls = [ + self.report_url, + ] + fail_urls = [ + self.index_url, + self.detail_url, + self.new_url, + self.new_id_url, + self.log_url, + self.edit_url, + self.remove_url, + self.state_new_url, + self.action_new_url, + self.deadline_new_url, + self.state_remove_url, + self.action_remove_url, + self.new_doc_url, + ] - Returns: - - """ - self.comp_action = CompensationAction.objects.create( - amount=10 - ) - self.compensation.actions.set([self.comp_action]) + self.assert_url_success(client, success_urls) + self.assert_url_fail(client, fail_urls) def test_logged_in_no_groups_shared(self): """ Check correct status code for all requests @@ -111,6 +127,7 @@ class ViewTestCase(BaseViewTestCase): client = Client() client.login(username=self.superuser.username, password=self.superuser_pw) self.superuser.groups.set([]) + # Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state self.intervention.users.set([]) # Since the user has no groups, it does not matter that data is unshared. There SHOULD not be any difference @@ -140,7 +157,8 @@ class ViewTestCase(BaseViewTestCase): def test_logged_in_default_group_shared(self): """ Check correct status code for all requests - Assumption: User logged in and has no groups and data is shared + Assumption: User logged in, is default group member and data is shared + --> Default group necessary since all base functionalities depend on this group membership Returns: @@ -149,6 +167,7 @@ class ViewTestCase(BaseViewTestCase): client.login(username=self.superuser.username, password=self.superuser_pw) group = self.groups.get(name=DEFAULT_GROUP) self.superuser.groups.set([group]) + # Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state self.intervention.users.set([self.superuser]) success_urls = [ @@ -169,34 +188,39 @@ class ViewTestCase(BaseViewTestCase): ] self.assert_url_success(client, success_urls) - def test_anonymous_user(self): + def test_logged_in_default_group_unshared(self): """ Check correct status code for all requests - Assumption: User not logged in + Assumption: User logged in, is default group member and data is NOT shared + --> Default group necessary since all base functionalities depend on this group membership Returns: """ client = Client() + client.login(username=self.superuser.username, password=self.superuser_pw) + group = self.groups.get(name=DEFAULT_GROUP) + self.superuser.groups.set([group]) + # Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state + self.intervention.users.set([]) success_urls = [ - self.report_url, - ] - fail_urls = [ self.index_url, self.detail_url, - self.new_url, + self.report_url, self.new_id_url, - self.log_url, + ] + fail_urls = [ + self.new_url, self.edit_url, - self.remove_url, self.state_new_url, self.action_new_url, self.deadline_new_url, self.state_remove_url, self.action_remove_url, self.new_doc_url, + self.log_url, + self.remove_url, ] - - self.assert_url_success(client, success_urls) self.assert_url_fail(client, fail_urls) + self.assert_url_success(client, success_urls) diff --git a/compensation/views/compensation_views.py b/compensation/views/compensation_views.py index 3175c38a..02bdde01 100644 --- a/compensation/views/compensation_views.py +++ b/compensation/views/compensation_views.py @@ -110,6 +110,7 @@ def new_id_view(request: HttpRequest): @login_required @default_group_required +@shared_access_required(Compensation, "id") def edit_view(request: HttpRequest, id: str): """ Renders a view for editing compensations @@ -377,17 +378,19 @@ def deadline_new_view(request: HttpRequest, id: str): @login_required @default_group_required -def state_remove_view(request: HttpRequest, id: str): +@shared_access_required(Compensation, "id") +def state_remove_view(request: HttpRequest, id: str, state_id: str): """ Renders a form for removing a compensation state Args: request (HttpRequest): The incoming request - id (str): The state's id + id (str): The compensation's id + state_id (str): The state's id Returns: """ - state = get_object_or_404(CompensationState, id=id) + state = get_object_or_404(CompensationState, id=state_id) form = RemoveModalForm(request.POST or None, instance=state, user=request.user) return form.process_request( request, @@ -397,17 +400,19 @@ def state_remove_view(request: HttpRequest, id: str): @login_required @default_group_required -def action_remove_view(request: HttpRequest, id: str): +@shared_access_required(Compensation, "id") +def action_remove_view(request: HttpRequest, id: str, action_id: str): """ Renders a form for removing a compensation action Args: request (HttpRequest): The incoming request + id (str): The compensation's id id (str): The action's id Returns: """ - action = get_object_or_404(CompensationAction, id=id) + action = get_object_or_404(CompensationAction, id=action_id) form = RemoveModalForm(request.POST or None, instance=action, user=request.user) return form.process_request( request, @@ -415,7 +420,7 @@ def action_remove_view(request: HttpRequest, id: str): ) -def report_view(request:HttpRequest, id: str): +def report_view(request: HttpRequest, id: str): """ Renders the public report view Args: diff --git a/compensation/views/eco_account_views.py b/compensation/views/eco_account_views.py index 63c85294..2d302dce 100644 --- a/compensation/views/eco_account_views.py +++ b/compensation/views/eco_account_views.py @@ -16,11 +16,12 @@ from django.shortcuts import render, get_object_or_404, redirect from compensation.forms.forms import NewEcoAccountForm, EditEcoAccountForm from compensation.forms.modalForms import NewStateModalForm, NewActionModalForm, NewDeadlineModalForm -from compensation.models import EcoAccount, EcoAccountDocument +from compensation.models import EcoAccount, EcoAccountDocument, CompensationState, CompensationAction from compensation.tables import EcoAccountTable from intervention.forms.modalForms import NewDeductionModalForm, ShareInterventionModalForm from konova.contexts import BaseContext -from konova.decorators import any_group_check, default_group_required, conservation_office_group_required +from konova.decorators import any_group_check, default_group_required, conservation_office_group_required, \ + shared_access_required from konova.forms import RemoveModalForm, SimpleGeomForm, NewDocumentForm, RecordModalForm from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP from konova.utils.documents import get_document, remove_document @@ -99,6 +100,7 @@ def new_view(request: HttpRequest): @login_required +@default_group_required def new_id_view(request: HttpRequest): """ JSON endpoint @@ -353,6 +355,50 @@ def action_new_view(request: HttpRequest, id: str): ) +@login_required +@default_group_required +@shared_access_required(EcoAccount, "id") +def state_remove_view(request: HttpRequest, id: str, state_id: str): + """ Renders a form for removing a compensation state + + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id + state_id (str): The state's id + + Returns: + + """ + state = get_object_or_404(CompensationState, id=state_id) + form = RemoveModalForm(request.POST or None, instance=state, user=request.user) + return form.process_request( + request, + msg_success=_("State removed") + ) + + +@login_required +@default_group_required +@shared_access_required(EcoAccount, "id") +def action_remove_view(request: HttpRequest, id: str, action_id: str): + """ Renders a form for removing a compensation action + + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id + id (str): The action's id + + Returns: + + """ + action = get_object_or_404(CompensationAction, id=action_id) + form = RemoveModalForm(request.POST or None, instance=action, user=request.user) + return form.process_request( + request, + msg_success=_("Action removed") + ) + + @login_required def deadline_new_view(request: HttpRequest, id: str): """ Renders a form for adding new states for an eco account diff --git a/ema/templates/ema/detail/includes/actions.html b/ema/templates/ema/detail/includes/actions.html index 8fab1234..56359703 100644 --- a/ema/templates/ema/detail/includes/actions.html +++ b/ema/templates/ema/detail/includes/actions.html @@ -48,7 +48,7 @@ {{ action.comment|default_if_none:"" }} {% if is_default_member and has_access %} - {% endif %} diff --git a/ema/templates/ema/detail/includes/states-after.html b/ema/templates/ema/detail/includes/states-after.html index 02253b38..0e6aa16a 100644 --- a/ema/templates/ema/detail/includes/states-after.html +++ b/ema/templates/ema/detail/includes/states-after.html @@ -49,7 +49,7 @@ {{ state.surface|floatformat:2 }} m² {% if is_default_member and has_access %} - {% endif %} diff --git a/ema/templates/ema/detail/includes/states-before.html b/ema/templates/ema/detail/includes/states-before.html index 74220659..42c0eb1b 100644 --- a/ema/templates/ema/detail/includes/states-before.html +++ b/ema/templates/ema/detail/includes/states-before.html @@ -49,7 +49,7 @@ {{ state.surface|floatformat:2 }} m² {% if is_default_member and has_access %} - {% endif %} diff --git a/ema/tests/__init__.py b/ema/tests/__init__.py new file mode 100644 index 00000000..10799e80 --- /dev/null +++ b/ema/tests/__init__.py @@ -0,0 +1,7 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 26.10.21 + +""" diff --git a/ema/tests/test_views.py b/ema/tests/test_views.py new file mode 100644 index 00000000..825dc4a1 --- /dev/null +++ b/ema/tests/test_views.py @@ -0,0 +1,200 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 26.10.21 + +""" +from django.db.models import Q +from django.urls import reverse +from django.test.client import Client + +from compensation.tests.test_views import CompensationViewTestCase +from ema.models import Ema +from intervention.models import ResponsibilityData +from konova.models import Geometry +from konova.settings import DEFAULT_GROUP, ETS_GROUP +from user.models import UserActionLogEntry, UserAction + + +class EmaViewTestCase(CompensationViewTestCase): + """ Test cases for EMA. + + Since we inherit most tests functions from CompensationViewTestCase, we only need to add some EMA specific + test functions + + """ + ema = None + + @classmethod + def setUpTestData(cls) -> None: + super().setUpTestData() + + # Create dummy data and related objects, like states or actions + cls.create_dummy_data() + state = cls.create_dummy_states() + action = cls.create_dummy_action() + cls.ema.before_states.set([state]) + cls.ema.after_states.set([state]) + cls.ema.actions.set([action]) + + # Prepare urls + cls.index_url = reverse("ema:index", args=()) + cls.new_url = reverse("ema:new", args=()) + cls.new_id_url = reverse("ema:new-id", args=()) + cls.detail_url = reverse("ema:detail", args=(cls.ema.id,)) + cls.log_url = reverse("ema:log", args=(cls.ema.id,)) + cls.edit_url = reverse("ema:edit", args=(cls.ema.id,)) + cls.remove_url = reverse("ema:remove", args=(cls.ema.id,)) + cls.share_url = reverse("ema:share", args=(cls.ema.id, cls.ema.access_token,)) + cls.share_create_url = reverse("ema:share-create", args=(cls.ema.id,)) + cls.record_url = reverse("ema:record", args=(cls.ema.id,)) + cls.report_url = reverse("ema:report", args=(cls.ema.id,)) + cls.new_doc_url = reverse("ema:new-doc", args=(cls.ema.id,)) + cls.state_new_url = reverse("ema:new-state", args=(cls.ema.id,)) + cls.action_new_url = reverse("ema:new-action", args=(cls.ema.id,)) + cls.deadline_new_url = reverse("ema:new-deadline", args=(cls.ema.id,)) + cls.state_remove_url = reverse("ema:state-remove", args=(cls.ema.id, state.id,)) + cls.action_remove_url = reverse("ema:action-remove", args=(cls.ema.id, action.id,)) + + @classmethod + def create_dummy_data(cls): + # Create dummy data + # Create log entry + action = UserActionLogEntry.objects.create( + user=cls.superuser, + action=UserAction.CREATED, + ) + # Create responsible data object + responsibility_data = ResponsibilityData.objects.create() + geometry = Geometry.objects.create() + cls.ema = Ema.objects.create( + identifier="TEST", + title="Test_title", + created=action, + geometry=geometry, + responsible=responsibility_data, + comment="Test", + ) + + def test_logged_in_default_group_shared(self): + """ Check correct status code for all requests + + OVERWRITES DEFAULT COMPENSATION TEST METHOD DUE TO SPECIFIC BEHAVIOUR OF EMAS + + Assumption: User logged in, is default group member and data is shared + + Normally default group would give access to all base functionalities. In case of EMAs we expect these + requests to fail, since a user must be part of the ets group as well, not only default. + + Returns: + + """ + client = Client() + client.login(username=self.superuser.username, password=self.superuser_pw) + group = self.groups.get(name=DEFAULT_GROUP) + self.superuser.groups.set([group]) + + # Sharing does not have any effect in here, since the default group will prohibit further functionality access + # to this user + self.ema.users.set([self.superuser]) + + success_urls = [ + self.index_url, + self.detail_url, + self.report_url, + ] + fail_urls = [ + self.new_url, + self.new_id_url, + self.edit_url, + self.state_new_url, + self.action_new_url, + self.deadline_new_url, + self.state_remove_url, + self.action_remove_url, + self.new_doc_url, + self.log_url, + self.remove_url, + ] + self.assert_url_fail(client, fail_urls) + self.assert_url_success(client, success_urls) + + def test_logged_in_ets_group_shared(self): + """ Check correct status code for all requests + + Assumption: User logged in, is conservation office group member and data is shared + + For EMAs we expect a user to be ETS and default group member to have full access to all functionalities, normally + provided for default group members. + + Returns: + + """ + client = Client() + client.login(username=self.superuser.username, password=self.superuser_pw) + groups = self.groups.filter(Q(name=ETS_GROUP)|Q(name=DEFAULT_GROUP)) + self.superuser.groups.set(groups) + # Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state + self.ema.users.set([self.superuser]) + + success_urls = [ + self.index_url, + self.detail_url, + self.report_url, + self.new_url, + self.new_id_url, + self.edit_url, + self.state_new_url, + self.action_new_url, + self.deadline_new_url, + self.state_remove_url, + self.action_remove_url, + self.new_doc_url, + self.log_url, + self.remove_url, + ] + self.assert_url_success(client, success_urls) + + def test_logged_in_ets_group_unshared(self): + """ Check correct status code for all requests + + Assumption: User logged in, is conservation office group member and data is NOT shared + + For EMAs we expect a user to be ETS and default group member to have full access to all functionalities, normally + provided for default group members. + + Returns: + + """ + client = Client() + client.login(username=self.superuser.username, password=self.superuser_pw) + groups = self.groups.filter(Q(name=ETS_GROUP)|Q(name=DEFAULT_GROUP)) + self.superuser.groups.set(groups) + # Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state + self.ema.users.set([]) + + success_urls = [ + self.index_url, + self.detail_url, + self.report_url, + self.new_url, + self.new_id_url, + ] + fail_urls = [ + self.edit_url, + self.state_new_url, + self.action_new_url, + self.deadline_new_url, + self.state_remove_url, + self.action_remove_url, + self.new_doc_url, + self.log_url, + self.remove_url, + ] + self.assert_url_success(client, success_urls) + self.assert_url_fail(client, fail_urls) + + def test_logged_in_default_group_unshared(self): + # overwrite this test, since it's not relevant for EMA but is inherited by the superclass + pass \ No newline at end of file diff --git a/ema/urls.py b/ema/urls.py index ee3d30f0..a732f327 100644 --- a/ema/urls.py +++ b/ema/urls.py @@ -21,6 +21,8 @@ urlpatterns = [ path('/report', report_view, name='report'), path('/state/new', state_new_view, name='new-state'), path('/action/new', action_new_view, name='new-action'), + path('/state//remove', state_remove_view, name='state-remove'), + path('/action//remove', action_remove_view, name='action-remove'), path('/deadline/new', deadline_new_view, name="new-deadline"), path('/share/', share_view, name='share'), path('/share', create_share_view, name='share-create'), @@ -31,9 +33,4 @@ urlpatterns = [ path('document/', get_document_view, name='get-doc'), path('document//remove/', remove_document_view, name='remove-doc'), - # Generic state routes - path('state//remove', state_remove_view, name='state-remove'), - - # Generic action routes - path('action//remove', action_remove_view, name='action-remove'), ] \ No newline at end of file diff --git a/ema/views.py b/ema/views.py index b180433f..30b7d09c 100644 --- a/ema/views.py +++ b/ema/views.py @@ -6,13 +6,13 @@ from django.shortcuts import render, get_object_or_404, redirect from django.urls import reverse from django.utils.translation import gettext_lazy as _ -import compensation from compensation.forms.modalForms import NewStateModalForm, NewActionModalForm, NewDeadlineModalForm +from compensation.models import CompensationAction, CompensationState from ema.forms import NewEmaForm, EditEmaForm from ema.tables import EmaTable from intervention.forms.modalForms import ShareInterventionModalForm from konova.contexts import BaseContext -from konova.decorators import conservation_office_group_required, default_group_required +from konova.decorators import conservation_office_group_required, shared_access_required from ema.models import Ema, EmaDocument from konova.forms import RemoveModalForm, NewDocumentForm, SimpleGeomForm, RecordModalForm from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP @@ -92,6 +92,7 @@ def new_view(request: HttpRequest): @login_required +@conservation_office_group_required def new_id_view(request: HttpRequest): """ JSON endpoint @@ -159,6 +160,8 @@ def detail_view(request: HttpRequest, id: str): @login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") def log_view(request: HttpRequest, id: str): """ Renders a log view using modal @@ -183,6 +186,8 @@ def log_view(request: HttpRequest, id: str): @login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") def edit_view(request: HttpRequest, id: str): """ Renders a view for editing compensations @@ -219,6 +224,8 @@ def edit_view(request: HttpRequest, id: str): @login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") def remove_view(request: HttpRequest, id: str): """ Renders a modal view for removing the EMA @@ -239,6 +246,8 @@ def remove_view(request: HttpRequest, id: str): @login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") def record_view(request: HttpRequest, id: str): """ Renders a modal view for recording the EMA @@ -259,6 +268,8 @@ def record_view(request: HttpRequest, id: str): @login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") def state_new_view(request: HttpRequest, id: str): """ Renders a form for adding new states for an EMA @@ -278,6 +289,8 @@ def state_new_view(request: HttpRequest, id: str): @login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") def action_new_view(request: HttpRequest, id: str): """ Renders a form for adding new actions for an EMA @@ -297,6 +310,8 @@ def action_new_view(request: HttpRequest, id: str): @login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") def deadline_new_view(request: HttpRequest, id: str): """ Renders a form for adding new states for an EMA @@ -316,6 +331,8 @@ def deadline_new_view(request: HttpRequest, id: str): @login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") def document_new_view(request: HttpRequest, id: str): """ Renders a form for uploading new documents @@ -334,6 +351,7 @@ def document_new_view(request: HttpRequest, id: str): @login_required +@conservation_office_group_required def get_document_view(request: HttpRequest, doc_id: str): """ Returns the document as downloadable file @@ -360,6 +378,7 @@ def get_document_view(request: HttpRequest, doc_id: str): @login_required +@conservation_office_group_required def remove_document_view(request: HttpRequest, doc_id: str): """ Removes the document from the database and file system @@ -380,37 +399,46 @@ def remove_document_view(request: HttpRequest, doc_id: str): @login_required -def state_remove_view(request: HttpRequest, id: str): +@conservation_office_group_required +@shared_access_required(Ema, "id") +def state_remove_view(request: HttpRequest, id: str, state_id: str): """ Renders a form for removing an EMA state Args: request (HttpRequest): The incoming request - id (str): The state's id + id (str): The ema id + state_id (str): The state's id Returns: """ - return compensation.views.compensation_views.state_remove_view( + state = get_object_or_404(CompensationState, id=state_id) + form = RemoveModalForm(request.POST or None, instance=state, user=request.user) + return form.process_request( request, - id + msg_success=_("State removed") ) @login_required -def action_remove_view(request: HttpRequest, id: str): - """ Renders a form for removing an EMA state +@conservation_office_group_required +@shared_access_required(Ema, "id") +def action_remove_view(request: HttpRequest, id: str, action_id: str): + """ Renders a form for removing an EMA action Args: request (HttpRequest): The incoming request - id (str): The state's id + id (str): The ema id + id (str): The action's id Returns: """ - # Reuses the route logic from compensation view - return compensation.views.compensation_views.action_remove_view( + action = get_object_or_404(CompensationAction, id=action_id) + form = RemoveModalForm(request.POST or None, instance=action, user=request.user) + return form.process_request( request, - id + msg_success=_("Action removed") ) @@ -505,7 +533,8 @@ def share_view(request: HttpRequest, id: str, token: str): @login_required -@default_group_required +@conservation_office_group_required +@shared_access_required(Ema, "id") def create_share_view(request: HttpRequest, id: str): """ Renders sharing form for an Ema diff --git a/intervention/tests/test_views.py b/intervention/tests/test_views.py index ff231e1e..f75bfe92 100644 --- a/intervention/tests/test_views.py +++ b/intervention/tests/test_views.py @@ -10,62 +10,29 @@ from django.test import Client from django.contrib.auth.models import Group from django.urls import reverse -from intervention.models import Intervention, LegalData, ResponsibilityData -from konova.models import Geometry from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP from konova.tests.test_views import BaseViewTestCase -from user.models import UserActionLogEntry, UserAction -class ViewTestCase(BaseViewTestCase): - def setUp(self) -> None: - super().setUp() +class InterventionViewTestCase(BaseViewTestCase): + + @classmethod + def setUpTestData(cls) -> None: + super().setUpTestData() # Prepare urls - self.index_url = reverse("intervention:index", args=()) - self.new_url = reverse("intervention:new", args=()) - self.new_id_url = reverse("intervention:new-id", args=()) - self.detail_url = reverse("intervention:detail", args=(self.intervention.id,)) - self.log_url = reverse("intervention:log", args=(self.intervention.id,)) - self.edit_url = reverse("intervention:edit", args=(self.intervention.id,)) - self.remove_url = reverse("intervention:remove", args=(self.intervention.id,)) - self.share_url = reverse("intervention:share", args=(self.intervention.id, self.intervention.access_token,)) - self.share_create_url = reverse("intervention:share-create", args=(self.intervention.id,)) - self.run_check_url = reverse("intervention:run-check", args=(self.intervention.id,)) - self.record_url = reverse("intervention:record", args=(self.intervention.id,)) - self.report_url = reverse("intervention:report", args=(self.intervention.id,)) - - def test_views_logged_in_no_groups(self): - """ Check correct status code for all requests - - Assumption: User logged in but has no groups - - Returns: - - """ - # Login client - client = Client() - client.login(username=self.superuser.username, password=self.superuser_pw) - self.superuser.groups.set([]) - success_urls = [ - self.index_url, - self.report_url, - self.detail_url, - ] - fail_urls = [ - self.log_url, - self.new_id_url, - self.new_url, - self.edit_url, - self.remove_url, - self.share_url, - self.share_create_url, - self.run_check_url, - self.record_url, - ] - - self.assert_url_success(client, success_urls) - self.assert_url_fail(client, fail_urls) + cls.index_url = reverse("intervention:index", args=()) + cls.new_url = reverse("intervention:new", args=()) + cls.new_id_url = reverse("intervention:new-id", args=()) + cls.detail_url = reverse("intervention:detail", args=(cls.intervention.id,)) + cls.log_url = reverse("intervention:log", args=(cls.intervention.id,)) + cls.edit_url = reverse("intervention:edit", args=(cls.intervention.id,)) + cls.remove_url = reverse("intervention:remove", args=(cls.intervention.id,)) + cls.share_url = reverse("intervention:share", args=(cls.intervention.id, cls.intervention.access_token,)) + cls.share_create_url = reverse("intervention:share-create", args=(cls.intervention.id,)) + cls.run_check_url = reverse("intervention:run-check", args=(cls.intervention.id,)) + cls.record_url = reverse("intervention:record", args=(cls.intervention.id,)) + cls.report_url = reverse("intervention:report", args=(cls.intervention.id,)) def test_views_anonymous_user(self): """ Check correct status code for all requests @@ -102,6 +69,38 @@ class ViewTestCase(BaseViewTestCase): response = client.get(url, follow=True) self.assertEqual(response.redirect_chain[0], (f"{self.login_url}?next={url}", 302), msg=f"Failed for {url}. Redirect chain is {response.redirect_chain}") + def test_views_logged_in_no_groups(self): + """ Check correct status code for all requests + + Assumption: User logged in but has no groups + + Returns: + + """ + # Login client + client = Client() + client.login(username=self.superuser.username, password=self.superuser_pw) + self.superuser.groups.set([]) + success_urls = [ + self.index_url, + self.report_url, + self.detail_url, + ] + fail_urls = [ + self.log_url, + self.new_id_url, + self.new_url, + self.edit_url, + self.remove_url, + self.share_url, + self.share_create_url, + self.run_check_url, + self.record_url, + ] + + self.assert_url_success(client, success_urls) + self.assert_url_fail(client, fail_urls) + def test_views_logged_in_default_group_shared(self): """ Check correct status code for all requests @@ -194,7 +193,7 @@ class ViewTestCase(BaseViewTestCase): client = Client() client.login(username=self.superuser.username, password=self.superuser_pw) - # Add user to default group + # Add user to zb group zb_group = self.groups.get(name=ZB_GROUP) self.superuser.groups.set([zb_group]) self.intervention.users.set([self.superuser]) @@ -234,7 +233,7 @@ class ViewTestCase(BaseViewTestCase): client = Client() client.login(username=self.superuser.username, password=self.superuser_pw) - # Add user to default group + # Add user to zb group zb_group = self.groups.get(name=ZB_GROUP) self.superuser.groups.set([zb_group]) self.intervention.users.set([]) @@ -265,7 +264,7 @@ class ViewTestCase(BaseViewTestCase): def test_views_logged_in_ets_group_shared(self): """ Check correct status code for all requests - Assumption: User logged in and is registration office member and data is shared with + Assumption: User logged in and is conservation office member and data is shared with Returns: @@ -274,7 +273,7 @@ class ViewTestCase(BaseViewTestCase): client = Client() client.login(username=self.superuser.username, password=self.superuser_pw) - # Add user to default group + # Add user to ets group ets_group = Group.objects.get(name=ETS_GROUP) self.superuser.groups.set([ets_group]) self.intervention.users.set([self.superuser]) diff --git a/konova/tests/test_views.py b/konova/tests/test_views.py index 62c576ed..7078fc16 100644 --- a/konova/tests/test_views.py +++ b/konova/tests/test_views.py @@ -11,7 +11,7 @@ from django.contrib.auth.models import User, Group from django.test import TestCase, Client from django.urls import reverse -from compensation.models import Compensation +from compensation.models import Compensation, CompensationState, CompensationAction from intervention.models import LegalData, ResponsibilityData, Intervention from konova.management.commands.setup_data import GROUPS_DATA from konova.models import Geometry @@ -30,33 +30,30 @@ class BaseTestCase(TestCase): superuser_pw = "root" user_pw = "root" - @abstractmethod - def setUp(self) -> None: - # To be implemented in the inheriting classes - raise NotImplementedError - - def create_users(self): + @classmethod + def create_users(cls): # Create superuser and regular user - self.superuser = User.objects.create_superuser( + cls.superuser = User.objects.create_superuser( username="root", email="root@root.com", - password=self.superuser_pw, + password=cls.superuser_pw, ) - self.user = User.objects.create_user( + cls.user = User.objects.create_user( username="user1", email="user@root.com", - password=self.user_pw + password=cls.user_pw ) - self.users = User.objects.all() + cls.users = User.objects.all() - def create_groups(self): + @classmethod + def create_groups(cls): # Create groups for group_data in GROUPS_DATA: name = group_data.get("name") Group.objects.get_or_create( name=name, ) - self.groups = Group.objects.all() + cls.groups = Group.objects.all() class Meta: abstract = True @@ -69,13 +66,16 @@ class BaseViewTestCase(BaseTestCase): login_url = None intervention = None compensation = None + comp_state = None + comp_action = None - def setUp(self) -> None: - self.create_users() - self.create_groups() - self.create_dummy_intervention() - self.create_dummy_compensation() - self.login_url = reverse("simple-sso-login") + @classmethod + def setUpTestData(cls) -> None: + cls.create_users() + cls.create_groups() + cls.create_dummy_intervention() + cls.create_dummy_compensation() + cls.login_url = reverse("simple-sso-login") def assert_url_success(self, client: Client, urls: list): """ Assert for all given urls a direct 200 response @@ -122,7 +122,8 @@ class BaseViewTestCase(BaseTestCase): response = client.get(url) self.assertEqual(response.status_code, 302, msg=f"Failed for {url}") - def create_dummy_intervention(self): + @classmethod + def create_dummy_intervention(cls): """ Creates an intervention which can be used for tests Returns: @@ -131,7 +132,7 @@ class BaseViewTestCase(BaseTestCase): # Create dummy data # Create log entry action = UserActionLogEntry.objects.create( - user=self.superuser, + user=cls.superuser, action=UserAction.CREATED, ) # Create legal data object (without M2M laws first) @@ -140,7 +141,7 @@ class BaseViewTestCase(BaseTestCase): responsibility_data = ResponsibilityData.objects.create() geometry = Geometry.objects.create() # Finally create main object, holding the other objects - self.intervention = Intervention.objects.create( + cls.intervention = Intervention.objects.create( identifier="TEST", title="Test_title", responsible=responsibility_data, @@ -149,43 +150,70 @@ class BaseViewTestCase(BaseTestCase): geometry=geometry, comment="Test", ) - self.intervention.generate_access_token(make_unique=True) + cls.intervention.generate_access_token(make_unique=True) - def create_dummy_compensation(self): + @classmethod + def create_dummy_compensation(cls): """ Creates an intervention which can be used for tests Returns: """ - if self.intervention is None: - self.create_dummy_intervention() + if cls.intervention is None: + cls.create_dummy_intervention() # Create dummy data # Create log entry action = UserActionLogEntry.objects.create( - user=self.superuser, + user=cls.superuser, action=UserAction.CREATED, ) geometry = Geometry.objects.create() # Finally create main object, holding the other objects - self.compensation = Compensation.objects.create( + cls.compensation = Compensation.objects.create( identifier="TEST", title="Test_title", - intervention=self.intervention, + intervention=cls.intervention, created=action, geometry=geometry, comment="Test", ) - self.intervention.generate_access_token(make_unique=True) + cls.intervention.generate_access_token(make_unique=True) + + @classmethod + def create_dummy_states(cls): + """ Creates an intervention which can be used for tests + + Returns: + + """ + cls.comp_state = CompensationState.objects.create( + surface=10.00, + biotope_type=None, + ) + return cls.comp_state + + @classmethod + def create_dummy_action(cls): + """ Creates an intervention which can be used for tests + + Returns: + + """ + cls.comp_action = CompensationAction.objects.create( + amount=10 + ) + return cls.comp_action class KonovaViewTestCase(BaseViewTestCase): """ Holds tests for all regular views, which are not app specific """ - def setUp(self) -> None: - super().setUp() + @classmethod + def setUpTestData(cls) -> None: + super().setUpTestData() - self.home_url = reverse("home") + cls.home_url = reverse("home") def test_views_logged_in_no_groups(self): """ Check correct status code for all requests @@ -221,17 +249,18 @@ class KonovaViewTestCase(BaseViewTestCase): class AutocompleteTestCase(BaseViewTestCase): - def setUp(self) -> None: - super().setUp() - self.atcmplt_accs = reverse("accounts-autocomplete") - self.atcmplt_interventions = reverse("interventions-autocomplete") - self.atcmplt_code_comp_action = reverse("codes-compensation-action-autocomplete") - self.atcmplt_code_comp_funding = reverse("codes-compensation-funding-autocomplete") - self.atcmplt_code_comp_biotope = reverse("codes-biotope-autocomplete") - self.atcmplt_code_comp_law = reverse("codes-law-autocomplete") - self.atcmplt_code_comp_process = reverse("codes-process-type-autocomplete") - self.atcmplt_code_comp_reg_off = reverse("codes-registration-office-autocomplete") - self.atcmplt_code_comp_cons_off = reverse("codes-conservation-office-autocomplete") + @classmethod + def setUpTestData(cls) -> None: + super().setUpTestData() + cls.atcmplt_accs = reverse("accounts-autocomplete") + cls.atcmplt_interventions = reverse("interventions-autocomplete") + cls.atcmplt_code_comp_action = reverse("codes-compensation-action-autocomplete") + cls.atcmplt_code_comp_funding = reverse("codes-compensation-funding-autocomplete") + cls.atcmplt_code_comp_biotope = reverse("codes-biotope-autocomplete") + cls.atcmplt_code_comp_law = reverse("codes-law-autocomplete") + cls.atcmplt_code_comp_process = reverse("codes-process-type-autocomplete") + cls.atcmplt_code_comp_reg_off = reverse("codes-registration-office-autocomplete") + cls.atcmplt_code_comp_cons_off = reverse("codes-conservation-office-autocomplete") def _test_views_anonymous_user(self): # ATTENTION: As of the current state of django-autocomplete-light, there is no way to check on authenticated