master #123
@ -109,8 +109,8 @@ class APIV1CreateTestCase(BaseAPIV1TestCase):
|
|||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.intervention.share_with(self.superuser)
|
self.intervention.share_with_user(self.superuser)
|
||||||
self.eco_account.share_with(self.superuser)
|
self.eco_account.share_with_user(self.superuser)
|
||||||
|
|
||||||
url = reverse("api:v1:deduction")
|
url = reverse("api:v1:deduction")
|
||||||
json_file_path = "api/tests/v1/create/deduction_create_post_body.json"
|
json_file_path = "api/tests/v1/create/deduction_create_post_body.json"
|
||||||
|
@ -57,7 +57,7 @@ class APIV1DeleteTestCase(BaseAPIV1TestCase):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
test_intervention = self.create_dummy_intervention()
|
test_intervention = self.create_dummy_intervention()
|
||||||
test_intervention.share_with(self.superuser)
|
test_intervention.share_with_user(self.superuser)
|
||||||
url = reverse("api:v1:intervention", args=(str(test_intervention.id),))
|
url = reverse("api:v1:intervention", args=(str(test_intervention.id),))
|
||||||
self._test_delete_object(test_intervention, url)
|
self._test_delete_object(test_intervention, url)
|
||||||
|
|
||||||
@ -68,7 +68,7 @@ class APIV1DeleteTestCase(BaseAPIV1TestCase):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
test_comp = self.create_dummy_compensation()
|
test_comp = self.create_dummy_compensation()
|
||||||
test_comp.share_with(self.superuser)
|
test_comp.share_with_user(self.superuser)
|
||||||
url = reverse("api:v1:compensation", args=(str(test_comp.id),))
|
url = reverse("api:v1:compensation", args=(str(test_comp.id),))
|
||||||
self._test_delete_object(test_comp, url)
|
self._test_delete_object(test_comp, url)
|
||||||
|
|
||||||
@ -79,7 +79,7 @@ class APIV1DeleteTestCase(BaseAPIV1TestCase):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
test_acc = self.create_dummy_eco_account()
|
test_acc = self.create_dummy_eco_account()
|
||||||
test_acc.share_with(self.superuser)
|
test_acc.share_with_user(self.superuser)
|
||||||
url = reverse("api:v1:ecoaccount", args=(str(test_acc.id),))
|
url = reverse("api:v1:ecoaccount", args=(str(test_acc.id),))
|
||||||
self._test_delete_object(test_acc, url)
|
self._test_delete_object(test_acc, url)
|
||||||
|
|
||||||
@ -90,7 +90,7 @@ class APIV1DeleteTestCase(BaseAPIV1TestCase):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
test_ema = self.create_dummy_ema()
|
test_ema = self.create_dummy_ema()
|
||||||
test_ema.share_with(self.superuser)
|
test_ema.share_with_user(self.superuser)
|
||||||
url = reverse("api:v1:ema", args=(str(test_ema.id),))
|
url = reverse("api:v1:ema", args=(str(test_ema.id),))
|
||||||
self._test_delete_object(test_ema, url)
|
self._test_delete_object(test_ema, url)
|
||||||
|
|
||||||
@ -101,7 +101,7 @@ class APIV1DeleteTestCase(BaseAPIV1TestCase):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
test_deduction = self.create_dummy_deduction()
|
test_deduction = self.create_dummy_deduction()
|
||||||
test_deduction.intervention.share_with(self.superuser)
|
test_deduction.intervention.share_with_user(self.superuser)
|
||||||
url = reverse("api:v1:deduction", args=(str(test_deduction.id),))
|
url = reverse("api:v1:deduction", args=(str(test_deduction.id),))
|
||||||
|
|
||||||
response = self._run_delete_request(url)
|
response = self._run_delete_request(url)
|
||||||
|
@ -36,7 +36,12 @@ class APIV1GetTestCase(BaseAPIV1TestCase):
|
|||||||
"""
|
"""
|
||||||
response = self._run_get_request(url)
|
response = self._run_get_request(url)
|
||||||
content = json.loads(response.content)
|
content = json.loads(response.content)
|
||||||
geojson = content[str(obj.id)]
|
self.assertIn("rpp", content)
|
||||||
|
self.assertIn("p", content)
|
||||||
|
self.assertIn("next", content)
|
||||||
|
self.assertIn("results", content)
|
||||||
|
paginated_content = content["results"]
|
||||||
|
geojson = paginated_content[str(obj.id)]
|
||||||
self.assertEqual(response.status_code, 200, msg=response.content)
|
self.assertEqual(response.status_code, 200, msg=response.content)
|
||||||
return geojson
|
return geojson
|
||||||
|
|
||||||
@ -59,7 +64,7 @@ class APIV1GetTestCase(BaseAPIV1TestCase):
|
|||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.intervention.share_with(self.superuser)
|
self.intervention.share_with_user(self.superuser)
|
||||||
url = reverse("api:v1:intervention", args=(str(self.intervention.id),))
|
url = reverse("api:v1:intervention", args=(str(self.intervention.id),))
|
||||||
geojson = self._test_get_object(self.intervention, url)
|
geojson = self._test_get_object(self.intervention, url)
|
||||||
self._assert_geojson_format(geojson)
|
self._assert_geojson_format(geojson)
|
||||||
@ -80,13 +85,33 @@ class APIV1GetTestCase(BaseAPIV1TestCase):
|
|||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
self.fail(e)
|
self.fail(e)
|
||||||
|
|
||||||
|
def test_get_shared(self):
|
||||||
|
""" Tests api GET on shared info of the intervention
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.intervention.share_with_user(self.superuser)
|
||||||
|
self.intervention.share_with_team(self.team)
|
||||||
|
url = reverse("api:v1:intervention-share", args=(str(self.intervention.id),))
|
||||||
|
response = self._run_get_request(url)
|
||||||
|
content = json.loads(response.content)
|
||||||
|
self.assertIn("users", content)
|
||||||
|
self.assertIn(self.superuser.username, content["users"])
|
||||||
|
self.assertEqual(1, len(content["users"]))
|
||||||
|
self.assertIn("teams", content)
|
||||||
|
self.assertEqual(1, len(content["teams"]))
|
||||||
|
for team in content["teams"]:
|
||||||
|
self.assertEqual(team["id"], str(self.team.id))
|
||||||
|
self.assertEqual(team["name"], self.team.name)
|
||||||
|
|
||||||
def test_get_compensation(self):
|
def test_get_compensation(self):
|
||||||
""" Tests api GET
|
""" Tests api GET
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.intervention.share_with(self.superuser)
|
self.intervention.share_with_user(self.superuser)
|
||||||
self.compensation.intervention = self.intervention
|
self.compensation.intervention = self.intervention
|
||||||
self.compensation.save()
|
self.compensation.save()
|
||||||
|
|
||||||
@ -114,7 +139,7 @@ class APIV1GetTestCase(BaseAPIV1TestCase):
|
|||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.eco_account.share_with(self.superuser)
|
self.eco_account.share_with_user(self.superuser)
|
||||||
|
|
||||||
url = reverse("api:v1:ecoaccount", args=(str(self.eco_account.id),))
|
url = reverse("api:v1:ecoaccount", args=(str(self.eco_account.id),))
|
||||||
geojson = self._test_get_object(self.eco_account, url)
|
geojson = self._test_get_object(self.eco_account, url)
|
||||||
@ -143,7 +168,7 @@ class APIV1GetTestCase(BaseAPIV1TestCase):
|
|||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.ema.share_with(self.superuser)
|
self.ema.share_with_user(self.superuser)
|
||||||
|
|
||||||
url = reverse("api:v1:ema", args=(str(self.ema.id),))
|
url = reverse("api:v1:ema", args=(str(self.ema.id),))
|
||||||
geojson = self._test_get_object(self.ema, url)
|
geojson = self._test_get_object(self.ema, url)
|
||||||
@ -167,7 +192,7 @@ class APIV1GetTestCase(BaseAPIV1TestCase):
|
|||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.deduction.intervention.share_with(self.superuser)
|
self.deduction.intervention.share_with_user(self.superuser)
|
||||||
|
|
||||||
url = reverse("api:v1:deduction", args=(str(self.deduction.id),))
|
url = reverse("api:v1:deduction", args=(str(self.deduction.id),))
|
||||||
_json = self._test_get_object(self.deduction, url)
|
_json = self._test_get_object(self.deduction, url)
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"users": [
|
||||||
|
"CHANGE_ME"
|
||||||
|
],
|
||||||
|
"teams": [
|
||||||
|
"CHANGE_ME"
|
||||||
|
]
|
||||||
|
}
|
@ -52,7 +52,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
|
|||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.intervention.share_with(self.superuser)
|
self.intervention.share_with_user(self.superuser)
|
||||||
modified_on = self.intervention.modified
|
modified_on = self.intervention.modified
|
||||||
url = reverse("api:v1:intervention", args=(str(self.intervention.id),))
|
url = reverse("api:v1:intervention", args=(str(self.intervention.id),))
|
||||||
json_file_path = "api/tests/v1/update/intervention_update_put_body.json"
|
json_file_path = "api/tests/v1/update/intervention_update_put_body.json"
|
||||||
@ -79,7 +79,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
|
|||||||
"""
|
"""
|
||||||
self.compensation.intervention = self.intervention
|
self.compensation.intervention = self.intervention
|
||||||
self.compensation.save()
|
self.compensation.save()
|
||||||
self.intervention.share_with(self.superuser)
|
self.intervention.share_with_user(self.superuser)
|
||||||
|
|
||||||
modified_on = self.compensation.modified
|
modified_on = self.compensation.modified
|
||||||
url = reverse("api:v1:compensation", args=(str(self.compensation.id),))
|
url = reverse("api:v1:compensation", args=(str(self.compensation.id),))
|
||||||
@ -108,7 +108,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
|
|||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.eco_account.share_with(self.superuser)
|
self.eco_account.share_with_user(self.superuser)
|
||||||
|
|
||||||
modified_on = self.eco_account.modified
|
modified_on = self.eco_account.modified
|
||||||
url = reverse("api:v1:ecoaccount", args=(str(self.eco_account.id),))
|
url = reverse("api:v1:ecoaccount", args=(str(self.eco_account.id),))
|
||||||
@ -139,7 +139,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
|
|||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.ema.share_with(self.superuser)
|
self.ema.share_with_user(self.superuser)
|
||||||
|
|
||||||
modified_on = self.ema.modified
|
modified_on = self.ema.modified
|
||||||
url = reverse("api:v1:ema", args=(str(self.ema.id),))
|
url = reverse("api:v1:ema", args=(str(self.ema.id),))
|
||||||
@ -168,8 +168,8 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
|
|||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.deduction.intervention.share_with(self.superuser)
|
self.deduction.intervention.share_with_user(self.superuser)
|
||||||
self.deduction.account.share_with(self.superuser)
|
self.deduction.account.share_with_user(self.superuser)
|
||||||
|
|
||||||
url = reverse("api:v1:deduction", args=(str(self.deduction.id),))
|
url = reverse("api:v1:deduction", args=(str(self.deduction.id),))
|
||||||
json_file_path = "api/tests/v1/update/deduction_update_put_body.json"
|
json_file_path = "api/tests/v1/update/deduction_update_put_body.json"
|
||||||
@ -184,3 +184,24 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
|
|||||||
self.assertEqual(put_body["intervention"], str(self.deduction.intervention.id))
|
self.assertEqual(put_body["intervention"], str(self.deduction.intervention.id))
|
||||||
self.assertEqual(put_body["eco_account"], str(self.deduction.account.id))
|
self.assertEqual(put_body["eco_account"], str(self.deduction.account.id))
|
||||||
self.assertEqual(put_body["surface"], self.deduction.surface)
|
self.assertEqual(put_body["surface"], self.deduction.surface)
|
||||||
|
|
||||||
|
def test_update_share_intervention(self):
|
||||||
|
self.intervention.share_with_user(self.superuser)
|
||||||
|
url = reverse("api:v1:intervention-share", args=(str(self.intervention.id),))
|
||||||
|
json_file_path = "api/tests/v1/update/intervention_share_update_put_body.json"
|
||||||
|
with open(json_file_path) as json_file:
|
||||||
|
put_body = json.load(fp=json_file)
|
||||||
|
put_body["users"] = [self.user.username]
|
||||||
|
put_body["teams"] = [self.team.name]
|
||||||
|
|
||||||
|
self.assertFalse(self.intervention.is_shared_with(self.user))
|
||||||
|
self.assertEqual(0, self.intervention.shared_teams.count())
|
||||||
|
|
||||||
|
response = self._run_update_request(url, put_body)
|
||||||
|
self.assertEqual(response.status_code, 200, msg=response.content)
|
||||||
|
self.intervention.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertEqual(1, self.intervention.shared_teams.count())
|
||||||
|
self.assertEqual(2, self.intervention.shared_users.count())
|
||||||
|
self.assertEqual(self.team.name, self.intervention.shared_teams.first().name)
|
||||||
|
self.assertTrue(self.intervention.is_shared_with(self.user))
|
||||||
|
@ -10,6 +10,7 @@ from abc import abstractmethod
|
|||||||
|
|
||||||
from django.contrib.gis import geos
|
from django.contrib.gis import geos
|
||||||
from django.contrib.gis.geos import GEOSGeometry
|
from django.contrib.gis.geos import GEOSGeometry
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
|
||||||
from konova.utils.message_templates import DATA_UNSHARED
|
from konova.utils.message_templates import DATA_UNSHARED
|
||||||
|
|
||||||
@ -19,6 +20,10 @@ class AbstractModelAPISerializer:
|
|||||||
lookup = None
|
lookup = None
|
||||||
properties_data = None
|
properties_data = None
|
||||||
|
|
||||||
|
rpp = None
|
||||||
|
page_number = None
|
||||||
|
paginator = None
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
@ -80,9 +85,12 @@ class AbstractModelAPISerializer:
|
|||||||
Returns:
|
Returns:
|
||||||
serialized_data (dict)
|
serialized_data (dict)
|
||||||
"""
|
"""
|
||||||
entries = self.model.objects.filter(**self.lookup)
|
entries = self.model.objects.filter(**self.lookup).order_by("id")
|
||||||
|
self.paginator = Paginator(entries, self.rpp)
|
||||||
|
requested_entries = self.paginator.page(self.page_number)
|
||||||
|
|
||||||
serialized_data = {}
|
serialized_data = {}
|
||||||
for entry in entries:
|
for entry in requested_entries.object_list:
|
||||||
serialized_data[str(entry.id)] = self._model_to_geo_json(entry)
|
serialized_data[str(entry.id)] = self._model_to_geo_json(entry)
|
||||||
return serialized_data
|
return serialized_data
|
||||||
|
|
||||||
|
@ -367,7 +367,9 @@ class AbstractCompensationAPISerializerV1Mixin:
|
|||||||
"""
|
"""
|
||||||
actions = []
|
actions = []
|
||||||
for entry in actions_data:
|
for entry in actions_data:
|
||||||
action = entry["action"]
|
action_types = [
|
||||||
|
self._konova_code_from_json(e, CODELIST_COMPENSATION_ACTION_ID) for e in entry["action_types"]
|
||||||
|
]
|
||||||
action_details = [
|
action_details = [
|
||||||
self._konova_code_from_json(e, CODELIST_COMPENSATION_ACTION_DETAIL_ID) for e in entry["action_details"]
|
self._konova_code_from_json(e, CODELIST_COMPENSATION_ACTION_DETAIL_ID) for e in entry["action_details"]
|
||||||
]
|
]
|
||||||
@ -384,7 +386,7 @@ class AbstractCompensationAPISerializerV1Mixin:
|
|||||||
# If this exact data is already existing, we do not create it new. Instead put it's id in the list of
|
# If this exact data is already existing, we do not create it new. Instead put it's id in the list of
|
||||||
# entries, we will use to set the new actions
|
# entries, we will use to set the new actions
|
||||||
action_entry = obj.actions.filter(
|
action_entry = obj.actions.filter(
|
||||||
action_type__atom_id=action,
|
action_type__in=action_types,
|
||||||
amount=amount,
|
amount=amount,
|
||||||
unit=unit,
|
unit=unit,
|
||||||
comment=comment,
|
comment=comment,
|
||||||
@ -396,13 +398,13 @@ class AbstractCompensationAPISerializerV1Mixin:
|
|||||||
else:
|
else:
|
||||||
# Create and add id to list
|
# Create and add id to list
|
||||||
action_entry = CompensationAction.objects.create(
|
action_entry = CompensationAction.objects.create(
|
||||||
action_type=self._konova_code_from_json(action, CODELIST_COMPENSATION_ACTION_ID),
|
|
||||||
amount=amount,
|
amount=amount,
|
||||||
unit=unit,
|
unit=unit,
|
||||||
comment=comment,
|
comment=comment,
|
||||||
)
|
)
|
||||||
actions.append(action_entry.id)
|
actions.append(action_entry.id)
|
||||||
|
|
||||||
|
action_entry.action_type.set(action_types)
|
||||||
action_entry.action_type_details.set(action_details)
|
action_entry.action_type_details.set(action_details)
|
||||||
obj.actions.set(actions)
|
obj.actions.set(actions)
|
||||||
return obj
|
return obj
|
||||||
@ -438,7 +440,9 @@ class AbstractCompensationAPISerializerV1Mixin:
|
|||||||
"""
|
"""
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"action": self._konova_code_to_json(entry.action_type),
|
"action_types": [
|
||||||
|
self._konova_code_to_json(action) for action in entry.action_type.all()
|
||||||
|
],
|
||||||
"action_details": [
|
"action_details": [
|
||||||
self._konova_code_to_json(detail) for detail in entry.action_type_details.all()
|
self._konova_code_to_json(detail) for detail in entry.action_type_details.all()
|
||||||
],
|
],
|
||||||
|
@ -21,7 +21,6 @@ class AbstractAPIViewV1(AbstractAPIView):
|
|||||||
""" Holds general serialization functions for API v1
|
""" Holds general serialization functions for API v1
|
||||||
|
|
||||||
"""
|
"""
|
||||||
serializer = None
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.lookup = {
|
self.lookup = {
|
||||||
@ -45,11 +44,17 @@ class AbstractAPIViewV1(AbstractAPIView):
|
|||||||
response (JsonResponse)
|
response (JsonResponse)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
self.rpp = int(request.GET.get("rpp", self.rpp))
|
||||||
|
self.page_number = int(request.GET.get("p", self.page_number))
|
||||||
|
|
||||||
|
self.serializer.rpp = self.rpp
|
||||||
|
self.serializer.page_number = self.page_number
|
||||||
|
|
||||||
self.serializer.prepare_lookup(id, self.user)
|
self.serializer.prepare_lookup(id, self.user)
|
||||||
data = self.serializer.fetch_and_serialize()
|
data = self.serializer.fetch_and_serialize()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return self.return_error_response(e, 500)
|
return self._return_error_response(e, 500)
|
||||||
return JsonResponse(data)
|
return self._return_response(request, data)
|
||||||
|
|
||||||
def post(self, request: HttpRequest):
|
def post(self, request: HttpRequest):
|
||||||
""" Handles the POST request
|
""" Handles the POST request
|
||||||
@ -67,7 +72,7 @@ class AbstractAPIViewV1(AbstractAPIView):
|
|||||||
body = json.loads(body)
|
body = json.loads(body)
|
||||||
created_id = self.serializer.create_model_from_json(body, self.user)
|
created_id = self.serializer.create_model_from_json(body, self.user)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return self.return_error_response(e, 500)
|
return self._return_error_response(e, 500)
|
||||||
return JsonResponse({"id": created_id})
|
return JsonResponse({"id": created_id})
|
||||||
|
|
||||||
def put(self, request: HttpRequest, id=None):
|
def put(self, request: HttpRequest, id=None):
|
||||||
@ -87,7 +92,7 @@ class AbstractAPIViewV1(AbstractAPIView):
|
|||||||
body = json.loads(body)
|
body = json.loads(body)
|
||||||
updated_id = self.serializer.update_model_from_json(id, body, self.user)
|
updated_id = self.serializer.update_model_from_json(id, body, self.user)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return self.return_error_response(e, 500)
|
return self._return_error_response(e, 500)
|
||||||
return JsonResponse({"id": updated_id})
|
return JsonResponse({"id": updated_id})
|
||||||
|
|
||||||
def delete(self, request: HttpRequest, id=None):
|
def delete(self, request: HttpRequest, id=None):
|
||||||
@ -104,7 +109,7 @@ class AbstractAPIViewV1(AbstractAPIView):
|
|||||||
try:
|
try:
|
||||||
success = self.serializer.delete_entry(id, self.user)
|
success = self.serializer.delete_entry(id, self.user)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return self.return_error_response(e, 500)
|
return self._return_error_response(e, 500)
|
||||||
return JsonResponse(
|
return JsonResponse(
|
||||||
{
|
{
|
||||||
"success": success,
|
"success": success,
|
||||||
|
@ -19,7 +19,7 @@ from ema.models import Ema
|
|||||||
from intervention.models import Intervention
|
from intervention.models import Intervention
|
||||||
from konova.utils.message_templates import DATA_UNSHARED
|
from konova.utils.message_templates import DATA_UNSHARED
|
||||||
from konova.utils.user_checks import is_default_group_only
|
from konova.utils.user_checks import is_default_group_only
|
||||||
from user.models import User
|
from user.models import User, Team
|
||||||
|
|
||||||
|
|
||||||
class AbstractAPIView(View):
|
class AbstractAPIView(View):
|
||||||
@ -31,10 +31,22 @@ class AbstractAPIView(View):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
user = None
|
user = None
|
||||||
|
serializer = None
|
||||||
|
rpp = 5 # Results per page default
|
||||||
|
page_number = 1 # Page number default
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.response_body_base = {
|
||||||
|
"rpp": None,
|
||||||
|
"p": None,
|
||||||
|
"next": None,
|
||||||
|
"results": None
|
||||||
|
}
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
@ -42,13 +54,14 @@ class AbstractAPIView(View):
|
|||||||
ksp_token = request.headers.get(KSP_TOKEN_HEADER_IDENTIFIER, None)
|
ksp_token = request.headers.get(KSP_TOKEN_HEADER_IDENTIFIER, None)
|
||||||
ksp_user = request.headers.get(KSP_USER_HEADER_IDENTIFIER, None)
|
ksp_user = request.headers.get(KSP_USER_HEADER_IDENTIFIER, None)
|
||||||
self.user = APIUserToken.get_user_from_token(ksp_token, ksp_user)
|
self.user = APIUserToken.get_user_from_token(ksp_token, ksp_user)
|
||||||
|
request.user = self.user
|
||||||
if not self.user.is_default_user():
|
if not self.user.is_default_user():
|
||||||
raise PermissionError("Default permissions required")
|
raise PermissionError("Default permissions required")
|
||||||
except PermissionError as e:
|
except PermissionError as e:
|
||||||
return self.return_error_response(e, 403)
|
return self._return_error_response(e, 403)
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def return_error_response(self, error, status_code=500):
|
def _return_error_response(self, error, status_code=500):
|
||||||
""" Returns an error as JsonReponse
|
""" Returns an error as JsonReponse
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -68,6 +81,31 @@ class AbstractAPIView(View):
|
|||||||
status=status_code
|
status=status_code
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _return_response(self, request: HttpRequest, data):
|
||||||
|
""" Returns all important data into a response object
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (HttpRequest): The incoming request
|
||||||
|
data (dict): The serialized data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
response (JsonResponse): The response to be returned
|
||||||
|
"""
|
||||||
|
response = self.response_body_base
|
||||||
|
next_page = self.page_number + 1
|
||||||
|
next_page = next_page if next_page in self.serializer.paginator.page_range else None
|
||||||
|
if next_page is not None:
|
||||||
|
next_url = request.build_absolute_uri(
|
||||||
|
request.path + f"?rpp={self.rpp}&p={next_page}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
next_url = None
|
||||||
|
response["rpp"] = self.rpp
|
||||||
|
response["p"] = self.page_number
|
||||||
|
response["next"] = next_url
|
||||||
|
response["results"] = data
|
||||||
|
return JsonResponse(response)
|
||||||
|
|
||||||
|
|
||||||
class InterventionCheckAPIView(AbstractAPIView):
|
class InterventionCheckAPIView(AbstractAPIView):
|
||||||
|
|
||||||
@ -82,14 +120,14 @@ class InterventionCheckAPIView(AbstractAPIView):
|
|||||||
response (JsonResponse)
|
response (JsonResponse)
|
||||||
"""
|
"""
|
||||||
if not self.user.is_zb_user():
|
if not self.user.is_zb_user():
|
||||||
return self.return_error_response("Permission not granted", 403)
|
return self._return_error_response("Permission not granted", 403)
|
||||||
try:
|
try:
|
||||||
obj = Intervention.objects.get(
|
obj = Intervention.objects.get(
|
||||||
id=id,
|
id=id,
|
||||||
users__in=[self.user]
|
users__in=[self.user]
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return self.return_error_response(e)
|
return self._return_error_response(e)
|
||||||
|
|
||||||
all_valid, check_details = self.run_quality_checks(obj)
|
all_valid, check_details = self.run_quality_checks(obj)
|
||||||
|
|
||||||
@ -160,13 +198,21 @@ class AbstractModelShareAPIView(AbstractAPIView):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
users = self._get_shared_users_of_object(id)
|
users = self._get_shared_users_of_object(id)
|
||||||
|
teams = self._get_shared_teams_of_object(id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return self.return_error_response(e)
|
return self._return_error_response(e)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"users": [
|
"users": [
|
||||||
user.username for user in users
|
user.username for user in users
|
||||||
]
|
],
|
||||||
|
"teams": [
|
||||||
|
{
|
||||||
|
"id": team.id,
|
||||||
|
"name": team.name,
|
||||||
|
}
|
||||||
|
for team in teams
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
return JsonResponse(data)
|
return JsonResponse(data)
|
||||||
@ -185,7 +231,7 @@ class AbstractModelShareAPIView(AbstractAPIView):
|
|||||||
try:
|
try:
|
||||||
success = self._process_put_body(request.body, id)
|
success = self._process_put_body(request.body, id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return self.return_error_response(e)
|
return self._return_error_response(e)
|
||||||
data = {
|
data = {
|
||||||
"success": success,
|
"success": success,
|
||||||
}
|
}
|
||||||
@ -220,6 +266,22 @@ class AbstractModelShareAPIView(AbstractAPIView):
|
|||||||
users = obj.shared_users
|
users = obj.shared_users
|
||||||
return users
|
return users
|
||||||
|
|
||||||
|
def _get_shared_teams_of_object(self, id) -> QuerySet:
|
||||||
|
""" Check permissions and get the teams
|
||||||
|
|
||||||
|
Args:
|
||||||
|
id (str): The object's id
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
users (QuerySet)
|
||||||
|
"""
|
||||||
|
obj = self.model.objects.get(
|
||||||
|
id=id
|
||||||
|
)
|
||||||
|
self._check_user_has_shared_access(obj)
|
||||||
|
teams = obj.shared_teams
|
||||||
|
return teams
|
||||||
|
|
||||||
def _process_put_body(self, body: bytes, id: str):
|
def _process_put_body(self, body: bytes, id: str):
|
||||||
""" Reads the body data, performs validity checks and sets the new users
|
""" Reads the body data, performs validity checks and sets the new users
|
||||||
|
|
||||||
@ -233,19 +295,26 @@ class AbstractModelShareAPIView(AbstractAPIView):
|
|||||||
obj = self.model.objects.get(id=id)
|
obj = self.model.objects.get(id=id)
|
||||||
self._check_user_has_shared_access(obj)
|
self._check_user_has_shared_access(obj)
|
||||||
|
|
||||||
new_users = json.loads(body.decode("utf-8"))
|
content = json.loads(body.decode("utf-8"))
|
||||||
new_users = new_users.get("users", [])
|
new_users = content.get("users", [])
|
||||||
if len(new_users) == 0:
|
if len(new_users) == 0:
|
||||||
raise ValueError("Shared user list must not be empty!")
|
raise ValueError("Shared user list must not be empty!")
|
||||||
|
new_teams = content.get("teams", [])
|
||||||
|
|
||||||
# Eliminate duplicates
|
# Eliminate duplicates
|
||||||
new_users = list(dict.fromkeys(new_users))
|
new_users = list(dict.fromkeys(new_users))
|
||||||
|
new_teams = list(dict.fromkeys(new_teams))
|
||||||
|
|
||||||
# Make sure each of these names exist as a user
|
# Make sure each of these names exist as a user
|
||||||
new_users_objs = []
|
new_users_objs = []
|
||||||
for user in new_users:
|
for user in new_users:
|
||||||
new_users_objs.append(User.objects.get(username=user))
|
new_users_objs.append(User.objects.get(username=user))
|
||||||
|
|
||||||
|
# Make sure each of these names exist as a user
|
||||||
|
new_teams_objs = []
|
||||||
|
for team_name in new_teams:
|
||||||
|
new_teams_objs.append(Team.objects.get(name=team_name))
|
||||||
|
|
||||||
if is_default_group_only(self.user):
|
if is_default_group_only(self.user):
|
||||||
# Default only users are not allowed to remove other users from having access. They can only add new ones!
|
# 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(
|
new_users_to_be_added = User.objects.filter(
|
||||||
@ -254,7 +323,16 @@ class AbstractModelShareAPIView(AbstractAPIView):
|
|||||||
id__in=obj.shared_users
|
id__in=obj.shared_users
|
||||||
)
|
)
|
||||||
new_users_objs = obj.shared_users.union(new_users_to_be_added)
|
new_users_objs = obj.shared_users.union(new_users_to_be_added)
|
||||||
obj.share_with_list(new_users_objs)
|
|
||||||
|
new_teams_to_be_added = Team.objects.filter(
|
||||||
|
name__in=new_teams
|
||||||
|
).exclude(
|
||||||
|
id__in=obj.shared_teams
|
||||||
|
)
|
||||||
|
new_teams_objs = obj.shared_teams.union(new_teams_to_be_added)
|
||||||
|
|
||||||
|
obj.share_with_user_list(new_users_objs)
|
||||||
|
obj.share_with_team_list(new_teams_objs)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -65,6 +65,26 @@ class KonovaCode(models.Model):
|
|||||||
ret_val += ", " + self.parent.long_name
|
ret_val += ", " + self.parent.long_name
|
||||||
return ret_val
|
return ret_val
|
||||||
|
|
||||||
|
def add_children(self):
|
||||||
|
""" Adds all children (resurcively until leaf) as .children to the KonovaCode
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
code (KonovaCode): The manipulated KonovaCode instance
|
||||||
|
"""
|
||||||
|
if self.is_leaf:
|
||||||
|
return None
|
||||||
|
|
||||||
|
children = KonovaCode.objects.filter(
|
||||||
|
code_lists__in=self.code_lists.all(),
|
||||||
|
parent=self
|
||||||
|
).order_by(
|
||||||
|
"long_name"
|
||||||
|
)
|
||||||
|
self.children = children
|
||||||
|
for child in children:
|
||||||
|
child.add_children()
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
class KonovaCodeList(models.Model):
|
class KonovaCodeList(models.Model):
|
||||||
"""
|
"""
|
||||||
|
@ -13,7 +13,8 @@ CODELIST_BASE_URL = "https://codelisten.naturschutz.rlp.de/repository/referenzli
|
|||||||
CODELIST_INTERVENTION_HANDLER_ID = 903 # CLMassnahmeträger
|
CODELIST_INTERVENTION_HANDLER_ID = 903 # CLMassnahmeträger
|
||||||
CODELIST_CONSERVATION_OFFICE_ID = 907 # CLNaturschutzbehörden
|
CODELIST_CONSERVATION_OFFICE_ID = 907 # CLNaturschutzbehörden
|
||||||
CODELIST_REGISTRATION_OFFICE_ID = 1053 # CLZulassungsbehörden
|
CODELIST_REGISTRATION_OFFICE_ID = 1053 # CLZulassungsbehörden
|
||||||
CODELIST_BIOTOPES_ID = 974 # CL_EIV_Biotoptypen
|
CODELIST_BIOTOPES_ID = 654 # CL_Biotoptypen
|
||||||
|
CODELIST_AFTER_STATE_BIOTOPES__ID = 974 # CL-KSP_ZielBiotoptypen - USAGE HAS BEEN DROPPED IN 2022 IN FAVOR OF 654
|
||||||
CODELIST_BIOTOPES_EXTRA_CODES_ID = 975 # CLZusatzbezeichnung
|
CODELIST_BIOTOPES_EXTRA_CODES_ID = 975 # CLZusatzbezeichnung
|
||||||
CODELIST_LAW_ID = 1048 # CLVerfahrensrecht
|
CODELIST_LAW_ID = 1048 # CLVerfahrensrecht
|
||||||
CODELIST_PROCESS_TYPE_ID = 44382 # CLVerfahrenstyp
|
CODELIST_PROCESS_TYPE_ID = 44382 # CLVerfahrenstyp
|
||||||
|
@ -59,8 +59,9 @@ class CheckboxCompensationTableFilter(CheckboxTableFilter):
|
|||||||
"""
|
"""
|
||||||
if not value:
|
if not value:
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
intervention__users__in=[self.user], # requesting user has access
|
Q(intervention__users__in=[self.user]) | # requesting user has access
|
||||||
)
|
Q(intervention__teams__users__in=[self.user])
|
||||||
|
).distinct()
|
||||||
else:
|
else:
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
@ -127,24 +128,6 @@ class CheckboxEcoAccountTableFilter(CheckboxTableFilter):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def filter_show_all(self, queryset, name, value) -> QuerySet:
|
|
||||||
""" Filters queryset depending on value of 'show_all' setting
|
|
||||||
|
|
||||||
Args:
|
|
||||||
queryset ():
|
|
||||||
name ():
|
|
||||||
value ():
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
|
|
||||||
"""
|
|
||||||
if not value:
|
|
||||||
return queryset.filter(
|
|
||||||
users__in=[self.user], # requesting user has access
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
def filter_only_show_unrecorded(self, queryset, name, value) -> QuerySet:
|
def filter_only_show_unrecorded(self, queryset, name, value) -> QuerySet:
|
||||||
""" Filters queryset depending on value of 'show_recorded' setting
|
""" Filters queryset depending on value of 'show_recorded' setting
|
||||||
|
|
||||||
|
@ -400,7 +400,7 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
|
|||||||
comment=comment,
|
comment=comment,
|
||||||
legal=legal
|
legal=legal
|
||||||
)
|
)
|
||||||
acc.share_with(user)
|
acc.share_with_user(user)
|
||||||
|
|
||||||
# Add the log entry to the main objects log list
|
# Add the log entry to the main objects log list
|
||||||
acc.log.add(action)
|
acc.log.add(action)
|
||||||
|
@ -17,10 +17,11 @@ from codelist.models import KonovaCode
|
|||||||
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID, \
|
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID, \
|
||||||
CODELIST_COMPENSATION_ACTION_DETAIL_ID
|
CODELIST_COMPENSATION_ACTION_DETAIL_ID
|
||||||
from compensation.models import CompensationDocument, EcoAccountDocument
|
from compensation.models import CompensationDocument, EcoAccountDocument
|
||||||
|
from intervention.inputs import CompensationActionTreeCheckboxSelectMultiple
|
||||||
from konova.contexts import BaseContext
|
from konova.contexts import BaseContext
|
||||||
from konova.forms import BaseModalForm, NewDocumentModalForm, RemoveModalForm
|
from konova.forms import BaseModalForm, NewDocumentModalForm, RemoveModalForm
|
||||||
from konova.models import DeadlineType
|
from konova.models import DeadlineType
|
||||||
from konova.utils.message_templates import FORM_INVALID, ADDED_COMPENSATION_STATE, ADDED_DEADLINE, \
|
from konova.utils.message_templates import FORM_INVALID, ADDED_COMPENSATION_STATE, \
|
||||||
ADDED_COMPENSATION_ACTION, PAYMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, DEADLINE_EDITED
|
ADDED_COMPENSATION_ACTION, PAYMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, DEADLINE_EDITED
|
||||||
|
|
||||||
|
|
||||||
@ -405,22 +406,13 @@ class NewActionModalForm(BaseModalForm):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
from compensation.models import UnitChoices
|
from compensation.models import UnitChoices
|
||||||
action_type = forms.ModelChoiceField(
|
action_type = forms.MultipleChoiceField(
|
||||||
label=_("Action Type"),
|
label=_("Action Type"),
|
||||||
label_suffix="",
|
label_suffix="",
|
||||||
required=True,
|
required=True,
|
||||||
help_text=_("Select the action type"),
|
help_text=_("An action can consist of multiple different action types. All the selected action types are expected to be performed according to the amount and unit below on this form."),
|
||||||
queryset=KonovaCode.objects.filter(
|
choices=[],
|
||||||
is_archived=False,
|
widget=CompensationActionTreeCheckboxSelectMultiple(),
|
||||||
is_leaf=True,
|
|
||||||
code_lists__in=[CODELIST_COMPENSATION_ACTION_ID],
|
|
||||||
),
|
|
||||||
widget=autocomplete.ModelSelect2(
|
|
||||||
url="codes-compensation-action-autocomplete",
|
|
||||||
attrs={
|
|
||||||
"data-placeholder": _("Action"),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
action_type_details = forms.ModelMultipleChoiceField(
|
action_type_details = forms.ModelMultipleChoiceField(
|
||||||
label=_("Action Type detail"),
|
label=_("Action Type detail"),
|
||||||
@ -482,6 +474,16 @@ class NewActionModalForm(BaseModalForm):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.form_title = _("New action")
|
self.form_title = _("New action")
|
||||||
self.form_caption = _("Insert data for the new action")
|
self.form_caption = _("Insert data for the new action")
|
||||||
|
choices =KonovaCode.objects.filter(
|
||||||
|
code_lists__in=[CODELIST_COMPENSATION_ACTION_ID],
|
||||||
|
is_archived=False,
|
||||||
|
is_leaf=True,
|
||||||
|
).values_list("id", flat=True)
|
||||||
|
choices = [
|
||||||
|
(choice, choice)
|
||||||
|
for choice in choices
|
||||||
|
]
|
||||||
|
self.fields["action_type"].choices = choices
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
action = self.instance.add_action(self)
|
action = self.instance.add_action(self)
|
||||||
@ -496,7 +498,7 @@ class EditCompensationActionModalForm(NewActionModalForm):
|
|||||||
self.action = kwargs.pop("action", None)
|
self.action = kwargs.pop("action", None)
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
form_data = {
|
form_data = {
|
||||||
"action_type": self.action.action_type,
|
"action_type": list(self.action.action_type.values_list("id", flat=True)),
|
||||||
"action_type_details": self.action.action_type_details.all(),
|
"action_type_details": self.action.action_type_details.all(),
|
||||||
"amount": self.action.amount,
|
"amount": self.action.amount,
|
||||||
"unit": self.action.unit,
|
"unit": self.action.unit,
|
||||||
@ -506,7 +508,7 @@ class EditCompensationActionModalForm(NewActionModalForm):
|
|||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
action = self.action
|
action = self.action
|
||||||
action.action_type = self.cleaned_data.get("action_type", None)
|
action.action_type.set(self.cleaned_data.get("action_type", []))
|
||||||
action.action_type_details.set(self.cleaned_data.get("action_type_details", []))
|
action.action_type_details.set(self.cleaned_data.get("action_type_details", []))
|
||||||
action.amount = self.cleaned_data.get("amount", None)
|
action.amount = self.cleaned_data.get("amount", None)
|
||||||
action.unit = self.cleaned_data.get("unit", None)
|
action.unit = self.cleaned_data.get("unit", None)
|
||||||
|
@ -8,17 +8,6 @@ Created on: 14.10.21
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
class CompensationActionManager(models.Manager):
|
|
||||||
""" Holds default db fetch setting for this model type
|
|
||||||
|
|
||||||
"""
|
|
||||||
def get_queryset(self):
|
|
||||||
return super().get_queryset().select_related(
|
|
||||||
"action_type",
|
|
||||||
"action_type__parent"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CompensationStateManager(models.Manager):
|
class CompensationStateManager(models.Manager):
|
||||||
""" Holds default db fetch setting for this model type
|
""" Holds default db fetch setting for this model type
|
||||||
|
|
||||||
|
42
compensation/migrations/0004_auto_20220210_1402.py
Normal file
42
compensation/migrations/0004_auto_20220210_1402.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# Generated by Django 3.1.3 on 2022-02-10 13:02
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_actions(apps, schema_editor):
|
||||||
|
CompensationAction = apps.get_model('compensation', 'CompensationAction')
|
||||||
|
actions = CompensationAction.objects.all()
|
||||||
|
|
||||||
|
for action in actions:
|
||||||
|
action_type = action.action_type or []
|
||||||
|
action.action_type_tmp.set([action_type])
|
||||||
|
action.save()
|
||||||
|
|
||||||
|
if not action.action_type_tmp.count() > 0:
|
||||||
|
raise ValueError("Migration of actions did not work! Stoped before data loss!")
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('codelist', '0001_initial'),
|
||||||
|
('compensation', '0003_auto_20220202_0846'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='compensationaction',
|
||||||
|
name='action_type_tmp',
|
||||||
|
field=models.ManyToManyField(blank=True, limit_choices_to={'code_lists__in': [1026], 'is_archived': False, 'is_selectable': True}, related_name='_compensationaction_action_type_+', to='codelist.KonovaCode'),
|
||||||
|
),
|
||||||
|
migrations.RunPython(migrate_actions),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='compensationaction',
|
||||||
|
name='action_type',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='compensationaction',
|
||||||
|
old_name='action_type_tmp',
|
||||||
|
new_name='action_type',
|
||||||
|
)
|
||||||
|
]
|
46
compensation/migrations/0005_auto_20220218_0917.py
Normal file
46
compensation/migrations/0005_auto_20220218_0917.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# Generated by Django 3.1.3 on 2022-02-18 08:17
|
||||||
|
|
||||||
|
from django.db import migrations, models, transaction
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_AFTER_STATE_BIOTOPES__ID
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_entries_974_to_654(apps, schema_editor):
|
||||||
|
CompensationState = apps.get_model("compensation", "CompensationState")
|
||||||
|
KonovaCode = apps.get_model("codelist", "KonovaCode")
|
||||||
|
all_states = CompensationState.objects.all()
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
for state in all_states:
|
||||||
|
code_from_654 = KonovaCode.objects.get(
|
||||||
|
short_name=state.biotope_type.short_name,
|
||||||
|
code_lists__in=[CODELIST_BIOTOPES_ID],
|
||||||
|
is_archived=False,
|
||||||
|
is_leaf=True,
|
||||||
|
)
|
||||||
|
state.biotope_type = code_from_654
|
||||||
|
state.save()
|
||||||
|
|
||||||
|
old_list_states = CompensationState.objects.filter(
|
||||||
|
biotope_type__code_lists__in=[CODELIST_AFTER_STATE_BIOTOPES__ID]
|
||||||
|
)
|
||||||
|
if old_list_states.count() > 0:
|
||||||
|
raise Exception("Still unmigrated values!")
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('codelist', '0001_initial'),
|
||||||
|
('compensation', '0004_auto_20220210_1402'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(migrate_entries_974_to_654),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='compensationstate',
|
||||||
|
name='biotope_type',
|
||||||
|
field=models.ForeignKey(blank=True, limit_choices_to={'code_lists__in': [654], 'is_archived': False, 'is_selectable': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='codelist.konovacode'),
|
||||||
|
),
|
||||||
|
]
|
19
compensation/migrations/0006_ecoaccount_teams.py
Normal file
19
compensation/migrations/0006_ecoaccount_teams.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 3.1.3 on 2022-02-18 09:13
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('user', '0003_team'),
|
||||||
|
('compensation', '0005_auto_20220218_0917'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='ecoaccount',
|
||||||
|
name='teams',
|
||||||
|
field=models.ManyToManyField(help_text='Teams having access (data shared with)', to='user.Team'),
|
||||||
|
),
|
||||||
|
]
|
@ -10,9 +10,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
from codelist.models import KonovaCode
|
from codelist.models import KonovaCode
|
||||||
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_DETAIL_ID
|
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_DETAIL_ID
|
||||||
from compensation.managers import CompensationActionManager
|
|
||||||
from konova.models import BaseResource
|
from konova.models import BaseResource
|
||||||
from konova.utils.message_templates import COMPENSATION_ACTION_REMOVED
|
|
||||||
|
|
||||||
|
|
||||||
class UnitChoices(models.TextChoices):
|
class UnitChoices(models.TextChoices):
|
||||||
@ -31,10 +29,8 @@ class CompensationAction(BaseResource):
|
|||||||
"""
|
"""
|
||||||
Compensations include actions like planting trees, refreshing rivers and so on.
|
Compensations include actions like planting trees, refreshing rivers and so on.
|
||||||
"""
|
"""
|
||||||
action_type = models.ForeignKey(
|
action_type = models.ManyToManyField(
|
||||||
KonovaCode,
|
KonovaCode,
|
||||||
on_delete=models.SET_NULL,
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
blank=True,
|
||||||
limit_choices_to={
|
limit_choices_to={
|
||||||
"code_lists__in": [CODELIST_COMPENSATION_ACTION_ID],
|
"code_lists__in": [CODELIST_COMPENSATION_ACTION_ID],
|
||||||
@ -57,10 +53,8 @@ class CompensationAction(BaseResource):
|
|||||||
unit = models.CharField(max_length=100, null=True, blank=True, choices=UnitChoices.choices)
|
unit = models.CharField(max_length=100, null=True, blank=True, choices=UnitChoices.choices)
|
||||||
comment = models.TextField(blank=True, null=True, help_text="Additional comment")
|
comment = models.TextField(blank=True, null=True, help_text="Additional comment")
|
||||||
|
|
||||||
objects = CompensationActionManager()
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.action_type} | {self.amount} {self.unit}"
|
return f"{self.action_type.all()} | {self.amount} {self.unit}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unit_humanize(self):
|
def unit_humanize(self):
|
||||||
|
@ -8,7 +8,7 @@ Created on: 16.11.21
|
|||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from user.models import User
|
from user.models import User, Team
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.db.models import QuerySet, Sum
|
from django.db.models import QuerySet, Sum
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
@ -104,12 +104,12 @@ class AbstractCompensation(BaseObject, GeoReferencedMixin):
|
|||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
user_action = UserActionLogEntry.get_created_action(user)
|
user_action = UserActionLogEntry.get_created_action(user)
|
||||||
comp_action = CompensationAction.objects.create(
|
comp_action = CompensationAction.objects.create(
|
||||||
action_type=form_data["action_type"],
|
|
||||||
amount=form_data["amount"],
|
amount=form_data["amount"],
|
||||||
unit=form_data["unit"],
|
unit=form_data["unit"],
|
||||||
comment=form_data["comment"],
|
comment=form_data["comment"],
|
||||||
created=user_action,
|
created=user_action,
|
||||||
)
|
)
|
||||||
|
comp_action.action_type.set(form_data.get("action_type", []))
|
||||||
comp_action_details = form_data["action_type_details"]
|
comp_action_details = form_data["action_type_details"]
|
||||||
comp_action.action_type_details.set(comp_action_details)
|
comp_action.action_type_details.set(comp_action_details)
|
||||||
self.actions.add(comp_action)
|
self.actions.add(comp_action)
|
||||||
@ -299,7 +299,7 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
|
|||||||
# Compensations inherit their shared state from the interventions
|
# Compensations inherit their shared state from the interventions
|
||||||
return self.intervention.is_shared_with(user)
|
return self.intervention.is_shared_with(user)
|
||||||
|
|
||||||
def share_with(self, user: User):
|
def share_with_user(self, user: User):
|
||||||
""" Adds user to list of shared access users
|
""" Adds user to list of shared access users
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -308,10 +308,9 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
|
|||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not self.intervention.is_shared_with(user):
|
self.intervention.users.add(user)
|
||||||
self.intervention.users.add(user)
|
|
||||||
|
|
||||||
def share_with_list(self, user_list: list):
|
def share_with_user_list(self, user_list: list):
|
||||||
""" Sets the list of shared access users
|
""" Sets the list of shared access users
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -322,6 +321,28 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
|
|||||||
"""
|
"""
|
||||||
self.intervention.users.set(user_list)
|
self.intervention.users.set(user_list)
|
||||||
|
|
||||||
|
def share_with_team(self, team: Team):
|
||||||
|
""" Adds team to list of shared access teams
|
||||||
|
|
||||||
|
Args:
|
||||||
|
team (Team): The team to be added to the object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.intervention.teams.add(team)
|
||||||
|
|
||||||
|
def share_with_team_list(self, team_list: list):
|
||||||
|
""" Sets the list of shared access teams
|
||||||
|
|
||||||
|
Args:
|
||||||
|
team_list (list): The teams to be added to the object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.intervention.teams.set(team_list)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def shared_users(self) -> QuerySet:
|
def shared_users(self) -> QuerySet:
|
||||||
""" Shortcut for fetching the users which have shared access on this object
|
""" Shortcut for fetching the users which have shared access on this object
|
||||||
@ -331,6 +352,15 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
|
|||||||
"""
|
"""
|
||||||
return self.intervention.users.all()
|
return self.intervention.users.all()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def shared_teams(self) -> QuerySet:
|
||||||
|
""" Shortcut for fetching the teams which have shared access on this object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
users (QuerySet)
|
||||||
|
"""
|
||||||
|
return self.intervention.teams.all()
|
||||||
|
|
||||||
def get_documents(self) -> QuerySet:
|
def get_documents(self) -> QuerySet:
|
||||||
""" Getter for all documents of a compensation
|
""" Getter for all documents of a compensation
|
||||||
|
|
||||||
|
@ -47,13 +47,15 @@
|
|||||||
{% for action in actions %}
|
{% for action in actions %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="">
|
<td class="">
|
||||||
<span>{{ action.action_type }}</span>
|
{% for type in action.action_type.all %}
|
||||||
{% if action.action_type_details.count > 0 %}
|
<div> {{type.parent.parent.long_name}} {% fa5_icon 'angle-right' %} {{type.parent.long_name}} {% fa5_icon 'angle-right' %} {{type.long_name}} </div>
|
||||||
<br>
|
<hr>
|
||||||
{% for detail in action.action_type_details.all %}
|
{% endfor %}
|
||||||
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
{% for detail in action.action_type_details.all %}
|
||||||
{% endfor %}
|
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
||||||
{% endif %}
|
{% empty %}
|
||||||
|
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No action type details' %}">{% trans 'No action type details' %}</span>
|
||||||
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td class="">{{ action.amount|floatformat:2|intcomma }} {{ action.unit_humanize }}</td>
|
<td class="">{{ action.amount|floatformat:2|intcomma }} {{ action.unit_humanize }}</td>
|
||||||
<td class="">
|
<td class="">
|
||||||
|
@ -48,13 +48,13 @@
|
|||||||
{% for state in after_states %}
|
{% for state in after_states %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<span>{{ state.biotope_type }}</span>
|
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
|
||||||
{% if state.biotope_type_details.count > 0 %}
|
<br>
|
||||||
<br>
|
{% for detail in state.biotope_type_details.all %}
|
||||||
{% for detail in state.biotope_type_details.all %}
|
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
||||||
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
{% empty %}
|
||||||
{% endfor %}
|
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
|
||||||
{% endif %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ state.surface|floatformat:2 }} m²</td>
|
<td>{{ state.surface|floatformat:2 }} m²</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
|
@ -48,13 +48,13 @@
|
|||||||
{% for state in before_states %}
|
{% for state in before_states %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<span>{{ state.biotope_type }}</span>
|
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
|
||||||
{% if state.biotope_type_details.count > 0 %}
|
<br>
|
||||||
<br>
|
{% for detail in state.biotope_type_details.all %}
|
||||||
{% for detail in state.biotope_type_details.all %}
|
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
||||||
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
{% empty %}
|
||||||
{% endfor %}
|
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
|
||||||
{% endif %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ state.surface|floatformat:2 }} m²</td>
|
<td>{{ state.surface|floatformat:2 }} m²</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
{% load i18n l10n static fontawesome_5 humanize ksp_filters %}
|
{% load i18n l10n static fontawesome_5 humanize ksp_filters %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
|
|
||||||
{% comment %}
|
{% comment %}
|
||||||
dal documentation (django-autocomplete-light) states using form.media for adding needed scripts.
|
dal documentation (django-autocomplete-light) states using form.media for adding needed scripts.
|
||||||
This does not work properly with modal forms, as the scripts are not loaded properly inside the modal.
|
This does not work properly with modal forms, as the scripts are not loaded properly inside the modal.
|
||||||
@ -97,6 +98,10 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans 'Shared with' %}</th>
|
<th scope="row">{% trans 'Shared with' %}</th>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
|
{% for team in obj.intervention.teams.all %}
|
||||||
|
{% include 'user/includes/team_data_modal_button.html' %}
|
||||||
|
{% endfor %}
|
||||||
|
<hr>
|
||||||
{% for user in obj.intervention.users.all %}
|
{% for user in obj.intervention.users.all %}
|
||||||
{% include 'user/includes/contact_modal_button.html' %}
|
{% include 'user/includes/contact_modal_button.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -46,13 +46,15 @@
|
|||||||
{% for action in actions %}
|
{% for action in actions %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="">
|
<td class="">
|
||||||
<span>{{ action.action_type }}</span>
|
{% for type in action.action_type.all %}
|
||||||
{% if action.action_type_details.count > 0 %}
|
<div> {{type.parent.parent.long_name}} {% fa5_icon 'angle-right' %} {{type.parent.long_name}} {% fa5_icon 'angle-right' %} {{type.long_name}} </div>
|
||||||
<br>
|
<hr>
|
||||||
{% for detail in action.action_type_details.all %}
|
{% endfor %}
|
||||||
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
{% for detail in action.action_type_details.all %}
|
||||||
{% endfor %}
|
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
||||||
{% endif %}
|
{% empty %}
|
||||||
|
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No action type details' %}">{% trans 'No action type details' %}</span>
|
||||||
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td class="">{{ action.amount|floatformat:2|intcomma }} {{ action.unit_humanize }}</td>
|
<td class="">{{ action.amount|floatformat:2|intcomma }} {{ action.unit_humanize }}</td>
|
||||||
<td class="">
|
<td class="">
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and obj.recorded %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-deduction' obj.id %}" title="{% trans 'Add new deduction' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-deduction' obj.id %}" title="{% trans 'Add new deduction' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'tree' %}
|
{% fa5_icon 'tree' %}
|
||||||
@ -61,7 +61,7 @@
|
|||||||
<td class="align-middle">{{ deduction.surface|floatformat:2|intcomma }} m²</td>
|
<td class="align-middle">{{ deduction.surface|floatformat:2|intcomma }} m²</td>
|
||||||
<td class="align-middle">{{ deduction.created.timestamp|default_if_none:""|naturalday}}</td>
|
<td class="align-middle">{{ deduction.created.timestamp|default_if_none:""|naturalday}}</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and has_access or is_default_member and user in deduction.intervention.shared_users %}
|
||||||
<button data-form-url="{% url 'compensation:acc:edit-deduction' deduction.account.id deduction.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit Deduction' %}">
|
<button data-form-url="{% url 'compensation:acc:edit-deduction' deduction.account.id deduction.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit Deduction' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -48,13 +48,13 @@
|
|||||||
{% for state in after_states %}
|
{% for state in after_states %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<span>{{ state.biotope_type }}</span>
|
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
|
||||||
{% if state.biotope_type_details.count > 0 %}
|
<br>
|
||||||
<br>
|
{% for detail in state.biotope_type_details.all %}
|
||||||
{% for detail in state.biotope_type_details.all %}
|
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
||||||
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
{% empty %}
|
||||||
{% endfor %}
|
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
|
||||||
{% endif %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ state.surface|floatformat:2 }} m²</td>
|
<td>{{ state.surface|floatformat:2 }} m²</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
|
@ -48,13 +48,13 @@
|
|||||||
{% for state in before_states %}
|
{% for state in before_states %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<span>{{ state.biotope_type }}</span>
|
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
|
||||||
{% if state.biotope_type_details.count > 0 %}
|
<br>
|
||||||
<br>
|
{% for detail in state.biotope_type_details.all %}
|
||||||
{% for detail in state.biotope_type_details.all %}
|
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
||||||
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
{% empty %}
|
||||||
{% endfor %}
|
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
|
||||||
{% endif %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ state.surface|floatformat:2 }} m²</td>
|
<td>{{ state.surface|floatformat:2 }} m²</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
{% load i18n l10n static fontawesome_5 humanize %}
|
{% load i18n l10n static fontawesome_5 humanize %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
|
|
||||||
{% comment %}
|
{% comment %}
|
||||||
dal documentation (django-autocomplete-light) states using form.media for adding needed scripts.
|
dal documentation (django-autocomplete-light) states using form.media for adding needed scripts.
|
||||||
This does not work properly with modal forms, as the scripts are not loaded properly inside the modal.
|
This does not work properly with modal forms, as the scripts are not loaded properly inside the modal.
|
||||||
@ -80,6 +81,10 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans 'Shared with' %}</th>
|
<th scope="row">{% trans 'Shared with' %}</th>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
|
{% for team in obj.teams.all %}
|
||||||
|
{% include 'user/includes/team_data_modal_button.html' %}
|
||||||
|
{% endfor %}
|
||||||
|
<hr>
|
||||||
{% for user in obj.users.all %}
|
{% for user in obj.users.all %}
|
||||||
{% include 'user/includes/contact_modal_button.html' %}
|
{% include 'user/includes/contact_modal_button.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -103,7 +103,7 @@ class CompensationViewTestCase(BaseViewTestCase):
|
|||||||
client = Client()
|
client = Client()
|
||||||
client.login(username=self.superuser.username, password=self.superuser_pw)
|
client.login(username=self.superuser.username, password=self.superuser_pw)
|
||||||
self.superuser.groups.set([])
|
self.superuser.groups.set([])
|
||||||
self.intervention.share_with_list([self.superuser])
|
self.intervention.share_with_user_list([self.superuser])
|
||||||
|
|
||||||
# Since the user has no groups, it does not matter that data has been shared. There SHOULD not be any difference
|
# Since the user has no groups, it does not matter that data has been shared. There SHOULD not be any difference
|
||||||
# to a user without access, since the important permissions are missing
|
# to a user without access, since the important permissions are missing
|
||||||
@ -143,7 +143,7 @@ class CompensationViewTestCase(BaseViewTestCase):
|
|||||||
client.login(username=self.superuser.username, password=self.superuser_pw)
|
client.login(username=self.superuser.username, password=self.superuser_pw)
|
||||||
self.superuser.groups.set([])
|
self.superuser.groups.set([])
|
||||||
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
|
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
|
||||||
self.intervention.share_with_list([])
|
self.intervention.share_with_user_list([])
|
||||||
|
|
||||||
# Since the user has no groups, it does not matter that data is unshared. There SHOULD not be any difference
|
# Since the user has no groups, it does not matter that data is unshared. There SHOULD not be any difference
|
||||||
# to a user having shared access, since all important permissions are missing
|
# to a user having shared access, since all important permissions are missing
|
||||||
@ -185,7 +185,7 @@ class CompensationViewTestCase(BaseViewTestCase):
|
|||||||
group = self.groups.get(name=DEFAULT_GROUP)
|
group = self.groups.get(name=DEFAULT_GROUP)
|
||||||
self.superuser.groups.set([group])
|
self.superuser.groups.set([group])
|
||||||
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
|
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
|
||||||
self.intervention.share_with_list([self.superuser])
|
self.intervention.share_with_user_list([self.superuser])
|
||||||
|
|
||||||
success_urls = [
|
success_urls = [
|
||||||
self.index_url,
|
self.index_url,
|
||||||
@ -221,7 +221,7 @@ class CompensationViewTestCase(BaseViewTestCase):
|
|||||||
group = self.groups.get(name=DEFAULT_GROUP)
|
group = self.groups.get(name=DEFAULT_GROUP)
|
||||||
self.superuser.groups.set([group])
|
self.superuser.groups.set([group])
|
||||||
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
|
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
|
||||||
self.intervention.share_with_list([])
|
self.intervention.share_with_user_list([])
|
||||||
|
|
||||||
success_urls = [
|
success_urls = [
|
||||||
self.index_url,
|
self.index_url,
|
||||||
|
@ -25,7 +25,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
|
|||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
# Give the user shared access to the dummy intervention -> inherits the access to the compensation
|
# Give the user shared access to the dummy intervention -> inherits the access to the compensation
|
||||||
self.intervention.share_with(self.superuser)
|
self.intervention.share_with_user(self.superuser)
|
||||||
|
|
||||||
# Make sure the intervention itself would be fine with valid data
|
# Make sure the intervention itself would be fine with valid data
|
||||||
self.intervention = self.fill_out_intervention(self.intervention)
|
self.intervention = self.fill_out_intervention(self.intervention)
|
||||||
|
@ -78,7 +78,7 @@ class EcoAccountViewTestCase(CompensationViewTestCase):
|
|||||||
client = Client()
|
client = Client()
|
||||||
client.login(username=self.superuser.username, password=self.superuser_pw)
|
client.login(username=self.superuser.username, password=self.superuser_pw)
|
||||||
self.superuser.groups.set([])
|
self.superuser.groups.set([])
|
||||||
self.eco_account.share_with_list([self.superuser])
|
self.eco_account.share_with_user_list([self.superuser])
|
||||||
|
|
||||||
# Since the user has no groups, it does not matter that data has been shared. There SHOULD not be any difference
|
# Since the user has no groups, it does not matter that data has been shared. There SHOULD not be any difference
|
||||||
# to a user without access, since the important permissions are missing
|
# to a user without access, since the important permissions are missing
|
||||||
@ -119,7 +119,7 @@ class EcoAccountViewTestCase(CompensationViewTestCase):
|
|||||||
client = Client()
|
client = Client()
|
||||||
client.login(username=self.superuser.username, password=self.superuser_pw)
|
client.login(username=self.superuser.username, password=self.superuser_pw)
|
||||||
self.superuser.groups.set([])
|
self.superuser.groups.set([])
|
||||||
self.eco_account.share_with_list([])
|
self.eco_account.share_with_user_list([])
|
||||||
|
|
||||||
# Since the user has no groups, it does not matter that data is unshared. There SHOULD not be any difference
|
# Since the user has no groups, it does not matter that data is unshared. There SHOULD not be any difference
|
||||||
# to a user having shared access, since all important permissions are missing
|
# to a user having shared access, since all important permissions are missing
|
||||||
@ -163,7 +163,7 @@ class EcoAccountViewTestCase(CompensationViewTestCase):
|
|||||||
group = self.groups.get(name=DEFAULT_GROUP)
|
group = self.groups.get(name=DEFAULT_GROUP)
|
||||||
self.superuser.groups.set([group])
|
self.superuser.groups.set([group])
|
||||||
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
|
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
|
||||||
self.eco_account.share_with_list([self.superuser])
|
self.eco_account.share_with_user_list([self.superuser])
|
||||||
|
|
||||||
success_urls = [
|
success_urls = [
|
||||||
self.index_url,
|
self.index_url,
|
||||||
@ -200,7 +200,7 @@ class EcoAccountViewTestCase(CompensationViewTestCase):
|
|||||||
client.login(username=self.superuser.username, password=self.superuser_pw)
|
client.login(username=self.superuser.username, password=self.superuser_pw)
|
||||||
group = self.groups.get(name=DEFAULT_GROUP)
|
group = self.groups.get(name=DEFAULT_GROUP)
|
||||||
self.superuser.groups.set([group])
|
self.superuser.groups.set([group])
|
||||||
self.eco_account.share_with_list([])
|
self.eco_account.share_with_user_list([])
|
||||||
|
|
||||||
success_urls = [
|
success_urls = [
|
||||||
self.index_url,
|
self.index_url,
|
||||||
|
@ -27,7 +27,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
|
|||||||
# Add user to conservation office group and give shared access to the account
|
# Add user to conservation office group and give shared access to the account
|
||||||
self.superuser.groups.add(self.groups.get(name=DEFAULT_GROUP))
|
self.superuser.groups.add(self.groups.get(name=DEFAULT_GROUP))
|
||||||
self.superuser.groups.add(self.groups.get(name=ETS_GROUP))
|
self.superuser.groups.add(self.groups.get(name=ETS_GROUP))
|
||||||
self.eco_account.share_with_list([self.superuser])
|
self.eco_account.share_with_user_list([self.superuser])
|
||||||
|
|
||||||
def test_new(self):
|
def test_new(self):
|
||||||
""" Test the creation of an EcoAccount
|
""" Test the creation of an EcoAccount
|
||||||
@ -73,7 +73,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
|
|||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.eco_account.share_with(self.superuser)
|
self.eco_account.share_with_user(self.superuser)
|
||||||
|
|
||||||
url = reverse("compensation:acc:edit", args=(self.eco_account.id,))
|
url = reverse("compensation:acc:edit", args=(self.eco_account.id,))
|
||||||
pre_edit_log_count = self.eco_account.log.count()
|
pre_edit_log_count = self.eco_account.log.count()
|
||||||
@ -129,7 +129,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
# Add proper privilege for the user
|
# Add proper privilege for the user
|
||||||
self.eco_account.share_with(self.superuser)
|
self.eco_account.share_with_user(self.superuser)
|
||||||
pre_record_log_count = self.eco_account.log.count()
|
pre_record_log_count = self.eco_account.log.count()
|
||||||
|
|
||||||
# Prepare url and form data
|
# Prepare url and form data
|
||||||
@ -178,7 +178,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
# Give user shared access to the dummy intervention, which will be needed here
|
# Give user shared access to the dummy intervention, which will be needed here
|
||||||
self.intervention.share_with(self.superuser)
|
self.intervention.share_with_user(self.superuser)
|
||||||
pre_deduction_acc_log_count = self.eco_account.log.count()
|
pre_deduction_acc_log_count = self.eco_account.log.count()
|
||||||
pre_deduction_int_log_count = self.intervention.log.count()
|
pre_deduction_int_log_count = self.intervention.log.count()
|
||||||
|
|
||||||
@ -231,7 +231,9 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
|
|||||||
def test_edit_deduction(self):
|
def test_edit_deduction(self):
|
||||||
test_surface = self.eco_account.get_available_rest()[0]
|
test_surface = self.eco_account.get_available_rest()[0]
|
||||||
self.eco_account.set_recorded(self.superuser)
|
self.eco_account.set_recorded(self.superuser)
|
||||||
|
self.intervention.share_with_user(self.superuser)
|
||||||
self.eco_account.refresh_from_db()
|
self.eco_account.refresh_from_db()
|
||||||
|
self.assertTrue(self.superuser, self.intervention.is_shared_with(self.superuser))
|
||||||
|
|
||||||
deduction = EcoAccountDeduction.objects.create(
|
deduction = EcoAccountDeduction.objects.create(
|
||||||
intervention=self.intervention,
|
intervention=self.intervention,
|
||||||
@ -279,8 +281,8 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
|
|||||||
"confirm": True,
|
"confirm": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
intervention.share_with(self.superuser)
|
intervention.share_with_user(self.superuser)
|
||||||
account.share_with(self.superuser)
|
account.share_with_user(self.superuser)
|
||||||
|
|
||||||
pre_edit_intervention_log_count = intervention.log.count()
|
pre_edit_intervention_log_count = intervention.log.count()
|
||||||
pre_edit_account_log_count = account.log.count()
|
pre_edit_account_log_count = account.log.count()
|
||||||
|
@ -64,7 +64,7 @@ class PaymentViewTestCase(BaseViewTestCase):
|
|||||||
client = Client()
|
client = Client()
|
||||||
client.login(username=self.superuser.username, password=self.superuser_pw)
|
client.login(username=self.superuser.username, password=self.superuser_pw)
|
||||||
self.superuser.groups.set([])
|
self.superuser.groups.set([])
|
||||||
self.intervention.share_with_list([self.superuser])
|
self.intervention.share_with_user_list([self.superuser])
|
||||||
|
|
||||||
# Since the user has no groups, it does not matter that data has been shared. There SHOULD not be any difference
|
# Since the user has no groups, it does not matter that data has been shared. There SHOULD not be any difference
|
||||||
# to a user without access, since the important permissions are missing
|
# to a user without access, since the important permissions are missing
|
||||||
@ -91,7 +91,7 @@ class PaymentViewTestCase(BaseViewTestCase):
|
|||||||
client.login(username=self.superuser.username, password=self.superuser_pw)
|
client.login(username=self.superuser.username, password=self.superuser_pw)
|
||||||
self.superuser.groups.set([])
|
self.superuser.groups.set([])
|
||||||
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
|
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
|
||||||
self.intervention.share_with_list([])
|
self.intervention.share_with_user_list([])
|
||||||
|
|
||||||
# Since the user has no groups, it does not matter that data is unshared. There SHOULD not be any difference
|
# Since the user has no groups, it does not matter that data is unshared. There SHOULD not be any difference
|
||||||
# to a user having shared access, since all important permissions are missing
|
# to a user having shared access, since all important permissions are missing
|
||||||
@ -120,7 +120,7 @@ class PaymentViewTestCase(BaseViewTestCase):
|
|||||||
group = self.groups.get(name=DEFAULT_GROUP)
|
group = self.groups.get(name=DEFAULT_GROUP)
|
||||||
self.superuser.groups.set([group])
|
self.superuser.groups.set([group])
|
||||||
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
|
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
|
||||||
self.intervention.share_with_list([self.superuser])
|
self.intervention.share_with_user_list([self.superuser])
|
||||||
|
|
||||||
success_urls = [
|
success_urls = [
|
||||||
self.new_url,
|
self.new_url,
|
||||||
@ -143,7 +143,7 @@ class PaymentViewTestCase(BaseViewTestCase):
|
|||||||
group = self.groups.get(name=DEFAULT_GROUP)
|
group = self.groups.get(name=DEFAULT_GROUP)
|
||||||
self.superuser.groups.set([group])
|
self.superuser.groups.set([group])
|
||||||
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
|
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
|
||||||
self.intervention.share_with_list([])
|
self.intervention.share_with_user_list([])
|
||||||
|
|
||||||
success_urls = [
|
success_urls = [
|
||||||
]
|
]
|
||||||
|
@ -21,7 +21,7 @@ class PaymentWorkflowTestCase(BaseWorkflowTestCase):
|
|||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
# Give the user shared access to the dummy intervention
|
# Give the user shared access to the dummy intervention
|
||||||
self.intervention.share_with(self.superuser)
|
self.intervention.share_with_user(self.superuser)
|
||||||
|
|
||||||
self.payment = Payment.objects.get_or_create(
|
self.payment = Payment.objects.get_or_create(
|
||||||
intervention=self.intervention,
|
intervention=self.intervention,
|
||||||
|
@ -272,7 +272,6 @@ def remove_view(request: HttpRequest, id: str):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@default_group_required
|
@default_group_required
|
||||||
@shared_access_required(EcoAccount, "id")
|
|
||||||
def deduction_remove_view(request: HttpRequest, id: str, deduction_id: str):
|
def deduction_remove_view(request: HttpRequest, id: str, deduction_id: str):
|
||||||
""" Renders a modal view for removing deductions
|
""" Renders a modal view for removing deductions
|
||||||
|
|
||||||
@ -287,6 +286,8 @@ def deduction_remove_view(request: HttpRequest, id: str, deduction_id: str):
|
|||||||
acc = get_object_or_404(EcoAccount, id=id)
|
acc = get_object_or_404(EcoAccount, id=id)
|
||||||
try:
|
try:
|
||||||
eco_deduction = acc.deductions.get(id=deduction_id)
|
eco_deduction = acc.deductions.get(id=deduction_id)
|
||||||
|
if not eco_deduction.intervention.is_shared_with(request.user):
|
||||||
|
raise ObjectDoesNotExist()
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
raise Http404("Unknown deduction")
|
raise Http404("Unknown deduction")
|
||||||
|
|
||||||
@ -300,7 +301,6 @@ def deduction_remove_view(request: HttpRequest, id: str, deduction_id: str):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@default_group_required
|
@default_group_required
|
||||||
@shared_access_required(EcoAccount, "id")
|
|
||||||
def deduction_edit_view(request: HttpRequest, id: str, deduction_id: str):
|
def deduction_edit_view(request: HttpRequest, id: str, deduction_id: str):
|
||||||
""" Renders a modal view for editing deductions
|
""" Renders a modal view for editing deductions
|
||||||
|
|
||||||
@ -315,6 +315,8 @@ def deduction_edit_view(request: HttpRequest, id: str, deduction_id: str):
|
|||||||
acc = get_object_or_404(EcoAccount, id=id)
|
acc = get_object_or_404(EcoAccount, id=id)
|
||||||
try:
|
try:
|
||||||
eco_deduction = acc.deductions.get(id=deduction_id)
|
eco_deduction = acc.deductions.get(id=deduction_id)
|
||||||
|
if not eco_deduction.intervention.is_shared_with(request.user):
|
||||||
|
raise ObjectDoesNotExist
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
raise Http404("Unknown deduction")
|
raise Http404("Unknown deduction")
|
||||||
|
|
||||||
@ -679,7 +681,6 @@ def remove_document_view(request: HttpRequest, id: str, doc_id: str):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@default_group_required
|
@default_group_required
|
||||||
@shared_access_required(EcoAccount, "id")
|
|
||||||
def new_deduction_view(request: HttpRequest, id: str):
|
def new_deduction_view(request: HttpRequest, id: str):
|
||||||
""" Renders a modal form view for creating deductions
|
""" Renders a modal form view for creating deductions
|
||||||
|
|
||||||
@ -691,6 +692,8 @@ def new_deduction_view(request: HttpRequest, id: str):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
acc = get_object_or_404(EcoAccount, id=id)
|
acc = get_object_or_404(EcoAccount, id=id)
|
||||||
|
if not acc.recorded:
|
||||||
|
raise Http404()
|
||||||
form = NewDeductionModalForm(request.POST or None, instance=acc, request=request)
|
form = NewDeductionModalForm(request.POST or None, instance=acc, request=request)
|
||||||
return form.process_request(
|
return form.process_request(
|
||||||
request,
|
request,
|
||||||
@ -793,7 +796,7 @@ def share_view(request: HttpRequest, id: str, token: str):
|
|||||||
request,
|
request,
|
||||||
_("{} has been shared with you").format(obj.identifier)
|
_("{} has been shared with you").format(obj.identifier)
|
||||||
)
|
)
|
||||||
obj.share_with(user)
|
obj.share_with_user(user)
|
||||||
return redirect("compensation:acc:detail", id=id)
|
return redirect("compensation:acc:detail", id=id)
|
||||||
else:
|
else:
|
||||||
messages.error(
|
messages.error(
|
||||||
|
@ -80,7 +80,7 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Add the creating user to the list of shared users
|
# Add the creating user to the list of shared users
|
||||||
acc.share_with(user)
|
acc.share_with_user(user)
|
||||||
|
|
||||||
# Add the log entry to the main objects log list
|
# Add the log entry to the main objects log list
|
||||||
acc.log.add(action)
|
acc.log.add(action)
|
||||||
|
19
ema/migrations/0003_ema_teams.py
Normal file
19
ema/migrations/0003_ema_teams.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 3.1.3 on 2022-02-18 09:13
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('user', '0003_team'),
|
||||||
|
('ema', '0002_auto_20220114_0936'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='ema',
|
||||||
|
name='teams',
|
||||||
|
field=models.ManyToManyField(help_text='Teams having access (data shared with)', to='user.Team'),
|
||||||
|
),
|
||||||
|
]
|
@ -44,13 +44,15 @@
|
|||||||
{% for action in obj.actions.all %}
|
{% for action in obj.actions.all %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="">
|
<td class="">
|
||||||
<span>{{ action.action_type }}</span>
|
{% for type in action.action_type.all %}
|
||||||
{% if action.action_type_details.count > 0 %}
|
<div> {{type.parent.parent.long_name}} {% fa5_icon 'angle-right' %} {{type.parent.long_name}} {% fa5_icon 'angle-right' %} {{type.long_name}} </div>
|
||||||
<br>
|
<hr>
|
||||||
{% for detail in action.action_type_details.all %}
|
{% endfor %}
|
||||||
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
{% for detail in action.action_type_details.all %}
|
||||||
{% endfor %}
|
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
||||||
{% endif %}
|
{% empty %}
|
||||||
|
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No action type details' %}">{% trans 'No action type details' %}</span>
|
||||||
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td class="">{{ action.amount|floatformat:2|intcomma }} {{ action.unit_humanize }}</td>
|
<td class="">{{ action.amount|floatformat:2|intcomma }} {{ action.unit_humanize }}</td>
|
||||||
<td class="">
|
<td class="">
|
||||||
|
@ -46,13 +46,13 @@
|
|||||||
{% for state in after_states %}
|
{% for state in after_states %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<span>{{ state.biotope_type }}</span>
|
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
|
||||||
{% if state.biotope_type_details.count > 0 %}
|
<br>
|
||||||
<br>
|
{% for detail in state.biotope_type_details.all %}
|
||||||
{% for detail in state.biotope_type_details.all %}
|
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
||||||
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
{% empty %}
|
||||||
{% endfor %}
|
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
|
||||||
{% endif %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ state.surface|floatformat:2 }} m²</td>
|
<td>{{ state.surface|floatformat:2 }} m²</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
|
@ -46,13 +46,13 @@
|
|||||||
{% for state in before_states %}
|
{% for state in before_states %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<span>{{ state.biotope_type }}</span>
|
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
|
||||||
{% if state.biotope_type_details.count > 0 %}
|
<br>
|
||||||
<br>
|
{% for detail in state.biotope_type_details.all %}
|
||||||
{% for detail in state.biotope_type_details.all %}
|
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
||||||
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
{% empty %}
|
||||||
{% endfor %}
|
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
|
||||||
{% endif %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ state.surface|floatformat:2 }} m²</td>
|
<td>{{ state.surface|floatformat:2 }} m²</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
|
@ -74,6 +74,10 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans 'Shared with' %}</th>
|
<th scope="row">{% trans 'Shared with' %}</th>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
|
{% for team in obj.teams.all %}
|
||||||
|
{% include 'user/includes/team_data_modal_button.html' %}
|
||||||
|
{% endfor %}
|
||||||
|
<hr>
|
||||||
{% for user in obj.users.all %}
|
{% for user in obj.users.all %}
|
||||||
{% include 'user/includes/contact_modal_button.html' %}
|
{% include 'user/includes/contact_modal_button.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -110,7 +110,7 @@ class EmaViewTestCase(CompensationViewTestCase):
|
|||||||
|
|
||||||
# Sharing does not have any effect in here, since the default group will prohibit further functionality access
|
# Sharing does not have any effect in here, since the default group will prohibit further functionality access
|
||||||
# to this user
|
# to this user
|
||||||
self.ema.share_with_list([self.superuser])
|
self.ema.share_with_user_list([self.superuser])
|
||||||
|
|
||||||
success_urls = [
|
success_urls = [
|
||||||
self.index_url,
|
self.index_url,
|
||||||
@ -160,7 +160,7 @@ class EmaViewTestCase(CompensationViewTestCase):
|
|||||||
|
|
||||||
# Sharing does not have any effect in here, since the default group will prohibit further functionality access
|
# Sharing does not have any effect in here, since the default group will prohibit further functionality access
|
||||||
# to this user
|
# to this user
|
||||||
self.ema.share_with_list([])
|
self.ema.share_with_user_list([])
|
||||||
|
|
||||||
success_urls = [
|
success_urls = [
|
||||||
self.index_url,
|
self.index_url,
|
||||||
@ -203,7 +203,7 @@ class EmaViewTestCase(CompensationViewTestCase):
|
|||||||
groups = self.groups.filter(Q(name=ETS_GROUP)|Q(name=DEFAULT_GROUP))
|
groups = self.groups.filter(Q(name=ETS_GROUP)|Q(name=DEFAULT_GROUP))
|
||||||
self.superuser.groups.set(groups)
|
self.superuser.groups.set(groups)
|
||||||
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
|
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
|
||||||
self.ema.share_with_list([self.superuser])
|
self.ema.share_with_user_list([self.superuser])
|
||||||
|
|
||||||
success_urls = [
|
success_urls = [
|
||||||
self.index_url,
|
self.index_url,
|
||||||
@ -243,7 +243,7 @@ class EmaViewTestCase(CompensationViewTestCase):
|
|||||||
groups = self.groups.filter(Q(name=ETS_GROUP)|Q(name=DEFAULT_GROUP))
|
groups = self.groups.filter(Q(name=ETS_GROUP)|Q(name=DEFAULT_GROUP))
|
||||||
self.superuser.groups.set(groups)
|
self.superuser.groups.set(groups)
|
||||||
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
|
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
|
||||||
self.ema.share_with_list([])
|
self.ema.share_with_user_list([])
|
||||||
|
|
||||||
success_urls = [
|
success_urls = [
|
||||||
self.index_url,
|
self.index_url,
|
||||||
|
@ -621,7 +621,7 @@ def share_view(request: HttpRequest, id: str, token: str):
|
|||||||
request,
|
request,
|
||||||
_("{} has been shared with you").format(obj.identifier)
|
_("{} has been shared with you").format(obj.identifier)
|
||||||
)
|
)
|
||||||
obj.share_with(user)
|
obj.share_with_user(user)
|
||||||
return redirect("ema:detail", id=id)
|
return redirect("ema:detail", id=id)
|
||||||
else:
|
else:
|
||||||
messages.error(
|
messages.error(
|
||||||
|
@ -253,7 +253,7 @@ class NewInterventionForm(BaseForm):
|
|||||||
intervention.log.add(action)
|
intervention.log.add(action)
|
||||||
|
|
||||||
# Add the performing user as the first user having access to the data
|
# Add the performing user as the first user having access to the data
|
||||||
intervention.share_with(user)
|
intervention.share_with_user(user)
|
||||||
return intervention
|
return intervention
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,11 +7,11 @@ Created on: 27.09.21
|
|||||||
"""
|
"""
|
||||||
from dal import autocomplete
|
from dal import autocomplete
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.db.models.fields.files import FieldFile
|
|
||||||
|
|
||||||
from konova.utils.message_templates import DEDUCTION_ADDED, REVOCATION_ADDED, DEDUCTION_REMOVED, DEDUCTION_EDITED, \
|
from konova.utils.message_templates import DEDUCTION_ADDED, REVOCATION_ADDED, DEDUCTION_REMOVED, DEDUCTION_EDITED, \
|
||||||
REVOCATION_EDITED, FILE_TYPE_UNSUPPORTED, FILE_SIZE_TOO_LARGE
|
REVOCATION_EDITED, ENTRY_REMOVE_MISSING_PERMISSION
|
||||||
from user.models import User, UserActionLogEntry
|
from user.models import User, Team
|
||||||
|
from user.models import UserActionLogEntry
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@ -37,7 +37,21 @@ class ShareModalForm(BaseModalForm):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
user_select = forms.ModelMultipleChoiceField(
|
teams = forms.ModelMultipleChoiceField(
|
||||||
|
label=_("Add team to share with"),
|
||||||
|
label_suffix="",
|
||||||
|
help_text=_("Multiple selection possible - You can only select teams which do not already have access."),
|
||||||
|
required=False,
|
||||||
|
queryset=Team.objects.all(),
|
||||||
|
widget=autocomplete.ModelSelect2Multiple(
|
||||||
|
url="share-team-autocomplete",
|
||||||
|
attrs={
|
||||||
|
"data-placeholder": _("Click for selection"),
|
||||||
|
"data-minimum-input-length": 3,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
users = forms.ModelMultipleChoiceField(
|
||||||
label=_("Add user to share with"),
|
label=_("Add user to share with"),
|
||||||
label_suffix="",
|
label_suffix="",
|
||||||
help_text=_("Multiple selection possible - You can only select users which do not already have access. Enter the full username."),
|
help_text=_("Multiple selection possible - You can only select users which do not already have access. Enter the full username."),
|
||||||
@ -49,21 +63,8 @@ class ShareModalForm(BaseModalForm):
|
|||||||
"data-placeholder": _("Click for selection"),
|
"data-placeholder": _("Click for selection"),
|
||||||
"data-minimum-input-length": 3,
|
"data-minimum-input-length": 3,
|
||||||
},
|
},
|
||||||
forward=["users"]
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
users = forms.MultipleChoiceField(
|
|
||||||
label=_("Shared with"),
|
|
||||||
label_suffix="",
|
|
||||||
required=True,
|
|
||||||
help_text=_("Remove check to remove access for this user"),
|
|
||||||
widget=forms.CheckboxSelectMultiple(
|
|
||||||
attrs={
|
|
||||||
"class": "list-unstyled",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
choices=[]
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@ -77,6 +78,48 @@ class ShareModalForm(BaseModalForm):
|
|||||||
|
|
||||||
self._init_fields()
|
self._init_fields()
|
||||||
|
|
||||||
|
def _user_team_valid(self):
|
||||||
|
""" Checks whether users and teams have been removed by the user and if the user is allowed to do so or not
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
users = self.cleaned_data.get("users", User.objects.none())
|
||||||
|
teams = self.cleaned_data.get("teams", Team.objects.none())
|
||||||
|
|
||||||
|
_is_valid = True
|
||||||
|
if is_default_group_only(self.user):
|
||||||
|
shared_users = self.instance.shared_users
|
||||||
|
shared_teams = self.instance.shared_teams
|
||||||
|
|
||||||
|
shared_users_are_removed = not set(shared_users).issubset(users)
|
||||||
|
shared_teams_are_removed = not set(shared_teams).issubset(teams)
|
||||||
|
|
||||||
|
if shared_users_are_removed:
|
||||||
|
self.add_error(
|
||||||
|
"users",
|
||||||
|
ENTRY_REMOVE_MISSING_PERMISSION
|
||||||
|
)
|
||||||
|
_is_valid = False
|
||||||
|
if shared_teams_are_removed:
|
||||||
|
self.add_error(
|
||||||
|
"teams",
|
||||||
|
ENTRY_REMOVE_MISSING_PERMISSION
|
||||||
|
)
|
||||||
|
_is_valid = False
|
||||||
|
return _is_valid
|
||||||
|
|
||||||
|
def is_valid(self):
|
||||||
|
""" Extended validity check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
super_valid = super().is_valid()
|
||||||
|
user_team_valid = self._user_team_valid()
|
||||||
|
_is_valid = super_valid and user_team_valid
|
||||||
|
return _is_valid
|
||||||
|
|
||||||
def _init_fields(self):
|
def _init_fields(self):
|
||||||
""" Wraps initializing of fields
|
""" Wraps initializing of fields
|
||||||
|
|
||||||
@ -91,34 +134,14 @@ class ShareModalForm(BaseModalForm):
|
|||||||
self.share_link
|
self.share_link
|
||||||
)
|
)
|
||||||
|
|
||||||
# Initialize users field
|
form_data = {
|
||||||
# Disable field if user is not in registration or conservation group
|
"teams": self.instance.teams.all(),
|
||||||
if is_default_group_only(self.request.user):
|
"users": self.instance.users.all(),
|
||||||
self.disable_form_field("users")
|
}
|
||||||
|
self.load_initial_data(form_data)
|
||||||
self._add_user_choices_to_field()
|
|
||||||
|
|
||||||
def _add_user_choices_to_field(self):
|
|
||||||
""" Transforms the instance's sharing users into a list for the form field
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
|
|
||||||
"""
|
|
||||||
users = self.instance.users.all()
|
|
||||||
choices = []
|
|
||||||
for n in users:
|
|
||||||
choices.append(
|
|
||||||
(n.id, n.username)
|
|
||||||
)
|
|
||||||
self.fields["users"].choices = choices
|
|
||||||
u_ids = list(users.values_list("id", flat=True))
|
|
||||||
self.initialize_form_field(
|
|
||||||
"users",
|
|
||||||
u_ids
|
|
||||||
)
|
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
self.instance.update_sharing_user(self)
|
self.instance.update_shared_access(self)
|
||||||
|
|
||||||
|
|
||||||
class NewRevocationModalForm(BaseModalForm):
|
class NewRevocationModalForm(BaseModalForm):
|
||||||
@ -267,7 +290,9 @@ class CheckModalForm(BaseModalForm):
|
|||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
comps = self.instance.compensations.all()
|
comps = self.instance.compensations.filter(
|
||||||
|
deleted=None,
|
||||||
|
)
|
||||||
comps_valid = True
|
comps_valid = True
|
||||||
for comp in comps:
|
for comp in comps:
|
||||||
checker = comp.quality_check()
|
checker = comp.quality_check()
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
|
from codelist.models import KonovaCode
|
||||||
|
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID
|
||||||
|
|
||||||
|
|
||||||
class DummyFilterInput(forms.HiddenInput):
|
class DummyFilterInput(forms.HiddenInput):
|
||||||
@ -30,3 +32,51 @@ class GenerateInput(forms.TextInput):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
template_name = "konova/widgets/generate-content-input.html"
|
template_name = "konova/widgets/generate-content-input.html"
|
||||||
|
|
||||||
|
|
||||||
|
class TreeCheckboxSelectMultiple(forms.CheckboxSelectMultiple):
|
||||||
|
""" Provides multiple selection of parent-child data
|
||||||
|
|
||||||
|
"""
|
||||||
|
template_name = "konova/widgets/checkbox-tree-select.html"
|
||||||
|
|
||||||
|
class meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
|
class KonovaCodeTreeCheckboxSelectMultiple(TreeCheckboxSelectMultiple):
|
||||||
|
""" Provides multiple selection of KonovaCodes
|
||||||
|
|
||||||
|
"""
|
||||||
|
filter = None
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.code_list = kwargs.pop("code_list", None)
|
||||||
|
self.filter = kwargs.pop("filter", {})
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_context(self, name, value, attrs):
|
||||||
|
context = super().get_context(name, value, attrs)
|
||||||
|
codes = KonovaCode.objects.filter(
|
||||||
|
**self.filter,
|
||||||
|
)
|
||||||
|
codes = [
|
||||||
|
parent_code.add_children()
|
||||||
|
for parent_code in codes
|
||||||
|
]
|
||||||
|
context["codes"] = codes
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class CompensationActionTreeCheckboxSelectMultiple(KonovaCodeTreeCheckboxSelectMultiple):
|
||||||
|
""" Provides multiple selection of CompensationActions
|
||||||
|
|
||||||
|
"""
|
||||||
|
filter = None
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.filter = {
|
||||||
|
"code_lists__in": [CODELIST_COMPENSATION_ACTION_ID],
|
||||||
|
"parent": None,
|
||||||
|
}
|
19
intervention/migrations/0003_intervention_teams.py
Normal file
19
intervention/migrations/0003_intervention_teams.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 3.1.3 on 2022-02-18 09:13
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('user', '0003_team'),
|
||||||
|
('intervention', '0002_auto_20220114_0936'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='intervention',
|
||||||
|
name='teams',
|
||||||
|
field=models.ManyToManyField(help_text='Teams having access (data shared with)', to='user.Team'),
|
||||||
|
),
|
||||||
|
]
|
@ -114,6 +114,10 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans 'Shared with' %}</th>
|
<th scope="row">{% trans 'Shared with' %}</th>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
|
{% for team in obj.teams.all %}
|
||||||
|
{% include 'user/includes/team_data_modal_button.html' %}
|
||||||
|
{% endfor %}
|
||||||
|
<hr>
|
||||||
{% for user in obj.users.all %}
|
{% for user in obj.users.all %}
|
||||||
{% include 'user/includes/contact_modal_button.html' %}
|
{% include 'user/includes/contact_modal_button.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -144,7 +144,7 @@ class InterventionViewTestCase(BaseViewTestCase):
|
|||||||
# Add user to default group
|
# Add user to default group
|
||||||
default_group = Group.objects.get(name=DEFAULT_GROUP)
|
default_group = Group.objects.get(name=DEFAULT_GROUP)
|
||||||
self.superuser.groups.set([default_group])
|
self.superuser.groups.set([default_group])
|
||||||
self.intervention.share_with_list([self.superuser])
|
self.intervention.share_with_user_list([self.superuser])
|
||||||
|
|
||||||
success_urls = [
|
success_urls = [
|
||||||
self.index_url,
|
self.index_url,
|
||||||
@ -190,7 +190,7 @@ class InterventionViewTestCase(BaseViewTestCase):
|
|||||||
# Add user to default group
|
# Add user to default group
|
||||||
default_group = Group.objects.get(name=DEFAULT_GROUP)
|
default_group = Group.objects.get(name=DEFAULT_GROUP)
|
||||||
self.superuser.groups.set([default_group])
|
self.superuser.groups.set([default_group])
|
||||||
self.intervention.share_with_list([])
|
self.intervention.share_with_user_list([])
|
||||||
|
|
||||||
success_urls = [
|
success_urls = [
|
||||||
self.index_url,
|
self.index_url,
|
||||||
@ -236,7 +236,7 @@ class InterventionViewTestCase(BaseViewTestCase):
|
|||||||
# Add user to zb group
|
# Add user to zb group
|
||||||
zb_group = self.groups.get(name=ZB_GROUP)
|
zb_group = self.groups.get(name=ZB_GROUP)
|
||||||
self.superuser.groups.set([zb_group])
|
self.superuser.groups.set([zb_group])
|
||||||
self.intervention.share_with_list([self.superuser])
|
self.intervention.share_with_user_list([self.superuser])
|
||||||
|
|
||||||
success_urls = [
|
success_urls = [
|
||||||
self.index_url,
|
self.index_url,
|
||||||
@ -282,7 +282,7 @@ class InterventionViewTestCase(BaseViewTestCase):
|
|||||||
# Add user to zb group
|
# Add user to zb group
|
||||||
zb_group = self.groups.get(name=ZB_GROUP)
|
zb_group = self.groups.get(name=ZB_GROUP)
|
||||||
self.superuser.groups.set([zb_group])
|
self.superuser.groups.set([zb_group])
|
||||||
self.intervention.share_with_list([])
|
self.intervention.share_with_user_list([])
|
||||||
|
|
||||||
success_urls = [
|
success_urls = [
|
||||||
self.index_url,
|
self.index_url,
|
||||||
@ -328,7 +328,7 @@ class InterventionViewTestCase(BaseViewTestCase):
|
|||||||
# Add user to ets group
|
# Add user to ets group
|
||||||
ets_group = Group.objects.get(name=ETS_GROUP)
|
ets_group = Group.objects.get(name=ETS_GROUP)
|
||||||
self.superuser.groups.set([ets_group])
|
self.superuser.groups.set([ets_group])
|
||||||
self.intervention.share_with_list([self.superuser])
|
self.intervention.share_with_user_list([self.superuser])
|
||||||
|
|
||||||
success_urls = [
|
success_urls = [
|
||||||
self.index_url,
|
self.index_url,
|
||||||
@ -374,7 +374,7 @@ class InterventionViewTestCase(BaseViewTestCase):
|
|||||||
# Add user to default group
|
# Add user to default group
|
||||||
ets_group = Group.objects.get(name=ETS_GROUP)
|
ets_group = Group.objects.get(name=ETS_GROUP)
|
||||||
self.superuser.groups.set([ets_group])
|
self.superuser.groups.set([ets_group])
|
||||||
self.intervention.share_with_list([])
|
self.intervention.share_with_user_list([])
|
||||||
|
|
||||||
success_urls = [
|
success_urls = [
|
||||||
self.index_url,
|
self.index_url,
|
||||||
|
@ -30,7 +30,7 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
|
|||||||
super().setUp()
|
super().setUp()
|
||||||
# Recreate a new (bare minimum) intervention before each test
|
# Recreate a new (bare minimum) intervention before each test
|
||||||
self.intervention = self.create_dummy_intervention()
|
self.intervention = self.create_dummy_intervention()
|
||||||
self.intervention.share_with(self.superuser)
|
self.intervention.share_with_user(self.superuser)
|
||||||
|
|
||||||
def test_new(self):
|
def test_new(self):
|
||||||
"""
|
"""
|
||||||
@ -303,7 +303,6 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
|
|||||||
Reasons for failing are:
|
Reasons for failing are:
|
||||||
* EcoAccount does not provide enough 'deductable_surface'
|
* EcoAccount does not provide enough 'deductable_surface'
|
||||||
* EcoAccount is not recorded (not "approved"), yet
|
* EcoAccount is not recorded (not "approved"), yet
|
||||||
* EcoAccount is not shared with performing user
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
new_url (str): The url to send the post data to
|
new_url (str): The url to send the post data to
|
||||||
@ -315,7 +314,6 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
|
|||||||
# Before running fail positive tests, we need to have an account in a (normally) fine working state
|
# Before running fail positive tests, we need to have an account in a (normally) fine working state
|
||||||
self.assertIsNotNone(self.eco_account.recorded) # -> is recorded
|
self.assertIsNotNone(self.eco_account.recorded) # -> is recorded
|
||||||
self.assertGreater(self.eco_account.deductable_surface, test_surface) # -> has more deductable surface than we need
|
self.assertGreater(self.eco_account.deductable_surface, test_surface) # -> has more deductable surface than we need
|
||||||
self.assertIn(self.superuser, self.eco_account.users.all()) # -> is shared with the performing user
|
|
||||||
|
|
||||||
# Count the number of already existing deductions in total and for the account for later comparison
|
# Count the number of already existing deductions in total and for the account for later comparison
|
||||||
num_deductions = self.eco_account.deductions.count()
|
num_deductions = self.eco_account.deductions.count()
|
||||||
@ -333,20 +331,11 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
|
|||||||
self.assertEqual(num_deductions, self.eco_account.deductions.count())
|
self.assertEqual(num_deductions, self.eco_account.deductions.count())
|
||||||
self.assertEqual(num_deductions_total, EcoAccountDeduction.objects.count())
|
self.assertEqual(num_deductions_total, EcoAccountDeduction.objects.count())
|
||||||
|
|
||||||
# Now restore the deductable surface to a valid size back again but remove the user from the shared list
|
# Now restore the deductable surface to a valid size back again
|
||||||
self.eco_account.deductable_surface = test_surface + 100.00
|
self.eco_account.deductable_surface = test_surface + 100.00
|
||||||
self.eco_account.share_with_list([])
|
|
||||||
self.eco_account.save()
|
self.eco_account.save()
|
||||||
|
|
||||||
# Now perform the (expected) failing request (again)
|
# Remove the recording state
|
||||||
self.client_user.post(new_url, post_data)
|
|
||||||
|
|
||||||
# Expect no changes at all, since the account is not shared
|
|
||||||
self.assertEqual(num_deductions, self.eco_account.deductions.count())
|
|
||||||
self.assertEqual(num_deductions_total, EcoAccountDeduction.objects.count())
|
|
||||||
|
|
||||||
# Restore the sharing but remove the recording state
|
|
||||||
self.eco_account.share_with_list([self.superuser])
|
|
||||||
self.eco_account.recorded.delete()
|
self.eco_account.recorded.delete()
|
||||||
self.eco_account.refresh_from_db()
|
self.eco_account.refresh_from_db()
|
||||||
self.eco_account.save()
|
self.eco_account.save()
|
||||||
@ -376,7 +365,7 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
|
|||||||
if self.eco_account.recorded is None:
|
if self.eco_account.recorded is None:
|
||||||
rec_action = UserActionLogEntry.get_recorded_action(self.superuser)
|
rec_action = UserActionLogEntry.get_recorded_action(self.superuser)
|
||||||
self.eco_account.recorded = rec_action
|
self.eco_account.recorded = rec_action
|
||||||
self.eco_account.share_with_list([self.superuser])
|
self.eco_account.share_with_user_list([self.superuser])
|
||||||
self.eco_account.save()
|
self.eco_account.save()
|
||||||
num_all_deducs = EcoAccountDeduction.objects.count()
|
num_all_deducs = EcoAccountDeduction.objects.count()
|
||||||
|
|
||||||
|
@ -432,7 +432,7 @@ def share_view(request: HttpRequest, id: str, token: str):
|
|||||||
request,
|
request,
|
||||||
_("{} has been shared with you").format(intervention.identifier)
|
_("{} has been shared with you").format(intervention.identifier)
|
||||||
)
|
)
|
||||||
intervention.share_with(user)
|
intervention.share_with_user(user)
|
||||||
return redirect("intervention:detail", id=id)
|
return redirect("intervention:detail", id=id)
|
||||||
else:
|
else:
|
||||||
messages.error(
|
messages.error(
|
||||||
|
@ -5,8 +5,13 @@ Contact: michel.peltriaux@sgdnord.rlp.de
|
|||||||
Created on: 07.12.20
|
Created on: 07.12.20
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
import collections
|
||||||
|
|
||||||
from dal_select2.views import Select2QuerySetView, Select2GroupQuerySetView
|
from dal_select2.views import Select2QuerySetView, Select2GroupQuerySetView
|
||||||
from user.models import User
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
|
from konova.utils.message_templates import UNGROUPED
|
||||||
|
from user.models import User, Team
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from codelist.models import KonovaCode
|
from codelist.models import KonovaCode
|
||||||
@ -20,7 +25,7 @@ from intervention.models import Intervention
|
|||||||
class EcoAccountAutocomplete(Select2QuerySetView):
|
class EcoAccountAutocomplete(Select2QuerySetView):
|
||||||
""" Autocomplete for ecoAccount entries
|
""" Autocomplete for ecoAccount entries
|
||||||
|
|
||||||
Only returns entries that are accessible for the requesting user and already are recorded
|
Only returns entries that are already recorded and not deleted
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@ -29,7 +34,6 @@ class EcoAccountAutocomplete(Select2QuerySetView):
|
|||||||
qs = EcoAccount.objects.filter(
|
qs = EcoAccount.objects.filter(
|
||||||
deleted=None,
|
deleted=None,
|
||||||
recorded__isnull=False,
|
recorded__isnull=False,
|
||||||
users__in=[self.request.user],
|
|
||||||
).order_by(
|
).order_by(
|
||||||
"identifier"
|
"identifier"
|
||||||
)
|
)
|
||||||
@ -65,27 +69,40 @@ class InterventionAutocomplete(Select2QuerySetView):
|
|||||||
|
|
||||||
|
|
||||||
class ShareUserAutocomplete(Select2QuerySetView):
|
class ShareUserAutocomplete(Select2QuerySetView):
|
||||||
""" Autocomplete for intervention entries
|
""" Autocomplete for share with single users
|
||||||
|
|
||||||
Only returns entries that are accessible for the requesting user
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
if self.request.user.is_anonymous:
|
if self.request.user.is_anonymous:
|
||||||
return User.objects.none()
|
return User.objects.none()
|
||||||
exclude_user_ids = self.forwarded.get("users", [])
|
qs = User.objects.all()
|
||||||
_exclude = {"id__in": exclude_user_ids}
|
|
||||||
qs = User.objects.all().exclude(
|
|
||||||
**_exclude
|
|
||||||
).order_by(
|
|
||||||
"username"
|
|
||||||
)
|
|
||||||
if self.q:
|
if self.q:
|
||||||
# Due to privacy concerns only a full username match will return the proper user entry
|
# Due to privacy concerns only a full username match will return the proper user entry
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
Q(username=self.q) |
|
Q(username=self.q) |
|
||||||
Q(email=self.q)
|
Q(email=self.q)
|
||||||
).distinct()
|
).distinct()
|
||||||
|
qs = qs.order_by("username")
|
||||||
|
return qs
|
||||||
|
|
||||||
|
|
||||||
|
class ShareTeamAutocomplete(Select2QuerySetView):
|
||||||
|
""" Autocomplete for share with teams
|
||||||
|
|
||||||
|
"""
|
||||||
|
def get_queryset(self):
|
||||||
|
if self.request.user.is_anonymous:
|
||||||
|
return Team.objects.none()
|
||||||
|
qs = Team.objects.all()
|
||||||
|
if self.q:
|
||||||
|
# Due to privacy concerns only a full username match will return the proper user entry
|
||||||
|
qs = qs.filter(
|
||||||
|
name__icontains=self.q
|
||||||
|
)
|
||||||
|
qs = qs.order_by(
|
||||||
|
"name"
|
||||||
|
)
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|
||||||
@ -139,6 +156,8 @@ class KonovaCodeAutocomplete(Select2GroupQuerySetView):
|
|||||||
q_or |= Q(short_name__icontains=keyword)
|
q_or |= Q(short_name__icontains=keyword)
|
||||||
q_or |= Q(parent__long_name__icontains=keyword)
|
q_or |= Q(parent__long_name__icontains=keyword)
|
||||||
q_or |= Q(parent__short_name__icontains=keyword)
|
q_or |= Q(parent__short_name__icontains=keyword)
|
||||||
|
q_or |= Q(parent__parent__long_name__icontains=keyword)
|
||||||
|
q_or |= Q(parent__parent__short_name__icontains=keyword)
|
||||||
_filter.add(q_or, Q.AND)
|
_filter.add(q_or, Q.AND)
|
||||||
qs = qs.filter(_filter).distinct()
|
qs = qs.filter(_filter).distinct()
|
||||||
return qs
|
return qs
|
||||||
@ -181,7 +200,7 @@ class CompensationActionDetailCodeAutocomplete(KonovaCodeAutocomplete):
|
|||||||
|
|
||||||
def order_by(self, qs):
|
def order_by(self, qs):
|
||||||
return qs.order_by(
|
return qs.order_by(
|
||||||
"parent__long_name"
|
"long_name"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -214,6 +233,41 @@ class BiotopeCodeAutocomplete(KonovaCodeAutocomplete):
|
|||||||
def get_result_label(self, result):
|
def get_result_label(self, result):
|
||||||
return f"{result.long_name} ({result.short_name})"
|
return f"{result.long_name} ({result.short_name})"
|
||||||
|
|
||||||
|
def get_results(self, context):
|
||||||
|
"""Return the options grouped by a common related model.
|
||||||
|
|
||||||
|
Raises ImproperlyConfigured if self.group_by_name is not configured
|
||||||
|
"""
|
||||||
|
if not self.group_by_related:
|
||||||
|
raise ImproperlyConfigured("Missing group_by_related.")
|
||||||
|
|
||||||
|
super_groups = collections.OrderedDict()
|
||||||
|
|
||||||
|
object_list = context['object_list']
|
||||||
|
|
||||||
|
for result in object_list:
|
||||||
|
group = result.parent if result.parent else None
|
||||||
|
group_name = f"{group.long_name} ({group.short_name})" if group else UNGROUPED
|
||||||
|
super_group = result.parent.parent if result.parent else None
|
||||||
|
super_group_name = f"{super_group.long_name} ({super_group.short_name})" if super_group else UNGROUPED
|
||||||
|
super_groups.setdefault(super_group_name, {})
|
||||||
|
super_groups[super_group_name].setdefault(group_name, [])
|
||||||
|
super_groups[super_group_name][group_name].append(result)
|
||||||
|
|
||||||
|
return [{
|
||||||
|
'id': None,
|
||||||
|
'text': super_group,
|
||||||
|
'children': [{
|
||||||
|
"id": None,
|
||||||
|
"text": group,
|
||||||
|
"children": [{
|
||||||
|
'id': self.get_result_value(result),
|
||||||
|
'text': self.get_result_label(result),
|
||||||
|
'selected_text': self.get_selected_result_label(result),
|
||||||
|
} for result in results]
|
||||||
|
} for group, results in groups.items()]
|
||||||
|
} for super_group, groups in super_groups.items()]
|
||||||
|
|
||||||
|
|
||||||
class BiotopeExtraCodeAutocomplete(KonovaCodeAutocomplete):
|
class BiotopeExtraCodeAutocomplete(KonovaCodeAutocomplete):
|
||||||
"""
|
"""
|
||||||
@ -239,7 +293,7 @@ class BiotopeExtraCodeAutocomplete(KonovaCodeAutocomplete):
|
|||||||
qs (QuerySet): The ordered queryset
|
qs (QuerySet): The ordered queryset
|
||||||
"""
|
"""
|
||||||
return qs.order_by(
|
return qs.order_by(
|
||||||
"parent__long_name",
|
"long_name",
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_result_label(self, result):
|
def get_result_label(self, result):
|
||||||
@ -284,6 +338,11 @@ class RegistrationOfficeCodeAutocomplete(KonovaCodeAutocomplete):
|
|||||||
self.c = CODELIST_REGISTRATION_OFFICE_ID
|
self.c = CODELIST_REGISTRATION_OFFICE_ID
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def order_by(self, qs):
|
||||||
|
return qs.order_by(
|
||||||
|
"parent__long_name"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ConservationOfficeCodeAutocomplete(KonovaCodeAutocomplete):
|
class ConservationOfficeCodeAutocomplete(KonovaCodeAutocomplete):
|
||||||
"""
|
"""
|
||||||
|
@ -297,8 +297,9 @@ class ShareableTableFilterMixin(django_filters.FilterSet):
|
|||||||
"""
|
"""
|
||||||
if not value:
|
if not value:
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
users__in=[self.user], # requesting user has access
|
Q(users__in=[self.user]) | # requesting user has access
|
||||||
)
|
Q(teams__users__in=[self.user])
|
||||||
|
).distinct()
|
||||||
else:
|
else:
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
40
konova/migrations/0005_auto_20220216_0856.py
Normal file
40
konova/migrations/0005_auto_20220216_0856.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# Generated by Django 3.1.3 on 2022-02-16 07:56
|
||||||
|
|
||||||
|
from django.db import migrations, transaction
|
||||||
|
|
||||||
|
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_AFTER_STATE_BIOTOPES__ID
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_biotopes_from_974_to_654(apps, schema_editor):
|
||||||
|
KonovaCode = apps.get_model("codelist", "KonovaCode")
|
||||||
|
CompensationState = apps.get_model("compensation", "CompensationState")
|
||||||
|
all_states = CompensationState.objects.all()
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
for state in all_states:
|
||||||
|
new_biotope_code = KonovaCode.objects.get(
|
||||||
|
code_lists__in=[CODELIST_BIOTOPES_ID],
|
||||||
|
is_selectable=True,
|
||||||
|
is_archived=False,
|
||||||
|
short_name=state.biotope_type.short_name,
|
||||||
|
)
|
||||||
|
state.biotope_type = new_biotope_code
|
||||||
|
state.save()
|
||||||
|
|
||||||
|
all_states = CompensationState.objects.all()
|
||||||
|
after_state_list_elements = all_states.filter(
|
||||||
|
biotope_type__code_lists__in=[CODELIST_AFTER_STATE_BIOTOPES__ID]
|
||||||
|
)
|
||||||
|
if after_state_list_elements.count() > 0:
|
||||||
|
raise Exception("Still states with wrong codelist entries!")
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('konova', '0004_auto_20220209_0839'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(migrate_biotopes_from_974_to_654),
|
||||||
|
]
|
@ -15,8 +15,10 @@ from django.db.models import QuerySet
|
|||||||
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP, LANIS_ZOOM_LUT, LANIS_LINK_TEMPLATE
|
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP, LANIS_ZOOM_LUT, LANIS_LINK_TEMPLATE
|
||||||
from konova.tasks import celery_send_mail_shared_access_removed, celery_send_mail_shared_access_given, \
|
from konova.tasks import celery_send_mail_shared_access_removed, celery_send_mail_shared_access_given, \
|
||||||
celery_send_mail_shared_data_recorded, celery_send_mail_shared_data_unrecorded, \
|
celery_send_mail_shared_data_recorded, celery_send_mail_shared_data_unrecorded, \
|
||||||
celery_send_mail_shared_data_deleted, celery_send_mail_shared_data_checked
|
celery_send_mail_shared_data_deleted, celery_send_mail_shared_data_checked, \
|
||||||
from user.models import User
|
celery_send_mail_shared_access_given_team, celery_send_mail_shared_access_removed_team, \
|
||||||
|
celery_send_mail_shared_data_checked_team, celery_send_mail_shared_data_deleted_team, \
|
||||||
|
celery_send_mail_shared_data_unrecorded_team, celery_send_mail_shared_data_recorded_team
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
@ -28,7 +30,6 @@ from intervention.settings import INTERVENTION_IDENTIFIER_LENGTH, INTERVENTION_I
|
|||||||
from konova.utils import generators
|
from konova.utils import generators
|
||||||
from konova.utils.generators import generate_random_string
|
from konova.utils.generators import generate_random_string
|
||||||
from konova.utils.message_templates import CHECKED_RECORDED_RESET, GEOMETRY_CONFLICT_WITH_TEMPLATE
|
from konova.utils.message_templates import CHECKED_RECORDED_RESET, GEOMETRY_CONFLICT_WITH_TEMPLATE
|
||||||
from user.models import UserActionLogEntry, UserAction
|
|
||||||
|
|
||||||
|
|
||||||
class UuidModel(models.Model):
|
class UuidModel(models.Model):
|
||||||
@ -50,14 +51,14 @@ class BaseResource(UuidModel):
|
|||||||
A basic resource model, which defines attributes for every derived model
|
A basic resource model, which defines attributes for every derived model
|
||||||
"""
|
"""
|
||||||
created = models.ForeignKey(
|
created = models.ForeignKey(
|
||||||
UserActionLogEntry,
|
"user.UserActionLogEntry",
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
related_name='+'
|
related_name='+'
|
||||||
)
|
)
|
||||||
modified = models.ForeignKey(
|
modified = models.ForeignKey(
|
||||||
UserActionLogEntry,
|
"user.UserActionLogEntry",
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
@ -94,9 +95,9 @@ class BaseObject(BaseResource):
|
|||||||
"""
|
"""
|
||||||
identifier = models.CharField(max_length=1000, null=True, blank=True)
|
identifier = models.CharField(max_length=1000, null=True, blank=True)
|
||||||
title = models.CharField(max_length=1000, null=True, blank=True)
|
title = models.CharField(max_length=1000, null=True, blank=True)
|
||||||
deleted = models.ForeignKey(UserActionLogEntry, on_delete=models.SET_NULL, null=True, blank=True, related_name='+')
|
deleted = models.ForeignKey("user.UserActionLogEntry", on_delete=models.SET_NULL, null=True, blank=True, related_name='+')
|
||||||
comment = models.TextField(null=True, blank=True)
|
comment = models.TextField(null=True, blank=True)
|
||||||
log = models.ManyToManyField(UserActionLogEntry, blank=True, help_text="Keeps all user actions of an object", editable=False)
|
log = models.ManyToManyField("user.UserActionLogEntry", blank=True, help_text="Keeps all user actions of an object", editable=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
@ -105,7 +106,7 @@ class BaseObject(BaseResource):
|
|||||||
def set_status_messages(self, request: HttpRequest):
|
def set_status_messages(self, request: HttpRequest):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def mark_as_deleted(self, user: User, send_mail: bool = True):
|
def mark_as_deleted(self, user, send_mail: bool = True):
|
||||||
""" Mark an entry as deleted
|
""" Mark an entry as deleted
|
||||||
|
|
||||||
Does not delete from database but sets a timestamp for being deleted on and which user deleted the object
|
Does not delete from database but sets a timestamp for being deleted on and which user deleted the object
|
||||||
@ -116,6 +117,7 @@ class BaseObject(BaseResource):
|
|||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from user.models import UserActionLogEntry
|
||||||
if self.deleted:
|
if self.deleted:
|
||||||
# Nothing to do here
|
# Nothing to do here
|
||||||
return
|
return
|
||||||
@ -131,9 +133,14 @@ class BaseObject(BaseResource):
|
|||||||
for user_id in shared_users:
|
for user_id in shared_users:
|
||||||
celery_send_mail_shared_data_deleted.delay(self.identifier, self.title, user_id)
|
celery_send_mail_shared_data_deleted.delay(self.identifier, self.title, user_id)
|
||||||
|
|
||||||
|
# Send mail
|
||||||
|
shared_teams = self.shared_teams.values_list("id", flat=True)
|
||||||
|
for team_id in shared_teams:
|
||||||
|
celery_send_mail_shared_data_deleted_team.delay(self.identifier, self.title, team_id)
|
||||||
|
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def mark_as_edited(self, performing_user: User, request: HttpRequest = None, edit_comment: str = None):
|
def mark_as_edited(self, performing_user, request: HttpRequest = None, edit_comment: str = None):
|
||||||
""" In case the object or a related object changed the log history needs to be updated
|
""" In case the object or a related object changed the log history needs to be updated
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -144,13 +151,14 @@ class BaseObject(BaseResource):
|
|||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from user.models import UserActionLogEntry
|
||||||
edit_action = UserActionLogEntry.get_edited_action(performing_user, edit_comment)
|
edit_action = UserActionLogEntry.get_edited_action(performing_user, edit_comment)
|
||||||
self.modified = edit_action
|
self.modified = edit_action
|
||||||
self.log.add(edit_action)
|
self.log.add(edit_action)
|
||||||
self.save()
|
self.save()
|
||||||
return edit_action
|
return edit_action
|
||||||
|
|
||||||
def add_log_entry(self, action: UserAction, user: User, comment: str):
|
def add_log_entry(self, action, user, comment: str):
|
||||||
""" Wraps adding of UserActionLogEntry to log
|
""" Wraps adding of UserActionLogEntry to log
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -161,6 +169,7 @@ class BaseObject(BaseResource):
|
|||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from user.models import UserActionLogEntry
|
||||||
user_action = UserActionLogEntry.objects.create(
|
user_action = UserActionLogEntry.objects.create(
|
||||||
user=user,
|
user=user,
|
||||||
action=action,
|
action=action,
|
||||||
@ -229,7 +238,7 @@ class RecordableObjectMixin(models.Model):
|
|||||||
"""
|
"""
|
||||||
# Refers to "verzeichnen"
|
# Refers to "verzeichnen"
|
||||||
recorded = models.OneToOneField(
|
recorded = models.OneToOneField(
|
||||||
UserActionLogEntry,
|
"user.UserActionLogEntry",
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
@ -240,7 +249,7 @@ class RecordableObjectMixin(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
def set_unrecorded(self, user: User):
|
def set_unrecorded(self, user):
|
||||||
""" Perform unrecording
|
""" Perform unrecording
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -249,6 +258,7 @@ class RecordableObjectMixin(models.Model):
|
|||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from user.models import UserActionLogEntry
|
||||||
if not self.recorded:
|
if not self.recorded:
|
||||||
return None
|
return None
|
||||||
action = UserActionLogEntry.get_unrecorded_action(user)
|
action = UserActionLogEntry.get_unrecorded_action(user)
|
||||||
@ -256,13 +266,18 @@ class RecordableObjectMixin(models.Model):
|
|||||||
self.save()
|
self.save()
|
||||||
self.log.add(action)
|
self.log.add(action)
|
||||||
|
|
||||||
shared_users = self.users.all().values_list("id", flat=True)
|
shared_users = self.shared_users.values_list("id", flat=True)
|
||||||
|
shared_teams = self.shared_teams.values_list("id", flat=True)
|
||||||
|
|
||||||
for user_id in shared_users:
|
for user_id in shared_users:
|
||||||
celery_send_mail_shared_data_unrecorded.delay(self.identifier, self.title, user_id)
|
celery_send_mail_shared_data_unrecorded.delay(self.identifier, self.title, user_id)
|
||||||
|
|
||||||
|
for team_id in shared_teams:
|
||||||
|
celery_send_mail_shared_data_unrecorded_team.delay(self.identifier, self.title, team_id)
|
||||||
|
|
||||||
return action
|
return action
|
||||||
|
|
||||||
def set_recorded(self, user: User):
|
def set_recorded(self, user):
|
||||||
""" Perform recording
|
""" Perform recording
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -271,6 +286,7 @@ class RecordableObjectMixin(models.Model):
|
|||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from user.models import UserActionLogEntry
|
||||||
if self.recorded:
|
if self.recorded:
|
||||||
return None
|
return None
|
||||||
action = UserActionLogEntry.get_recorded_action(user)
|
action = UserActionLogEntry.get_recorded_action(user)
|
||||||
@ -278,13 +294,18 @@ class RecordableObjectMixin(models.Model):
|
|||||||
self.save()
|
self.save()
|
||||||
self.log.add(action)
|
self.log.add(action)
|
||||||
|
|
||||||
shared_users = self.users.all().values_list("id", flat=True)
|
shared_users = self.shared_users.values_list("id", flat=True)
|
||||||
|
shared_teams = self.shared_teams.values_list("id", flat=True)
|
||||||
|
|
||||||
for user_id in shared_users:
|
for user_id in shared_users:
|
||||||
celery_send_mail_shared_data_recorded.delay(self.identifier, self.title, user_id)
|
celery_send_mail_shared_data_recorded.delay(self.identifier, self.title, user_id)
|
||||||
|
|
||||||
|
for team_id in shared_teams:
|
||||||
|
celery_send_mail_shared_data_recorded_team.delay(self.identifier, self.title, team_id)
|
||||||
|
|
||||||
return action
|
return action
|
||||||
|
|
||||||
def unrecord(self, performing_user: User, request: HttpRequest = None):
|
def unrecord(self, performing_user, request: HttpRequest = None):
|
||||||
""" Unrecords a dataset
|
""" Unrecords a dataset
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -318,7 +339,7 @@ class RecordableObjectMixin(models.Model):
|
|||||||
class CheckableObjectMixin(models.Model):
|
class CheckableObjectMixin(models.Model):
|
||||||
# Checks - Refers to "Genehmigen" but optional
|
# Checks - Refers to "Genehmigen" but optional
|
||||||
checked = models.OneToOneField(
|
checked = models.OneToOneField(
|
||||||
UserActionLogEntry,
|
"user.UserActionLogEntry",
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
@ -346,7 +367,7 @@ class CheckableObjectMixin(models.Model):
|
|||||||
self.save()
|
self.save()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def set_checked(self, user: User) -> UserActionLogEntry:
|
def set_checked(self, user):
|
||||||
""" Perform checking
|
""" Perform checking
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -355,6 +376,7 @@ class CheckableObjectMixin(models.Model):
|
|||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from user.models import UserActionLogEntry
|
||||||
if self.checked:
|
if self.checked:
|
||||||
# Nothing to do
|
# Nothing to do
|
||||||
return
|
return
|
||||||
@ -363,17 +385,23 @@ class CheckableObjectMixin(models.Model):
|
|||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
# Send mail
|
# Send mail
|
||||||
shared_users = self.users.all().values_list("id", flat=True)
|
shared_users = self.shared_users.values_list("id", flat=True)
|
||||||
for user_id in shared_users:
|
for user_id in shared_users:
|
||||||
celery_send_mail_shared_data_checked.delay(self.identifier, self.title, user_id)
|
celery_send_mail_shared_data_checked.delay(self.identifier, self.title, user_id)
|
||||||
|
|
||||||
|
# Send mail
|
||||||
|
shared_teams = self.shared_teams.values_list("id", flat=True)
|
||||||
|
for team_id in shared_teams:
|
||||||
|
celery_send_mail_shared_data_checked_team.delay(self.identifier, self.title, team_id)
|
||||||
|
|
||||||
self.log.add(action)
|
self.log.add(action)
|
||||||
return action
|
return action
|
||||||
|
|
||||||
|
|
||||||
class ShareableObjectMixin(models.Model):
|
class ShareableObjectMixin(models.Model):
|
||||||
# Users having access on this object
|
# Users having access on this object
|
||||||
users = models.ManyToManyField(User, help_text="Users having access (data shared with)")
|
users = models.ManyToManyField("user.User", help_text="Users having access (data shared with)")
|
||||||
|
teams = models.ManyToManyField("user.Team", help_text="Teams having access (data shared with)")
|
||||||
access_token = models.CharField(
|
access_token = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
null=True,
|
null=True,
|
||||||
@ -420,7 +448,7 @@ class ShareableObjectMixin(models.Model):
|
|||||||
self.access_token = token
|
self.access_token = token
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def is_shared_with(self, user: User):
|
def is_shared_with(self, user):
|
||||||
""" Access check
|
""" Access check
|
||||||
|
|
||||||
Checks whether a given user has access to this object
|
Checks whether a given user has access to this object
|
||||||
@ -431,9 +459,36 @@ class ShareableObjectMixin(models.Model):
|
|||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return self.users.filter(id=user.id)
|
directly_shared = self.users.filter(id=user.id).exists()
|
||||||
|
team_shared = self.teams.filter(
|
||||||
|
users__in=[user]
|
||||||
|
).exists()
|
||||||
|
is_shared = directly_shared or team_shared
|
||||||
|
return is_shared
|
||||||
|
|
||||||
def share_with(self, user: User):
|
def share_with_team(self, team):
|
||||||
|
""" Adds team to list of shared access teans
|
||||||
|
|
||||||
|
Args:
|
||||||
|
team (Team): The team to be added to the object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.teams.add(team)
|
||||||
|
|
||||||
|
def share_with_team_list(self, team_list: list):
|
||||||
|
""" Sets the list of shared access teams
|
||||||
|
|
||||||
|
Args:
|
||||||
|
team_list (list): The teams to be added to the object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.teams.set(team_list)
|
||||||
|
|
||||||
|
def share_with_user(self, user):
|
||||||
""" Adds user to list of shared access users
|
""" Adds user to list of shared access users
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -445,7 +500,7 @@ class ShareableObjectMixin(models.Model):
|
|||||||
if not self.is_shared_with(user):
|
if not self.is_shared_with(user):
|
||||||
self.users.add(user)
|
self.users.add(user)
|
||||||
|
|
||||||
def share_with_list(self, user_list: list):
|
def share_with_user_list(self, user_list: list):
|
||||||
""" Sets the list of shared access users
|
""" Sets the list of shared access users
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -456,8 +511,8 @@ class ShareableObjectMixin(models.Model):
|
|||||||
"""
|
"""
|
||||||
self.users.set(user_list)
|
self.users.set(user_list)
|
||||||
|
|
||||||
def update_sharing_user(self, form):
|
def _update_shared_teams(self, form):
|
||||||
""" Adds a new user with shared access to the object
|
""" Updates shared access on the object for teams
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
form (ShareModalForm): The form holding the data
|
form (ShareModalForm): The form holding the data
|
||||||
@ -466,25 +521,65 @@ class ShareableObjectMixin(models.Model):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
form_data = form.cleaned_data
|
form_data = form.cleaned_data
|
||||||
|
shared_teams = self.shared_teams
|
||||||
|
|
||||||
keep_accessing_users = form_data["users"]
|
# Fetch selected teams and find out which user IDs are in removed teams -> mails need to be sent
|
||||||
new_accessing_users = list(form_data["user_select"].values_list("id", flat=True))
|
accessing_teams = form_data["teams"]
|
||||||
accessing_users = keep_accessing_users + new_accessing_users
|
removed_teams = shared_teams.exclude(
|
||||||
users = User.objects.filter(
|
id__in=accessing_teams
|
||||||
|
).values_list("id", flat=True)
|
||||||
|
new_teams = accessing_teams.exclude(
|
||||||
|
id__in=shared_teams
|
||||||
|
).values_list("id", flat=True)
|
||||||
|
|
||||||
|
for team_id in new_teams:
|
||||||
|
celery_send_mail_shared_access_given_team.delay(self.identifier, self.title, team_id)
|
||||||
|
for team_id in removed_teams:
|
||||||
|
celery_send_mail_shared_access_removed_team.delay(self.identifier, self.title, team_id)
|
||||||
|
|
||||||
|
self.share_with_team_list(accessing_teams)
|
||||||
|
|
||||||
|
def _update_shared_users(self, form):
|
||||||
|
""" Updates shared access on the object for single users
|
||||||
|
|
||||||
|
Args:
|
||||||
|
form (ShareModalForm): The form holding the data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
form_data = form.cleaned_data
|
||||||
|
shared_users = self.shared_users
|
||||||
|
|
||||||
|
# Fetch selected users
|
||||||
|
accessing_users = form_data["users"]
|
||||||
|
removed_users = shared_users.exclude(
|
||||||
id__in=accessing_users
|
id__in=accessing_users
|
||||||
)
|
).values_list("id", flat=True)
|
||||||
removed_users = self.users.all().exclude(
|
new_users = accessing_users.exclude(
|
||||||
id__in=accessing_users
|
id__in=shared_users
|
||||||
).values("id")
|
).values_list("id", flat=True)
|
||||||
|
|
||||||
# Send mails
|
# Send mails
|
||||||
for user in removed_users:
|
for user_id in removed_users:
|
||||||
celery_send_mail_shared_access_removed.delay(self.identifier, self.title, user["id"])
|
celery_send_mail_shared_access_removed.delay(self.identifier, self.title, user_id)
|
||||||
for user in new_accessing_users:
|
for user_id in new_users:
|
||||||
celery_send_mail_shared_access_given.delay(self.identifier, self.title, user)
|
celery_send_mail_shared_access_given.delay(self.identifier, self.title, user_id)
|
||||||
|
|
||||||
# Set new shared users
|
# Set new shared users
|
||||||
self.share_with_list(users)
|
self.share_with_user_list(accessing_users)
|
||||||
|
|
||||||
|
def update_shared_access(self, form):
|
||||||
|
""" Updates shared access on the object
|
||||||
|
|
||||||
|
Args:
|
||||||
|
form (ShareModalForm): The form holding the data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
self._update_shared_teams(form)
|
||||||
|
self._update_shared_users(form)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def shared_users(self) -> QuerySet:
|
def shared_users(self) -> QuerySet:
|
||||||
@ -495,6 +590,15 @@ class ShareableObjectMixin(models.Model):
|
|||||||
"""
|
"""
|
||||||
return self.users.all()
|
return self.users.all()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def shared_teams(self) -> QuerySet:
|
||||||
|
""" Shortcut for fetching the teams which have shared access on this object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
teams (QuerySet)
|
||||||
|
"""
|
||||||
|
return self.teams.all()
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_share_url(self):
|
def get_share_url(self):
|
||||||
""" Returns the share url for the object
|
""" Returns the share url for the object
|
||||||
|
@ -242,15 +242,24 @@ Similar to bootstraps 'shadow-lg'
|
|||||||
.select2-results__option--highlighted{
|
.select2-results__option--highlighted{
|
||||||
background-color: var(--rlp-red) !important;
|
background-color: var(--rlp-red) !important;
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
.select2-container--default .select2-results__group{
|
.select2-container--default .select2-results__group{
|
||||||
background-color: var(--rlp-gray-light);
|
background-color: var(--rlp-gray-light);
|
||||||
}
|
}
|
||||||
.select2-container--default .select2-results__option .select2-results__option{
|
.select2-container--default .select2-results__option .select2-results__option{
|
||||||
padding-left: 2em !important;
|
padding-left: 1em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
.select2-results__options--nested{
|
||||||
|
padding-left: 1em !important;
|
||||||
}
|
}
|
||||||
.select2-container--default .select2-results > .select2-results__options{
|
.select2-container--default .select2-results > .select2-results__options{
|
||||||
max-height: 500px !important;
|
max-height: 500px !important;
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
.select2-container--default .select2-results__option .select2-results__option{
|
.select2-container--default .select2-results__option .select2-results__option{
|
||||||
padding-left: 2em;
|
padding-left: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*/
|
@ -38,6 +38,20 @@ def celery_send_mail_shared_access_given(obj_identifier, obj_title=None, user_id
|
|||||||
user.send_mail_shared_access_given(obj_identifier, obj_title)
|
user.send_mail_shared_access_given(obj_identifier, obj_title)
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def celery_send_mail_shared_access_removed_team(obj_identifier, obj_title=None, team_id=None):
|
||||||
|
from user.models import Team
|
||||||
|
team = Team.objects.get(id=team_id)
|
||||||
|
team.send_mail_shared_access_removed(obj_identifier, obj_title)
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def celery_send_mail_shared_access_given_team(obj_identifier, obj_title=None, team_id=None):
|
||||||
|
from user.models import Team
|
||||||
|
team = Team.objects.get(id=team_id)
|
||||||
|
team.send_mail_shared_access_given_team(obj_identifier, obj_title)
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def celery_send_mail_shared_data_recorded(obj_identifier, obj_title=None, user_id=None):
|
def celery_send_mail_shared_data_recorded(obj_identifier, obj_title=None, user_id=None):
|
||||||
from user.models import User
|
from user.models import User
|
||||||
@ -52,6 +66,20 @@ def celery_send_mail_shared_data_unrecorded(obj_identifier, obj_title=None, user
|
|||||||
user.send_mail_shared_data_unrecorded(obj_identifier, obj_title)
|
user.send_mail_shared_data_unrecorded(obj_identifier, obj_title)
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def celery_send_mail_shared_data_recorded_team(obj_identifier, obj_title=None, team_id=None):
|
||||||
|
from user.models import Team
|
||||||
|
team = Team.objects.get(id=team_id)
|
||||||
|
team.send_mail_shared_data_recorded(obj_identifier, obj_title)
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def celery_send_mail_shared_data_unrecorded_team(obj_identifier, obj_title=None, team_id=None):
|
||||||
|
from user.models import Team
|
||||||
|
team = Team.objects.get(id=team_id)
|
||||||
|
team.send_mail_shared_data_unrecorded(obj_identifier, obj_title)
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def celery_send_mail_shared_data_deleted(obj_identifier, obj_title=None, user_id=None):
|
def celery_send_mail_shared_data_deleted(obj_identifier, obj_title=None, user_id=None):
|
||||||
from user.models import User
|
from user.models import User
|
||||||
@ -64,3 +92,17 @@ def celery_send_mail_shared_data_checked(obj_identifier, obj_title=None, user_id
|
|||||||
from user.models import User
|
from user.models import User
|
||||||
user = User.objects.get(id=user_id)
|
user = User.objects.get(id=user_id)
|
||||||
user.send_mail_shared_data_checked(obj_identifier, obj_title)
|
user.send_mail_shared_data_checked(obj_identifier, obj_title)
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def celery_send_mail_shared_data_deleted_team(obj_identifier, obj_title=None, team_id=None):
|
||||||
|
from user.models import Team
|
||||||
|
team = Team.objects.get(id=team_id)
|
||||||
|
team.send_mail_shared_data_deleted(obj_identifier, obj_title)
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def celery_send_mail_shared_data_checked_team(obj_identifier, obj_title=None, team_id=None):
|
||||||
|
from user.models import Team
|
||||||
|
team = Team.objects.get(id=team_id)
|
||||||
|
team.send_mail_shared_data_checked(obj_identifier, obj_title)
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
{% load l10n fontawesome_5 %}
|
||||||
|
|
||||||
|
{% for code in codes %}
|
||||||
|
<div class="ml-4 tree-element">
|
||||||
|
<label class="tree-label" role="{% if not code.is_leaf%}button{% endif %}" for="input_{{code.pk|unlocalize}}" id="{{code.pk|unlocalize}}" data-toggle="collapse" data-target="#children_{{code.pk|unlocalize}}" aria-expanded="true" aria-controls="children_{{code.pk|unlocalize}}">
|
||||||
|
{% if code.is_leaf%}
|
||||||
|
<input class="tree-input" id="input_{{code.pk|unlocalize}}" name="{{ widget.name }}" type="checkbox" value="{{code.pk|unlocalize}}" {% if code.pk|unlocalize in widget.value %}checked{% endif %}/>
|
||||||
|
{% else %}
|
||||||
|
{% fa5_icon 'angle-right' %}
|
||||||
|
{% endif %}
|
||||||
|
{{code.long_name}}
|
||||||
|
</label>
|
||||||
|
{% if not code.is_leaf %}
|
||||||
|
<div id="children_{{code.pk|unlocalize}}" data-toggle="collapse" class="collapse tree-element-children">
|
||||||
|
{% with code.children as codes %}
|
||||||
|
{% include 'konova/widgets/checkbox-tree-select-content.html' %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
68
konova/templates/konova/widgets/checkbox-tree-select.html
Normal file
68
konova/templates/konova/widgets/checkbox-tree-select.html
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<div class="ml-4 mb-4">
|
||||||
|
<input id="tree-search-input" class="form-control" type="text" placeholder="{% trans 'Search' %}"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tree-root">
|
||||||
|
{% include 'konova/widgets/checkbox-tree-select-content.html' %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleSelectedCssClass(element){
|
||||||
|
element = $(element);
|
||||||
|
var cssClass = "badge rlp-r"
|
||||||
|
|
||||||
|
var directParent = element.closest(".tree-element-children")
|
||||||
|
var root = element.parents(".tree-element-children")
|
||||||
|
|
||||||
|
var otherCheckedInputsOfParent = directParent.find('.tree-input:checked');
|
||||||
|
var otherCheckedInputsOfRoot = root.find('.tree-input:checked');
|
||||||
|
|
||||||
|
if(otherCheckedInputsOfParent.length == 0){
|
||||||
|
var parentLabel = directParent.siblings(".tree-label");
|
||||||
|
parentLabel.removeClass(cssClass)
|
||||||
|
if(otherCheckedInputsOfRoot.length == 0){
|
||||||
|
var rootLabel = root.siblings(".tree-label")
|
||||||
|
rootLabel.removeClass(cssClass)
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
var rootAndParentLabel = root.siblings(".tree-label");
|
||||||
|
rootAndParentLabel.addClass(cssClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeHandler(event){
|
||||||
|
toggleSelectedCssClass(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchInputHandler(event){
|
||||||
|
var elem = $(this);
|
||||||
|
var val = elem.val()
|
||||||
|
var allTreeElements = $(".tree-element")
|
||||||
|
var allTreeElementsContain = $(".tree-element").filter(function(){
|
||||||
|
var reg = new RegExp(val, "i");
|
||||||
|
return reg.test($(this).text());
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if(val.length > 0){
|
||||||
|
allTreeElements.hide()
|
||||||
|
allTreeElementsContain.show()
|
||||||
|
}else{
|
||||||
|
allTreeElements.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listener on search input
|
||||||
|
$("#tree-search-input").keyup(searchInputHandler)
|
||||||
|
|
||||||
|
// Add event listener on changed checkboxes
|
||||||
|
$(".tree-input").change(changeHandler);
|
||||||
|
|
||||||
|
// initialize all pre-checked checkboxes (e.g. on an edit form)
|
||||||
|
var preCheckedElements = $(".tree-input:checked");
|
||||||
|
preCheckedElements.each(function (index, element){
|
||||||
|
toggleSelectedCssClass(element);
|
||||||
|
})
|
||||||
|
</script>
|
@ -71,6 +71,7 @@ class AutocompleteTestCase(BaseTestCase):
|
|||||||
"codes-registration-office-autocomplete",
|
"codes-registration-office-autocomplete",
|
||||||
"codes-conservation-office-autocomplete",
|
"codes-conservation-office-autocomplete",
|
||||||
"share-user-autocomplete",
|
"share-user-autocomplete",
|
||||||
|
"share-team-autocomplete",
|
||||||
]
|
]
|
||||||
for test in tests:
|
for test in tests:
|
||||||
self.client.login(username=self.superuser.username, password=self.superuser_pw)
|
self.client.login(username=self.superuser.username, password=self.superuser_pw)
|
||||||
|
@ -9,7 +9,7 @@ import datetime
|
|||||||
|
|
||||||
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID
|
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID
|
||||||
from ema.models import Ema
|
from ema.models import Ema
|
||||||
from user.models import User
|
from user.models import User, Team
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
from django.contrib.gis.geos import MultiPolygon, Polygon
|
from django.contrib.gis.geos import MultiPolygon, Polygon
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
@ -65,6 +65,7 @@ class BaseTestCase(TestCase):
|
|||||||
self.create_dummy_states()
|
self.create_dummy_states()
|
||||||
self.create_dummy_action()
|
self.create_dummy_action()
|
||||||
self.codes = self.create_dummy_codes()
|
self.codes = self.create_dummy_codes()
|
||||||
|
self.team = self.create_dummy_team()
|
||||||
|
|
||||||
# Set the default group as only group for the user
|
# Set the default group as only group for the user
|
||||||
default_group = self.groups.get(name=DEFAULT_GROUP)
|
default_group = self.groups.get(name=DEFAULT_GROUP)
|
||||||
@ -251,6 +252,24 @@ class BaseTestCase(TestCase):
|
|||||||
])
|
])
|
||||||
return codes
|
return codes
|
||||||
|
|
||||||
|
def create_dummy_team(self):
|
||||||
|
""" Creates a dummy team
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
if self.superuser is None:
|
||||||
|
self.create_users()
|
||||||
|
|
||||||
|
team = Team.objects.get_or_create(
|
||||||
|
name="Testteam",
|
||||||
|
description="Testdescription",
|
||||||
|
admin=self.superuser,
|
||||||
|
)[0]
|
||||||
|
team.users.add(self.superuser)
|
||||||
|
|
||||||
|
return team
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_dummy_geometry() -> MultiPolygon:
|
def create_dummy_geometry() -> MultiPolygon:
|
||||||
""" Creates some geometry
|
""" Creates some geometry
|
||||||
|
@ -20,7 +20,7 @@ from django.urls import path, include
|
|||||||
from konova.autocompletes import EcoAccountAutocomplete, \
|
from konova.autocompletes import EcoAccountAutocomplete, \
|
||||||
InterventionAutocomplete, CompensationActionCodeAutocomplete, BiotopeCodeAutocomplete, LawCodeAutocomplete, \
|
InterventionAutocomplete, CompensationActionCodeAutocomplete, BiotopeCodeAutocomplete, LawCodeAutocomplete, \
|
||||||
RegistrationOfficeCodeAutocomplete, ConservationOfficeCodeAutocomplete, ProcessTypeCodeAutocomplete, \
|
RegistrationOfficeCodeAutocomplete, ConservationOfficeCodeAutocomplete, ProcessTypeCodeAutocomplete, \
|
||||||
ShareUserAutocomplete, BiotopeExtraCodeAutocomplete, CompensationActionDetailCodeAutocomplete
|
ShareUserAutocomplete, BiotopeExtraCodeAutocomplete, CompensationActionDetailCodeAutocomplete, ShareTeamAutocomplete
|
||||||
from konova.settings import SSO_SERVER, SSO_PUBLIC_KEY, SSO_PRIVATE_KEY, DEBUG
|
from konova.settings import SSO_SERVER, SSO_PUBLIC_KEY, SSO_PRIVATE_KEY, DEBUG
|
||||||
from konova.sso.sso import KonovaSSOClient
|
from konova.sso.sso import KonovaSSOClient
|
||||||
from konova.views import logout_view, home_view
|
from konova.views import logout_view, home_view
|
||||||
@ -52,6 +52,7 @@ urlpatterns = [
|
|||||||
path("atcmplt/codes/reg-off", RegistrationOfficeCodeAutocomplete.as_view(), name="codes-registration-office-autocomplete"),
|
path("atcmplt/codes/reg-off", RegistrationOfficeCodeAutocomplete.as_view(), name="codes-registration-office-autocomplete"),
|
||||||
path("atcmplt/codes/cons-off", ConservationOfficeCodeAutocomplete.as_view(), name="codes-conservation-office-autocomplete"),
|
path("atcmplt/codes/cons-off", ConservationOfficeCodeAutocomplete.as_view(), name="codes-conservation-office-autocomplete"),
|
||||||
path("atcmplt/share/u", ShareUserAutocomplete.as_view(), name="share-user-autocomplete"),
|
path("atcmplt/share/u", ShareUserAutocomplete.as_view(), name="share-user-autocomplete"),
|
||||||
|
path("atcmplt/share/t", ShareTeamAutocomplete.as_view(), name="share-team-autocomplete"),
|
||||||
]
|
]
|
||||||
|
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
|
@ -92,6 +92,144 @@ class Mailer:
|
|||||||
msg
|
msg
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def send_mail_shared_access_given_team(self, obj_identifier, obj_title, team):
|
||||||
|
""" Send a mail if a team just got access to the object
|
||||||
|
|
||||||
|
Args:
|
||||||
|
obj_identifier (str): The object identifier
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
context = {
|
||||||
|
"team": team,
|
||||||
|
"obj_identifier": obj_identifier,
|
||||||
|
"obj_title": obj_title,
|
||||||
|
"EMAIL_REPLY_TO": EMAIL_REPLY_TO,
|
||||||
|
}
|
||||||
|
msg = render_to_string("email/sharing/shared_access_given_team.html", context)
|
||||||
|
user_mail_address = team.users.values_list("email", flat=True)
|
||||||
|
self.send(
|
||||||
|
user_mail_address,
|
||||||
|
_("{} - Shared access given").format(obj_identifier),
|
||||||
|
msg
|
||||||
|
)
|
||||||
|
|
||||||
|
def send_mail_shared_access_removed_team(self, obj_identifier, obj_title, team):
|
||||||
|
""" Send a mail if a team just lost access to the object
|
||||||
|
|
||||||
|
Args:
|
||||||
|
obj_identifier (str): The object identifier
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
context = {
|
||||||
|
"team": team,
|
||||||
|
"obj_identifier": obj_identifier,
|
||||||
|
"obj_title": obj_title,
|
||||||
|
"EMAIL_REPLY_TO": EMAIL_REPLY_TO,
|
||||||
|
}
|
||||||
|
msg = render_to_string("email/sharing/shared_access_removed_team.html", context)
|
||||||
|
user_mail_address = team.users.values_list("email", flat=True)
|
||||||
|
self.send(
|
||||||
|
user_mail_address,
|
||||||
|
_("{} - Shared access removed").format(obj_identifier),
|
||||||
|
msg
|
||||||
|
)
|
||||||
|
|
||||||
|
def send_mail_shared_data_unrecorded_team(self, obj_identifier, obj_title, team):
|
||||||
|
""" Send a mail if data has just been unrecorded
|
||||||
|
|
||||||
|
Args:
|
||||||
|
obj_identifier (str): The object identifier
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
context = {
|
||||||
|
"team": team,
|
||||||
|
"obj_identifier": obj_identifier,
|
||||||
|
"obj_title": obj_title,
|
||||||
|
"EMAIL_REPLY_TO": EMAIL_REPLY_TO,
|
||||||
|
}
|
||||||
|
msg = render_to_string("email/recording/shared_data_unrecorded_team.html", context)
|
||||||
|
user_mail_address = team.users.values_list("email", flat=True)
|
||||||
|
self.send(
|
||||||
|
user_mail_address,
|
||||||
|
_("{} - Shared data unrecorded").format(obj_identifier),
|
||||||
|
msg
|
||||||
|
)
|
||||||
|
|
||||||
|
def send_mail_shared_data_recorded_team(self, obj_identifier, obj_title, team):
|
||||||
|
""" Send a mail if data has just been recorded
|
||||||
|
|
||||||
|
Args:
|
||||||
|
obj_identifier (str): The object identifier
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
context = {
|
||||||
|
"team": team,
|
||||||
|
"obj_identifier": obj_identifier,
|
||||||
|
"obj_title": obj_title,
|
||||||
|
"EMAIL_REPLY_TO": EMAIL_REPLY_TO,
|
||||||
|
}
|
||||||
|
msg = render_to_string("email/recording/shared_data_recorded_team.html", context)
|
||||||
|
user_mail_address = team.users.values_list("email", flat=True)
|
||||||
|
self.send(
|
||||||
|
user_mail_address,
|
||||||
|
_("{} - Shared data recorded").format(obj_identifier),
|
||||||
|
msg
|
||||||
|
)
|
||||||
|
|
||||||
|
def send_mail_shared_data_checked_team(self, obj_identifier, obj_title, team):
|
||||||
|
""" Send a mail if data has just been checked
|
||||||
|
|
||||||
|
Args:
|
||||||
|
obj_identifier (str): The object identifier
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
context = {
|
||||||
|
"team": team,
|
||||||
|
"obj_identifier": obj_identifier,
|
||||||
|
"obj_title": obj_title,
|
||||||
|
"EMAIL_REPLY_TO": EMAIL_REPLY_TO,
|
||||||
|
}
|
||||||
|
msg = render_to_string("email/checking/shared_data_checked_team.html", context)
|
||||||
|
user_mail_address = team.users.values_list("email", flat=True)
|
||||||
|
self.send(
|
||||||
|
user_mail_address,
|
||||||
|
_("{} - Shared data checked").format(obj_identifier),
|
||||||
|
msg
|
||||||
|
)
|
||||||
|
|
||||||
|
def send_mail_shared_data_deleted_team(self, obj_identifier, obj_title, team):
|
||||||
|
""" Send a mail if data has just been deleted
|
||||||
|
|
||||||
|
Args:
|
||||||
|
obj_identifier (str): The object identifier
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
context = {
|
||||||
|
"team": team,
|
||||||
|
"obj_identifier": obj_identifier,
|
||||||
|
"obj_title": obj_title,
|
||||||
|
"EMAIL_REPLY_TO": EMAIL_REPLY_TO,
|
||||||
|
}
|
||||||
|
msg = render_to_string("email/deleting/shared_data_deleted_team.html", context)
|
||||||
|
user_mail_address = team.users.values_list("email", flat=True)
|
||||||
|
self.send(
|
||||||
|
user_mail_address,
|
||||||
|
_("{} - Shared data deleted").format(obj_identifier),
|
||||||
|
msg
|
||||||
|
)
|
||||||
|
|
||||||
def send_mail_shared_data_recorded(self, obj_identifier, obj_title, user):
|
def send_mail_shared_data_recorded(self, obj_identifier, obj_title, user):
|
||||||
""" Send a mail if the user's shared data has just been unrecorded
|
""" Send a mail if the user's shared data has just been unrecorded
|
||||||
|
|
||||||
|
@ -7,16 +7,18 @@ Created on: 02.08.21
|
|||||||
"""
|
"""
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
UNGROUPED = _("Ungrouped")
|
||||||
FORM_INVALID = _("There was an error on this form.")
|
FORM_INVALID = _("There was an error on this form.")
|
||||||
PARAMS_INVALID = _("Invalid parameters")
|
PARAMS_INVALID = _("Invalid parameters")
|
||||||
INTERVENTION_INVALID = _("There are errors in this intervention.")
|
INTERVENTION_INVALID = _("There are errors in this intervention.")
|
||||||
IDENTIFIER_REPLACED = _("The identifier '{}' had to be changed to '{}' since another entry has been added in the meanwhile, which uses this identifier")
|
IDENTIFIER_REPLACED = _("The identifier '{}' had to be changed to '{}' since another entry has been added in the meanwhile, which uses this identifier")
|
||||||
|
ENTRY_REMOVE_MISSING_PERMISSION = _("Only conservation or registration office users are allowed to remove entries.")
|
||||||
|
MISSING_GROUP_PERMISSION = _("You need to be part of another user group.")
|
||||||
|
CHECKED_RECORDED_RESET = _("Status of Checked and Recorded reseted")
|
||||||
|
|
||||||
|
# SHARE
|
||||||
DATA_UNSHARED = _("This data is not shared with you")
|
DATA_UNSHARED = _("This data is not shared with you")
|
||||||
DATA_UNSHARED_EXPLANATION = _("Remember: This data has not been shared with you, yet. This means you can only read but can not edit or perform any actions like running a check or recording.")
|
DATA_UNSHARED_EXPLANATION = _("Remember: This data has not been shared with you, yet. This means you can only read but can not edit or perform any actions like running a check or recording.")
|
||||||
MISSING_GROUP_PERMISSION = _("You need to be part of another user group.")
|
|
||||||
|
|
||||||
CHECKED_RECORDED_RESET = _("Status of Checked and Recorded reseted")
|
|
||||||
|
|
||||||
# FILES
|
# FILES
|
||||||
FILE_TYPE_UNSUPPORTED = _("Unsupported file type")
|
FILE_TYPE_UNSUPPORTED = _("Unsupported file type")
|
||||||
|
@ -9,9 +9,12 @@ from django.contrib.auth import logout
|
|||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.http import HttpRequest, FileResponse
|
from django.http import HttpRequest, FileResponse
|
||||||
from django.shortcuts import redirect, render, get_object_or_404
|
from django.shortcuts import redirect, render, get_object_or_404
|
||||||
|
from django.template.loader import render_to_string
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from codelist.models import KonovaCode
|
||||||
|
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID
|
||||||
from compensation.models import Compensation, EcoAccount
|
from compensation.models import Compensation, EcoAccount
|
||||||
from intervention.models import Intervention
|
from intervention.models import Intervention
|
||||||
from konova.contexts import BaseContext
|
from konova.contexts import BaseContext
|
||||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
28
templates/email/checking/shared_data_checked_team.html
Normal file
28
templates/email/checking/shared_data_checked_team.html
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>{% trans 'Shared data checked' %}</h2>
|
||||||
|
<h4>{{obj_identifier}}</h4>
|
||||||
|
<hr>
|
||||||
|
<article>
|
||||||
|
{% trans 'Hello team' %} {{team.name}},
|
||||||
|
<br>
|
||||||
|
{% trans 'the following dataset has just been checked' %}
|
||||||
|
<br>
|
||||||
|
<strong>{{obj_identifier}}</strong>
|
||||||
|
<br>
|
||||||
|
<strong>{{obj_title}}</strong>
|
||||||
|
<br>
|
||||||
|
{% trans 'This means, the responsible registration office just confirmed the correctness of this dataset.' %}
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
{% trans 'Best regards' %}
|
||||||
|
<br>
|
||||||
|
KSP
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
{% include 'email/signature.html' %}
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
28
templates/email/deleting/shared_data_deleted_team.html
Normal file
28
templates/email/deleting/shared_data_deleted_team.html
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>{% trans 'Shared data deleted' %}</h2>
|
||||||
|
<h4>{{obj_identifier}}</h4>
|
||||||
|
<hr>
|
||||||
|
<article>
|
||||||
|
{% trans 'Hello team' %} {{team.name}},
|
||||||
|
<br>
|
||||||
|
{% trans 'the following dataset has just been deleted' %}
|
||||||
|
<br>
|
||||||
|
<strong>{{obj_identifier}}</strong>
|
||||||
|
<br>
|
||||||
|
<strong>"{{obj_title}}"</strong>
|
||||||
|
<br>
|
||||||
|
{% trans 'If this should not have been happened, please contact us. See the signature for details.' %}
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
{% trans 'Best regards' %}
|
||||||
|
<br>
|
||||||
|
KSP
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
{% include 'email/signature.html' %}
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
33
templates/email/recording/shared_data_recorded_team.html
Normal file
33
templates/email/recording/shared_data_recorded_team.html
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>{% trans 'Shared data recorded' %}</h2>
|
||||||
|
<h4>{{obj_identifier}}</h4>
|
||||||
|
<hr>
|
||||||
|
<article>
|
||||||
|
{% trans 'Hello team' %} {{team.name}},
|
||||||
|
<br>
|
||||||
|
{% trans 'the following dataset has just been recorded' %}
|
||||||
|
<br>
|
||||||
|
<strong>{{obj_identifier}}</strong>
|
||||||
|
<br>
|
||||||
|
<strong>"{{obj_title}}"</strong>
|
||||||
|
<br>
|
||||||
|
{% trans 'This means the data is now publicly available, e.g. in LANIS' %}
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
{% trans 'Best regards' %}
|
||||||
|
<br>
|
||||||
|
KSP
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<small>
|
||||||
|
{% trans 'Please note: Recorded intervention means the compensations are recorded as well.' %}
|
||||||
|
</small>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
{% include 'email/signature.html' %}
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
33
templates/email/recording/shared_data_unrecorded_team.html
Normal file
33
templates/email/recording/shared_data_unrecorded_team.html
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>{% trans 'Shared data unrecorded' %}</h2>
|
||||||
|
<h4>{{obj_identifier}}</h4>
|
||||||
|
<hr>
|
||||||
|
<article>
|
||||||
|
{% trans 'Hello team' %} {{team.name}},
|
||||||
|
<br>
|
||||||
|
{% trans 'the following dataset has just been unrecorded' %}
|
||||||
|
<br>
|
||||||
|
<strong>{{obj_identifier}}</strong>
|
||||||
|
<br>
|
||||||
|
<strong>"{{obj_title}}"</strong>
|
||||||
|
<br>
|
||||||
|
{% trans 'This means the data is no longer publicly available.' %}
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
{% trans 'Best regards' %}
|
||||||
|
<br>
|
||||||
|
KSP
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<small>
|
||||||
|
{% trans 'Please note: Unrecorded intervention means the compensations are unrecorded as well.' %}
|
||||||
|
</small>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
{% include 'email/signature.html' %}
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
34
templates/email/sharing/shared_access_given_team.html
Normal file
34
templates/email/sharing/shared_access_given_team.html
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>{% trans 'Access shared' %}</h2>
|
||||||
|
<h4>{{obj_identifier}}</h4>
|
||||||
|
<hr>
|
||||||
|
<article>
|
||||||
|
{% trans 'Hello team' %} {{team.name}},
|
||||||
|
<br>
|
||||||
|
{% trans 'the following dataset has just been shared with your team' %}
|
||||||
|
<br>
|
||||||
|
<strong>{{obj_identifier}}</strong>
|
||||||
|
<br>
|
||||||
|
<strong>"{{obj_title}}"</strong>
|
||||||
|
<br>
|
||||||
|
{% trans 'This means you can now edit this dataset.' %}
|
||||||
|
{% trans 'The shared dataset appears now by default on your overview for this dataset type.' %}
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
{% trans 'Best regards' %}
|
||||||
|
<br>
|
||||||
|
KSP
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<small>
|
||||||
|
{% trans 'Please note: Shared access on an intervention means you automatically have editing access to related compensations.' %}
|
||||||
|
</small>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
{% include 'email/signature.html' %}
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
29
templates/email/sharing/shared_access_removed_team.html
Normal file
29
templates/email/sharing/shared_access_removed_team.html
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>{% trans 'Shared access removed' %}</h2>
|
||||||
|
<h4>{{obj_identifier}}</h4>
|
||||||
|
<hr>
|
||||||
|
<article>
|
||||||
|
{% trans 'Hello team' %} {{team.name}},
|
||||||
|
<br>
|
||||||
|
{% trans 'your teams shared access, including editing, has been revoked for the dataset ' %}
|
||||||
|
<br>
|
||||||
|
<strong>{{obj_identifier}}</strong>
|
||||||
|
<br>
|
||||||
|
<strong>"{{obj_title}}"</strong>
|
||||||
|
<br>
|
||||||
|
{% trans 'However, you are still able to view the dataset content.' %}
|
||||||
|
{% trans 'Please use the provided search filter on the dataset`s overview pages to find them.' %}
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
{% trans 'Best regards' %}
|
||||||
|
<br>
|
||||||
|
KSP
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
{% include 'email/signature.html' %}
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
@ -1,6 +1,6 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from user.models import UserNotification, UserActionLogEntry, User
|
from user.models import UserNotification, UserActionLogEntry, User, Team
|
||||||
|
|
||||||
|
|
||||||
class UserNotificationAdmin(admin.ModelAdmin):
|
class UserNotificationAdmin(admin.ModelAdmin):
|
||||||
@ -64,7 +64,20 @@ class UserActionLogEntryAdmin(admin.ModelAdmin):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class TeamAdmin(admin.ModelAdmin):
|
||||||
|
list_display = [
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"admin",
|
||||||
|
]
|
||||||
|
search_fields = [
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(User, UserAdmin)
|
admin.site.register(User, UserAdmin)
|
||||||
|
admin.site.register(Team, TeamAdmin)
|
||||||
|
|
||||||
# Outcommented for a cleaner admin backend on production
|
# Outcommented for a cleaner admin backend on production
|
||||||
#admin.site.register(UserNotification, UserNotificationAdmin)
|
#admin.site.register(UserNotification, UserNotificationAdmin)
|
||||||
|
206
user/forms.py
206
user/forms.py
@ -5,17 +5,17 @@ Contact: michel.peltriaux@sgdnord.rlp.de
|
|||||||
Created on: 08.07.21
|
Created on: 08.07.21
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from dal import autocomplete
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError, transaction
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from api.models import APIUserToken
|
from api.models import APIUserToken
|
||||||
from intervention.inputs import GenerateInput
|
from intervention.inputs import GenerateInput
|
||||||
from user.models import User
|
from user.models import User, UserNotification, Team
|
||||||
|
|
||||||
from konova.forms import BaseForm, BaseModalForm
|
from konova.forms import BaseForm, BaseModalForm, RemoveModalForm
|
||||||
from user.models import UserNotification
|
|
||||||
|
|
||||||
|
|
||||||
class UserNotificationForm(BaseForm):
|
class UserNotificationForm(BaseForm):
|
||||||
@ -160,3 +160,201 @@ class UserAPITokenForm(BaseForm):
|
|||||||
user.api_token = new_token
|
user.api_token = new_token
|
||||||
user.save()
|
user.save()
|
||||||
return new_token
|
return new_token
|
||||||
|
|
||||||
|
|
||||||
|
class NewTeamModalForm(BaseModalForm):
|
||||||
|
name = forms.CharField(
|
||||||
|
label_suffix="",
|
||||||
|
label=_("Team name"),
|
||||||
|
max_length=500,
|
||||||
|
widget=forms.TextInput(
|
||||||
|
attrs={
|
||||||
|
"placeholder": _("Team name"),
|
||||||
|
"class": "form-control",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
label_suffix="",
|
||||||
|
label=_("Description"),
|
||||||
|
widget=forms.Textarea(
|
||||||
|
attrs={
|
||||||
|
"rows": 5,
|
||||||
|
"class": "form-control"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
members = forms.ModelMultipleChoiceField(
|
||||||
|
label=_("Manage team members"),
|
||||||
|
label_suffix="",
|
||||||
|
help_text=_("Multiple selection possible - You can only select users which are not already a team member. Enter the full username or e-mail."),
|
||||||
|
required=True,
|
||||||
|
queryset=User.objects.all(),
|
||||||
|
widget=autocomplete.ModelSelect2Multiple(
|
||||||
|
url="share-user-autocomplete",
|
||||||
|
attrs={
|
||||||
|
"data-placeholder": _("Click for selection"),
|
||||||
|
"data-minimum-input-length": 3,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.form_title = _("Create new team")
|
||||||
|
self.form_caption = _("You will become the administrator for this group by default. You do not need to add yourself to the list of members.")
|
||||||
|
self.action_url = reverse("user:team-new")
|
||||||
|
self.cancel_redirect = reverse("user:team-index")
|
||||||
|
|
||||||
|
def _is_name_valid(self):
|
||||||
|
name = self.cleaned_data.get("name", None)
|
||||||
|
teams_with_same_name = Team.objects.filter(
|
||||||
|
name=name
|
||||||
|
)
|
||||||
|
name_valid = not teams_with_same_name.exists()
|
||||||
|
if not name_valid:
|
||||||
|
self.add_error(
|
||||||
|
"name",
|
||||||
|
_("Name already taken. Try another.")
|
||||||
|
)
|
||||||
|
|
||||||
|
return name_valid
|
||||||
|
|
||||||
|
def is_valid(self):
|
||||||
|
super_valid = super().is_valid()
|
||||||
|
name_valid = self._is_name_valid()
|
||||||
|
return super_valid and name_valid
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
with transaction.atomic():
|
||||||
|
team = Team.objects.create(
|
||||||
|
name=self.cleaned_data.get("name", None),
|
||||||
|
description=self.cleaned_data.get("description", None),
|
||||||
|
admin=self.user,
|
||||||
|
)
|
||||||
|
members = self.cleaned_data.get("members", User.objects.none())
|
||||||
|
if self.user.id not in members:
|
||||||
|
members = members.union(
|
||||||
|
User.objects.filter(
|
||||||
|
id=self.user.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
team.users.set(members)
|
||||||
|
return team
|
||||||
|
|
||||||
|
|
||||||
|
class EditTeamModalForm(NewTeamModalForm):
|
||||||
|
admin = forms.ModelChoiceField(
|
||||||
|
label_suffix="",
|
||||||
|
label=_("Admin"),
|
||||||
|
help_text=_("Administrators manage team details and members"),
|
||||||
|
queryset=User.objects.none(),
|
||||||
|
empty_label=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __is_admin_valid(self):
|
||||||
|
admin = self.cleaned_data.get("admin", None)
|
||||||
|
members = self.cleaned_data.get("members", None)
|
||||||
|
_is_valid = admin in members
|
||||||
|
|
||||||
|
if not _is_valid:
|
||||||
|
self.add_error(
|
||||||
|
"members",
|
||||||
|
_("Selected admin ({}) needs to be a member of this team.").format(admin.username)
|
||||||
|
)
|
||||||
|
|
||||||
|
return _is_valid
|
||||||
|
|
||||||
|
def _is_name_valid(self):
|
||||||
|
name = self.cleaned_data.get("name", None)
|
||||||
|
teams_with_same_name = Team.objects.filter(
|
||||||
|
name=name
|
||||||
|
).exclude(
|
||||||
|
id=self.instance.id
|
||||||
|
)
|
||||||
|
name_valid = not teams_with_same_name.exists()
|
||||||
|
if not name_valid:
|
||||||
|
self.add_error(
|
||||||
|
"name",
|
||||||
|
_("Name already taken. Try another.")
|
||||||
|
)
|
||||||
|
|
||||||
|
return name_valid
|
||||||
|
|
||||||
|
def is_valid(self):
|
||||||
|
super_valid = super().is_valid()
|
||||||
|
admin_valid = self.__is_admin_valid()
|
||||||
|
return super_valid and admin_valid
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.form_title = _("Edit team")
|
||||||
|
self.action_url = reverse("user:team-edit", args=(self.instance.id,))
|
||||||
|
self.cancel_redirect = reverse("user:team-index")
|
||||||
|
|
||||||
|
members = self.instance.users.all()
|
||||||
|
self.fields["admin"].queryset = members
|
||||||
|
|
||||||
|
form_data = {
|
||||||
|
"members": members,
|
||||||
|
"name": self.instance.name,
|
||||||
|
"description": self.instance.description,
|
||||||
|
"admin": self.instance.admin,
|
||||||
|
}
|
||||||
|
self.load_initial_data(form_data)
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
with transaction.atomic():
|
||||||
|
self.instance.name = self.cleaned_data.get("name", None)
|
||||||
|
self.instance.description = self.cleaned_data.get("description", None)
|
||||||
|
self.instance.admin = self.cleaned_data.get("admin", None)
|
||||||
|
self.instance.save()
|
||||||
|
self.instance.users.set(self.cleaned_data.get("members", []))
|
||||||
|
return self.instance
|
||||||
|
|
||||||
|
|
||||||
|
class RemoveTeamModalForm(RemoveModalForm):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TeamDataForm(BaseModalForm):
|
||||||
|
name = forms.CharField(
|
||||||
|
label_suffix="",
|
||||||
|
label=_("Team name"),
|
||||||
|
max_length=500,
|
||||||
|
required=False,
|
||||||
|
widget=forms.TextInput(
|
||||||
|
attrs={
|
||||||
|
"placeholder": _("Team name"),
|
||||||
|
"class": "form-control",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
label_suffix="",
|
||||||
|
required=False,
|
||||||
|
label=_("Description"),
|
||||||
|
widget=forms.Textarea(
|
||||||
|
attrs={
|
||||||
|
"rows": 5,
|
||||||
|
"class": "form-control"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.form_title = _("Team")
|
||||||
|
self.form_caption = ""
|
||||||
|
self.render_submit = False
|
||||||
|
form_data = {
|
||||||
|
"name": self.instance.name,
|
||||||
|
"description": self.instance.description,
|
||||||
|
}
|
||||||
|
self.load_initial_data(
|
||||||
|
form_data,
|
||||||
|
[
|
||||||
|
"name",
|
||||||
|
"description"
|
||||||
|
]
|
||||||
|
)
|
29
user/migrations/0003_team.py
Normal file
29
user/migrations/0003_team.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Generated by Django 3.1.3 on 2022-02-17 10:22
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('user', '0002_user_api_token'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Team',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('name', models.CharField(blank=True, max_length=500, null=True)),
|
||||||
|
('description', models.TextField(blank=True, null=True)),
|
||||||
|
('admin', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('users', models.ManyToManyField(blank=True, related_name='teams', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -5,6 +5,7 @@ Contact: michel.peltriaux@sgdnord.rlp.de
|
|||||||
Created on: 15.11.21
|
Created on: 15.11.21
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from .user_action import *
|
from .user_action import UserActionLogEntry, UserAction
|
||||||
from .user import *
|
from .user import User
|
||||||
from .notification import *
|
from .notification import UserNotification, UserNotificationEnum
|
||||||
|
from .team import Team
|
||||||
|
95
user/models/team.py
Normal file
95
user/models/team.py
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from konova.models import UuidModel
|
||||||
|
from konova.utils.mailer import Mailer
|
||||||
|
|
||||||
|
|
||||||
|
class Team(UuidModel):
|
||||||
|
""" Groups users in self managed teams. Can be used for multi-sharing of data
|
||||||
|
|
||||||
|
"""
|
||||||
|
name = models.CharField(max_length=500, null=True, blank=True)
|
||||||
|
description = models.TextField(null=True, blank=True)
|
||||||
|
users = models.ManyToManyField("user.User", blank=True, related_name="teams")
|
||||||
|
admin = models.ForeignKey("user.User", blank=True, null=True, related_name="+", on_delete=models.SET_NULL)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def send_mail_shared_access_given_team(self, obj_identifier, obj_title):
|
||||||
|
""" Sends a mail to the team members in case of given shared access
|
||||||
|
|
||||||
|
Args:
|
||||||
|
obj_identifier ():
|
||||||
|
obj_title ():
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
mailer = Mailer()
|
||||||
|
mailer.send_mail_shared_access_given_team(obj_identifier, obj_title, self)
|
||||||
|
|
||||||
|
def send_mail_shared_access_removed(self, obj_identifier, obj_title):
|
||||||
|
""" Sends a mail to the team members in case of removed shared access
|
||||||
|
|
||||||
|
Args:
|
||||||
|
obj_identifier ():
|
||||||
|
obj_title ():
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
mailer = Mailer()
|
||||||
|
mailer.send_mail_shared_access_removed_team(obj_identifier, obj_title, self)
|
||||||
|
|
||||||
|
def send_mail_shared_data_unrecorded(self, obj_identifier, obj_title):
|
||||||
|
""" Sends a mail to the team members in case of unrecorded data
|
||||||
|
|
||||||
|
Args:
|
||||||
|
obj_identifier ():
|
||||||
|
obj_title ():
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
mailer = Mailer()
|
||||||
|
mailer.send_mail_shared_data_unrecorded_team(obj_identifier, obj_title, self)
|
||||||
|
|
||||||
|
def send_mail_shared_data_recorded(self, obj_identifier, obj_title):
|
||||||
|
""" Sends a mail to the team members in case of unrecorded data
|
||||||
|
|
||||||
|
Args:
|
||||||
|
obj_identifier ():
|
||||||
|
obj_title ():
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
mailer = Mailer()
|
||||||
|
mailer.send_mail_shared_data_recorded_team(obj_identifier, obj_title, self)
|
||||||
|
|
||||||
|
def send_mail_shared_data_checked(self, obj_identifier, obj_title):
|
||||||
|
""" Sends a mail to the team members in case of checked data
|
||||||
|
|
||||||
|
Args:
|
||||||
|
obj_identifier ():
|
||||||
|
obj_title ():
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
mailer = Mailer()
|
||||||
|
mailer.send_mail_shared_data_checked_team(obj_identifier, obj_title, self)
|
||||||
|
|
||||||
|
def send_mail_shared_data_deleted(self, obj_identifier, obj_title):
|
||||||
|
""" Sends a mail to the team members in case of deleted data
|
||||||
|
|
||||||
|
Args:
|
||||||
|
obj_identifier ():
|
||||||
|
obj_title ():
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
mailer = Mailer()
|
||||||
|
mailer.send_mail_shared_data_deleted_team(obj_identifier, obj_title, self)
|
@ -1,6 +1,6 @@
|
|||||||
{% load fontawesome_5 i18n %}
|
{% load fontawesome_5 i18n %}
|
||||||
|
|
||||||
<button class="btn btn-default btn-modal" data-form-url="{% url 'user:contact' user.id %}" title="{% trans 'Show contact data' %}">
|
<button class="btn btn-default btn-modal" data-form-url="{% url 'user:contact' user.id %}" title="{% trans 'Show contact data' %}">
|
||||||
{% fa5_icon 'id-card' %}
|
{% fa5_icon 'user' %}
|
||||||
<span>{{user.username}}</span>
|
<span>{{user.username}}</span>
|
||||||
</button>
|
</button>
|
6
user/templates/user/includes/team_data_modal_button.html
Normal file
6
user/templates/user/includes/team_data_modal_button.html
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{% load fontawesome_5 i18n %}
|
||||||
|
|
||||||
|
<button class="btn btn-default btn-modal" data-form-url="{% url 'user:team-data' team.id %}" title="{% trans 'Show team data' %}">
|
||||||
|
{% fa5_icon 'users' %}
|
||||||
|
<span>{{team.name}}</span>
|
||||||
|
</button>
|
@ -62,6 +62,14 @@
|
|||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row mb-2">
|
||||||
|
<a href="{% url 'user:team-index' %}" title="{% trans 'Manage teams' %}">
|
||||||
|
<button class="btn btn-default">
|
||||||
|
{% fa5_icon 'users' %}
|
||||||
|
<span>{% trans 'Teams' %}</span>
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
69
user/templates/user/team/index.html
Normal file
69
user/templates/user/team/index.html
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n fontawesome_5 %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
|
||||||
|
{% comment %}
|
||||||
|
dal documentation (django-autocomplete-light) states using form.media for adding needed scripts.
|
||||||
|
This does not work properly with modal forms, as the scripts are not loaded properly inside the modal.
|
||||||
|
Therefore the script linkages from form.media have been extracted and put inside dal/scripts.html to ensure
|
||||||
|
these scripts are loaded when needed.
|
||||||
|
{% endcomment %}
|
||||||
|
{% include 'dal/scripts.html' %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div class="container">
|
||||||
|
<h4>{% trans 'Teams' %}</h4>
|
||||||
|
<div class="col-md">
|
||||||
|
<button class="btn rlp-r btn-modal" data-form-url="{% url 'user:team-new' %}" title="{% trans 'Add new team' %}">
|
||||||
|
{% fa5_icon 'plus' %}
|
||||||
|
{% trans 'New' %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="table-container">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="align-middle">{% trans 'Name' %}</th>
|
||||||
|
<th scope="col" class="align-middle w-20">{% trans 'Description' %}</th>
|
||||||
|
<th scope="col" class="align-middle">{% trans 'Members' %}</th>
|
||||||
|
<th scope="col" class="align-middle">{% trans 'Action' %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for team in teams %}
|
||||||
|
<tr>
|
||||||
|
<td>{{team.name}}</td>
|
||||||
|
<td>
|
||||||
|
<div class="scroll-150">
|
||||||
|
{{team.description}}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% for member in team.users.all %}
|
||||||
|
<span class="badge badge-pill rlp-r">{{member.username}}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if team.admin == user %}
|
||||||
|
<button class="btn rlp-r btn-modal" data-form-url="{% url 'user:team-edit' team.id %}" title="{% trans 'Edit team' %}">
|
||||||
|
{% fa5_icon 'edit' %}
|
||||||
|
</button>
|
||||||
|
<button class="btn rlp-r btn-modal" data-form-url="{% url 'user:team-remove' team.id %}" title="{% trans 'Remove team' %}">
|
||||||
|
{% fa5_icon 'trash' %}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% with 'btn-modal' as btn_class %}
|
||||||
|
{% include 'modal/modal_form_script.html' %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -15,5 +15,10 @@ urlpatterns = [
|
|||||||
path("notifications/", notifications_view, name="notifications"),
|
path("notifications/", notifications_view, name="notifications"),
|
||||||
path("token/api", api_token_view, name="api-token"),
|
path("token/api", api_token_view, name="api-token"),
|
||||||
path("contact/<id>", contact_view, name="contact"),
|
path("contact/<id>", contact_view, name="contact"),
|
||||||
|
path("team/", index_team_view, name="team-index"),
|
||||||
|
path("team/<id>", data_team_view, name="team-data"),
|
||||||
|
path("team/new", new_team_view, name="team-new"),
|
||||||
|
path("team/<id>/edit", edit_team_view, name="team-edit"),
|
||||||
|
path("team/<id>/remove", remove_team_view, name="team-remove"),
|
||||||
|
|
||||||
]
|
]
|
@ -1,17 +1,19 @@
|
|||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||||
from konova.utils.mailer import Mailer
|
from konova.utils.mailer import Mailer
|
||||||
from konova.utils.message_templates import FORM_INVALID
|
from konova.utils.message_templates import FORM_INVALID
|
||||||
from user.models import User
|
from user.models import User, Team
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest, Http404
|
||||||
from django.shortcuts import render, redirect, get_object_or_404
|
from django.shortcuts import render, redirect, get_object_or_404
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from konova.contexts import BaseContext
|
from konova.contexts import BaseContext
|
||||||
from konova.decorators import any_group_check, default_group_required
|
from konova.decorators import any_group_check, default_group_required
|
||||||
from user.forms import UserNotificationForm, UserContactForm, UserAPITokenForm
|
from user.forms import UserNotificationForm, UserContactForm, UserAPITokenForm, NewTeamModalForm, EditTeamModalForm, \
|
||||||
|
RemoveTeamModalForm, TeamDataForm
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@ -129,3 +131,76 @@ def contact_view(request: HttpRequest, id: str):
|
|||||||
template,
|
template,
|
||||||
context
|
context
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def data_team_view(request: HttpRequest, id: str):
|
||||||
|
""" Renders team data
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (HttpRequest): The incoming request
|
||||||
|
id (str): The team's id
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
team = get_object_or_404(Team, id=id)
|
||||||
|
form = TeamDataForm(request.POST or None, instance=team, request=request)
|
||||||
|
template = "modal/modal_form.html"
|
||||||
|
context = {
|
||||||
|
"form": form,
|
||||||
|
}
|
||||||
|
context = BaseContext(request, context).context
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
template,
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def index_team_view(request: HttpRequest):
|
||||||
|
template = "user/team/index.html"
|
||||||
|
user = request.user
|
||||||
|
context = {
|
||||||
|
"teams": user.teams.all(),
|
||||||
|
"tab_title": _("Teams"),
|
||||||
|
}
|
||||||
|
context = BaseContext(request, context).context
|
||||||
|
return render(request, template, context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def new_team_view(request: HttpRequest):
|
||||||
|
form = NewTeamModalForm(request.POST or None, request=request)
|
||||||
|
return form.process_request(
|
||||||
|
request,
|
||||||
|
_("New team added"),
|
||||||
|
redirect_url=reverse("user:team-index")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def edit_team_view(request: HttpRequest, id: str):
|
||||||
|
team = get_object_or_404(Team, id=id)
|
||||||
|
if request.user != team.admin:
|
||||||
|
raise Http404()
|
||||||
|
form = EditTeamModalForm(request.POST or None, instance=team, request=request)
|
||||||
|
return form.process_request(
|
||||||
|
request,
|
||||||
|
_("Team edited"),
|
||||||
|
redirect_url=reverse("user:team-index")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def remove_team_view(request: HttpRequest, id: str):
|
||||||
|
team = get_object_or_404(Team, id=id)
|
||||||
|
if request.user != team.admin:
|
||||||
|
raise Http404()
|
||||||
|
form = RemoveTeamModalForm(request.POST or None, instance=team, request=request)
|
||||||
|
return form.process_request(
|
||||||
|
request,
|
||||||
|
_("Team removed"),
|
||||||
|
redirect_url=reverse("user:team-index")
|
||||||
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user