Merge pull request '169_Unknown_admin_on_teams' (#170) from 169_Unknown_admin_on_teams into master
Reviewed-on: SGD-Nord/konova#170
This commit is contained in:
		
						commit
						ab9af7ae2f
					
				@ -109,7 +109,7 @@
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <th scope="row">{% trans 'Shared with' %}</th>
 | 
			
		||||
                        <td class="align-middle">
 | 
			
		||||
                            {% for team in obj.intervention.teams.all %}
 | 
			
		||||
                            {% for team in obj.intervention.shared_teams %}
 | 
			
		||||
                                {% include 'user/includes/team_data_modal_button.html' %}
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                            <hr>
 | 
			
		||||
 | 
			
		||||
@ -87,7 +87,7 @@
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <th scope="row">{% trans 'Shared with' %}</th>
 | 
			
		||||
                        <td class="align-middle">
 | 
			
		||||
                            {% for team in obj.teams.all %}
 | 
			
		||||
                            {% for team in obj.shared_teams %}
 | 
			
		||||
                                {% include 'user/includes/team_data_modal_button.html' %}
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                            <hr>
 | 
			
		||||
 | 
			
		||||
@ -73,11 +73,11 @@
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <th scope="row">{% trans 'Shared with' %}</th>
 | 
			
		||||
                        <td class="align-middle">
 | 
			
		||||
                            {% for team in obj.teams.all %}
 | 
			
		||||
                            {% for team in obj.shared_teams %}
 | 
			
		||||
                                {% include 'user/includes/team_data_modal_button.html' %}
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                            <hr>
 | 
			
		||||
                            {% for user in obj.users.all %}
 | 
			
		||||
                            {% for user in obj.user.all %}
 | 
			
		||||
                                {% include 'user/includes/contact_modal_button.html' %}
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                        </td>
 | 
			
		||||
 | 
			
		||||
@ -125,7 +125,7 @@
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <th scope="row">{% trans 'Shared with' %}</th>
 | 
			
		||||
                        <td class="align-middle">
 | 
			
		||||
                            {% for team in obj.teams.all %}
 | 
			
		||||
                            {% for team in obj.shared_teams %}
 | 
			
		||||
                                {% include 'user/includes/team_data_modal_button.html' %}
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                            <hr>
 | 
			
		||||
 | 
			
		||||
@ -98,6 +98,18 @@ class DeadlineAdmin(admin.ModelAdmin):
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DeletableObjectMixinAdmin(admin.ModelAdmin):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        abstract = True
 | 
			
		||||
 | 
			
		||||
    def restore_deleted_data(self, request, queryset):
 | 
			
		||||
        queryset = queryset.filter(
 | 
			
		||||
            deleted__isnull=False
 | 
			
		||||
        )
 | 
			
		||||
        for entry in queryset:
 | 
			
		||||
            entry.deleted.delete()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BaseResourceAdmin(admin.ModelAdmin):
 | 
			
		||||
    fields = [
 | 
			
		||||
        "created",
 | 
			
		||||
@ -109,7 +121,7 @@ class BaseResourceAdmin(admin.ModelAdmin):
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BaseObjectAdmin(BaseResourceAdmin):
 | 
			
		||||
class BaseObjectAdmin(BaseResourceAdmin, DeletableObjectMixinAdmin):
 | 
			
		||||
    search_fields = [
 | 
			
		||||
        "identifier",
 | 
			
		||||
        "title",
 | 
			
		||||
@ -126,13 +138,6 @@ class BaseObjectAdmin(BaseResourceAdmin):
 | 
			
		||||
            "deleted",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    def restore_deleted_data(self, request, queryset):
 | 
			
		||||
        queryset = queryset.filter(
 | 
			
		||||
            deleted__isnull=False
 | 
			
		||||
        )
 | 
			
		||||
        for entry in queryset:
 | 
			
		||||
            entry.deleted.delete()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Outcommented for a cleaner admin backend on production
 | 
			
		||||
 | 
			
		||||
@ -96,7 +96,9 @@ class ShareTeamAutocomplete(Select2QuerySetView):
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        if self.request.user.is_anonymous:
 | 
			
		||||
            return Team.objects.none()
 | 
			
		||||
        qs = Team.objects.all()
 | 
			
		||||
        qs = Team.objects.filter(
 | 
			
		||||
            deleted__isnull=True
 | 
			
		||||
        )
 | 
			
		||||
        if self.q:
 | 
			
		||||
            # Due to privacy concerns only a full username match will return the proper user entry
 | 
			
		||||
            qs = qs.filter(
 | 
			
		||||
@ -108,6 +110,29 @@ class ShareTeamAutocomplete(Select2QuerySetView):
 | 
			
		||||
        return qs
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TeamAdminAutocomplete(Select2QuerySetView):
 | 
			
		||||
    """ Autocomplete for share with teams
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        if self.request.user.is_anonymous:
 | 
			
		||||
            return User.objects.none()
 | 
			
		||||
        qs = User.objects.filter(
 | 
			
		||||
            id__in=self.forwarded.get("members", [])
 | 
			
		||||
        ).exclude(
 | 
			
		||||
            id__in=self.forwarded.get("admins", [])
 | 
			
		||||
        )
 | 
			
		||||
        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(
 | 
			
		||||
            "username"
 | 
			
		||||
        )
 | 
			
		||||
        return qs
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class KonovaCodeAutocomplete(Select2GroupQuerySetView):
 | 
			
		||||
    """
 | 
			
		||||
    Provides simple autocomplete functionality for codes
 | 
			
		||||
 | 
			
		||||
@ -326,7 +326,7 @@ class SimpleGeomForm(BaseForm):
 | 
			
		||||
        features = []
 | 
			
		||||
        features_json = geom.get("features", [])
 | 
			
		||||
        for feature in features_json:
 | 
			
		||||
            g = gdal.OGRGeometry(json.dumps(feature["geometry"]), srs=DEFAULT_SRID_RLP)
 | 
			
		||||
            g = gdal.OGRGeometry(json.dumps(feature.get("geometry", feature)), srs=DEFAULT_SRID_RLP)
 | 
			
		||||
            if g.geom_type not in ["Polygon", "MultiPolygon"]:
 | 
			
		||||
                self.add_error("geom", _("Only surfaces allowed. Points or lines must be buffered."))
 | 
			
		||||
                is_valid = False
 | 
			
		||||
 | 
			
		||||
@ -87,25 +87,15 @@ class BaseResource(UuidModel):
 | 
			
		||||
        super().delete()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BaseObject(BaseResource):
 | 
			
		||||
    """
 | 
			
		||||
    A basic object model, which specifies BaseResource.
 | 
			
		||||
class DeletableObjectMixin(models.Model):
 | 
			
		||||
    """ Wraps deleted field and related functionality
 | 
			
		||||
 | 
			
		||||
    Mainly used for intervention, compensation, ecoaccount
 | 
			
		||||
    """
 | 
			
		||||
    identifier = models.CharField(max_length=1000, null=True, blank=True)
 | 
			
		||||
    title = models.CharField(max_length=1000, null=True, blank=True)
 | 
			
		||||
    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("user.UserActionLogEntry", blank=True, help_text="Keeps all user actions of an object", editable=False)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        abstract = True
 | 
			
		||||
 | 
			
		||||
    @abstractmethod
 | 
			
		||||
    def set_status_messages(self, request: HttpRequest):
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    def mark_as_deleted(self, user, send_mail: bool = True):
 | 
			
		||||
        """ Mark an entry as deleted
 | 
			
		||||
 | 
			
		||||
@ -140,6 +130,25 @@ class BaseObject(BaseResource):
 | 
			
		||||
 | 
			
		||||
            self.save()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BaseObject(BaseResource, DeletableObjectMixin):
 | 
			
		||||
    """
 | 
			
		||||
    A basic object model, which specifies BaseResource.
 | 
			
		||||
 | 
			
		||||
    Mainly used for intervention, compensation, ecoaccount
 | 
			
		||||
    """
 | 
			
		||||
    identifier = models.CharField(max_length=1000, null=True, blank=True)
 | 
			
		||||
    title = models.CharField(max_length=1000, null=True, blank=True)
 | 
			
		||||
    comment = models.TextField(null=True, blank=True)
 | 
			
		||||
    log = models.ManyToManyField("user.UserActionLogEntry", blank=True, help_text="Keeps all user actions of an object", editable=False)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        abstract = True
 | 
			
		||||
 | 
			
		||||
    @abstractmethod
 | 
			
		||||
    def set_status_messages(self, request: HttpRequest):
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
 | 
			
		||||
@ -484,8 +493,8 @@ class ShareableObjectMixin(models.Model):
 | 
			
		||||
        Returns:
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        directly_shared = self.users.filter(id=user.id).exists()
 | 
			
		||||
        team_shared = self.teams.filter(
 | 
			
		||||
        directly_shared = self.shared_users.filter(id=user.id).exists()
 | 
			
		||||
        team_shared = self.shared_teams.filter(
 | 
			
		||||
            users__in=[user]
 | 
			
		||||
        ).exists()
 | 
			
		||||
        is_shared = directly_shared or team_shared
 | 
			
		||||
@ -622,7 +631,9 @@ class ShareableObjectMixin(models.Model):
 | 
			
		||||
        Returns:
 | 
			
		||||
            teams (QuerySet)
 | 
			
		||||
        """
 | 
			
		||||
        return self.teams.all()
 | 
			
		||||
        return self.teams.filter(
 | 
			
		||||
            deleted__isnull=True
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @abstractmethod
 | 
			
		||||
    def get_share_url(self):
 | 
			
		||||
 | 
			
		||||
@ -72,6 +72,7 @@ class AutocompleteTestCase(BaseTestCase):
 | 
			
		||||
            "codes-conservation-office-autocomplete",
 | 
			
		||||
            "share-user-autocomplete",
 | 
			
		||||
            "share-team-autocomplete",
 | 
			
		||||
            "team-admin-autocomplete",
 | 
			
		||||
        ]
 | 
			
		||||
        for test in tests:
 | 
			
		||||
            self.client.login(username=self.superuser.username, password=self.superuser_pw)
 | 
			
		||||
 | 
			
		||||
@ -274,7 +274,6 @@ class BaseTestCase(TestCase):
 | 
			
		||||
        team = Team.objects.get_or_create(
 | 
			
		||||
            name="Testteam",
 | 
			
		||||
            description="Testdescription",
 | 
			
		||||
            admin=self.superuser,
 | 
			
		||||
        )[0]
 | 
			
		||||
        team.users.add(self.superuser)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -21,7 +21,7 @@ from konova.autocompletes import EcoAccountAutocomplete, \
 | 
			
		||||
    InterventionAutocomplete, CompensationActionCodeAutocomplete, BiotopeCodeAutocomplete, LawCodeAutocomplete, \
 | 
			
		||||
    RegistrationOfficeCodeAutocomplete, ConservationOfficeCodeAutocomplete, ProcessTypeCodeAutocomplete, \
 | 
			
		||||
    ShareUserAutocomplete, BiotopeExtraCodeAutocomplete, CompensationActionDetailCodeAutocomplete, \
 | 
			
		||||
    ShareTeamAutocomplete, HandlerCodeAutocomplete
 | 
			
		||||
    ShareTeamAutocomplete, HandlerCodeAutocomplete, TeamAdminAutocomplete
 | 
			
		||||
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, get_geom_parcels, get_geom_parcels_content, map_client_proxy_view
 | 
			
		||||
@ -58,6 +58,7 @@ urlpatterns = [
 | 
			
		||||
    path("atcmplt/codes/handler", HandlerCodeAutocomplete.as_view(), name="codes-handler-autocomplete"),
 | 
			
		||||
    path("atcmplt/share/u", ShareUserAutocomplete.as_view(), name="share-user-autocomplete"),
 | 
			
		||||
    path("atcmplt/share/t", ShareTeamAutocomplete.as_view(), name="share-team-autocomplete"),
 | 
			
		||||
    path("atcmplt/team/admin", TeamAdminAutocomplete.as_view(), name="team-admin-autocomplete"),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
if DEBUG:
 | 
			
		||||
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							@ -26,7 +26,7 @@ msgid ""
 | 
			
		||||
msgstr ""
 | 
			
		||||
"Project-Id-Version: PACKAGE VERSION\n"
 | 
			
		||||
"Report-Msgid-Bugs-To: \n"
 | 
			
		||||
"POT-Creation-Date: 2022-05-30 09:16+0200\n"
 | 
			
		||||
"POT-Creation-Date: 2022-05-30 11:51+0200\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"
 | 
			
		||||
@ -65,7 +65,7 @@ msgstr "Verantwortliche Stelle"
 | 
			
		||||
#: intervention/forms/forms.py:64 intervention/forms/forms.py:81
 | 
			
		||||
#: intervention/forms/forms.py:97 intervention/forms/forms.py:113
 | 
			
		||||
#: intervention/forms/forms.py:154 intervention/forms/modalForms.py:49
 | 
			
		||||
#: intervention/forms/modalForms.py:63 user/forms.py:196
 | 
			
		||||
#: intervention/forms/modalForms.py:63 user/forms.py:196 user/forms.py:260
 | 
			
		||||
msgid "Click for selection"
 | 
			
		||||
msgstr "Auswählen..."
 | 
			
		||||
 | 
			
		||||
@ -138,11 +138,11 @@ msgstr "Zuständigkeitsbereich"
 | 
			
		||||
#: analysis/templates/analysis/reports/includes/intervention/amount.html:17
 | 
			
		||||
#: analysis/templates/analysis/reports/includes/intervention/compensated_by.html:8
 | 
			
		||||
#: analysis/templates/analysis/reports/includes/intervention/laws.html:17
 | 
			
		||||
#: compensation/tables.py:41
 | 
			
		||||
#: compensation/tables.py:38
 | 
			
		||||
#: compensation/templates/compensation/detail/compensation/view.html:64
 | 
			
		||||
#: intervention/tables.py:40
 | 
			
		||||
#: intervention/tables.py:38
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:68
 | 
			
		||||
#: user/models/user_action.py:20
 | 
			
		||||
#: user/models/user_action.py:21
 | 
			
		||||
msgid "Checked"
 | 
			
		||||
msgstr "Geprüft"
 | 
			
		||||
 | 
			
		||||
@ -154,14 +154,14 @@ msgstr "Geprüft"
 | 
			
		||||
#: analysis/templates/analysis/reports/includes/intervention/compensated_by.html:9
 | 
			
		||||
#: analysis/templates/analysis/reports/includes/intervention/laws.html:20
 | 
			
		||||
#: analysis/templates/analysis/reports/includes/old_data/amount.html:18
 | 
			
		||||
#: compensation/tables.py:47 compensation/tables.py:230
 | 
			
		||||
#: compensation/templates/compensation/detail/compensation/view.html:78
 | 
			
		||||
#: compensation/tables.py:44 compensation/tables.py:219
 | 
			
		||||
#: compensation/templates/compensation/detail/compensation/view.html:83
 | 
			
		||||
#: compensation/templates/compensation/detail/eco_account/includes/deductions.html:31
 | 
			
		||||
#: compensation/templates/compensation/detail/eco_account/view.html:45
 | 
			
		||||
#: ema/tables.py:44 ema/templates/ema/detail/view.html:35
 | 
			
		||||
#: intervention/tables.py:46
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:82
 | 
			
		||||
#: user/models/user_action.py:21
 | 
			
		||||
#: intervention/tables.py:44
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:87
 | 
			
		||||
#: user/models/user_action.py:22
 | 
			
		||||
msgid "Recorded"
 | 
			
		||||
msgstr "Verzeichnet"
 | 
			
		||||
 | 
			
		||||
@ -198,7 +198,7 @@ msgid "Other registration office"
 | 
			
		||||
msgstr "Andere Zulassungsbehörden"
 | 
			
		||||
 | 
			
		||||
#: analysis/templates/analysis/reports/includes/compensation/card_compensation.html:11
 | 
			
		||||
#: compensation/tables.py:68
 | 
			
		||||
#: compensation/tables.py:65
 | 
			
		||||
#: intervention/templates/intervention/detail/includes/compensations.html:8
 | 
			
		||||
#: intervention/templates/intervention/report/report.html:45
 | 
			
		||||
msgid "Compensations"
 | 
			
		||||
@ -227,7 +227,7 @@ msgid "Surface"
 | 
			
		||||
msgstr "Fläche"
 | 
			
		||||
 | 
			
		||||
#: analysis/templates/analysis/reports/includes/intervention/card_intervention.html:10
 | 
			
		||||
#: intervention/tables.py:67
 | 
			
		||||
#: intervention/tables.py:65
 | 
			
		||||
msgid "Interventions"
 | 
			
		||||
msgstr "Eingriffe"
 | 
			
		||||
 | 
			
		||||
@ -285,8 +285,8 @@ msgid "Type"
 | 
			
		||||
msgstr "Typ"
 | 
			
		||||
 | 
			
		||||
#: analysis/templates/analysis/reports/includes/old_data/amount.html:24
 | 
			
		||||
#: compensation/tables.py:90 intervention/forms/modalForms.py:375
 | 
			
		||||
#: intervention/forms/modalForms.py:382 intervention/tables.py:89
 | 
			
		||||
#: compensation/tables.py:87 intervention/forms/modalForms.py:375
 | 
			
		||||
#: intervention/forms/modalForms.py:382 intervention/tables.py:87
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:19
 | 
			
		||||
#: konova/templates/konova/includes/quickstart/interventions.html:4
 | 
			
		||||
#: templates/navbars/navbar.html:22
 | 
			
		||||
@ -294,7 +294,7 @@ msgid "Intervention"
 | 
			
		||||
msgstr "Eingriff"
 | 
			
		||||
 | 
			
		||||
#: analysis/templates/analysis/reports/includes/old_data/amount.html:34
 | 
			
		||||
#: compensation/tables.py:274
 | 
			
		||||
#: compensation/tables.py:263
 | 
			
		||||
#: compensation/templates/compensation/detail/eco_account/view.html:20
 | 
			
		||||
#: intervention/forms/modalForms.py:348 intervention/forms/modalForms.py:355
 | 
			
		||||
#: konova/templates/konova/includes/quickstart/ecoaccounts.html:4
 | 
			
		||||
@ -314,9 +314,9 @@ msgstr "Vor"
 | 
			
		||||
msgid "Show only unrecorded"
 | 
			
		||||
msgstr "Nur unverzeichnete anzeigen"
 | 
			
		||||
 | 
			
		||||
#: compensation/forms/forms.py:32 compensation/tables.py:26
 | 
			
		||||
#: compensation/tables.py:205 ema/tables.py:29 intervention/forms/forms.py:28
 | 
			
		||||
#: intervention/tables.py:25
 | 
			
		||||
#: compensation/forms/forms.py:32 compensation/tables.py:23
 | 
			
		||||
#: compensation/tables.py:194 ema/tables.py:29 intervention/forms/forms.py:28
 | 
			
		||||
#: intervention/tables.py:23
 | 
			
		||||
#: intervention/templates/intervention/detail/includes/compensations.html:30
 | 
			
		||||
msgid "Identifier"
 | 
			
		||||
msgstr "Kennung"
 | 
			
		||||
@ -326,8 +326,8 @@ msgstr "Kennung"
 | 
			
		||||
msgid "Generated automatically"
 | 
			
		||||
msgstr "Automatisch generiert"
 | 
			
		||||
 | 
			
		||||
#: compensation/forms/forms.py:44 compensation/tables.py:31
 | 
			
		||||
#: compensation/tables.py:210
 | 
			
		||||
#: compensation/forms/forms.py:44 compensation/tables.py:28
 | 
			
		||||
#: compensation/tables.py:199
 | 
			
		||||
#: compensation/templates/compensation/detail/compensation/includes/documents.html:28
 | 
			
		||||
#: compensation/templates/compensation/detail/compensation/view.html:32
 | 
			
		||||
#: compensation/templates/compensation/detail/eco_account/includes/documents.html:28
 | 
			
		||||
@ -337,7 +337,7 @@ msgstr "Automatisch generiert"
 | 
			
		||||
#: ema/tables.py:34 ema/templates/ema/detail/includes/documents.html:28
 | 
			
		||||
#: ema/templates/ema/detail/view.html:31
 | 
			
		||||
#: ema/templates/ema/report/report.html:12 intervention/forms/forms.py:40
 | 
			
		||||
#: intervention/tables.py:30
 | 
			
		||||
#: intervention/tables.py:28
 | 
			
		||||
#: intervention/templates/intervention/detail/includes/compensations.html:33
 | 
			
		||||
#: intervention/templates/intervention/detail/includes/documents.html:28
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:31
 | 
			
		||||
@ -675,62 +675,62 @@ msgstr ""
 | 
			
		||||
"Es wurde bereits mehr Fläche abgebucht, als Sie nun als abbuchbar einstellen "
 | 
			
		||||
"wollen. Kontaktieren Sie die für die Abbuchungen verantwortlichen Nutzer!"
 | 
			
		||||
 | 
			
		||||
#: compensation/tables.py:36 compensation/tables.py:215 ema/tables.py:39
 | 
			
		||||
#: intervention/tables.py:35 konova/filters/mixins.py:98
 | 
			
		||||
#: compensation/tables.py:33 compensation/tables.py:204 ema/tables.py:39
 | 
			
		||||
#: intervention/tables.py:33 konova/filters/mixins.py:98
 | 
			
		||||
msgid "Parcel gmrkng"
 | 
			
		||||
msgstr "Gemarkung"
 | 
			
		||||
 | 
			
		||||
#: compensation/tables.py:53 compensation/tables.py:236 ema/tables.py:50
 | 
			
		||||
#: intervention/tables.py:52
 | 
			
		||||
#: compensation/tables.py:50 compensation/tables.py:225 ema/tables.py:50
 | 
			
		||||
#: intervention/tables.py:50
 | 
			
		||||
msgid "Editable"
 | 
			
		||||
msgstr "Freigegeben"
 | 
			
		||||
 | 
			
		||||
#: compensation/tables.py:59 compensation/tables.py:242 ema/tables.py:56
 | 
			
		||||
#: intervention/tables.py:58
 | 
			
		||||
#: compensation/tables.py:56 compensation/tables.py:231 ema/tables.py:56
 | 
			
		||||
#: intervention/tables.py:56
 | 
			
		||||
msgid "Last edit"
 | 
			
		||||
msgstr "Zuletzt bearbeitet"
 | 
			
		||||
 | 
			
		||||
#: compensation/tables.py:90 compensation/tables.py:274 ema/tables.py:89
 | 
			
		||||
#: intervention/tables.py:89
 | 
			
		||||
#: compensation/tables.py:87 compensation/tables.py:263 ema/tables.py:89
 | 
			
		||||
#: intervention/tables.py:87
 | 
			
		||||
msgid "Open {}"
 | 
			
		||||
msgstr "Öffne {}"
 | 
			
		||||
 | 
			
		||||
#: compensation/tables.py:170
 | 
			
		||||
#: compensation/templates/compensation/detail/compensation/view.html:81
 | 
			
		||||
#: compensation/tables.py:163
 | 
			
		||||
#: compensation/templates/compensation/detail/compensation/view.html:86
 | 
			
		||||
#: compensation/templates/compensation/detail/eco_account/includes/deductions.html:58
 | 
			
		||||
#: compensation/templates/compensation/detail/eco_account/view.html:48
 | 
			
		||||
#: ema/tables.py:131 ema/templates/ema/detail/view.html:38
 | 
			
		||||
#: intervention/tables.py:167
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:85
 | 
			
		||||
#: ema/tables.py:130 ema/templates/ema/detail/view.html:38
 | 
			
		||||
#: intervention/tables.py:161
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:90
 | 
			
		||||
msgid "Not recorded yet"
 | 
			
		||||
msgstr "Noch nicht verzeichnet"
 | 
			
		||||
 | 
			
		||||
#: compensation/tables.py:175 compensation/tables.py:334 ema/tables.py:136
 | 
			
		||||
#: intervention/tables.py:172
 | 
			
		||||
#: compensation/tables.py:166 compensation/tables.py:321 ema/tables.py:133
 | 
			
		||||
#: intervention/tables.py:164
 | 
			
		||||
msgid "Recorded on {} by {}"
 | 
			
		||||
msgstr "Am {} von {} verzeichnet worden"
 | 
			
		||||
 | 
			
		||||
#: compensation/tables.py:197 compensation/tables.py:356 ema/tables.py:157
 | 
			
		||||
#: intervention/tables.py:193
 | 
			
		||||
#: compensation/tables.py:186 compensation/tables.py:343 ema/tables.py:154
 | 
			
		||||
#: intervention/tables.py:185
 | 
			
		||||
msgid "Full access granted"
 | 
			
		||||
msgstr "Für Sie freigegeben - Datensatz kann bearbeitet werden"
 | 
			
		||||
 | 
			
		||||
#: compensation/tables.py:197 compensation/tables.py:356 ema/tables.py:157
 | 
			
		||||
#: intervention/tables.py:193
 | 
			
		||||
#: compensation/tables.py:186 compensation/tables.py:343 ema/tables.py:154
 | 
			
		||||
#: intervention/tables.py:185
 | 
			
		||||
msgid "Access not granted"
 | 
			
		||||
msgstr "Nicht freigegeben - Datensatz nur lesbar"
 | 
			
		||||
 | 
			
		||||
#: compensation/tables.py:220
 | 
			
		||||
#: compensation/tables.py:209
 | 
			
		||||
#: compensation/templates/compensation/detail/eco_account/view.html:36
 | 
			
		||||
#: konova/templates/konova/widgets/progressbar.html:3
 | 
			
		||||
msgid "Available"
 | 
			
		||||
msgstr "Verfügbar"
 | 
			
		||||
 | 
			
		||||
#: compensation/tables.py:251
 | 
			
		||||
#: compensation/tables.py:240
 | 
			
		||||
msgid "Eco Accounts"
 | 
			
		||||
msgstr "Ökokonten"
 | 
			
		||||
 | 
			
		||||
#: compensation/tables.py:329
 | 
			
		||||
#: compensation/tables.py:318
 | 
			
		||||
msgid "Not recorded yet. Can not be used for deductions, yet."
 | 
			
		||||
msgstr ""
 | 
			
		||||
"Noch nicht verzeichnet. Kann noch nicht für Abbuchungen genutzt werden."
 | 
			
		||||
@ -782,7 +782,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 user/templates/user/team/index.html:32
 | 
			
		||||
#: templates/log.html:10 user/templates/user/team/index.html:33
 | 
			
		||||
msgid "Action"
 | 
			
		||||
msgstr "Aktionen"
 | 
			
		||||
 | 
			
		||||
@ -975,43 +975,43 @@ msgstr "Nein"
 | 
			
		||||
msgid "Is Coherence keeping compensation"
 | 
			
		||||
msgstr "Ist Kohärenzsicherungsmaßnahme"
 | 
			
		||||
 | 
			
		||||
#: compensation/templates/compensation/detail/compensation/view.html:71
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:75
 | 
			
		||||
#: compensation/templates/compensation/detail/compensation/view.html:76
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:80
 | 
			
		||||
msgid "Checked on "
 | 
			
		||||
msgstr "Geprüft am "
 | 
			
		||||
 | 
			
		||||
#: compensation/templates/compensation/detail/compensation/view.html:71
 | 
			
		||||
#: compensation/templates/compensation/detail/compensation/view.html:85
 | 
			
		||||
#: compensation/templates/compensation/detail/compensation/view.html:76
 | 
			
		||||
#: compensation/templates/compensation/detail/compensation/view.html:90
 | 
			
		||||
#: compensation/templates/compensation/detail/eco_account/includes/deductions.html:56
 | 
			
		||||
#: compensation/templates/compensation/detail/eco_account/view.html:52
 | 
			
		||||
#: ema/templates/ema/detail/view.html:42
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:75
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:89
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:80
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:94
 | 
			
		||||
msgid "by"
 | 
			
		||||
msgstr "von"
 | 
			
		||||
 | 
			
		||||
#: compensation/templates/compensation/detail/compensation/view.html:85
 | 
			
		||||
#: compensation/templates/compensation/detail/compensation/view.html:90
 | 
			
		||||
#: compensation/templates/compensation/detail/eco_account/view.html:52
 | 
			
		||||
#: ema/templates/ema/detail/view.html:42
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:89
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:94
 | 
			
		||||
msgid "Recorded on "
 | 
			
		||||
msgstr "Verzeichnet am"
 | 
			
		||||
 | 
			
		||||
#: compensation/templates/compensation/detail/compensation/view.html:92
 | 
			
		||||
#: compensation/templates/compensation/detail/compensation/view.html:97
 | 
			
		||||
#: compensation/templates/compensation/detail/eco_account/view.html:75
 | 
			
		||||
#: compensation/templates/compensation/report/compensation/report.html:24
 | 
			
		||||
#: compensation/templates/compensation/report/eco_account/report.html:37
 | 
			
		||||
#: ema/templates/ema/detail/view.html:61
 | 
			
		||||
#: ema/templates/ema/report/report.html:24
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:108
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:113
 | 
			
		||||
#: intervention/templates/intervention/report/report.html:87
 | 
			
		||||
msgid "Last modified"
 | 
			
		||||
msgstr "Zuletzt bearbeitet"
 | 
			
		||||
 | 
			
		||||
#: compensation/templates/compensation/detail/compensation/view.html:106
 | 
			
		||||
#: compensation/templates/compensation/detail/compensation/view.html:111
 | 
			
		||||
#: compensation/templates/compensation/detail/eco_account/view.html:89
 | 
			
		||||
#: ema/templates/ema/detail/view.html:75
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:122
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:127
 | 
			
		||||
msgid "Shared with"
 | 
			
		||||
msgstr "Freigegeben für"
 | 
			
		||||
 | 
			
		||||
@ -1050,7 +1050,7 @@ msgstr "Eingriffskennung"
 | 
			
		||||
 | 
			
		||||
#: compensation/templates/compensation/detail/eco_account/includes/deductions.html:37
 | 
			
		||||
#: intervention/templates/intervention/detail/includes/deductions.html:34
 | 
			
		||||
#: user/models/user_action.py:23
 | 
			
		||||
#: user/models/user_action.py:24
 | 
			
		||||
msgid "Created"
 | 
			
		||||
msgstr "Erstellt"
 | 
			
		||||
 | 
			
		||||
@ -1087,8 +1087,8 @@ msgstr "Keine Flächenmenge für Abbuchungen eingegeben. Bitte bearbeiten."
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:55
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:59
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:63
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:95
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:99
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:100
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:104
 | 
			
		||||
msgid "Missing"
 | 
			
		||||
msgstr "fehlt"
 | 
			
		||||
 | 
			
		||||
@ -1142,17 +1142,17 @@ msgid "Compensation {} edited"
 | 
			
		||||
msgstr "Kompensation {} bearbeitet"
 | 
			
		||||
 | 
			
		||||
#: compensation/views/compensation.py:182 compensation/views/eco_account.py:173
 | 
			
		||||
#: ema/views.py:240 intervention/views.py:332
 | 
			
		||||
#: ema/views.py:240 intervention/views.py:338
 | 
			
		||||
msgid "Edit {}"
 | 
			
		||||
msgstr "Bearbeite {}"
 | 
			
		||||
 | 
			
		||||
#: compensation/views/compensation.py:261 compensation/views/eco_account.py:359
 | 
			
		||||
#: ema/views.py:194 intervention/views.py:536
 | 
			
		||||
#: compensation/views/compensation.py:268 compensation/views/eco_account.py:359
 | 
			
		||||
#: ema/views.py:194 intervention/views.py:542
 | 
			
		||||
msgid "Log"
 | 
			
		||||
msgstr "Log"
 | 
			
		||||
 | 
			
		||||
#: compensation/views/compensation.py:605 compensation/views/eco_account.py:727
 | 
			
		||||
#: ema/views.py:558 intervention/views.py:682
 | 
			
		||||
#: compensation/views/compensation.py:612 compensation/views/eco_account.py:727
 | 
			
		||||
#: ema/views.py:558 intervention/views.py:688
 | 
			
		||||
msgid "Report {}"
 | 
			
		||||
msgstr "Bericht {}"
 | 
			
		||||
 | 
			
		||||
@ -1173,32 +1173,32 @@ msgid "Eco-account removed"
 | 
			
		||||
msgstr "Ökokonto entfernt"
 | 
			
		||||
 | 
			
		||||
#: compensation/views/eco_account.py:380 ema/views.py:282
 | 
			
		||||
#: intervention/views.py:635
 | 
			
		||||
#: intervention/views.py:641
 | 
			
		||||
msgid "{} unrecorded"
 | 
			
		||||
msgstr "{} entzeichnet"
 | 
			
		||||
 | 
			
		||||
#: compensation/views/eco_account.py:380 ema/views.py:282
 | 
			
		||||
#: intervention/views.py:635
 | 
			
		||||
#: intervention/views.py:641
 | 
			
		||||
msgid "{} recorded"
 | 
			
		||||
msgstr "{} verzeichnet"
 | 
			
		||||
 | 
			
		||||
#: compensation/views/eco_account.py:804 ema/views.py:628
 | 
			
		||||
#: intervention/views.py:433
 | 
			
		||||
#: intervention/views.py:439
 | 
			
		||||
msgid "{} has already been shared with you"
 | 
			
		||||
msgstr "{} wurde bereits für Sie freigegeben"
 | 
			
		||||
 | 
			
		||||
#: compensation/views/eco_account.py:809 ema/views.py:633
 | 
			
		||||
#: intervention/views.py:438
 | 
			
		||||
#: intervention/views.py:444
 | 
			
		||||
msgid "{} has been shared with you"
 | 
			
		||||
msgstr "{} ist nun für Sie freigegeben"
 | 
			
		||||
 | 
			
		||||
#: compensation/views/eco_account.py:816 ema/views.py:640
 | 
			
		||||
#: intervention/views.py:445
 | 
			
		||||
#: intervention/views.py:451
 | 
			
		||||
msgid "Share link invalid"
 | 
			
		||||
msgstr "Freigabelink ungültig"
 | 
			
		||||
 | 
			
		||||
#: compensation/views/eco_account.py:839 ema/views.py:663
 | 
			
		||||
#: intervention/views.py:468
 | 
			
		||||
#: intervention/views.py:474
 | 
			
		||||
msgid "Share settings updated"
 | 
			
		||||
msgstr "Freigabe Einstellungen aktualisiert"
 | 
			
		||||
 | 
			
		||||
@ -1292,14 +1292,14 @@ msgid "Intervention handler detail"
 | 
			
		||||
msgstr "Detailangabe zum Eingriffsverursacher"
 | 
			
		||||
 | 
			
		||||
#: intervention/forms/forms.py:173
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:96
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:101
 | 
			
		||||
#: intervention/templates/intervention/report/report.html:79
 | 
			
		||||
#: intervention/utils/quality.py:73
 | 
			
		||||
msgid "Registration date"
 | 
			
		||||
msgstr "Datum Zulassung bzw. Satzungsbeschluss"
 | 
			
		||||
 | 
			
		||||
#: intervention/forms/forms.py:185
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:100
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:105
 | 
			
		||||
#: intervention/templates/intervention/report/report.html:83
 | 
			
		||||
msgid "Binding on"
 | 
			
		||||
msgstr "Datum Bestandskraft bzw. Rechtskraft"
 | 
			
		||||
@ -1471,7 +1471,7 @@ msgid "Remove payment"
 | 
			
		||||
msgstr "Zahlung entfernen"
 | 
			
		||||
 | 
			
		||||
#: intervention/templates/intervention/detail/includes/revocation.html:8
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:104
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:109
 | 
			
		||||
msgid "Revocations"
 | 
			
		||||
msgstr "Widersprüche"
 | 
			
		||||
 | 
			
		||||
@ -1493,7 +1493,7 @@ msgstr "Widerspruch entfernen"
 | 
			
		||||
msgid "Intervention handler"
 | 
			
		||||
msgstr "Eingriffsverursacher"
 | 
			
		||||
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:103
 | 
			
		||||
#: intervention/templates/intervention/detail/view.html:108
 | 
			
		||||
msgid "Exists"
 | 
			
		||||
msgstr "vorhanden"
 | 
			
		||||
 | 
			
		||||
@ -1532,19 +1532,19 @@ msgstr "Eingriffe - Übersicht"
 | 
			
		||||
msgid "Intervention {} added"
 | 
			
		||||
msgstr "Eingriff {} hinzugefügt"
 | 
			
		||||
 | 
			
		||||
#: intervention/views.py:320
 | 
			
		||||
#: intervention/views.py:326
 | 
			
		||||
msgid "Intervention {} edited"
 | 
			
		||||
msgstr "Eingriff {} bearbeitet"
 | 
			
		||||
 | 
			
		||||
#: intervention/views.py:356
 | 
			
		||||
#: intervention/views.py:362
 | 
			
		||||
msgid "{} removed"
 | 
			
		||||
msgstr "{} entfernt"
 | 
			
		||||
 | 
			
		||||
#: intervention/views.py:489
 | 
			
		||||
#: intervention/views.py:495
 | 
			
		||||
msgid "Check performed"
 | 
			
		||||
msgstr "Prüfung durchgeführt"
 | 
			
		||||
 | 
			
		||||
#: intervention/views.py:640
 | 
			
		||||
#: intervention/views.py:646
 | 
			
		||||
msgid "There are errors on this intervention:"
 | 
			
		||||
msgstr "Es liegen Fehler in diesem Eingriff vor:"
 | 
			
		||||
 | 
			
		||||
@ -2058,7 +2058,8 @@ msgstr "Am {} von {} geprüft worden"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:87
 | 
			
		||||
msgid "Data has changed since last check on {} by {}"
 | 
			
		||||
msgstr "Daten wurden nach der letzten Prüfung geändert. Letzte Prüfung am {} durch {}"
 | 
			
		||||
msgstr ""
 | 
			
		||||
"Daten wurden nach der letzten Prüfung geändert. Letzte Prüfung am {} durch {}"
 | 
			
		||||
 | 
			
		||||
#: konova/utils/message_templates.py:88
 | 
			
		||||
msgid "Current data not checked yet"
 | 
			
		||||
@ -2547,11 +2548,11 @@ 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:332 user/forms.py:337
 | 
			
		||||
#: user/forms.py:168 user/forms.py:172 user/forms.py:351 user/forms.py:356
 | 
			
		||||
msgid "Team name"
 | 
			
		||||
msgstr "Team Name"
 | 
			
		||||
 | 
			
		||||
#: user/forms.py:179 user/forms.py:345 user/templates/user/team/index.html:30
 | 
			
		||||
#: user/forms.py:179 user/forms.py:364 user/templates/user/team/index.html:30
 | 
			
		||||
msgid "Description"
 | 
			
		||||
msgstr "Beschreibung"
 | 
			
		||||
 | 
			
		||||
@ -2579,43 +2580,61 @@ 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
 | 
			
		||||
#: user/forms.py:218 user/forms.py:296
 | 
			
		||||
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:248
 | 
			
		||||
msgid "Admins"
 | 
			
		||||
msgstr "Administratoren"
 | 
			
		||||
 | 
			
		||||
#: 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:273
 | 
			
		||||
msgid "Selected admins need to be members of this team."
 | 
			
		||||
msgstr "Gewählte Administratoren müssen Teammitglieder sein."
 | 
			
		||||
 | 
			
		||||
#: user/forms.py:291 user/templates/user/team/index.html:54
 | 
			
		||||
#: user/forms.py:280
 | 
			
		||||
msgid "There must be at least one admin on this team."
 | 
			
		||||
msgstr "Es muss mindestens einen Administrator für das Team geben."
 | 
			
		||||
 | 
			
		||||
#: user/forms.py:308 user/templates/user/team/index.html:60
 | 
			
		||||
msgid "Edit team"
 | 
			
		||||
msgstr "Team bearbeiten"
 | 
			
		||||
 | 
			
		||||
#: user/forms.py:323 user/templates/user/team/index.html:50
 | 
			
		||||
#: user/forms.py:336
 | 
			
		||||
msgid ""
 | 
			
		||||
"ATTENTION!\n"
 | 
			
		||||
"\n"
 | 
			
		||||
"Removing the team means all members will lose their access to data, based on "
 | 
			
		||||
"this team! \n"
 | 
			
		||||
"\n"
 | 
			
		||||
"Are you sure to remove this team?"
 | 
			
		||||
msgstr ""
 | 
			
		||||
"ACHTUNG!\n\n"
 | 
			
		||||
"Wenn dieses Team gelöscht wird, verlieren alle Teammitglieder den Zugriff auf die Daten, die nur über dieses Team freigegeben sind!\n"
 | 
			
		||||
"\n"
 | 
			
		||||
"Sind Sie sicher, dass Sie dieses Team löschen möchten?"
 | 
			
		||||
 | 
			
		||||
#: user/forms.py:342 user/templates/user/team/index.html:56
 | 
			
		||||
msgid "Leave team"
 | 
			
		||||
msgstr "Team verlassen"
 | 
			
		||||
 | 
			
		||||
#: user/forms.py:356
 | 
			
		||||
#: user/forms.py:375
 | 
			
		||||
msgid "Team"
 | 
			
		||||
msgstr "Team"
 | 
			
		||||
 | 
			
		||||
#: user/models/user_action.py:22
 | 
			
		||||
#: user/models/user_action.py:23
 | 
			
		||||
msgid "Unrecorded"
 | 
			
		||||
msgstr "Entzeichnet"
 | 
			
		||||
 | 
			
		||||
#: user/models/user_action.py:24
 | 
			
		||||
#: user/models/user_action.py:25
 | 
			
		||||
msgid "Edited"
 | 
			
		||||
msgstr "Bearbeitet"
 | 
			
		||||
 | 
			
		||||
#: user/models/user_action.py:25
 | 
			
		||||
#: user/models/user_action.py:26
 | 
			
		||||
msgid "Deleted"
 | 
			
		||||
msgstr "Gelöscht"
 | 
			
		||||
 | 
			
		||||
@ -2632,8 +2651,8 @@ msgid "Name"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: user/templates/user/index.html:21
 | 
			
		||||
msgid "Groups"
 | 
			
		||||
msgstr "Gruppen"
 | 
			
		||||
msgid "Permissions"
 | 
			
		||||
msgstr "Berechtigungen"
 | 
			
		||||
 | 
			
		||||
#: user/templates/user/index.html:34
 | 
			
		||||
msgid ""
 | 
			
		||||
@ -2689,7 +2708,11 @@ msgstr "Neues Team hinzufügen"
 | 
			
		||||
msgid "Members"
 | 
			
		||||
msgstr "Mitglieder"
 | 
			
		||||
 | 
			
		||||
#: user/templates/user/team/index.html:57
 | 
			
		||||
#: user/templates/user/team/index.html:32
 | 
			
		||||
msgid "Administrator"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: user/templates/user/team/index.html:63
 | 
			
		||||
msgid "Remove team"
 | 
			
		||||
msgstr "Team entfernen"
 | 
			
		||||
 | 
			
		||||
@ -2741,19 +2764,19 @@ msgstr "API Nutzer Token"
 | 
			
		||||
msgid "New team added"
 | 
			
		||||
msgstr "Neues Team hinzugefügt"
 | 
			
		||||
 | 
			
		||||
#: user/views.py:191
 | 
			
		||||
#: user/views.py:192
 | 
			
		||||
msgid "Team edited"
 | 
			
		||||
msgstr "Team bearbeitet"
 | 
			
		||||
 | 
			
		||||
#: user/views.py:204
 | 
			
		||||
#: user/views.py:206
 | 
			
		||||
msgid "Team removed"
 | 
			
		||||
msgstr "Team gelöscht"
 | 
			
		||||
 | 
			
		||||
#: user/views.py:218
 | 
			
		||||
#: user/views.py:220
 | 
			
		||||
msgid "You are not a member of this team"
 | 
			
		||||
msgstr "Sie sind kein Mitglied dieses Teams"
 | 
			
		||||
 | 
			
		||||
#: user/views.py:225
 | 
			
		||||
#: user/views.py:227
 | 
			
		||||
msgid "Left Team"
 | 
			
		||||
msgstr "Team verlassen"
 | 
			
		||||
 | 
			
		||||
@ -4256,6 +4279,9 @@ msgstr ""
 | 
			
		||||
msgid "Unable to connect to qpid with SASL mechanism %s"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#~ msgid "Groups"
 | 
			
		||||
#~ msgstr "Gruppen"
 | 
			
		||||
 | 
			
		||||
#~ msgid "Show more..."
 | 
			
		||||
#~ msgstr "Mehr anzeigen..."
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,7 @@
 | 
			
		||||
    </h4>
 | 
			
		||||
    {% if form.form_caption is not None %}
 | 
			
		||||
    <small>
 | 
			
		||||
        {{ form.form_caption }}
 | 
			
		||||
        {{ form.form_caption|linebreaks }}
 | 
			
		||||
    </small>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
    <form method="post" action="{{ form.action_url }}" {% for attr_key, attr_val in form.form_attrs.items %} {{attr_key}}="{{attr_val}}"{% endfor %}>
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,7 @@
 | 
			
		||||
 | 
			
		||||
    <div class="modal-body">
 | 
			
		||||
        <article>
 | 
			
		||||
            {{ form.form_caption }}
 | 
			
		||||
            {{ form.form_caption|linebreaks }}
 | 
			
		||||
        </article>
 | 
			
		||||
        {% include 'form/table/generic_table_form_body.html' %}
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
from django.contrib import admin
 | 
			
		||||
 | 
			
		||||
from user.models import UserNotification, UserActionLogEntry, User, Team
 | 
			
		||||
from konova.admin import DeletableObjectMixinAdmin
 | 
			
		||||
from user.models import User, Team
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserNotificationAdmin(admin.ModelAdmin):
 | 
			
		||||
@ -64,26 +65,28 @@ class UserActionLogEntryAdmin(admin.ModelAdmin):
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TeamAdmin(admin.ModelAdmin):
 | 
			
		||||
class TeamAdmin(DeletableObjectMixinAdmin, admin.ModelAdmin):
 | 
			
		||||
    list_display = [
 | 
			
		||||
        "name",
 | 
			
		||||
        "description",
 | 
			
		||||
        "admin",
 | 
			
		||||
        "deleted",
 | 
			
		||||
    ]
 | 
			
		||||
    search_fields = [
 | 
			
		||||
        "name",
 | 
			
		||||
        "description",
 | 
			
		||||
    ]
 | 
			
		||||
    filter_horizontal = [
 | 
			
		||||
        "users"
 | 
			
		||||
        "users",
 | 
			
		||||
        "admins",
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    def formfield_for_foreignkey(self, db_field, request, **kwargs):
 | 
			
		||||
        if db_field.name == "admin":
 | 
			
		||||
            team_id = request.resolver_match.kwargs.get("object_id", None)
 | 
			
		||||
            kwargs["queryset"] = User.objects.filter(teams__id__in=[team_id])
 | 
			
		||||
        return super().formfield_for_foreignkey(db_field, request, **kwargs)
 | 
			
		||||
    readonly_fields = [
 | 
			
		||||
        "deleted"
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    actions = [
 | 
			
		||||
        "restore_deleted_data"
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
admin.site.register(User, UserAdmin)
 | 
			
		||||
admin.site.register(Team, TeamAdmin)
 | 
			
		||||
 | 
			
		||||
@ -230,8 +230,8 @@ class NewTeamModalForm(BaseModalForm):
 | 
			
		||||
            team = Team.objects.create(
 | 
			
		||||
                name=self.cleaned_data.get("name", None),
 | 
			
		||||
                description=self.cleaned_data.get("description", None),
 | 
			
		||||
                admin=self.user,
 | 
			
		||||
            )
 | 
			
		||||
            team.admins.add(self.user)
 | 
			
		||||
            members = self.cleaned_data.get("members", User.objects.none())
 | 
			
		||||
            if self.user.id not in members:
 | 
			
		||||
                members = members.union(
 | 
			
		||||
@ -244,23 +244,40 @@ class NewTeamModalForm(BaseModalForm):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EditTeamModalForm(NewTeamModalForm):
 | 
			
		||||
    admin = forms.ModelChoiceField(
 | 
			
		||||
    admins = forms.ModelMultipleChoiceField(
 | 
			
		||||
        label=_("Admins"),
 | 
			
		||||
        label_suffix="",
 | 
			
		||||
        label=_("Admin"),
 | 
			
		||||
        help_text=_("Administrators manage team details and members"),
 | 
			
		||||
        queryset=User.objects.none(),
 | 
			
		||||
        empty_label=None,
 | 
			
		||||
        required=True,
 | 
			
		||||
        queryset=User.objects.all(),
 | 
			
		||||
        widget=autocomplete.ModelSelect2Multiple(
 | 
			
		||||
            url="team-admin-autocomplete",
 | 
			
		||||
            forward=[
 | 
			
		||||
                "members",
 | 
			
		||||
                "admins",
 | 
			
		||||
            ],
 | 
			
		||||
            attrs={
 | 
			
		||||
                "data-placeholder": _("Click for selection"),
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def __is_admin_valid(self):
 | 
			
		||||
        admin = self.cleaned_data.get("admin", None)
 | 
			
		||||
        members = self.cleaned_data.get("members", None)
 | 
			
		||||
        _is_valid = admin in members
 | 
			
		||||
    def __is_admins_valid(self):
 | 
			
		||||
        admins = set(self.cleaned_data.get("admins", {}))
 | 
			
		||||
        members = set(self.cleaned_data.get("members", {}))
 | 
			
		||||
        _is_valid = admins.issubset(members)
 | 
			
		||||
 | 
			
		||||
        if not _is_valid:
 | 
			
		||||
            self.add_error(
 | 
			
		||||
                "members",
 | 
			
		||||
                _("Selected admin ({}) needs to be a member of this team.").format(admin.username)
 | 
			
		||||
                "admins",
 | 
			
		||||
                _("Selected admins need to be members of this team.")
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        _is_admin_length_valid = len(admins) > 0
 | 
			
		||||
        if not _is_admin_length_valid:
 | 
			
		||||
            self.add_error(
 | 
			
		||||
                "admins",
 | 
			
		||||
                _("There must be at least one admin on this team.")
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        return _is_valid
 | 
			
		||||
@ -283,7 +300,7 @@ class EditTeamModalForm(NewTeamModalForm):
 | 
			
		||||
 | 
			
		||||
    def is_valid(self):
 | 
			
		||||
        super_valid = super().is_valid()
 | 
			
		||||
        admin_valid = self.__is_admin_valid()
 | 
			
		||||
        admin_valid = self.__is_admins_valid()
 | 
			
		||||
        return super_valid and admin_valid
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
@ -293,13 +310,12 @@ class EditTeamModalForm(NewTeamModalForm):
 | 
			
		||||
        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,
 | 
			
		||||
            "admins": self.instance.admins.all(),
 | 
			
		||||
        }
 | 
			
		||||
        self.load_initial_data(form_data)
 | 
			
		||||
 | 
			
		||||
@ -307,14 +323,20 @@ class EditTeamModalForm(NewTeamModalForm):
 | 
			
		||||
        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", []))
 | 
			
		||||
            self.instance.admins.set(self.cleaned_data.get("admins", []))
 | 
			
		||||
        return self.instance
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RemoveTeamModalForm(RemoveModalForm):
 | 
			
		||||
    pass
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
        self.form_caption = _("ATTENTION!\n\nRemoving the team means all members will lose their access to data, based on this team! \n\nAre you sure to remove this team?")
 | 
			
		||||
 | 
			
		||||
    def save(self):
 | 
			
		||||
        self.instance.mark_as_deleted(self.user)
 | 
			
		||||
        return self.instance
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LeaveTeamModalForm(RemoveModalForm):
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										35
									
								
								user/migrations/0004_auto_20220530_1105.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								user/migrations/0004_auto_20220530_1105.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,35 @@
 | 
			
		||||
# Generated by Django 3.1.3 on 2022-05-30 09:05
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def migrate_fkey_admin_to_m2m(apps, schema_editor):
 | 
			
		||||
    Team = apps.get_model('user', 'Team')
 | 
			
		||||
    all_teams = Team.objects.all()
 | 
			
		||||
    for team in all_teams:
 | 
			
		||||
        admin = team.admin
 | 
			
		||||
        if admin is None:
 | 
			
		||||
            continue
 | 
			
		||||
        team.admins.add(admin)
 | 
			
		||||
        team.save()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('user', '0003_team'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='team',
 | 
			
		||||
            name='admins',
 | 
			
		||||
            field=models.ManyToManyField(blank=True, related_name='_team_admins_+', to=settings.AUTH_USER_MODEL),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RunPython(migrate_fkey_admin_to_m2m),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='team',
 | 
			
		||||
            name='admin',
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										19
									
								
								user/migrations/0005_team_deleted.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								user/migrations/0005_team_deleted.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
			
		||||
# Generated by Django 3.1.3 on 2022-05-30 12:42
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('user', '0004_auto_20220530_1105'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='team',
 | 
			
		||||
            name='deleted',
 | 
			
		||||
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='user.useractionlogentry'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -1,21 +1,35 @@
 | 
			
		||||
from django.db import models
 | 
			
		||||
 | 
			
		||||
from konova.models import UuidModel
 | 
			
		||||
from konova.models import UuidModel, DeletableObjectMixin
 | 
			
		||||
from konova.utils.mailer import Mailer
 | 
			
		||||
from user.models import UserActionLogEntry
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Team(UuidModel):
 | 
			
		||||
class Team(UuidModel, DeletableObjectMixin):
 | 
			
		||||
    """ 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)
 | 
			
		||||
    admins = models.ManyToManyField("user.User", blank=True, related_name="+")
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.name
 | 
			
		||||
 | 
			
		||||
    def mark_as_deleted(self, user):
 | 
			
		||||
        """ Creates an UserAction entry and stores it in the correct field
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            user (User): The performing user
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        delete_action = UserActionLogEntry.get_deleted_action(user, "Team deleted")
 | 
			
		||||
        self.deleted = delete_action
 | 
			
		||||
        self.save()
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
 | 
			
		||||
@ -104,6 +118,19 @@ class Team(UuidModel):
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        self.users.remove(user)
 | 
			
		||||
        if self.admin == user:
 | 
			
		||||
            self.admin = self.users.first()
 | 
			
		||||
            self.save()
 | 
			
		||||
        self.admins.remove(user)
 | 
			
		||||
        self.save()
 | 
			
		||||
 | 
			
		||||
    def is_user_admin(self, user) -> bool:
 | 
			
		||||
        """ Returns whether a given user is an admin of the team
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            user (User): The user
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
            user_is_admin (bool): Whether the user is an admin or not
 | 
			
		||||
        """
 | 
			
		||||
        user_is_admin = self.admins.filter(
 | 
			
		||||
            id=user.id
 | 
			
		||||
        ).exists()
 | 
			
		||||
        return user_is_admin
 | 
			
		||||
 | 
			
		||||
@ -160,3 +160,15 @@ class User(AbstractUser):
 | 
			
		||||
        else:
 | 
			
		||||
            token = self.api_token
 | 
			
		||||
        return token
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def shared_teams(self):
 | 
			
		||||
        """ Wrapper for fetching active teams of this user
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        shared_teams = self.teams.filter(
 | 
			
		||||
            deleted__isnull=True
 | 
			
		||||
        )
 | 
			
		||||
        return shared_teams
 | 
			
		||||
@ -18,7 +18,7 @@
 | 
			
		||||
                    <td>{{user.email}}</td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <th scope="row">{% trans 'Groups' %}</th>
 | 
			
		||||
                    <th scope="row">{% trans 'Permissions' %}</th>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        {% for group in user.groups.all %}
 | 
			
		||||
                            <span class="badge badge-pill rlp-r">{% trans group.name %}</span>
 | 
			
		||||
 | 
			
		||||
@ -28,6 +28,7 @@
 | 
			
		||||
                    <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 'Administrator' %}</th>
 | 
			
		||||
                    <th scope="col" class="align-middle">{% trans 'Action' %}</th>
 | 
			
		||||
                </tr>
 | 
			
		||||
            </thead>
 | 
			
		||||
@ -45,11 +46,16 @@
 | 
			
		||||
                                <span class="badge badge-pill rlp-r">{{member.username}}</span>
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                        </td>
 | 
			
		||||
                        <td>
 | 
			
		||||
                            {% for admin in team.admins.all %}
 | 
			
		||||
                                <span class="badge badge-pill rlp-r">{{admin.username}}</span>
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                        </td>
 | 
			
		||||
                        <td>
 | 
			
		||||
                            <button class="btn rlp-r btn-modal" data-form-url="{% url 'user:team-leave' team.id %}" title="{% trans 'Leave team' %}">
 | 
			
		||||
                                {% fa5_icon 'sign-out-alt' %}
 | 
			
		||||
                            </button>
 | 
			
		||||
                            {% if team.admin == user %}
 | 
			
		||||
                            {% if user in team.admins.all  %}
 | 
			
		||||
                                <button class="btn rlp-r btn-modal" data-form-url="{% url 'user:team-edit' team.id %}" title="{% trans 'Edit team' %}">
 | 
			
		||||
                                    {% fa5_icon 'edit' %}
 | 
			
		||||
                                </button>
 | 
			
		||||
 | 
			
		||||
@ -1,3 +0,0 @@
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
 | 
			
		||||
# Create your tests here.
 | 
			
		||||
							
								
								
									
										7
									
								
								user/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								user/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
			
		||||
"""
 | 
			
		||||
Author: Michel Peltriaux
 | 
			
		||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
 | 
			
		||||
Contact: michel.peltriaux@sgdnord.rlp.de
 | 
			
		||||
Created on: 30.05.22
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
							
								
								
									
										112
									
								
								user/tests/test_views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								user/tests/test_views.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,112 @@
 | 
			
		||||
"""
 | 
			
		||||
Author: Michel Peltriaux
 | 
			
		||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
 | 
			
		||||
Contact: michel.peltriaux@sgdnord.rlp.de
 | 
			
		||||
Created on: 30.05.22
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from django.test import Client
 | 
			
		||||
 | 
			
		||||
from django.contrib.auth.models import Group
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
 | 
			
		||||
from intervention.models import Revocation
 | 
			
		||||
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
 | 
			
		||||
from konova.tests.test_views import BaseViewTestCase
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserViewTestCase(BaseViewTestCase):
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def setUpTestData(cls) -> None:
 | 
			
		||||
        super().setUpTestData()
 | 
			
		||||
 | 
			
		||||
    def setUp(self) -> None:
 | 
			
		||||
        super().setUp()
 | 
			
		||||
        self.team.users.add(self.superuser)
 | 
			
		||||
        self.team.admins.add(self.superuser)
 | 
			
		||||
        # Prepare urls
 | 
			
		||||
        self.index_url = reverse("user:index", args=())
 | 
			
		||||
        self.notification_url = reverse("user:notifications", args=())
 | 
			
		||||
        self.api_token_url = reverse("user:api-token", args=())
 | 
			
		||||
        self.contact_url = reverse("user:contact", args=(self.superuser.id,))
 | 
			
		||||
        self.team_url = reverse("user:team-index", args=())
 | 
			
		||||
        self.new_team_url = reverse("user:team-new", args=())
 | 
			
		||||
        self.data_team_url = reverse("user:team-data", args=(self.team.id,))
 | 
			
		||||
        self.edit_team_url = reverse("user:team-edit", args=(self.team.id,))
 | 
			
		||||
        self.remove_team_url = reverse("user:team-remove", args=(self.team.id,))
 | 
			
		||||
        self.leave_team_url = reverse("user:team-leave", args=(self.team.id,))
 | 
			
		||||
 | 
			
		||||
    def test_views_anonymous_user(self):
 | 
			
		||||
        """ Check correct status code for all requests
 | 
			
		||||
 | 
			
		||||
        Assumption: User not logged in
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        # Unknown client
 | 
			
		||||
        client = Client()
 | 
			
		||||
 | 
			
		||||
        login_redirect_base = f"{self.login_url}?next="
 | 
			
		||||
        fail_urls = {
 | 
			
		||||
            self.index_url: f"{login_redirect_base}{self.index_url}",
 | 
			
		||||
            self.notification_url: f"{login_redirect_base}{self.notification_url}",
 | 
			
		||||
            self.api_token_url: f"{login_redirect_base}{self.api_token_url}",
 | 
			
		||||
            self.contact_url: f"{login_redirect_base}{self.contact_url}",
 | 
			
		||||
            self.team_url: f"{login_redirect_base}{self.team_url}",
 | 
			
		||||
            self.new_team_url: f"{login_redirect_base}{self.new_team_url}",
 | 
			
		||||
            self.data_team_url: f"{login_redirect_base}{self.data_team_url}",
 | 
			
		||||
            self.edit_team_url: f"{login_redirect_base}{self.edit_team_url}",
 | 
			
		||||
            self.remove_team_url: f"{login_redirect_base}{self.remove_team_url}",
 | 
			
		||||
            self.leave_team_url: f"{login_redirect_base}{self.leave_team_url}",
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for url in fail_urls:
 | 
			
		||||
            response = client.get(url, follow=True)
 | 
			
		||||
            self.assertEqual(response.redirect_chain[0], (f"{self.login_url}?next={url}", 302), msg=f"Failed for {url}. Redirect chain is {response.redirect_chain}")
 | 
			
		||||
 | 
			
		||||
    def test_views_logged_in(self):
 | 
			
		||||
        """ Check correct status code for all requests
 | 
			
		||||
 | 
			
		||||
        Assumption: User logged in but has no groups
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        # Login client
 | 
			
		||||
        client = Client()
 | 
			
		||||
        client.login(username=self.superuser.username, password=self.superuser_pw)
 | 
			
		||||
        self.superuser.groups.set([])
 | 
			
		||||
        success_urls = [
 | 
			
		||||
            self.index_url,
 | 
			
		||||
            self.notification_url,
 | 
			
		||||
            self.contact_url,
 | 
			
		||||
            self.team_url,
 | 
			
		||||
            self.new_team_url,
 | 
			
		||||
            self.data_team_url,
 | 
			
		||||
            self.edit_team_url,
 | 
			
		||||
            self.remove_team_url,
 | 
			
		||||
            self.leave_team_url,
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        fail_urls = [
 | 
			
		||||
            self.api_token_url,  # expects default permission
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        self.assert_url_success(client, success_urls)
 | 
			
		||||
        self.assert_url_fail(client, fail_urls)
 | 
			
		||||
 | 
			
		||||
        # Check for modified default user permission
 | 
			
		||||
        self.superuser.groups.add(
 | 
			
		||||
            Group.objects.get(
 | 
			
		||||
                name=DEFAULT_GROUP
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        success_url = [
 | 
			
		||||
            self.api_token_url,  # must work now
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        self.assert_url_success(client, success_url)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										158
									
								
								user/tests/test_workflow.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								user/tests/test_workflow.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,158 @@
 | 
			
		||||
"""
 | 
			
		||||
Author: Michel Peltriaux
 | 
			
		||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
 | 
			
		||||
Contact: michel.peltriaux@sgdnord.rlp.de
 | 
			
		||||
Created on: 30.05.22
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
from konova.tests.test_views import BaseWorkflowTestCase
 | 
			
		||||
from user.models import Team
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserWorkflowTestCase(BaseWorkflowTestCase):
 | 
			
		||||
    """ This test case adds workflow tests
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def setUpTestData(cls):
 | 
			
		||||
        super().setUpTestData()
 | 
			
		||||
 | 
			
		||||
    def setUp(self) -> None:
 | 
			
		||||
        super().setUp()
 | 
			
		||||
 | 
			
		||||
        # Add user to team
 | 
			
		||||
        self.team.users.add(self.superuser)
 | 
			
		||||
 | 
			
		||||
    def test_new_team(self):
 | 
			
		||||
        """
 | 
			
		||||
        Check a normal creation of a new team.
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        team_name = self.create_dummy_string()
 | 
			
		||||
        team_description = self.create_dummy_string()
 | 
			
		||||
 | 
			
		||||
        new_url = reverse("user:team-new", args=())
 | 
			
		||||
 | 
			
		||||
        post_data = {
 | 
			
		||||
            "name": team_name,
 | 
			
		||||
            "description": team_description,
 | 
			
		||||
            "members": [self.superuser.id],
 | 
			
		||||
        }
 | 
			
		||||
        response = self.client_user.post(
 | 
			
		||||
            new_url,
 | 
			
		||||
            post_data
 | 
			
		||||
        )
 | 
			
		||||
        response_code = response.status_code
 | 
			
		||||
        self.assertEqual(response_code, 302, msg=f"Unexpected status code received from response ({response_code})")
 | 
			
		||||
        new_team = Team.objects.get(
 | 
			
		||||
            name=team_name
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(new_team.description, team_description)
 | 
			
		||||
        self.assertEqual([self.superuser], list(new_team.users.all()))
 | 
			
		||||
        self.assertEqual([self.superuser], list(new_team.admins.all()), msg="Creator is not admin by default but should!")
 | 
			
		||||
 | 
			
		||||
    def test_edit_team(self):
 | 
			
		||||
        """
 | 
			
		||||
        Check editing of an existing team.
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        existing_team = self.team
 | 
			
		||||
        existing_team_name = existing_team.name
 | 
			
		||||
        existing_team_description = existing_team.description
 | 
			
		||||
 | 
			
		||||
        edited_team_name = self.create_dummy_string()
 | 
			
		||||
        edited_team_description = self.create_dummy_string()
 | 
			
		||||
 | 
			
		||||
        new_url = reverse("user:team-edit", args=(existing_team.id,))
 | 
			
		||||
 | 
			
		||||
        post_data = {
 | 
			
		||||
            "name": edited_team_name,
 | 
			
		||||
            "description": edited_team_description,
 | 
			
		||||
        }
 | 
			
		||||
        # Expect the first try to fail since user is member but not admin of the team
 | 
			
		||||
        response = self.client_user.post(
 | 
			
		||||
            new_url,
 | 
			
		||||
            post_data
 | 
			
		||||
        )
 | 
			
		||||
        response_code = response.status_code
 | 
			
		||||
        self.assertEqual(response_code, 404, msg=f"Unexpected status code received from response ({response_code})")
 | 
			
		||||
 | 
			
		||||
        # Now add the user to the list of team admins and try again!
 | 
			
		||||
        existing_team.admins.add(self.superuser)
 | 
			
		||||
        response = self.client_user.post(
 | 
			
		||||
            new_url,
 | 
			
		||||
            post_data
 | 
			
		||||
        )
 | 
			
		||||
        response_code = response.status_code
 | 
			
		||||
        self.assertEqual(response_code, 200, msg=f"Unexpected status code received from response ({response_code})")
 | 
			
		||||
 | 
			
		||||
        existing_team.refresh_from_db()
 | 
			
		||||
        self.assertEqual(existing_team.description, existing_team_description)
 | 
			
		||||
        self.assertEqual(existing_team.name, existing_team_name)
 | 
			
		||||
        self.assertEqual([self.superuser], list(existing_team.users.all()))
 | 
			
		||||
        self.assertEqual([self.superuser], list(existing_team.admins.all()), msg="Creator is not admin by default but should!")
 | 
			
		||||
 | 
			
		||||
    def test_leave_team(self):
 | 
			
		||||
        """
 | 
			
		||||
        Checks leaving of a user from an existing team.
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        existing_team = self.team
 | 
			
		||||
 | 
			
		||||
        new_url = reverse("user:team-leave", args=(existing_team.id,))
 | 
			
		||||
 | 
			
		||||
        post_data = {
 | 
			
		||||
            "confirm": True,
 | 
			
		||||
        }
 | 
			
		||||
        response = self.client_user.post(
 | 
			
		||||
            new_url,
 | 
			
		||||
            post_data
 | 
			
		||||
        )
 | 
			
		||||
        response_code = response.status_code
 | 
			
		||||
        self.assertEqual(response_code, 302, msg=f"Unexpected status code received from response ({response_code})")
 | 
			
		||||
        existing_team.refresh_from_db()
 | 
			
		||||
 | 
			
		||||
        self.assertEqual([], list(existing_team.users.all()))
 | 
			
		||||
        self.assertEqual([], list(existing_team.admins.all()))
 | 
			
		||||
 | 
			
		||||
    def test_remove_team(self):
 | 
			
		||||
        """
 | 
			
		||||
        Checks removing of an existing team.
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        existing_team = self.team
 | 
			
		||||
 | 
			
		||||
        new_url = reverse("user:team-remove", args=(existing_team.id,))
 | 
			
		||||
 | 
			
		||||
        post_data = {
 | 
			
		||||
            "confirm": True,
 | 
			
		||||
        }
 | 
			
		||||
        # User is member but not admin. This response must fail!
 | 
			
		||||
        response = self.client_user.post(
 | 
			
		||||
            new_url,
 | 
			
		||||
            post_data
 | 
			
		||||
        )
 | 
			
		||||
        response_code = response.status_code
 | 
			
		||||
        self.assertEqual(response_code, 404, msg=f"Unexpected status code received from response ({response_code})")
 | 
			
		||||
 | 
			
		||||
        # Add user to admins and try again
 | 
			
		||||
        existing_team.admins.add(self.superuser)
 | 
			
		||||
        response = self.client_user.post(
 | 
			
		||||
            new_url,
 | 
			
		||||
            post_data
 | 
			
		||||
        )
 | 
			
		||||
        response_code = response.status_code
 | 
			
		||||
        self.assertEqual(response_code, 302, msg=f"Unexpected status code received from response ({response_code})")
 | 
			
		||||
        existing_team.refresh_from_db()
 | 
			
		||||
        self.assertIsNotNone(existing_team.deleted, msg="Deleted action not created")
 | 
			
		||||
        self.assertNotIn(existing_team, self.superuser.shared_teams)
 | 
			
		||||
@ -163,7 +163,7 @@ def index_team_view(request: HttpRequest):
 | 
			
		||||
    template = "user/team/index.html"
 | 
			
		||||
    user = request.user
 | 
			
		||||
    context = {
 | 
			
		||||
        "teams": user.teams.all(),
 | 
			
		||||
        "teams": user.shared_teams,
 | 
			
		||||
        "tab_title": _("Teams"),
 | 
			
		||||
    }
 | 
			
		||||
    context = BaseContext(request, context).context
 | 
			
		||||
@ -183,7 +183,8 @@ def new_team_view(request: HttpRequest):
 | 
			
		||||
@login_required
 | 
			
		||||
def edit_team_view(request: HttpRequest, id: str):
 | 
			
		||||
    team = get_object_or_404(Team, id=id)
 | 
			
		||||
    if request.user != team.admin:
 | 
			
		||||
    user_is_admin = team.is_user_admin(request.user)
 | 
			
		||||
    if not user_is_admin:
 | 
			
		||||
        raise Http404()
 | 
			
		||||
    form = EditTeamModalForm(request.POST or None, instance=team, request=request)
 | 
			
		||||
    return form.process_request(
 | 
			
		||||
@ -196,7 +197,8 @@ def edit_team_view(request: HttpRequest, id: str):
 | 
			
		||||
@login_required
 | 
			
		||||
def remove_team_view(request: HttpRequest, id: str):
 | 
			
		||||
    team = get_object_or_404(Team, id=id)
 | 
			
		||||
    if request.user != team.admin:
 | 
			
		||||
    user_is_admin = team.is_user_admin(request.user)
 | 
			
		||||
    if not user_is_admin:
 | 
			
		||||
        raise Http404()
 | 
			
		||||
    form = RemoveTeamModalForm(request.POST or None, instance=team, request=request)
 | 
			
		||||
    return form.process_request(
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user