Merge pull request '101_Team_based_sharing' (#122) from 101_Team_based_sharing into master
Reviewed-on: SGD-Nord/konova#122
This commit is contained in:
		
						commit
						25d04006d8
					
				@ -109,8 +109,8 @@ class APIV1CreateTestCase(BaseAPIV1TestCase):
 | 
			
		||||
        Returns:
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        self.intervention.share_with(self.superuser)
 | 
			
		||||
        self.eco_account.share_with(self.superuser)
 | 
			
		||||
        self.intervention.share_with_user(self.superuser)
 | 
			
		||||
        self.eco_account.share_with_user(self.superuser)
 | 
			
		||||
 | 
			
		||||
        url = reverse("api:v1:deduction")
 | 
			
		||||
        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.share_with(self.superuser)
 | 
			
		||||
        test_intervention.share_with_user(self.superuser)
 | 
			
		||||
        url = reverse("api:v1:intervention", args=(str(test_intervention.id),))
 | 
			
		||||
        self._test_delete_object(test_intervention, url)
 | 
			
		||||
 | 
			
		||||
@ -68,7 +68,7 @@ class APIV1DeleteTestCase(BaseAPIV1TestCase):
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        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),))
 | 
			
		||||
        self._test_delete_object(test_comp, url)
 | 
			
		||||
 | 
			
		||||
@ -79,7 +79,7 @@ class APIV1DeleteTestCase(BaseAPIV1TestCase):
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        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),))
 | 
			
		||||
        self._test_delete_object(test_acc, url)
 | 
			
		||||
 | 
			
		||||
@ -90,7 +90,7 @@ class APIV1DeleteTestCase(BaseAPIV1TestCase):
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        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),))
 | 
			
		||||
        self._test_delete_object(test_ema, url)
 | 
			
		||||
 | 
			
		||||
@ -101,7 +101,7 @@ class APIV1DeleteTestCase(BaseAPIV1TestCase):
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        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),))
 | 
			
		||||
 | 
			
		||||
        response = self._run_delete_request(url)
 | 
			
		||||
 | 
			
		||||
@ -64,7 +64,7 @@ class APIV1GetTestCase(BaseAPIV1TestCase):
 | 
			
		||||
        Returns:
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        self.intervention.share_with(self.superuser)
 | 
			
		||||
        self.intervention.share_with_user(self.superuser)
 | 
			
		||||
        url = reverse("api:v1:intervention", args=(str(self.intervention.id),))
 | 
			
		||||
        geojson = self._test_get_object(self.intervention, url)
 | 
			
		||||
        self._assert_geojson_format(geojson)
 | 
			
		||||
@ -85,13 +85,33 @@ class APIV1GetTestCase(BaseAPIV1TestCase):
 | 
			
		||||
        except KeyError as 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):
 | 
			
		||||
        """ Tests api GET
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        self.intervention.share_with(self.superuser)
 | 
			
		||||
        self.intervention.share_with_user(self.superuser)
 | 
			
		||||
        self.compensation.intervention = self.intervention
 | 
			
		||||
        self.compensation.save()
 | 
			
		||||
 | 
			
		||||
@ -119,7 +139,7 @@ class APIV1GetTestCase(BaseAPIV1TestCase):
 | 
			
		||||
        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),))
 | 
			
		||||
        geojson = self._test_get_object(self.eco_account, url)
 | 
			
		||||
@ -148,7 +168,7 @@ class APIV1GetTestCase(BaseAPIV1TestCase):
 | 
			
		||||
        Returns:
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        self.ema.share_with(self.superuser)
 | 
			
		||||
        self.ema.share_with_user(self.superuser)
 | 
			
		||||
 | 
			
		||||
        url = reverse("api:v1:ema", args=(str(self.ema.id),))
 | 
			
		||||
        geojson = self._test_get_object(self.ema, url)
 | 
			
		||||
@ -172,7 +192,7 @@ class APIV1GetTestCase(BaseAPIV1TestCase):
 | 
			
		||||
        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),))
 | 
			
		||||
        _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:
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        self.intervention.share_with(self.superuser)
 | 
			
		||||
        self.intervention.share_with_user(self.superuser)
 | 
			
		||||
        modified_on = self.intervention.modified
 | 
			
		||||
        url = reverse("api:v1:intervention", args=(str(self.intervention.id),))
 | 
			
		||||
        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.save()
 | 
			
		||||
        self.intervention.share_with(self.superuser)
 | 
			
		||||
        self.intervention.share_with_user(self.superuser)
 | 
			
		||||
 | 
			
		||||
        modified_on = self.compensation.modified
 | 
			
		||||
        url = reverse("api:v1:compensation", args=(str(self.compensation.id),))
 | 
			
		||||
@ -108,7 +108,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
 | 
			
		||||
        Returns:
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        self.eco_account.share_with(self.superuser)
 | 
			
		||||
        self.eco_account.share_with_user(self.superuser)
 | 
			
		||||
 | 
			
		||||
        modified_on = self.eco_account.modified
 | 
			
		||||
        url = reverse("api:v1:ecoaccount", args=(str(self.eco_account.id),))
 | 
			
		||||
@ -139,7 +139,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
 | 
			
		||||
        Returns:
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        self.ema.share_with(self.superuser)
 | 
			
		||||
        self.ema.share_with_user(self.superuser)
 | 
			
		||||
 | 
			
		||||
        modified_on = self.ema.modified
 | 
			
		||||
        url = reverse("api:v1:ema", args=(str(self.ema.id),))
 | 
			
		||||
@ -168,8 +168,8 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
 | 
			
		||||
        Returns:
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        self.deduction.intervention.share_with(self.superuser)
 | 
			
		||||
        self.deduction.account.share_with(self.superuser)
 | 
			
		||||
        self.deduction.intervention.share_with_user(self.superuser)
 | 
			
		||||
        self.deduction.account.share_with_user(self.superuser)
 | 
			
		||||
 | 
			
		||||
        url = reverse("api:v1:deduction", args=(str(self.deduction.id),))
 | 
			
		||||
        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["eco_account"], str(self.deduction.account.id))
 | 
			
		||||
        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))
 | 
			
		||||
 | 
			
		||||
@ -19,7 +19,7 @@ from ema.models import Ema
 | 
			
		||||
from intervention.models import Intervention
 | 
			
		||||
from konova.utils.message_templates import DATA_UNSHARED
 | 
			
		||||
from konova.utils.user_checks import is_default_group_only
 | 
			
		||||
from user.models import User
 | 
			
		||||
from user.models import User, Team
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AbstractAPIView(View):
 | 
			
		||||
@ -198,13 +198,21 @@ class AbstractModelShareAPIView(AbstractAPIView):
 | 
			
		||||
        """
 | 
			
		||||
        try:
 | 
			
		||||
            users = self._get_shared_users_of_object(id)
 | 
			
		||||
            teams = self._get_shared_teams_of_object(id)
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            return self._return_error_response(e)
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            "users": [
 | 
			
		||||
                user.username for user in users
 | 
			
		||||
            ]
 | 
			
		||||
            ],
 | 
			
		||||
            "teams": [
 | 
			
		||||
                {
 | 
			
		||||
                    "id": team.id,
 | 
			
		||||
                    "name": team.name,
 | 
			
		||||
                }
 | 
			
		||||
                for team in teams
 | 
			
		||||
            ],
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return JsonResponse(data)
 | 
			
		||||
@ -258,6 +266,22 @@ class AbstractModelShareAPIView(AbstractAPIView):
 | 
			
		||||
        users = obj.shared_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):
 | 
			
		||||
        """ Reads the body data, performs validity checks and sets the new users
 | 
			
		||||
 | 
			
		||||
@ -271,19 +295,26 @@ class AbstractModelShareAPIView(AbstractAPIView):
 | 
			
		||||
        obj = self.model.objects.get(id=id)
 | 
			
		||||
        self._check_user_has_shared_access(obj)
 | 
			
		||||
 | 
			
		||||
        new_users = json.loads(body.decode("utf-8"))
 | 
			
		||||
        new_users = new_users.get("users", [])
 | 
			
		||||
        content = json.loads(body.decode("utf-8"))
 | 
			
		||||
        new_users = content.get("users", [])
 | 
			
		||||
        if len(new_users) == 0:
 | 
			
		||||
            raise ValueError("Shared user list must not be empty!")
 | 
			
		||||
        new_teams = content.get("teams", [])
 | 
			
		||||
 | 
			
		||||
        # Eliminate duplicates
 | 
			
		||||
        new_users = list(dict.fromkeys(new_users))
 | 
			
		||||
        new_teams = list(dict.fromkeys(new_teams))
 | 
			
		||||
 | 
			
		||||
        # Make sure each of these names exist as a user
 | 
			
		||||
        new_users_objs = []
 | 
			
		||||
        for user in new_users:
 | 
			
		||||
            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):
 | 
			
		||||
            # 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(
 | 
			
		||||
@ -292,7 +323,16 @@ class AbstractModelShareAPIView(AbstractAPIView):
 | 
			
		||||
                id__in=obj.shared_users
 | 
			
		||||
            )
 | 
			
		||||
            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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -59,8 +59,9 @@ class CheckboxCompensationTableFilter(CheckboxTableFilter):
 | 
			
		||||
        """
 | 
			
		||||
        if not value:
 | 
			
		||||
            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:
 | 
			
		||||
            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:
 | 
			
		||||
        """ Filters queryset depending on value of 'show_recorded' setting
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -400,7 +400,7 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
 | 
			
		||||
                comment=comment,
 | 
			
		||||
                legal=legal
 | 
			
		||||
            )
 | 
			
		||||
            acc.share_with(user)
 | 
			
		||||
            acc.share_with_user(user)
 | 
			
		||||
 | 
			
		||||
            # Add the log entry to the main objects log list
 | 
			
		||||
            acc.log.add(action)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -8,7 +8,7 @@ Created on: 16.11.21
 | 
			
		||||
import shutil
 | 
			
		||||
 | 
			
		||||
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.models import QuerySet, Sum
 | 
			
		||||
from django.http import HttpRequest
 | 
			
		||||
@ -299,7 +299,7 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
 | 
			
		||||
        # Compensations inherit their shared state from the interventions
 | 
			
		||||
        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
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
@ -308,10 +308,9 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
 | 
			
		||||
        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
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
@ -322,6 +321,28 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
 | 
			
		||||
        """
 | 
			
		||||
        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
 | 
			
		||||
    def shared_users(self) -> QuerySet:
 | 
			
		||||
        """ 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()
 | 
			
		||||
 | 
			
		||||
    @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:
 | 
			
		||||
        """ Getter for all documents of a compensation
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -98,6 +98,10 @@
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <th scope="row">{% trans 'Shared with' %}</th>
 | 
			
		||||
                        <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 %}
 | 
			
		||||
                                {% include 'user/includes/contact_modal_button.html' %}
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
 | 
			
		||||
@ -81,6 +81,10 @@
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <th scope="row">{% trans 'Shared with' %}</th>
 | 
			
		||||
                        <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 %}
 | 
			
		||||
                                {% include 'user/includes/contact_modal_button.html' %}
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
 | 
			
		||||
@ -103,7 +103,7 @@ class CompensationViewTestCase(BaseViewTestCase):
 | 
			
		||||
        client = Client()
 | 
			
		||||
        client.login(username=self.superuser.username, password=self.superuser_pw)
 | 
			
		||||
        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
 | 
			
		||||
        # 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)
 | 
			
		||||
        self.superuser.groups.set([])
 | 
			
		||||
        # 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
 | 
			
		||||
        # 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)
 | 
			
		||||
        self.superuser.groups.set([group])
 | 
			
		||||
        # 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 = [
 | 
			
		||||
            self.index_url,
 | 
			
		||||
@ -221,7 +221,7 @@ class CompensationViewTestCase(BaseViewTestCase):
 | 
			
		||||
        group = self.groups.get(name=DEFAULT_GROUP)
 | 
			
		||||
        self.superuser.groups.set([group])
 | 
			
		||||
        # Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
 | 
			
		||||
        self.intervention.share_with_list([])
 | 
			
		||||
        self.intervention.share_with_user_list([])
 | 
			
		||||
 | 
			
		||||
        success_urls = [
 | 
			
		||||
            self.index_url,
 | 
			
		||||
 | 
			
		||||
@ -25,7 +25,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
 | 
			
		||||
        super().setUp()
 | 
			
		||||
 | 
			
		||||
        # 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
 | 
			
		||||
        self.intervention = self.fill_out_intervention(self.intervention)
 | 
			
		||||
 | 
			
		||||
@ -78,7 +78,7 @@ class EcoAccountViewTestCase(CompensationViewTestCase):
 | 
			
		||||
        client = Client()
 | 
			
		||||
        client.login(username=self.superuser.username, password=self.superuser_pw)
 | 
			
		||||
        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
 | 
			
		||||
        # to a user without access, since the important permissions are missing
 | 
			
		||||
@ -119,7 +119,7 @@ class EcoAccountViewTestCase(CompensationViewTestCase):
 | 
			
		||||
        client = Client()
 | 
			
		||||
        client.login(username=self.superuser.username, password=self.superuser_pw)
 | 
			
		||||
        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
 | 
			
		||||
        # 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)
 | 
			
		||||
        self.superuser.groups.set([group])
 | 
			
		||||
        # 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 = [
 | 
			
		||||
            self.index_url,
 | 
			
		||||
@ -200,7 +200,7 @@ class EcoAccountViewTestCase(CompensationViewTestCase):
 | 
			
		||||
        client.login(username=self.superuser.username, password=self.superuser_pw)
 | 
			
		||||
        group = self.groups.get(name=DEFAULT_GROUP)
 | 
			
		||||
        self.superuser.groups.set([group])
 | 
			
		||||
        self.eco_account.share_with_list([])
 | 
			
		||||
        self.eco_account.share_with_user_list([])
 | 
			
		||||
 | 
			
		||||
        success_urls = [
 | 
			
		||||
            self.index_url,
 | 
			
		||||
 | 
			
		||||
@ -27,7 +27,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
 | 
			
		||||
        # 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=ETS_GROUP))
 | 
			
		||||
        self.eco_account.share_with_list([self.superuser])
 | 
			
		||||
        self.eco_account.share_with_user_list([self.superuser])
 | 
			
		||||
 | 
			
		||||
    def test_new(self):
 | 
			
		||||
        """ Test the creation of an EcoAccount
 | 
			
		||||
@ -73,7 +73,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
 | 
			
		||||
        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,))
 | 
			
		||||
        pre_edit_log_count = self.eco_account.log.count()
 | 
			
		||||
@ -129,7 +129,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        # 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()
 | 
			
		||||
 | 
			
		||||
        # 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
 | 
			
		||||
        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_int_log_count = self.intervention.log.count()
 | 
			
		||||
 | 
			
		||||
@ -231,9 +231,9 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
 | 
			
		||||
    def test_edit_deduction(self):
 | 
			
		||||
        test_surface = self.eco_account.get_available_rest()[0]
 | 
			
		||||
        self.eco_account.set_recorded(self.superuser)
 | 
			
		||||
        self.intervention.share_with(self.superuser)
 | 
			
		||||
        self.intervention.share_with_user(self.superuser)
 | 
			
		||||
        self.eco_account.refresh_from_db()
 | 
			
		||||
        self.assertIn(self.superuser, self.intervention.is_shared_with(self.superuser))
 | 
			
		||||
        self.assertTrue(self.superuser, self.intervention.is_shared_with(self.superuser))
 | 
			
		||||
 | 
			
		||||
        deduction = EcoAccountDeduction.objects.create(
 | 
			
		||||
            intervention=self.intervention,
 | 
			
		||||
@ -281,8 +281,8 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
 | 
			
		||||
            "confirm": True,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        intervention.share_with(self.superuser)
 | 
			
		||||
        account.share_with(self.superuser)
 | 
			
		||||
        intervention.share_with_user(self.superuser)
 | 
			
		||||
        account.share_with_user(self.superuser)
 | 
			
		||||
 | 
			
		||||
        pre_edit_intervention_log_count = intervention.log.count()
 | 
			
		||||
        pre_edit_account_log_count = account.log.count()
 | 
			
		||||
 | 
			
		||||
@ -64,7 +64,7 @@ class PaymentViewTestCase(BaseViewTestCase):
 | 
			
		||||
        client = Client()
 | 
			
		||||
        client.login(username=self.superuser.username, password=self.superuser_pw)
 | 
			
		||||
        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
 | 
			
		||||
        # 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)
 | 
			
		||||
        self.superuser.groups.set([])
 | 
			
		||||
        # 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
 | 
			
		||||
        # 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)
 | 
			
		||||
        self.superuser.groups.set([group])
 | 
			
		||||
        # 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 = [
 | 
			
		||||
            self.new_url,
 | 
			
		||||
@ -143,7 +143,7 @@ class PaymentViewTestCase(BaseViewTestCase):
 | 
			
		||||
        group = self.groups.get(name=DEFAULT_GROUP)
 | 
			
		||||
        self.superuser.groups.set([group])
 | 
			
		||||
        # Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
 | 
			
		||||
        self.intervention.share_with_list([])
 | 
			
		||||
        self.intervention.share_with_user_list([])
 | 
			
		||||
 | 
			
		||||
        success_urls = [
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
@ -21,7 +21,7 @@ class PaymentWorkflowTestCase(BaseWorkflowTestCase):
 | 
			
		||||
    def setUp(self) -> None:
 | 
			
		||||
        super().setUp()
 | 
			
		||||
        # 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(
 | 
			
		||||
            intervention=self.intervention,
 | 
			
		||||
 | 
			
		||||
@ -796,7 +796,7 @@ def share_view(request: HttpRequest, id: str, token: str):
 | 
			
		||||
                request,
 | 
			
		||||
                _("{} has been shared with you").format(obj.identifier)
 | 
			
		||||
            )
 | 
			
		||||
            obj.share_with(user)
 | 
			
		||||
            obj.share_with_user(user)
 | 
			
		||||
        return redirect("compensation:acc:detail", id=id)
 | 
			
		||||
    else:
 | 
			
		||||
        messages.error(
 | 
			
		||||
 | 
			
		||||
@ -80,7 +80,7 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            # 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
 | 
			
		||||
            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'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -74,6 +74,10 @@
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <th scope="row">{% trans 'Shared with' %}</th>
 | 
			
		||||
                        <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 %}
 | 
			
		||||
                                {% include 'user/includes/contact_modal_button.html' %}
 | 
			
		||||
                            {% 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
 | 
			
		||||
        # to this user
 | 
			
		||||
        self.ema.share_with_list([self.superuser])
 | 
			
		||||
        self.ema.share_with_user_list([self.superuser])
 | 
			
		||||
 | 
			
		||||
        success_urls = [
 | 
			
		||||
            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
 | 
			
		||||
        # to this user
 | 
			
		||||
        self.ema.share_with_list([])
 | 
			
		||||
        self.ema.share_with_user_list([])
 | 
			
		||||
 | 
			
		||||
        success_urls = [
 | 
			
		||||
            self.index_url,
 | 
			
		||||
@ -203,7 +203,7 @@ class EmaViewTestCase(CompensationViewTestCase):
 | 
			
		||||
        groups = self.groups.filter(Q(name=ETS_GROUP)|Q(name=DEFAULT_GROUP))
 | 
			
		||||
        self.superuser.groups.set(groups)
 | 
			
		||||
        # Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
 | 
			
		||||
        self.ema.share_with_list([self.superuser])
 | 
			
		||||
        self.ema.share_with_user_list([self.superuser])
 | 
			
		||||
 | 
			
		||||
        success_urls = [
 | 
			
		||||
            self.index_url,
 | 
			
		||||
@ -243,7 +243,7 @@ class EmaViewTestCase(CompensationViewTestCase):
 | 
			
		||||
        groups = self.groups.filter(Q(name=ETS_GROUP)|Q(name=DEFAULT_GROUP))
 | 
			
		||||
        self.superuser.groups.set(groups)
 | 
			
		||||
        # Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
 | 
			
		||||
        self.ema.share_with_list([])
 | 
			
		||||
        self.ema.share_with_user_list([])
 | 
			
		||||
 | 
			
		||||
        success_urls = [
 | 
			
		||||
            self.index_url,
 | 
			
		||||
 | 
			
		||||
@ -621,7 +621,7 @@ def share_view(request: HttpRequest, id: str, token: str):
 | 
			
		||||
                request,
 | 
			
		||||
                _("{} has been shared with you").format(obj.identifier)
 | 
			
		||||
            )
 | 
			
		||||
            obj.share_with(user)
 | 
			
		||||
            obj.share_with_user(user)
 | 
			
		||||
        return redirect("ema:detail", id=id)
 | 
			
		||||
    else:
 | 
			
		||||
        messages.error(
 | 
			
		||||
 | 
			
		||||
@ -253,7 +253,7 @@ class NewInterventionForm(BaseForm):
 | 
			
		||||
            intervention.log.add(action)
 | 
			
		||||
 | 
			
		||||
            # Add the performing user as the first user having access to the data
 | 
			
		||||
            intervention.share_with(user)
 | 
			
		||||
            intervention.share_with_user(user)
 | 
			
		||||
        return intervention
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -7,11 +7,11 @@ Created on: 27.09.21
 | 
			
		||||
"""
 | 
			
		||||
from dal import autocomplete
 | 
			
		||||
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, \
 | 
			
		||||
    REVOCATION_EDITED, FILE_TYPE_UNSUPPORTED, FILE_SIZE_TOO_LARGE
 | 
			
		||||
from user.models import User, UserActionLogEntry
 | 
			
		||||
    REVOCATION_EDITED, ENTRY_REMOVE_MISSING_PERMISSION
 | 
			
		||||
from user.models import User, Team
 | 
			
		||||
from user.models import UserActionLogEntry
 | 
			
		||||
from django.db import transaction
 | 
			
		||||
from django import forms
 | 
			
		||||
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_suffix="",
 | 
			
		||||
        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-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):
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
@ -77,6 +78,48 @@ class ShareModalForm(BaseModalForm):
 | 
			
		||||
 | 
			
		||||
        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):
 | 
			
		||||
        """ Wraps initializing of fields
 | 
			
		||||
 | 
			
		||||
@ -91,34 +134,14 @@ class ShareModalForm(BaseModalForm):
 | 
			
		||||
            self.share_link
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Initialize users field
 | 
			
		||||
        # Disable field if user is not in registration or conservation group
 | 
			
		||||
        if is_default_group_only(self.request.user):
 | 
			
		||||
            self.disable_form_field("users")
 | 
			
		||||
 | 
			
		||||
        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
 | 
			
		||||
        )
 | 
			
		||||
        form_data = {
 | 
			
		||||
            "teams": self.instance.teams.all(),
 | 
			
		||||
            "users": self.instance.users.all(),
 | 
			
		||||
        }
 | 
			
		||||
        self.load_initial_data(form_data)
 | 
			
		||||
 | 
			
		||||
    def save(self):
 | 
			
		||||
        self.instance.update_sharing_user(self)
 | 
			
		||||
        self.instance.update_shared_access(self)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NewRevocationModalForm(BaseModalForm):
 | 
			
		||||
@ -267,7 +290,9 @@ class CheckModalForm(BaseModalForm):
 | 
			
		||||
        Returns:
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        comps = self.instance.compensations.all()
 | 
			
		||||
        comps = self.instance.compensations.filter(
 | 
			
		||||
            deleted=None,
 | 
			
		||||
        )
 | 
			
		||||
        comps_valid = True
 | 
			
		||||
        for comp in comps:
 | 
			
		||||
            checker = comp.quality_check()
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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>
 | 
			
		||||
                        <th scope="row">{% trans 'Shared with' %}</th>
 | 
			
		||||
                        <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 %}
 | 
			
		||||
                                {% include 'user/includes/contact_modal_button.html' %}
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
 | 
			
		||||
@ -144,7 +144,7 @@ class InterventionViewTestCase(BaseViewTestCase):
 | 
			
		||||
        # Add user to default group
 | 
			
		||||
        default_group = Group.objects.get(name=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 = [
 | 
			
		||||
            self.index_url,
 | 
			
		||||
@ -190,7 +190,7 @@ class InterventionViewTestCase(BaseViewTestCase):
 | 
			
		||||
        # Add user to default group
 | 
			
		||||
        default_group = Group.objects.get(name=DEFAULT_GROUP)
 | 
			
		||||
        self.superuser.groups.set([default_group])
 | 
			
		||||
        self.intervention.share_with_list([])
 | 
			
		||||
        self.intervention.share_with_user_list([])
 | 
			
		||||
 | 
			
		||||
        success_urls = [
 | 
			
		||||
            self.index_url,
 | 
			
		||||
@ -236,7 +236,7 @@ class InterventionViewTestCase(BaseViewTestCase):
 | 
			
		||||
        # Add user to zb group
 | 
			
		||||
        zb_group = self.groups.get(name=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 = [
 | 
			
		||||
            self.index_url,
 | 
			
		||||
@ -282,7 +282,7 @@ class InterventionViewTestCase(BaseViewTestCase):
 | 
			
		||||
        # Add user to zb group
 | 
			
		||||
        zb_group = self.groups.get(name=ZB_GROUP)
 | 
			
		||||
        self.superuser.groups.set([zb_group])
 | 
			
		||||
        self.intervention.share_with_list([])
 | 
			
		||||
        self.intervention.share_with_user_list([])
 | 
			
		||||
 | 
			
		||||
        success_urls = [
 | 
			
		||||
            self.index_url,
 | 
			
		||||
@ -328,7 +328,7 @@ class InterventionViewTestCase(BaseViewTestCase):
 | 
			
		||||
        # Add user to ets group
 | 
			
		||||
        ets_group = Group.objects.get(name=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 = [
 | 
			
		||||
            self.index_url,
 | 
			
		||||
@ -374,7 +374,7 @@ class InterventionViewTestCase(BaseViewTestCase):
 | 
			
		||||
        # Add user to default group
 | 
			
		||||
        ets_group = Group.objects.get(name=ETS_GROUP)
 | 
			
		||||
        self.superuser.groups.set([ets_group])
 | 
			
		||||
        self.intervention.share_with_list([])
 | 
			
		||||
        self.intervention.share_with_user_list([])
 | 
			
		||||
 | 
			
		||||
        success_urls = [
 | 
			
		||||
            self.index_url,
 | 
			
		||||
 | 
			
		||||
@ -30,7 +30,7 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
 | 
			
		||||
        super().setUp()
 | 
			
		||||
        # Recreate a new (bare minimum) intervention before each test
 | 
			
		||||
        self.intervention = self.create_dummy_intervention()
 | 
			
		||||
        self.intervention.share_with(self.superuser)
 | 
			
		||||
        self.intervention.share_with_user(self.superuser)
 | 
			
		||||
 | 
			
		||||
    def test_new(self):
 | 
			
		||||
        """
 | 
			
		||||
@ -365,7 +365,7 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
 | 
			
		||||
        if self.eco_account.recorded is None:
 | 
			
		||||
            rec_action = UserActionLogEntry.get_recorded_action(self.superuser)
 | 
			
		||||
            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()
 | 
			
		||||
        num_all_deducs = EcoAccountDeduction.objects.count()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -432,7 +432,7 @@ def share_view(request: HttpRequest, id: str, token: str):
 | 
			
		||||
                request,
 | 
			
		||||
                _("{} has been shared with you").format(intervention.identifier)
 | 
			
		||||
            )
 | 
			
		||||
            intervention.share_with(user)
 | 
			
		||||
            intervention.share_with_user(user)
 | 
			
		||||
        return redirect("intervention:detail", id=id)
 | 
			
		||||
    else:
 | 
			
		||||
        messages.error(
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,7 @@ from dal_select2.views import Select2QuerySetView, Select2GroupQuerySetView
 | 
			
		||||
from django.core.exceptions import ImproperlyConfigured
 | 
			
		||||
 | 
			
		||||
from konova.utils.message_templates import UNGROUPED
 | 
			
		||||
from user.models import User
 | 
			
		||||
from user.models import User, Team
 | 
			
		||||
from django.db.models import Q
 | 
			
		||||
 | 
			
		||||
from codelist.models import KonovaCode
 | 
			
		||||
@ -69,27 +69,40 @@ class InterventionAutocomplete(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):
 | 
			
		||||
        if self.request.user.is_anonymous:
 | 
			
		||||
            return User.objects.none()
 | 
			
		||||
        exclude_user_ids = self.forwarded.get("users", [])
 | 
			
		||||
        _exclude = {"id__in": exclude_user_ids}
 | 
			
		||||
        qs = User.objects.all().exclude(
 | 
			
		||||
            **_exclude
 | 
			
		||||
        ).order_by(
 | 
			
		||||
            "username"
 | 
			
		||||
        )
 | 
			
		||||
        qs = User.objects.all()
 | 
			
		||||
        if self.q:
 | 
			
		||||
            # Due to privacy concerns only a full username match will return the proper user entry
 | 
			
		||||
            qs = qs.filter(
 | 
			
		||||
                Q(username=self.q) |
 | 
			
		||||
                Q(email=self.q)
 | 
			
		||||
            ).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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -297,8 +297,9 @@ class ShareableTableFilterMixin(django_filters.FilterSet):
 | 
			
		||||
        """
 | 
			
		||||
        if not value:
 | 
			
		||||
            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:
 | 
			
		||||
            return queryset
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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.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_deleted, celery_send_mail_shared_data_checked
 | 
			
		||||
from user.models import User
 | 
			
		||||
    celery_send_mail_shared_data_deleted, celery_send_mail_shared_data_checked, \
 | 
			
		||||
    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.http import HttpRequest
 | 
			
		||||
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.generators import generate_random_string
 | 
			
		||||
from konova.utils.message_templates import CHECKED_RECORDED_RESET, GEOMETRY_CONFLICT_WITH_TEMPLATE
 | 
			
		||||
from user.models import UserActionLogEntry, UserAction
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UuidModel(models.Model):
 | 
			
		||||
@ -50,14 +51,14 @@ class BaseResource(UuidModel):
 | 
			
		||||
    A basic resource model, which defines attributes for every derived model
 | 
			
		||||
    """
 | 
			
		||||
    created = models.ForeignKey(
 | 
			
		||||
        UserActionLogEntry,
 | 
			
		||||
        "user.UserActionLogEntry",
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        related_name='+'
 | 
			
		||||
    )
 | 
			
		||||
    modified = models.ForeignKey(
 | 
			
		||||
        UserActionLogEntry,
 | 
			
		||||
        "user.UserActionLogEntry",
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
@ -94,9 +95,9 @@ class BaseObject(BaseResource):
 | 
			
		||||
    """
 | 
			
		||||
    identifier = 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)
 | 
			
		||||
    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:
 | 
			
		||||
        abstract = True
 | 
			
		||||
@ -105,7 +106,7 @@ class BaseObject(BaseResource):
 | 
			
		||||
    def set_status_messages(self, request: HttpRequest):
 | 
			
		||||
        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
 | 
			
		||||
 | 
			
		||||
        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:
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        from user.models import UserActionLogEntry
 | 
			
		||||
        if self.deleted:
 | 
			
		||||
            # Nothing to do here
 | 
			
		||||
            return
 | 
			
		||||
@ -131,9 +133,14 @@ class BaseObject(BaseResource):
 | 
			
		||||
                for user_id in shared_users:
 | 
			
		||||
                    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()
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
@ -144,13 +151,14 @@ class BaseObject(BaseResource):
 | 
			
		||||
        Returns:
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        from user.models import UserActionLogEntry
 | 
			
		||||
        edit_action = UserActionLogEntry.get_edited_action(performing_user, edit_comment)
 | 
			
		||||
        self.modified = edit_action
 | 
			
		||||
        self.log.add(edit_action)
 | 
			
		||||
        self.save()
 | 
			
		||||
        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
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
@ -161,6 +169,7 @@ class BaseObject(BaseResource):
 | 
			
		||||
        Returns:
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        from user.models import UserActionLogEntry
 | 
			
		||||
        user_action = UserActionLogEntry.objects.create(
 | 
			
		||||
            user=user,
 | 
			
		||||
            action=action,
 | 
			
		||||
@ -229,7 +238,7 @@ class RecordableObjectMixin(models.Model):
 | 
			
		||||
    """
 | 
			
		||||
    # Refers to "verzeichnen"
 | 
			
		||||
    recorded = models.OneToOneField(
 | 
			
		||||
        UserActionLogEntry,
 | 
			
		||||
        "user.UserActionLogEntry",
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
@ -240,7 +249,7 @@ class RecordableObjectMixin(models.Model):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        abstract = True
 | 
			
		||||
 | 
			
		||||
    def set_unrecorded(self, user: User):
 | 
			
		||||
    def set_unrecorded(self, user):
 | 
			
		||||
        """ Perform unrecording
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
@ -249,6 +258,7 @@ class RecordableObjectMixin(models.Model):
 | 
			
		||||
        Returns:
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        from user.models import UserActionLogEntry
 | 
			
		||||
        if not self.recorded:
 | 
			
		||||
            return None
 | 
			
		||||
        action = UserActionLogEntry.get_unrecorded_action(user)
 | 
			
		||||
@ -256,13 +266,18 @@ class RecordableObjectMixin(models.Model):
 | 
			
		||||
        self.save()
 | 
			
		||||
        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:
 | 
			
		||||
            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
 | 
			
		||||
 | 
			
		||||
    def set_recorded(self, user: User):
 | 
			
		||||
    def set_recorded(self, user):
 | 
			
		||||
        """ Perform recording
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
@ -271,6 +286,7 @@ class RecordableObjectMixin(models.Model):
 | 
			
		||||
        Returns:
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        from user.models import UserActionLogEntry
 | 
			
		||||
        if self.recorded:
 | 
			
		||||
            return None
 | 
			
		||||
        action = UserActionLogEntry.get_recorded_action(user)
 | 
			
		||||
@ -278,13 +294,18 @@ class RecordableObjectMixin(models.Model):
 | 
			
		||||
        self.save()
 | 
			
		||||
        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:
 | 
			
		||||
            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
 | 
			
		||||
 | 
			
		||||
    def unrecord(self, performing_user: User, request: HttpRequest = None):
 | 
			
		||||
    def unrecord(self, performing_user, request: HttpRequest = None):
 | 
			
		||||
        """ Unrecords a dataset
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
@ -318,7 +339,7 @@ class RecordableObjectMixin(models.Model):
 | 
			
		||||
class CheckableObjectMixin(models.Model):
 | 
			
		||||
    # Checks - Refers to "Genehmigen" but optional
 | 
			
		||||
    checked = models.OneToOneField(
 | 
			
		||||
        UserActionLogEntry,
 | 
			
		||||
        "user.UserActionLogEntry",
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
@ -346,7 +367,7 @@ class CheckableObjectMixin(models.Model):
 | 
			
		||||
        self.save()
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    def set_checked(self, user: User) -> UserActionLogEntry:
 | 
			
		||||
    def set_checked(self, user):
 | 
			
		||||
        """ Perform checking
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
@ -355,6 +376,7 @@ class CheckableObjectMixin(models.Model):
 | 
			
		||||
        Returns:
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        from user.models import UserActionLogEntry
 | 
			
		||||
        if self.checked:
 | 
			
		||||
            # Nothing to do
 | 
			
		||||
            return
 | 
			
		||||
@ -363,17 +385,23 @@ class CheckableObjectMixin(models.Model):
 | 
			
		||||
        self.save()
 | 
			
		||||
 | 
			
		||||
        # 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:
 | 
			
		||||
            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)
 | 
			
		||||
        return action
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ShareableObjectMixin(models.Model):
 | 
			
		||||
    # 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(
 | 
			
		||||
        max_length=255,
 | 
			
		||||
        null=True,
 | 
			
		||||
@ -420,7 +448,7 @@ class ShareableObjectMixin(models.Model):
 | 
			
		||||
            self.access_token = token
 | 
			
		||||
            self.save()
 | 
			
		||||
 | 
			
		||||
    def is_shared_with(self, user: User):
 | 
			
		||||
    def is_shared_with(self, user):
 | 
			
		||||
        """ Access check
 | 
			
		||||
 | 
			
		||||
        Checks whether a given user has access to this object
 | 
			
		||||
@ -431,9 +459,36 @@ class ShareableObjectMixin(models.Model):
 | 
			
		||||
        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
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
@ -445,7 +500,7 @@ class ShareableObjectMixin(models.Model):
 | 
			
		||||
        if not self.is_shared_with(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
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
@ -456,8 +511,8 @@ class ShareableObjectMixin(models.Model):
 | 
			
		||||
        """
 | 
			
		||||
        self.users.set(user_list)
 | 
			
		||||
 | 
			
		||||
    def update_sharing_user(self, form):
 | 
			
		||||
        """ Adds a new user with shared access to the object
 | 
			
		||||
    def _update_shared_teams(self, form):
 | 
			
		||||
        """ Updates shared access on the object for teams
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            form (ShareModalForm): The form holding the data
 | 
			
		||||
@ -466,25 +521,65 @@ class ShareableObjectMixin(models.Model):
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        form_data = form.cleaned_data
 | 
			
		||||
        shared_teams = self.shared_teams
 | 
			
		||||
 | 
			
		||||
        keep_accessing_users = form_data["users"]
 | 
			
		||||
        new_accessing_users = list(form_data["user_select"].values_list("id", flat=True))
 | 
			
		||||
        accessing_users = keep_accessing_users + new_accessing_users
 | 
			
		||||
        users = User.objects.filter(
 | 
			
		||||
        # Fetch selected teams and find out which user IDs are in removed teams -> mails need to be sent
 | 
			
		||||
        accessing_teams = form_data["teams"]
 | 
			
		||||
        removed_teams = shared_teams.exclude(
 | 
			
		||||
            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
 | 
			
		||||
        )
 | 
			
		||||
        removed_users = self.users.all().exclude(
 | 
			
		||||
            id__in=accessing_users
 | 
			
		||||
        ).values("id")
 | 
			
		||||
        ).values_list("id", flat=True)
 | 
			
		||||
        new_users = accessing_users.exclude(
 | 
			
		||||
            id__in=shared_users
 | 
			
		||||
        ).values_list("id", flat=True)
 | 
			
		||||
 | 
			
		||||
        # Send mails
 | 
			
		||||
        for user in removed_users:
 | 
			
		||||
            celery_send_mail_shared_access_removed.delay(self.identifier, self.title, user["id"])
 | 
			
		||||
        for user in new_accessing_users:
 | 
			
		||||
            celery_send_mail_shared_access_given.delay(self.identifier, self.title,  user)
 | 
			
		||||
        for user_id in removed_users:
 | 
			
		||||
            celery_send_mail_shared_access_removed.delay(self.identifier, self.title, user_id)
 | 
			
		||||
        for user_id in new_users:
 | 
			
		||||
            celery_send_mail_shared_access_given.delay(self.identifier, self.title,  user_id)
 | 
			
		||||
 | 
			
		||||
        # 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
 | 
			
		||||
    def shared_users(self) -> QuerySet:
 | 
			
		||||
@ -495,6 +590,15 @@ class ShareableObjectMixin(models.Model):
 | 
			
		||||
        """
 | 
			
		||||
        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
 | 
			
		||||
    def get_share_url(self):
 | 
			
		||||
        """ Returns the share url for the object
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@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
 | 
			
		||||
def celery_send_mail_shared_data_recorded(obj_identifier, obj_title=None, user_id=None):
 | 
			
		||||
    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)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@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
 | 
			
		||||
def celery_send_mail_shared_data_deleted(obj_identifier, obj_title=None, user_id=None):
 | 
			
		||||
    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
 | 
			
		||||
    user = User.objects.get(id=user_id)
 | 
			
		||||
    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)
 | 
			
		||||
 | 
			
		||||
@ -71,6 +71,7 @@ class AutocompleteTestCase(BaseTestCase):
 | 
			
		||||
            "codes-registration-office-autocomplete",
 | 
			
		||||
            "codes-conservation-office-autocomplete",
 | 
			
		||||
            "share-user-autocomplete",
 | 
			
		||||
            "share-team-autocomplete",
 | 
			
		||||
        ]
 | 
			
		||||
        for test in tests:
 | 
			
		||||
            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 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.gis.geos import MultiPolygon, Polygon
 | 
			
		||||
from django.core.exceptions import ObjectDoesNotExist
 | 
			
		||||
@ -65,6 +65,7 @@ class BaseTestCase(TestCase):
 | 
			
		||||
        self.create_dummy_states()
 | 
			
		||||
        self.create_dummy_action()
 | 
			
		||||
        self.codes = self.create_dummy_codes()
 | 
			
		||||
        self.team = self.create_dummy_team()
 | 
			
		||||
 | 
			
		||||
        # Set the default group as only group for the user
 | 
			
		||||
        default_group = self.groups.get(name=DEFAULT_GROUP)
 | 
			
		||||
@ -251,6 +252,24 @@ class BaseTestCase(TestCase):
 | 
			
		||||
        ])
 | 
			
		||||
        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
 | 
			
		||||
    def create_dummy_geometry() -> MultiPolygon:
 | 
			
		||||
        """ Creates some geometry
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,7 @@ from django.urls import path, include
 | 
			
		||||
from konova.autocompletes import EcoAccountAutocomplete, \
 | 
			
		||||
    InterventionAutocomplete, CompensationActionCodeAutocomplete, BiotopeCodeAutocomplete, LawCodeAutocomplete, \
 | 
			
		||||
    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.sso.sso import KonovaSSOClient
 | 
			
		||||
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/cons-off", ConservationOfficeCodeAutocomplete.as_view(), name="codes-conservation-office-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:
 | 
			
		||||
 | 
			
		||||
@ -92,6 +92,144 @@ class Mailer:
 | 
			
		||||
            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):
 | 
			
		||||
        """ Send a mail if the user's shared data has just been unrecorded
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -12,11 +12,13 @@ FORM_INVALID = _("There was an error on this form.")
 | 
			
		||||
PARAMS_INVALID = _("Invalid parameters")
 | 
			
		||||
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")
 | 
			
		||||
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_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
 | 
			
		||||
FILE_TYPE_UNSUPPORTED = _("Unsupported file type")
 | 
			
		||||
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							@ -3,21 +3,21 @@
 | 
			
		||||
# This file is distributed under the same license as the PACKAGE package.
 | 
			
		||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
 | 
			
		||||
#
 | 
			
		||||
#: compensation/filters.py:122 compensation/forms/modalForms.py:36
 | 
			
		||||
#: compensation/filters.py:123 compensation/forms/modalForms.py:36
 | 
			
		||||
#: compensation/forms/modalForms.py:47 compensation/forms/modalForms.py:63
 | 
			
		||||
#: compensation/forms/modalForms.py:356 compensation/forms/modalForms.py:463
 | 
			
		||||
#: intervention/forms/forms.py:54 intervention/forms/forms.py:156
 | 
			
		||||
#: intervention/forms/forms.py:168 intervention/forms/modalForms.py:127
 | 
			
		||||
#: intervention/forms/modalForms.py:140 intervention/forms/modalForms.py:153
 | 
			
		||||
#: intervention/forms/forms.py:168 intervention/forms/modalForms.py:150
 | 
			
		||||
#: intervention/forms/modalForms.py:163 intervention/forms/modalForms.py:176
 | 
			
		||||
#: konova/filters/mixins.py:53 konova/filters/mixins.py:54
 | 
			
		||||
#: konova/filters/mixins.py:81 konova/filters/mixins.py:82
 | 
			
		||||
#: konova/filters/mixins.py:94 konova/filters/mixins.py:95
 | 
			
		||||
#: konova/filters/mixins.py:107 konova/filters/mixins.py:108
 | 
			
		||||
#: konova/filters/mixins.py:120 konova/filters/mixins.py:121
 | 
			
		||||
#: konova/filters/mixins.py:134 konova/filters/mixins.py:135
 | 
			
		||||
#: konova/filters/mixins.py:270 konova/filters/mixins.py:315
 | 
			
		||||
#: konova/filters/mixins.py:353 konova/filters/mixins.py:354
 | 
			
		||||
#: konova/filters/mixins.py:385 konova/filters/mixins.py:386
 | 
			
		||||
#: konova/filters/mixins.py:270 konova/filters/mixins.py:316
 | 
			
		||||
#: konova/filters/mixins.py:354 konova/filters/mixins.py:355
 | 
			
		||||
#: konova/filters/mixins.py:386 konova/filters/mixins.py:387
 | 
			
		||||
#: konova/forms.py:143 konova/forms.py:244 konova/forms.py:315
 | 
			
		||||
#: konova/forms.py:359 konova/forms.py:369 konova/forms.py:382
 | 
			
		||||
#: konova/forms.py:394 konova/forms.py:412 user/forms.py:42
 | 
			
		||||
@ -26,7 +26,7 @@ msgid ""
 | 
			
		||||
msgstr ""
 | 
			
		||||
"Project-Id-Version: PACKAGE VERSION\n"
 | 
			
		||||
"Report-Msgid-Bugs-To: \n"
 | 
			
		||||
"POT-Creation-Date: 2022-02-15 15:29+0100\n"
 | 
			
		||||
"POT-Creation-Date: 2022-02-18 14:50+0100\n"
 | 
			
		||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 | 
			
		||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 | 
			
		||||
"Language-Team: LANGUAGE <LL@li.org>\n"
 | 
			
		||||
@ -52,7 +52,7 @@ msgstr "Bis"
 | 
			
		||||
#: intervention/forms/forms.py:102
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:56
 | 
			
		||||
#: intervention/templates/intervention/report/report.html:37
 | 
			
		||||
#: intervention/utils/quality.py:49 konova/filters/mixins.py:395
 | 
			
		||||
#: intervention/utils/quality.py:49 konova/filters/mixins.py:396
 | 
			
		||||
msgid "Conservation office"
 | 
			
		||||
msgstr "Eintragungsstelle"
 | 
			
		||||
 | 
			
		||||
@ -64,6 +64,7 @@ msgstr "Verantwortliche Stelle"
 | 
			
		||||
#: compensation/forms/forms.py:165 intervention/forms/forms.py:64
 | 
			
		||||
#: intervention/forms/forms.py:81 intervention/forms/forms.py:97
 | 
			
		||||
#: intervention/forms/forms.py:113 intervention/forms/modalForms.py:49
 | 
			
		||||
#: intervention/forms/modalForms.py:63 user/forms.py:196
 | 
			
		||||
msgid "Click for selection"
 | 
			
		||||
msgstr "Auswählen..."
 | 
			
		||||
 | 
			
		||||
@ -220,7 +221,7 @@ msgstr "Abbuchungen"
 | 
			
		||||
#: compensation/templates/compensation/detail/eco_account/includes/states-before.html:36
 | 
			
		||||
#: ema/templates/ema/detail/includes/states-after.html:36
 | 
			
		||||
#: ema/templates/ema/detail/includes/states-before.html:36
 | 
			
		||||
#: intervention/forms/modalForms.py:338
 | 
			
		||||
#: intervention/forms/modalForms.py:361
 | 
			
		||||
msgid "Surface"
 | 
			
		||||
msgstr "Fläche"
 | 
			
		||||
 | 
			
		||||
@ -283,8 +284,8 @@ msgid "Type"
 | 
			
		||||
msgstr "Typ"
 | 
			
		||||
 | 
			
		||||
#: analysis/templates/analysis/reports/includes/old_data/amount.html:24
 | 
			
		||||
#: compensation/tables.py:89 intervention/forms/modalForms.py:349
 | 
			
		||||
#: intervention/forms/modalForms.py:356 intervention/tables.py:88
 | 
			
		||||
#: compensation/tables.py:89 intervention/forms/modalForms.py:372
 | 
			
		||||
#: intervention/forms/modalForms.py:379 intervention/tables.py:88
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:19
 | 
			
		||||
#: konova/templates/konova/includes/quickstart/interventions.html:4
 | 
			
		||||
#: templates/navbars/navbar.html:22
 | 
			
		||||
@ -294,7 +295,7 @@ msgstr "Eingriff"
 | 
			
		||||
#: analysis/templates/analysis/reports/includes/old_data/amount.html:34
 | 
			
		||||
#: compensation/tables.py:266
 | 
			
		||||
#: compensation/templates/compensation/detail/eco_account/view.html:20
 | 
			
		||||
#: intervention/forms/modalForms.py:322 intervention/forms/modalForms.py:329
 | 
			
		||||
#: intervention/forms/modalForms.py:345 intervention/forms/modalForms.py:352
 | 
			
		||||
#: konova/templates/konova/includes/quickstart/ecoaccounts.html:4
 | 
			
		||||
#: templates/navbars/navbar.html:34
 | 
			
		||||
msgid "Eco-account"
 | 
			
		||||
@ -308,7 +309,7 @@ msgstr "Altfälle"
 | 
			
		||||
msgid "Before"
 | 
			
		||||
msgstr "Vor"
 | 
			
		||||
 | 
			
		||||
#: compensation/filters.py:121
 | 
			
		||||
#: compensation/filters.py:122
 | 
			
		||||
msgid "Show only unrecorded"
 | 
			
		||||
msgstr "Nur unverzeichnete anzeigen"
 | 
			
		||||
 | 
			
		||||
@ -363,7 +364,7 @@ msgstr "Kompensation XY; Flur ABC"
 | 
			
		||||
#: ema/templates/ema/detail/includes/actions.html:34
 | 
			
		||||
#: ema/templates/ema/detail/includes/deadlines.html:34
 | 
			
		||||
#: ema/templates/ema/detail/includes/documents.html:34
 | 
			
		||||
#: intervention/forms/forms.py:180 intervention/forms/modalForms.py:152
 | 
			
		||||
#: intervention/forms/forms.py:180 intervention/forms/modalForms.py:175
 | 
			
		||||
#: intervention/templates/intervention/detail/includes/documents.html:34
 | 
			
		||||
#: intervention/templates/intervention/detail/includes/payments.html:34
 | 
			
		||||
#: intervention/templates/intervention/detail/includes/revocation.html:38
 | 
			
		||||
@ -483,7 +484,7 @@ msgid "Due on which date"
 | 
			
		||||
msgstr "Zahlung wird an diesem Datum erwartet"
 | 
			
		||||
 | 
			
		||||
#: compensation/forms/modalForms.py:64 compensation/forms/modalForms.py:357
 | 
			
		||||
#: intervention/forms/modalForms.py:154 konova/forms.py:395
 | 
			
		||||
#: intervention/forms/modalForms.py:177 konova/forms.py:395
 | 
			
		||||
msgid "Additional comment, maximum {} letters"
 | 
			
		||||
msgstr "Zusätzlicher Kommentar, maximal {} Zeichen"
 | 
			
		||||
 | 
			
		||||
@ -511,7 +512,7 @@ msgstr "Zusatzbezeichnung"
 | 
			
		||||
msgid "Select an additional biotope type"
 | 
			
		||||
msgstr "Zusatzbezeichnung wählen"
 | 
			
		||||
 | 
			
		||||
#: compensation/forms/modalForms.py:197 intervention/forms/modalForms.py:340
 | 
			
		||||
#: compensation/forms/modalForms.py:197 intervention/forms/modalForms.py:363
 | 
			
		||||
msgid "in m²"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
@ -539,7 +540,7 @@ msgstr "Fristart wählen"
 | 
			
		||||
#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:31
 | 
			
		||||
#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:31
 | 
			
		||||
#: ema/templates/ema/detail/includes/deadlines.html:31
 | 
			
		||||
#: intervention/forms/modalForms.py:126
 | 
			
		||||
#: intervention/forms/modalForms.py:149
 | 
			
		||||
msgid "Date"
 | 
			
		||||
msgstr "Datum"
 | 
			
		||||
 | 
			
		||||
@ -751,7 +752,7 @@ msgstr "Menge"
 | 
			
		||||
#: intervention/templates/intervention/detail/includes/documents.html:39
 | 
			
		||||
#: intervention/templates/intervention/detail/includes/payments.html:39
 | 
			
		||||
#: intervention/templates/intervention/detail/includes/revocation.html:43
 | 
			
		||||
#: templates/log.html:10
 | 
			
		||||
#: templates/log.html:10 user/templates/user/team/index.html:32
 | 
			
		||||
msgid "Action"
 | 
			
		||||
msgstr "Aktionen"
 | 
			
		||||
 | 
			
		||||
@ -999,14 +1000,14 @@ msgstr "Zuletzt bearbeitet"
 | 
			
		||||
 | 
			
		||||
#: compensation/templates/compensation/detail/compensation/view.html:100
 | 
			
		||||
#: compensation/templates/compensation/detail/eco_account/view.html:83
 | 
			
		||||
#: ema/templates/ema/detail/view.html:76 intervention/forms/modalForms.py:56
 | 
			
		||||
#: ema/templates/ema/detail/view.html:76
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:116
 | 
			
		||||
msgid "Shared with"
 | 
			
		||||
msgstr "Freigegeben für"
 | 
			
		||||
 | 
			
		||||
#: compensation/templates/compensation/detail/eco_account/includes/controls.html:15
 | 
			
		||||
#: ema/templates/ema/detail/includes/controls.html:15
 | 
			
		||||
#: intervention/forms/modalForms.py:70
 | 
			
		||||
#: intervention/forms/modalForms.py:71
 | 
			
		||||
#: intervention/templates/intervention/detail/includes/controls.html:15
 | 
			
		||||
msgid "Share"
 | 
			
		||||
msgstr "Freigabe"
 | 
			
		||||
@ -1141,7 +1142,7 @@ msgstr "Daten zu den verantwortlichen Stellen"
 | 
			
		||||
msgid "Compensations - Overview"
 | 
			
		||||
msgstr "Kompensationen - Übersicht"
 | 
			
		||||
 | 
			
		||||
#: compensation/views/compensation.py:151 konova/utils/message_templates.py:31
 | 
			
		||||
#: compensation/views/compensation.py:151 konova/utils/message_templates.py:33
 | 
			
		||||
msgid "Compensation {} edited"
 | 
			
		||||
msgstr "Kompensation {} bearbeitet"
 | 
			
		||||
 | 
			
		||||
@ -1150,12 +1151,12 @@ msgstr "Kompensation {} bearbeitet"
 | 
			
		||||
msgid "Edit {}"
 | 
			
		||||
msgstr "Bearbeite {}"
 | 
			
		||||
 | 
			
		||||
#: compensation/views/compensation.py:240 compensation/views/eco_account.py:349
 | 
			
		||||
#: compensation/views/compensation.py:240 compensation/views/eco_account.py:351
 | 
			
		||||
#: ema/views.py:194 intervention/views.py:531
 | 
			
		||||
msgid "Log"
 | 
			
		||||
msgstr "Log"
 | 
			
		||||
 | 
			
		||||
#: compensation/views/compensation.py:584 compensation/views/eco_account.py:716
 | 
			
		||||
#: compensation/views/compensation.py:584 compensation/views/eco_account.py:719
 | 
			
		||||
#: ema/views.py:551 intervention/views.py:677
 | 
			
		||||
msgid "Report {}"
 | 
			
		||||
msgstr "Bericht {}"
 | 
			
		||||
@ -1176,32 +1177,32 @@ msgstr "Ökokonto {} bearbeitet"
 | 
			
		||||
msgid "Eco-account removed"
 | 
			
		||||
msgstr "Ökokonto entfernt"
 | 
			
		||||
 | 
			
		||||
#: compensation/views/eco_account.py:370 ema/views.py:275
 | 
			
		||||
#: compensation/views/eco_account.py:372 ema/views.py:275
 | 
			
		||||
#: intervention/views.py:630
 | 
			
		||||
msgid "{} unrecorded"
 | 
			
		||||
msgstr "{} entzeichnet"
 | 
			
		||||
 | 
			
		||||
#: compensation/views/eco_account.py:370 ema/views.py:275
 | 
			
		||||
#: compensation/views/eco_account.py:372 ema/views.py:275
 | 
			
		||||
#: intervention/views.py:630
 | 
			
		||||
msgid "{} recorded"
 | 
			
		||||
msgstr "{} verzeichnet"
 | 
			
		||||
 | 
			
		||||
#: compensation/views/eco_account.py:789 ema/views.py:617
 | 
			
		||||
#: compensation/views/eco_account.py:792 ema/views.py:617
 | 
			
		||||
#: intervention/views.py:428
 | 
			
		||||
msgid "{} has already been shared with you"
 | 
			
		||||
msgstr "{} wurde bereits für Sie freigegeben"
 | 
			
		||||
 | 
			
		||||
#: compensation/views/eco_account.py:794 ema/views.py:622
 | 
			
		||||
#: compensation/views/eco_account.py:797 ema/views.py:622
 | 
			
		||||
#: intervention/views.py:433
 | 
			
		||||
msgid "{} has been shared with you"
 | 
			
		||||
msgstr "{} ist nun für Sie freigegeben"
 | 
			
		||||
 | 
			
		||||
#: compensation/views/eco_account.py:801 ema/views.py:629
 | 
			
		||||
#: compensation/views/eco_account.py:804 ema/views.py:629
 | 
			
		||||
#: intervention/views.py:440
 | 
			
		||||
msgid "Share link invalid"
 | 
			
		||||
msgstr "Freigabelink ungültig"
 | 
			
		||||
 | 
			
		||||
#: compensation/views/eco_account.py:824 ema/views.py:652
 | 
			
		||||
#: compensation/views/eco_account.py:827 ema/views.py:652
 | 
			
		||||
#: intervention/views.py:463
 | 
			
		||||
msgid "Share settings updated"
 | 
			
		||||
msgstr "Freigabe Einstellungen aktualisiert"
 | 
			
		||||
@ -1268,7 +1269,7 @@ msgstr "Mehrfachauswahl möglich"
 | 
			
		||||
#: intervention/forms/forms.py:86
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:48
 | 
			
		||||
#: intervention/templates/intervention/report/report.html:29
 | 
			
		||||
#: intervention/utils/quality.py:46 konova/filters/mixins.py:363
 | 
			
		||||
#: intervention/utils/quality.py:46 konova/filters/mixins.py:364
 | 
			
		||||
msgid "Registration office"
 | 
			
		||||
msgstr "Zulassungsbehörde"
 | 
			
		||||
 | 
			
		||||
@ -1321,60 +1322,68 @@ msgstr "Freigabelink"
 | 
			
		||||
 | 
			
		||||
#: intervention/forms/modalForms.py:31
 | 
			
		||||
msgid "Send this link to users who you want to have writing access on the data"
 | 
			
		||||
msgstr "Andere Nutzer erhalten über diesen Link Zugriff auf die Daten"
 | 
			
		||||
msgstr "Einzelne Nutzer erhalten über diesen Link Zugriff auf die Daten"
 | 
			
		||||
 | 
			
		||||
#: intervention/forms/modalForms.py:41
 | 
			
		||||
msgid "Add user to share with"
 | 
			
		||||
msgstr "Nutzer direkt hinzufügen"
 | 
			
		||||
msgid "Add team to share with"
 | 
			
		||||
msgstr "Team hinzufügen"
 | 
			
		||||
 | 
			
		||||
#: intervention/forms/modalForms.py:43
 | 
			
		||||
msgid ""
 | 
			
		||||
"Multiple selection possible - You can only select teams which do not already "
 | 
			
		||||
"have access."
 | 
			
		||||
msgstr ""
 | 
			
		||||
"Mehrfachauswahl möglich - Sie können nur Teams wählen, für die der Eintrag "
 | 
			
		||||
"noch nicht freigegeben wurde."
 | 
			
		||||
 | 
			
		||||
#: intervention/forms/modalForms.py:55
 | 
			
		||||
msgid "Add user to share with"
 | 
			
		||||
msgstr "Nutzer einzeln hinzufügen"
 | 
			
		||||
 | 
			
		||||
#: intervention/forms/modalForms.py:57
 | 
			
		||||
msgid ""
 | 
			
		||||
"Multiple selection possible - You can only select users which do not already "
 | 
			
		||||
"have access. Enter the full username."
 | 
			
		||||
msgstr ""
 | 
			
		||||
"Mehrfachauswahl möglich - Sie können nur Nutzer wählen, für die der Eintrag "
 | 
			
		||||
"noch nicht freigegeben wurde. Geben Sie den ganzen Nutzernamen an."
 | 
			
		||||
 | 
			
		||||
#: intervention/forms/modalForms.py:59
 | 
			
		||||
msgid "Remove check to remove access for this user"
 | 
			
		||||
msgstr "Wählen Sie die Nutzer ab, die keinen Zugriff mehr haben sollen"
 | 
			
		||||
 | 
			
		||||
#: intervention/forms/modalForms.py:71
 | 
			
		||||
#: intervention/forms/modalForms.py:72
 | 
			
		||||
msgid "Share settings for {}"
 | 
			
		||||
msgstr "Freigabe Einstellungen für {}"
 | 
			
		||||
 | 
			
		||||
#: intervention/forms/modalForms.py:128
 | 
			
		||||
#: intervention/forms/modalForms.py:151
 | 
			
		||||
msgid "Date of revocation"
 | 
			
		||||
msgstr "Datum des Widerspruchs"
 | 
			
		||||
 | 
			
		||||
#: intervention/forms/modalForms.py:139
 | 
			
		||||
#: intervention/forms/modalForms.py:162
 | 
			
		||||
#: intervention/templates/intervention/detail/includes/revocation.html:35
 | 
			
		||||
msgid "Document"
 | 
			
		||||
msgstr "Dokument"
 | 
			
		||||
 | 
			
		||||
#: intervention/forms/modalForms.py:142
 | 
			
		||||
#: intervention/forms/modalForms.py:165
 | 
			
		||||
msgid "Must be smaller than 15 Mb"
 | 
			
		||||
msgstr "Muss kleiner als 15 Mb sein"
 | 
			
		||||
 | 
			
		||||
#: intervention/forms/modalForms.py:167
 | 
			
		||||
#: intervention/forms/modalForms.py:190
 | 
			
		||||
#: intervention/templates/intervention/detail/includes/revocation.html:18
 | 
			
		||||
msgid "Add revocation"
 | 
			
		||||
msgstr "Widerspruch hinzufügen"
 | 
			
		||||
 | 
			
		||||
#: intervention/forms/modalForms.py:224
 | 
			
		||||
#: intervention/forms/modalForms.py:247
 | 
			
		||||
msgid "Checked intervention data"
 | 
			
		||||
msgstr "Eingriffsdaten geprüft"
 | 
			
		||||
 | 
			
		||||
#: intervention/forms/modalForms.py:230
 | 
			
		||||
#: intervention/forms/modalForms.py:253
 | 
			
		||||
msgid "Checked compensations data and payments"
 | 
			
		||||
msgstr "Kompensationen und Zahlungen geprüft"
 | 
			
		||||
 | 
			
		||||
#: intervention/forms/modalForms.py:239
 | 
			
		||||
#: intervention/forms/modalForms.py:262
 | 
			
		||||
#: intervention/templates/intervention/detail/includes/controls.html:19
 | 
			
		||||
msgid "Run check"
 | 
			
		||||
msgstr "Prüfung vornehmen"
 | 
			
		||||
 | 
			
		||||
#: intervention/forms/modalForms.py:240 konova/forms.py:514
 | 
			
		||||
#: intervention/forms/modalForms.py:263 konova/forms.py:514
 | 
			
		||||
msgid ""
 | 
			
		||||
"I, {} {}, confirm that all necessary control steps have been performed by "
 | 
			
		||||
"myself."
 | 
			
		||||
@ -1382,23 +1391,23 @@ msgstr ""
 | 
			
		||||
"Ich, {} {}, bestätige, dass die notwendigen Kontrollschritte durchgeführt "
 | 
			
		||||
"wurden:"
 | 
			
		||||
 | 
			
		||||
#: intervention/forms/modalForms.py:324
 | 
			
		||||
#: intervention/forms/modalForms.py:347
 | 
			
		||||
msgid "Only recorded accounts can be selected for deductions"
 | 
			
		||||
msgstr "Nur verzeichnete Ökokonten können für Abbuchungen verwendet werden."
 | 
			
		||||
 | 
			
		||||
#: intervention/forms/modalForms.py:351
 | 
			
		||||
#: intervention/forms/modalForms.py:374
 | 
			
		||||
msgid "Only shared interventions can be selected"
 | 
			
		||||
msgstr "Nur freigegebene Eingriffe können gewählt werden"
 | 
			
		||||
 | 
			
		||||
#: intervention/forms/modalForms.py:364
 | 
			
		||||
#: intervention/forms/modalForms.py:387
 | 
			
		||||
msgid "New Deduction"
 | 
			
		||||
msgstr "Neue Abbuchung"
 | 
			
		||||
 | 
			
		||||
#: intervention/forms/modalForms.py:365
 | 
			
		||||
#: intervention/forms/modalForms.py:388
 | 
			
		||||
msgid "Enter the information for a new deduction from a chosen eco-account"
 | 
			
		||||
msgstr "Geben Sie die Informationen für eine neue Abbuchung ein."
 | 
			
		||||
 | 
			
		||||
#: intervention/forms/modalForms.py:408
 | 
			
		||||
#: intervention/forms/modalForms.py:431
 | 
			
		||||
msgid ""
 | 
			
		||||
"Eco-account {} is not recorded yet. You can only deduct from recorded "
 | 
			
		||||
"accounts."
 | 
			
		||||
@ -1406,7 +1415,7 @@ msgstr ""
 | 
			
		||||
"Ökokonto {} ist noch nicht verzeichnet. Abbuchungen können nur von "
 | 
			
		||||
"verzeichneten Ökokonten erfolgen."
 | 
			
		||||
 | 
			
		||||
#: intervention/forms/modalForms.py:418
 | 
			
		||||
#: intervention/forms/modalForms.py:441
 | 
			
		||||
msgid ""
 | 
			
		||||
"The account {} has not enough surface for a deduction of {} m². There are "
 | 
			
		||||
"only {} m² left"
 | 
			
		||||
@ -1601,15 +1610,15 @@ msgstr "Nach Flurstücknenner suchen"
 | 
			
		||||
msgid "Show unshared"
 | 
			
		||||
msgstr "Nicht freigegebene anzeigen"
 | 
			
		||||
 | 
			
		||||
#: konova/filters/mixins.py:314
 | 
			
		||||
#: konova/filters/mixins.py:315
 | 
			
		||||
msgid "Show recorded"
 | 
			
		||||
msgstr "Verzeichnete anzeigen"
 | 
			
		||||
 | 
			
		||||
#: konova/filters/mixins.py:364
 | 
			
		||||
#: konova/filters/mixins.py:365
 | 
			
		||||
msgid "Search for registration office"
 | 
			
		||||
msgstr "Nach Zulassungsbehörde suchen"
 | 
			
		||||
 | 
			
		||||
#: konova/filters/mixins.py:396
 | 
			
		||||
#: konova/filters/mixins.py:397
 | 
			
		||||
msgid "Search for conservation office"
 | 
			
		||||
msgstr "Nch Eintragungsstelle suchen"
 | 
			
		||||
 | 
			
		||||
@ -1787,27 +1796,27 @@ msgstr "In Zwischenablage kopiert"
 | 
			
		||||
msgid "{} - Shared access removed"
 | 
			
		||||
msgstr "{} - Zugriff entzogen"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/mailer.py:91
 | 
			
		||||
#: konova/utils/mailer.py:91 konova/utils/mailer.py:114
 | 
			
		||||
msgid "{} - Shared access given"
 | 
			
		||||
msgstr "{} - Zugriff freigegeben"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/mailer.py:114
 | 
			
		||||
#: konova/utils/mailer.py:137
 | 
			
		||||
msgid "{} - Shared data recorded"
 | 
			
		||||
msgstr "{} - Freigegebene Daten verzeichnet"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/mailer.py:137
 | 
			
		||||
#: konova/utils/mailer.py:160
 | 
			
		||||
msgid "{} - Shared data unrecorded"
 | 
			
		||||
msgstr "{} - Freigegebene Daten entzeichnet"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/mailer.py:160
 | 
			
		||||
#: konova/utils/mailer.py:183
 | 
			
		||||
msgid "{} - Shared data deleted"
 | 
			
		||||
msgstr "{} - Freigegebene Daten gelöscht"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/mailer.py:183
 | 
			
		||||
#: konova/utils/mailer.py:206
 | 
			
		||||
msgid "{} - Shared data checked"
 | 
			
		||||
msgstr "{} - Freigegebene Daten geprüft"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/mailer.py:204 templates/email/api/verify_token.html:4
 | 
			
		||||
#: konova/utils/mailer.py:227 templates/email/api/verify_token.html:4
 | 
			
		||||
msgid "Request for new API token"
 | 
			
		||||
msgstr "Anfrage für neuen API Token"
 | 
			
		||||
 | 
			
		||||
@ -1836,10 +1845,25 @@ msgstr ""
 | 
			
		||||
"der Zwischenzeit angelegt wurde, welcher diese Kennung nun bereits verwendet"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:15
 | 
			
		||||
msgid ""
 | 
			
		||||
"Only conservation or registration office users are allowed to remove entries."
 | 
			
		||||
msgstr ""
 | 
			
		||||
"Nur Mitarbeiter der Naturschutz- oder Zulassungsbehördengruppe dürfen "
 | 
			
		||||
"Einträge entfernen"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:16
 | 
			
		||||
msgid "You need to be part of another user group."
 | 
			
		||||
msgstr "Hierfür müssen Sie einer anderen Nutzergruppe angehören!"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:17
 | 
			
		||||
msgid "Status of Checked and Recorded reseted"
 | 
			
		||||
msgstr "'Geprüft'/'Verzeichnet' wurde zurückgesetzt"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:20
 | 
			
		||||
msgid "This data is not shared with you"
 | 
			
		||||
msgstr "Diese Daten sind für Sie nicht freigegeben"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:16
 | 
			
		||||
#: konova/utils/message_templates.py:21
 | 
			
		||||
msgid ""
 | 
			
		||||
"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 "
 | 
			
		||||
@ -1849,23 +1873,15 @@ msgstr ""
 | 
			
		||||
"bedeutet, dass Sie nur lesenden Zugriff hierauf haben und weder bearbeiten, "
 | 
			
		||||
"noch Prüfungen durchführen oder verzeichnen können."
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:17
 | 
			
		||||
msgid "You need to be part of another user group."
 | 
			
		||||
msgstr "Hierfür müssen Sie einer anderen Nutzergruppe angehören!"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:19
 | 
			
		||||
msgid "Status of Checked and Recorded reseted"
 | 
			
		||||
msgstr "'Geprüft'/'Verzeichnet' wurde zurückgesetzt"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:22
 | 
			
		||||
#: konova/utils/message_templates.py:24
 | 
			
		||||
msgid "Unsupported file type"
 | 
			
		||||
msgstr "Dateiformat nicht unterstützt"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:23
 | 
			
		||||
#: konova/utils/message_templates.py:25
 | 
			
		||||
msgid "File too large"
 | 
			
		||||
msgstr "Datei zu groß"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:26
 | 
			
		||||
#: konova/utils/message_templates.py:28
 | 
			
		||||
msgid ""
 | 
			
		||||
"Action canceled. Eco account is recorded or deductions exist. Only "
 | 
			
		||||
"conservation office member can perform this action."
 | 
			
		||||
@ -1873,119 +1889,119 @@ msgstr ""
 | 
			
		||||
"Aktion abgebrochen. Ökokonto ist bereits verzeichnet oder Abbuchungen liegen "
 | 
			
		||||
"vor. Nur Eintragungsstellennutzer können diese Aktion jetzt durchführen."
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:29
 | 
			
		||||
#: konova/utils/message_templates.py:31
 | 
			
		||||
msgid "Compensation {} added"
 | 
			
		||||
msgstr "Kompensation {} hinzugefügt"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:30
 | 
			
		||||
#: konova/utils/message_templates.py:32
 | 
			
		||||
msgid "Compensation {} removed"
 | 
			
		||||
msgstr "Kompensation {} entfernt"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:32
 | 
			
		||||
#: konova/utils/message_templates.py:34
 | 
			
		||||
msgid "Added compensation action"
 | 
			
		||||
msgstr "Maßnahme hinzugefügt"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:33
 | 
			
		||||
#: konova/utils/message_templates.py:35
 | 
			
		||||
msgid "Added compensation state"
 | 
			
		||||
msgstr "Zustand hinzugefügt"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:36
 | 
			
		||||
#: konova/utils/message_templates.py:38
 | 
			
		||||
msgid "State removed"
 | 
			
		||||
msgstr "Zustand gelöscht"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:37
 | 
			
		||||
#: konova/utils/message_templates.py:39
 | 
			
		||||
msgid "State edited"
 | 
			
		||||
msgstr "Zustand bearbeitet"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:38
 | 
			
		||||
#: konova/utils/message_templates.py:40
 | 
			
		||||
msgid "State added"
 | 
			
		||||
msgstr "Zustand hinzugefügt"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:41
 | 
			
		||||
#: konova/utils/message_templates.py:43
 | 
			
		||||
msgid "Action added"
 | 
			
		||||
msgstr "Maßnahme hinzugefügt"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:42
 | 
			
		||||
#: konova/utils/message_templates.py:44
 | 
			
		||||
msgid "Action edited"
 | 
			
		||||
msgstr "Maßnahme bearbeitet"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:43
 | 
			
		||||
#: konova/utils/message_templates.py:45
 | 
			
		||||
msgid "Action removed"
 | 
			
		||||
msgstr "Maßnahme entfernt"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:46
 | 
			
		||||
#: konova/utils/message_templates.py:48
 | 
			
		||||
msgid "Deduction added"
 | 
			
		||||
msgstr "Abbuchung hinzugefügt"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:47
 | 
			
		||||
#: konova/utils/message_templates.py:49
 | 
			
		||||
msgid "Deduction edited"
 | 
			
		||||
msgstr "Abbuchung bearbeitet"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:48
 | 
			
		||||
#: konova/utils/message_templates.py:50
 | 
			
		||||
msgid "Deduction removed"
 | 
			
		||||
msgstr "Abbuchung entfernt"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:51
 | 
			
		||||
#: konova/utils/message_templates.py:53
 | 
			
		||||
msgid "Deadline added"
 | 
			
		||||
msgstr "Frist/Termin hinzugefügt"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:52
 | 
			
		||||
#: konova/utils/message_templates.py:54
 | 
			
		||||
msgid "Deadline edited"
 | 
			
		||||
msgstr "Frist/Termin bearbeitet"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:53
 | 
			
		||||
#: konova/utils/message_templates.py:55
 | 
			
		||||
msgid "Deadline removed"
 | 
			
		||||
msgstr "Frist/Termin gelöscht"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:56
 | 
			
		||||
#: konova/utils/message_templates.py:58
 | 
			
		||||
msgid "Payment added"
 | 
			
		||||
msgstr "Zahlung hinzugefügt"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:57
 | 
			
		||||
#: konova/utils/message_templates.py:59
 | 
			
		||||
msgid "Payment edited"
 | 
			
		||||
msgstr "Zahlung bearbeitet"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:58
 | 
			
		||||
#: konova/utils/message_templates.py:60
 | 
			
		||||
msgid "Payment removed"
 | 
			
		||||
msgstr "Zahlung gelöscht"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:61
 | 
			
		||||
#: konova/utils/message_templates.py:63
 | 
			
		||||
msgid "Revocation added"
 | 
			
		||||
msgstr "Widerspruch hinzugefügt"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:62
 | 
			
		||||
#: konova/utils/message_templates.py:64
 | 
			
		||||
msgid "Revocation edited"
 | 
			
		||||
msgstr "Widerspruch bearbeitet"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:63
 | 
			
		||||
#: konova/utils/message_templates.py:65
 | 
			
		||||
msgid "Revocation removed"
 | 
			
		||||
msgstr "Widerspruch entfernt"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:66
 | 
			
		||||
#: konova/utils/message_templates.py:68
 | 
			
		||||
msgid "Document '{}' deleted"
 | 
			
		||||
msgstr "Dokument '{}' gelöscht"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:67
 | 
			
		||||
#: konova/utils/message_templates.py:69
 | 
			
		||||
msgid "Document added"
 | 
			
		||||
msgstr "Dokument hinzugefügt"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:68
 | 
			
		||||
#: konova/utils/message_templates.py:70
 | 
			
		||||
msgid "Document edited"
 | 
			
		||||
msgstr "Dokument bearbeitet"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:71
 | 
			
		||||
#: konova/utils/message_templates.py:73
 | 
			
		||||
msgid "Edited general data"
 | 
			
		||||
msgstr "Allgemeine Daten bearbeitet"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:72
 | 
			
		||||
#: konova/utils/message_templates.py:74
 | 
			
		||||
msgid "Added deadline"
 | 
			
		||||
msgstr "Frist/Termin hinzugefügt"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:75
 | 
			
		||||
#: konova/utils/message_templates.py:77
 | 
			
		||||
msgid "Geometry conflict detected with {}"
 | 
			
		||||
msgstr "Geometriekonflikt mit folgenden Einträgen erkannt: {}"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:78
 | 
			
		||||
#: konova/utils/message_templates.py:80
 | 
			
		||||
msgid "This intervention has {} revocations"
 | 
			
		||||
msgstr "Dem Eingriff liegen {} Widersprüche vor"
 | 
			
		||||
 | 
			
		||||
@ -2076,7 +2092,9 @@ msgstr ""
 | 
			
		||||
#: templates/email/recording/shared_data_recorded.html:19
 | 
			
		||||
#: templates/email/recording/shared_data_unrecorded.html:19
 | 
			
		||||
#: templates/email/sharing/shared_access_given.html:20
 | 
			
		||||
#: templates/email/sharing/shared_access_given_team.html:20
 | 
			
		||||
#: templates/email/sharing/shared_access_removed.html:20
 | 
			
		||||
#: templates/email/sharing/shared_access_removed_team.html:20
 | 
			
		||||
msgid "Best regards"
 | 
			
		||||
msgstr "Beste Grüße"
 | 
			
		||||
 | 
			
		||||
@ -2163,6 +2181,7 @@ msgstr ""
 | 
			
		||||
"zugehörigen Kompensationen automatisch entzeichnet worden sind."
 | 
			
		||||
 | 
			
		||||
#: templates/email/sharing/shared_access_given.html:4
 | 
			
		||||
#: templates/email/sharing/shared_access_given_team.html:4
 | 
			
		||||
msgid "Access shared"
 | 
			
		||||
msgstr "Zugriff freigegeben"
 | 
			
		||||
 | 
			
		||||
@ -2171,10 +2190,12 @@ msgid "the following dataset has just been shared with you"
 | 
			
		||||
msgstr "der folgende Datensatz wurde soeben für Sie freigegeben "
 | 
			
		||||
 | 
			
		||||
#: templates/email/sharing/shared_access_given.html:16
 | 
			
		||||
#: templates/email/sharing/shared_access_given_team.html:16
 | 
			
		||||
msgid "This means you can now edit this dataset."
 | 
			
		||||
msgstr "Das bedeutet, dass Sie diesen Datensatz nun auch bearbeiten können."
 | 
			
		||||
 | 
			
		||||
#: templates/email/sharing/shared_access_given.html:17
 | 
			
		||||
#: templates/email/sharing/shared_access_given_team.html:17
 | 
			
		||||
msgid ""
 | 
			
		||||
"The shared dataset appears now by default on your overview for this dataset "
 | 
			
		||||
"type."
 | 
			
		||||
@ -2183,6 +2204,7 @@ msgstr ""
 | 
			
		||||
"Datensatztyp im KSP gelistet."
 | 
			
		||||
 | 
			
		||||
#: templates/email/sharing/shared_access_given.html:27
 | 
			
		||||
#: templates/email/sharing/shared_access_given_team.html:27
 | 
			
		||||
msgid ""
 | 
			
		||||
"Please note: Shared access on an intervention means you automatically have "
 | 
			
		||||
"editing access to related compensations."
 | 
			
		||||
@ -2191,7 +2213,17 @@ msgstr ""
 | 
			
		||||
"Sie automatisch auch Zugriff auf die zugehörigen Kompensationen erhalten "
 | 
			
		||||
"haben."
 | 
			
		||||
 | 
			
		||||
#: templates/email/sharing/shared_access_given_team.html:8
 | 
			
		||||
#: templates/email/sharing/shared_access_removed_team.html:8
 | 
			
		||||
msgid "Hello team"
 | 
			
		||||
msgstr "Hallo Team"
 | 
			
		||||
 | 
			
		||||
#: templates/email/sharing/shared_access_given_team.html:10
 | 
			
		||||
msgid "the following dataset has just been shared with your team"
 | 
			
		||||
msgstr "der folgende Datensatz wurde soeben für Ihr Team freigegeben "
 | 
			
		||||
 | 
			
		||||
#: templates/email/sharing/shared_access_removed.html:4
 | 
			
		||||
#: templates/email/sharing/shared_access_removed_team.html:4
 | 
			
		||||
msgid "Shared access removed"
 | 
			
		||||
msgstr "Freigegebener Zugriff entzogen"
 | 
			
		||||
 | 
			
		||||
@ -2203,10 +2235,12 @@ msgstr ""
 | 
			
		||||
"entzogen: "
 | 
			
		||||
 | 
			
		||||
#: templates/email/sharing/shared_access_removed.html:16
 | 
			
		||||
#: templates/email/sharing/shared_access_removed_team.html:16
 | 
			
		||||
msgid "However, you are still able to view the dataset content."
 | 
			
		||||
msgstr "Sie können den Datensatz aber immer noch im KSP einsehen."
 | 
			
		||||
 | 
			
		||||
#: templates/email/sharing/shared_access_removed.html:17
 | 
			
		||||
#: templates/email/sharing/shared_access_removed_team.html:17
 | 
			
		||||
msgid ""
 | 
			
		||||
"Please use the provided search filter on the dataset`s overview pages to "
 | 
			
		||||
"find them."
 | 
			
		||||
@ -2214,6 +2248,14 @@ msgstr ""
 | 
			
		||||
"Nutzen Sie hierzu einfach die entsprechenden Suchfilter auf den "
 | 
			
		||||
"Übersichtsseiten"
 | 
			
		||||
 | 
			
		||||
#: templates/email/sharing/shared_access_removed_team.html:10
 | 
			
		||||
msgid ""
 | 
			
		||||
"your teams shared access, including editing, has been revoked for the "
 | 
			
		||||
"dataset "
 | 
			
		||||
msgstr ""
 | 
			
		||||
"Ihrem Team wurde soeben der bearbeitende Zugriff auf den folgenden Datensatz "
 | 
			
		||||
"entzogen: "
 | 
			
		||||
 | 
			
		||||
#: templates/email/signature.html:6
 | 
			
		||||
msgid "Please do not reply on this mail."
 | 
			
		||||
msgstr "Bitte antworten Sie nicht auf diese Mail."
 | 
			
		||||
@ -2272,7 +2314,7 @@ msgstr "* sind Pflichtfelder."
 | 
			
		||||
msgid "New entry"
 | 
			
		||||
msgstr "Neuer Eintrag"
 | 
			
		||||
 | 
			
		||||
#: templates/generic_index.html:41
 | 
			
		||||
#: templates/generic_index.html:41 user/templates/user/team/index.html:22
 | 
			
		||||
msgid "New"
 | 
			
		||||
msgstr "Neu"
 | 
			
		||||
 | 
			
		||||
@ -2401,6 +2443,62 @@ msgstr "Neuen Token generieren"
 | 
			
		||||
msgid "A new token needs to be validated by an administrator!"
 | 
			
		||||
msgstr "Neue Tokens müssen durch Administratoren freigeschaltet werden!"
 | 
			
		||||
 | 
			
		||||
#: user/forms.py:168 user/forms.py:172 user/forms.py:323 user/forms.py:328
 | 
			
		||||
msgid "Team name"
 | 
			
		||||
msgstr "Team Name"
 | 
			
		||||
 | 
			
		||||
#: user/forms.py:179 user/forms.py:336 user/templates/user/team/index.html:30
 | 
			
		||||
msgid "Description"
 | 
			
		||||
msgstr "Beschreibung"
 | 
			
		||||
 | 
			
		||||
#: user/forms.py:188
 | 
			
		||||
msgid "Manage team members"
 | 
			
		||||
msgstr "Mitglieder verwalten"
 | 
			
		||||
 | 
			
		||||
#: user/forms.py:190
 | 
			
		||||
msgid ""
 | 
			
		||||
"Multiple selection possible - You can only select users which are not "
 | 
			
		||||
"already a team member. Enter the full username or e-mail."
 | 
			
		||||
msgstr ""
 | 
			
		||||
"Mehrfachauswahl möglich - Sie können nur Nutzer wählen, die noch nicht "
 | 
			
		||||
"Mitglieder dieses Teams sind. Geben Sie den ganzen Nutzernamen an."
 | 
			
		||||
 | 
			
		||||
#: user/forms.py:204
 | 
			
		||||
msgid "Create new team"
 | 
			
		||||
msgstr "Neues Team anlegen"
 | 
			
		||||
 | 
			
		||||
#: user/forms.py:205
 | 
			
		||||
msgid ""
 | 
			
		||||
"You will become the administrator for this group by default. You do not need "
 | 
			
		||||
"to add yourself to the list of members."
 | 
			
		||||
msgstr ""
 | 
			
		||||
"Sie werden standardmäßig der Administrator dieses Teams. Sie müssen sich "
 | 
			
		||||
"selbst nicht zur Liste der Mitglieder hinzufügen."
 | 
			
		||||
 | 
			
		||||
#: user/forms.py:218 user/forms.py:279
 | 
			
		||||
msgid "Name already taken. Try another."
 | 
			
		||||
msgstr "Name bereits vergeben. Probieren Sie einen anderen."
 | 
			
		||||
 | 
			
		||||
#: user/forms.py:249
 | 
			
		||||
msgid "Admin"
 | 
			
		||||
msgstr "Administrator"
 | 
			
		||||
 | 
			
		||||
#: user/forms.py:250
 | 
			
		||||
msgid "Administrators manage team details and members"
 | 
			
		||||
msgstr "Administratoren verwalten die Teamdaten und Mitglieder"
 | 
			
		||||
 | 
			
		||||
#: user/forms.py:263
 | 
			
		||||
msgid "Selected admin ({}) needs to be a member of this team."
 | 
			
		||||
msgstr "Gewählter Administrator ({}) muss ein Mitglied des Teams sein."
 | 
			
		||||
 | 
			
		||||
#: user/forms.py:291 user/templates/user/team/index.html:51
 | 
			
		||||
msgid "Edit team"
 | 
			
		||||
msgstr "Team bearbeiten"
 | 
			
		||||
 | 
			
		||||
#: user/forms.py:347
 | 
			
		||||
msgid "Team"
 | 
			
		||||
msgstr "Team"
 | 
			
		||||
 | 
			
		||||
#: user/models/user_action.py:22
 | 
			
		||||
msgid "Unrecorded"
 | 
			
		||||
msgstr "Entzeichnet"
 | 
			
		||||
@ -2417,7 +2515,11 @@ msgstr "Gelöscht"
 | 
			
		||||
msgid "Show contact data"
 | 
			
		||||
msgstr "Zeige Kontaktdaten"
 | 
			
		||||
 | 
			
		||||
#: user/templates/user/index.html:13
 | 
			
		||||
#: user/templates/user/includes/team_data_modal_button.html:3
 | 
			
		||||
msgid "Show team data"
 | 
			
		||||
msgstr "Zeige Teamdaten"
 | 
			
		||||
 | 
			
		||||
#: user/templates/user/index.html:13 user/templates/user/team/index.html:29
 | 
			
		||||
msgid "Name"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
@ -2462,6 +2564,27 @@ msgstr "API token einsehen oder neu generieren"
 | 
			
		||||
msgid "API"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: user/templates/user/index.html:66
 | 
			
		||||
msgid "Manage teams"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: user/templates/user/index.html:69 user/templates/user/team/index.html:18
 | 
			
		||||
#: user/views.py:167
 | 
			
		||||
msgid "Teams"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: user/templates/user/team/index.html:20
 | 
			
		||||
msgid "Add new team"
 | 
			
		||||
msgstr "Neues Team hinzufügen"
 | 
			
		||||
 | 
			
		||||
#: user/templates/user/team/index.html:31
 | 
			
		||||
msgid "Members"
 | 
			
		||||
msgstr "Mitglieder"
 | 
			
		||||
 | 
			
		||||
#: user/templates/user/team/index.html:54
 | 
			
		||||
msgid "Remove team"
 | 
			
		||||
msgstr "Team entfernen"
 | 
			
		||||
 | 
			
		||||
#: user/templates/user/token.html:6
 | 
			
		||||
msgid "API settings"
 | 
			
		||||
msgstr "API Einstellungen"
 | 
			
		||||
@ -2486,26 +2609,38 @@ msgstr "Token noch nicht freigeschaltet"
 | 
			
		||||
msgid "Valid until"
 | 
			
		||||
msgstr "Läuft ab am"
 | 
			
		||||
 | 
			
		||||
#: user/views.py:31
 | 
			
		||||
#: user/views.py:33
 | 
			
		||||
msgid "User settings"
 | 
			
		||||
msgstr "Einstellungen"
 | 
			
		||||
 | 
			
		||||
#: user/views.py:57
 | 
			
		||||
#: user/views.py:59
 | 
			
		||||
msgid "Notifications edited"
 | 
			
		||||
msgstr "Benachrichtigungen bearbeitet"
 | 
			
		||||
 | 
			
		||||
#: user/views.py:69
 | 
			
		||||
#: user/views.py:71
 | 
			
		||||
msgid "User notifications"
 | 
			
		||||
msgstr "Benachrichtigungen"
 | 
			
		||||
 | 
			
		||||
#: user/views.py:92
 | 
			
		||||
#: user/views.py:94
 | 
			
		||||
msgid "New token generated. Administrators need to validate."
 | 
			
		||||
msgstr "Neuer Token generiert. Administratoren sind informiert."
 | 
			
		||||
 | 
			
		||||
#: user/views.py:103
 | 
			
		||||
#: user/views.py:105
 | 
			
		||||
msgid "User API token"
 | 
			
		||||
msgstr "API Nutzer Token"
 | 
			
		||||
 | 
			
		||||
#: user/views.py:178
 | 
			
		||||
msgid "New team added"
 | 
			
		||||
msgstr "Neues Team hinzugefügt"
 | 
			
		||||
 | 
			
		||||
#: user/views.py:191
 | 
			
		||||
msgid "Team edited"
 | 
			
		||||
msgstr "Team bearbeitet"
 | 
			
		||||
 | 
			
		||||
#: user/views.py:204
 | 
			
		||||
msgid "Team removed"
 | 
			
		||||
msgstr "Team gelöscht"
 | 
			
		||||
 | 
			
		||||
#: venv/lib/python3.7/site-packages/bootstrap4/components.py:17
 | 
			
		||||
#: venv/lib/python3.7/site-packages/bootstrap4/templates/bootstrap4/form_errors.html:3
 | 
			
		||||
#: venv/lib/python3.7/site-packages/bootstrap4/templates/bootstrap4/messages.html:4
 | 
			
		||||
@ -4009,6 +4144,12 @@ msgstr ""
 | 
			
		||||
msgid "Unable to connect to qpid with SASL mechanism %s"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#~ msgid "your teams"
 | 
			
		||||
#~ msgstr "Team entfernen"
 | 
			
		||||
 | 
			
		||||
#~ msgid "Remove check to remove access for this user"
 | 
			
		||||
#~ msgstr "Wählen Sie die Nutzer ab, die keinen Zugriff mehr haben sollen"
 | 
			
		||||
 | 
			
		||||
#~ msgid "Select the action type"
 | 
			
		||||
#~ msgstr "Maßnahmentyp wählen"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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 user.models import UserNotification, UserActionLogEntry, User
 | 
			
		||||
from user.models import UserNotification, UserActionLogEntry, User, Team
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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(Team, TeamAdmin)
 | 
			
		||||
 | 
			
		||||
# Outcommented for a cleaner admin backend on production
 | 
			
		||||
#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
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from dal import autocomplete
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.db import IntegrityError
 | 
			
		||||
from django.db import IntegrityError, transaction
 | 
			
		||||
from django.urls import reverse, reverse_lazy
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from api.models import APIUserToken
 | 
			
		||||
from intervention.inputs import GenerateInput
 | 
			
		||||
from user.models import User
 | 
			
		||||
from user.models import User, UserNotification, Team
 | 
			
		||||
 | 
			
		||||
from konova.forms import BaseForm, BaseModalForm
 | 
			
		||||
from user.models import UserNotification
 | 
			
		||||
from konova.forms import BaseForm, BaseModalForm, RemoveModalForm
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserNotificationForm(BaseForm):
 | 
			
		||||
@ -160,3 +160,201 @@ class UserAPITokenForm(BaseForm):
 | 
			
		||||
        user.api_token = new_token
 | 
			
		||||
        user.save()
 | 
			
		||||
        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
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from .user_action import *
 | 
			
		||||
from .user import *
 | 
			
		||||
from .notification import *
 | 
			
		||||
from .user_action import UserActionLogEntry, UserAction
 | 
			
		||||
from .user import User
 | 
			
		||||
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 %}
 | 
			
		||||
 | 
			
		||||
<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>
 | 
			
		||||
</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>
 | 
			
		||||
                    </a>
 | 
			
		||||
                </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>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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("token/api", api_token_view, name="api-token"),
 | 
			
		||||
    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.auth.decorators import login_required
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
 | 
			
		||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
 | 
			
		||||
from konova.utils.mailer import Mailer
 | 
			
		||||
from konova.utils.message_templates import FORM_INVALID
 | 
			
		||||
from user.models import User
 | 
			
		||||
from django.http import HttpRequest
 | 
			
		||||
from user.models import User, Team
 | 
			
		||||
from django.http import HttpRequest, Http404
 | 
			
		||||
from django.shortcuts import render, redirect, get_object_or_404
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from konova.contexts import BaseContext
 | 
			
		||||
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
 | 
			
		||||
@ -128,4 +130,77 @@ def contact_view(request: HttpRequest, id: str):
 | 
			
		||||
        request,
 | 
			
		||||
        template,
 | 
			
		||||
        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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user