Merge pull request '169_Unknown_admin_on_teams' (#170) from 169_Unknown_admin_on_teams into master

Reviewed-on: SGD-Nord/konova#170
pull/172/head
mpeltriaux 2 years ago
commit 8b6c8dc1aa

@ -109,7 +109,7 @@
<tr> <tr>
<th scope="row">{% trans 'Shared with' %}</th> <th scope="row">{% trans 'Shared with' %}</th>
<td class="align-middle"> <td class="align-middle">
{% for team in obj.intervention.teams.all %} {% for team in obj.intervention.shared_teams %}
{% include 'user/includes/team_data_modal_button.html' %} {% include 'user/includes/team_data_modal_button.html' %}
{% endfor %} {% endfor %}
<hr> <hr>

@ -87,7 +87,7 @@
<tr> <tr>
<th scope="row">{% trans 'Shared with' %}</th> <th scope="row">{% trans 'Shared with' %}</th>
<td class="align-middle"> <td class="align-middle">
{% for team in obj.teams.all %} {% for team in obj.shared_teams %}
{% include 'user/includes/team_data_modal_button.html' %} {% include 'user/includes/team_data_modal_button.html' %}
{% endfor %} {% endfor %}
<hr> <hr>

@ -73,11 +73,11 @@
<tr> <tr>
<th scope="row">{% trans 'Shared with' %}</th> <th scope="row">{% trans 'Shared with' %}</th>
<td class="align-middle"> <td class="align-middle">
{% for team in obj.teams.all %} {% for team in obj.shared_teams %}
{% include 'user/includes/team_data_modal_button.html' %} {% include 'user/includes/team_data_modal_button.html' %}
{% endfor %} {% endfor %}
<hr> <hr>
{% for user in obj.users.all %} {% for user in obj.user.all %}
{% include 'user/includes/contact_modal_button.html' %} {% include 'user/includes/contact_modal_button.html' %}
{% endfor %} {% endfor %}
</td> </td>

@ -125,7 +125,7 @@
<tr> <tr>
<th scope="row">{% trans 'Shared with' %}</th> <th scope="row">{% trans 'Shared with' %}</th>
<td class="align-middle"> <td class="align-middle">
{% for team in obj.teams.all %} {% for team in obj.shared_teams %}
{% include 'user/includes/team_data_modal_button.html' %} {% include 'user/includes/team_data_modal_button.html' %}
{% endfor %} {% endfor %}
<hr> <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): class BaseResourceAdmin(admin.ModelAdmin):
fields = [ fields = [
"created", "created",
@ -109,7 +121,7 @@ class BaseResourceAdmin(admin.ModelAdmin):
] ]
class BaseObjectAdmin(BaseResourceAdmin): class BaseObjectAdmin(BaseResourceAdmin, DeletableObjectMixinAdmin):
search_fields = [ search_fields = [
"identifier", "identifier",
"title", "title",
@ -126,13 +138,6 @@ class BaseObjectAdmin(BaseResourceAdmin):
"deleted", "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 # Outcommented for a cleaner admin backend on production

@ -96,7 +96,9 @@ class ShareTeamAutocomplete(Select2QuerySetView):
def get_queryset(self): def get_queryset(self):
if self.request.user.is_anonymous: if self.request.user.is_anonymous:
return Team.objects.none() return Team.objects.none()
qs = Team.objects.all() qs = Team.objects.filter(
deleted__isnull=True
)
if self.q: if self.q:
# Due to privacy concerns only a full username match will return the proper user entry # Due to privacy concerns only a full username match will return the proper user entry
qs = qs.filter( qs = qs.filter(
@ -108,6 +110,29 @@ class ShareTeamAutocomplete(Select2QuerySetView):
return qs 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): class KonovaCodeAutocomplete(Select2GroupQuerySetView):
""" """
Provides simple autocomplete functionality for codes Provides simple autocomplete functionality for codes

@ -326,7 +326,7 @@ class SimpleGeomForm(BaseForm):
features = [] features = []
features_json = geom.get("features", []) features_json = geom.get("features", [])
for feature in features_json: 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"]: if g.geom_type not in ["Polygon", "MultiPolygon"]:
self.add_error("geom", _("Only surfaces allowed. Points or lines must be buffered.")) self.add_error("geom", _("Only surfaces allowed. Points or lines must be buffered."))
is_valid = False is_valid = False

@ -87,25 +87,15 @@ class BaseResource(UuidModel):
super().delete() super().delete()
class BaseObject(BaseResource): class DeletableObjectMixin(models.Model):
""" """ Wraps deleted field and related functionality
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)
deleted = models.ForeignKey("user.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("user.UserActionLogEntry", blank=True, help_text="Keeps all user actions of an object", editable=False)
class Meta: class Meta:
abstract = True abstract = True
@abstractmethod
def set_status_messages(self, request: HttpRequest):
raise NotImplementedError
def mark_as_deleted(self, user, send_mail: bool = True): def mark_as_deleted(self, user, send_mail: bool = True):
""" Mark an entry as deleted """ Mark an entry as deleted
@ -140,6 +130,25 @@ class BaseObject(BaseResource):
self.save() 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): def mark_as_edited(self, performing_user, request: HttpRequest = None, edit_comment: str = None):
""" In case the object or a related object changed the log history needs to be updated """ In case the object or a related object changed the log history needs to be updated
@ -484,8 +493,8 @@ class ShareableObjectMixin(models.Model):
Returns: Returns:
""" """
directly_shared = self.users.filter(id=user.id).exists() directly_shared = self.shared_users.filter(id=user.id).exists()
team_shared = self.teams.filter( team_shared = self.shared_teams.filter(
users__in=[user] users__in=[user]
).exists() ).exists()
is_shared = directly_shared or team_shared is_shared = directly_shared or team_shared
@ -622,7 +631,9 @@ class ShareableObjectMixin(models.Model):
Returns: Returns:
teams (QuerySet) teams (QuerySet)
""" """
return self.teams.all() return self.teams.filter(
deleted__isnull=True
)
@abstractmethod @abstractmethod
def get_share_url(self): def get_share_url(self):

@ -72,6 +72,7 @@ class AutocompleteTestCase(BaseTestCase):
"codes-conservation-office-autocomplete", "codes-conservation-office-autocomplete",
"share-user-autocomplete", "share-user-autocomplete",
"share-team-autocomplete", "share-team-autocomplete",
"team-admin-autocomplete",
] ]
for test in tests: for test in tests:
self.client.login(username=self.superuser.username, password=self.superuser_pw) self.client.login(username=self.superuser.username, password=self.superuser_pw)

@ -274,7 +274,6 @@ class BaseTestCase(TestCase):
team = Team.objects.get_or_create( team = Team.objects.get_or_create(
name="Testteam", name="Testteam",
description="Testdescription", description="Testdescription",
admin=self.superuser,
)[0] )[0]
team.users.add(self.superuser) team.users.add(self.superuser)

@ -21,7 +21,7 @@ from konova.autocompletes import EcoAccountAutocomplete, \
InterventionAutocomplete, CompensationActionCodeAutocomplete, BiotopeCodeAutocomplete, LawCodeAutocomplete, \ InterventionAutocomplete, CompensationActionCodeAutocomplete, BiotopeCodeAutocomplete, LawCodeAutocomplete, \
RegistrationOfficeCodeAutocomplete, ConservationOfficeCodeAutocomplete, ProcessTypeCodeAutocomplete, \ RegistrationOfficeCodeAutocomplete, ConservationOfficeCodeAutocomplete, ProcessTypeCodeAutocomplete, \
ShareUserAutocomplete, BiotopeExtraCodeAutocomplete, CompensationActionDetailCodeAutocomplete, \ ShareUserAutocomplete, BiotopeExtraCodeAutocomplete, CompensationActionDetailCodeAutocomplete, \
ShareTeamAutocomplete, HandlerCodeAutocomplete ShareTeamAutocomplete, HandlerCodeAutocomplete, TeamAdminAutocomplete
from konova.settings import SSO_SERVER, SSO_PUBLIC_KEY, SSO_PRIVATE_KEY, DEBUG from konova.settings import SSO_SERVER, SSO_PUBLIC_KEY, SSO_PRIVATE_KEY, DEBUG
from konova.sso.sso import KonovaSSOClient from konova.sso.sso import KonovaSSOClient
from konova.views import logout_view, home_view, get_geom_parcels, get_geom_parcels_content, map_client_proxy_view 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/codes/handler", HandlerCodeAutocomplete.as_view(), name="codes-handler-autocomplete"),
path("atcmplt/share/u", ShareUserAutocomplete.as_view(), name="share-user-autocomplete"), path("atcmplt/share/u", ShareUserAutocomplete.as_view(), name="share-user-autocomplete"),
path("atcmplt/share/t", ShareTeamAutocomplete.as_view(), name="share-team-autocomplete"), path("atcmplt/share/t", ShareTeamAutocomplete.as_view(), name="share-team-autocomplete"),
path("atcmplt/team/admin", TeamAdminAutocomplete.as_view(), name="team-admin-autocomplete"),
] ]
if DEBUG: if DEBUG:

Binary file not shown.

@ -26,7 +26,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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:64 intervention/forms/forms.py:81
#: intervention/forms/forms.py:97 intervention/forms/forms.py:113 #: intervention/forms/forms.py:97 intervention/forms/forms.py:113
#: intervention/forms/forms.py:154 intervention/forms/modalForms.py:49 #: 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" msgid "Click for selection"
msgstr "Auswählen..." 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/amount.html:17
#: analysis/templates/analysis/reports/includes/intervention/compensated_by.html:8 #: analysis/templates/analysis/reports/includes/intervention/compensated_by.html:8
#: analysis/templates/analysis/reports/includes/intervention/laws.html:17 #: 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 #: compensation/templates/compensation/detail/compensation/view.html:64
#: intervention/tables.py:40 #: intervention/tables.py:38
#: intervention/templates/intervention/detail/view.html:68 #: intervention/templates/intervention/detail/view.html:68
#: user/models/user_action.py:20 #: user/models/user_action.py:21
msgid "Checked" msgid "Checked"
msgstr "Geprüft" 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/compensated_by.html:9
#: analysis/templates/analysis/reports/includes/intervention/laws.html:20 #: analysis/templates/analysis/reports/includes/intervention/laws.html:20
#: analysis/templates/analysis/reports/includes/old_data/amount.html:18 #: analysis/templates/analysis/reports/includes/old_data/amount.html:18
#: compensation/tables.py:47 compensation/tables.py:230 #: compensation/tables.py:44 compensation/tables.py:219
#: compensation/templates/compensation/detail/compensation/view.html:78 #: compensation/templates/compensation/detail/compensation/view.html:83
#: compensation/templates/compensation/detail/eco_account/includes/deductions.html:31 #: compensation/templates/compensation/detail/eco_account/includes/deductions.html:31
#: compensation/templates/compensation/detail/eco_account/view.html:45 #: compensation/templates/compensation/detail/eco_account/view.html:45
#: ema/tables.py:44 ema/templates/ema/detail/view.html:35 #: ema/tables.py:44 ema/templates/ema/detail/view.html:35
#: intervention/tables.py:46 #: intervention/tables.py:44
#: intervention/templates/intervention/detail/view.html:82 #: intervention/templates/intervention/detail/view.html:87
#: user/models/user_action.py:21 #: user/models/user_action.py:22
msgid "Recorded" msgid "Recorded"
msgstr "Verzeichnet" msgstr "Verzeichnet"
@ -198,7 +198,7 @@ msgid "Other registration office"
msgstr "Andere Zulassungsbehörden" msgstr "Andere Zulassungsbehörden"
#: analysis/templates/analysis/reports/includes/compensation/card_compensation.html:11 #: 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/detail/includes/compensations.html:8
#: intervention/templates/intervention/report/report.html:45 #: intervention/templates/intervention/report/report.html:45
msgid "Compensations" msgid "Compensations"
@ -227,7 +227,7 @@ msgid "Surface"
msgstr "Fläche" msgstr "Fläche"
#: analysis/templates/analysis/reports/includes/intervention/card_intervention.html:10 #: analysis/templates/analysis/reports/includes/intervention/card_intervention.html:10
#: intervention/tables.py:67 #: intervention/tables.py:65
msgid "Interventions" msgid "Interventions"
msgstr "Eingriffe" msgstr "Eingriffe"
@ -285,8 +285,8 @@ msgid "Type"
msgstr "Typ" msgstr "Typ"
#: analysis/templates/analysis/reports/includes/old_data/amount.html:24 #: analysis/templates/analysis/reports/includes/old_data/amount.html:24
#: compensation/tables.py:90 intervention/forms/modalForms.py:375 #: compensation/tables.py:87 intervention/forms/modalForms.py:375
#: intervention/forms/modalForms.py:382 intervention/tables.py:89 #: intervention/forms/modalForms.py:382 intervention/tables.py:87
#: intervention/templates/intervention/detail/view.html:19 #: intervention/templates/intervention/detail/view.html:19
#: konova/templates/konova/includes/quickstart/interventions.html:4 #: konova/templates/konova/includes/quickstart/interventions.html:4
#: templates/navbars/navbar.html:22 #: templates/navbars/navbar.html:22
@ -294,7 +294,7 @@ msgid "Intervention"
msgstr "Eingriff" msgstr "Eingriff"
#: analysis/templates/analysis/reports/includes/old_data/amount.html:34 #: 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 #: compensation/templates/compensation/detail/eco_account/view.html:20
#: intervention/forms/modalForms.py:348 intervention/forms/modalForms.py:355 #: intervention/forms/modalForms.py:348 intervention/forms/modalForms.py:355
#: konova/templates/konova/includes/quickstart/ecoaccounts.html:4 #: konova/templates/konova/includes/quickstart/ecoaccounts.html:4
@ -314,9 +314,9 @@ msgstr "Vor"
msgid "Show only unrecorded" msgid "Show only unrecorded"
msgstr "Nur unverzeichnete anzeigen" msgstr "Nur unverzeichnete anzeigen"
#: compensation/forms/forms.py:32 compensation/tables.py:26 #: compensation/forms/forms.py:32 compensation/tables.py:23
#: compensation/tables.py:205 ema/tables.py:29 intervention/forms/forms.py:28 #: compensation/tables.py:194 ema/tables.py:29 intervention/forms/forms.py:28
#: intervention/tables.py:25 #: intervention/tables.py:23
#: intervention/templates/intervention/detail/includes/compensations.html:30 #: intervention/templates/intervention/detail/includes/compensations.html:30
msgid "Identifier" msgid "Identifier"
msgstr "Kennung" msgstr "Kennung"
@ -326,8 +326,8 @@ msgstr "Kennung"
msgid "Generated automatically" msgid "Generated automatically"
msgstr "Automatisch generiert" msgstr "Automatisch generiert"
#: compensation/forms/forms.py:44 compensation/tables.py:31 #: compensation/forms/forms.py:44 compensation/tables.py:28
#: compensation/tables.py:210 #: compensation/tables.py:199
#: compensation/templates/compensation/detail/compensation/includes/documents.html:28 #: compensation/templates/compensation/detail/compensation/includes/documents.html:28
#: compensation/templates/compensation/detail/compensation/view.html:32 #: compensation/templates/compensation/detail/compensation/view.html:32
#: compensation/templates/compensation/detail/eco_account/includes/documents.html:28 #: 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/tables.py:34 ema/templates/ema/detail/includes/documents.html:28
#: ema/templates/ema/detail/view.html:31 #: ema/templates/ema/detail/view.html:31
#: ema/templates/ema/report/report.html:12 intervention/forms/forms.py:40 #: 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/compensations.html:33
#: intervention/templates/intervention/detail/includes/documents.html:28 #: intervention/templates/intervention/detail/includes/documents.html:28
#: intervention/templates/intervention/detail/view.html:31 #: 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 " "Es wurde bereits mehr Fläche abgebucht, als Sie nun als abbuchbar einstellen "
"wollen. Kontaktieren Sie die für die Abbuchungen verantwortlichen Nutzer!" "wollen. Kontaktieren Sie die für die Abbuchungen verantwortlichen Nutzer!"
#: compensation/tables.py:36 compensation/tables.py:215 ema/tables.py:39 #: compensation/tables.py:33 compensation/tables.py:204 ema/tables.py:39
#: intervention/tables.py:35 konova/filters/mixins.py:98 #: intervention/tables.py:33 konova/filters/mixins.py:98
msgid "Parcel gmrkng" msgid "Parcel gmrkng"
msgstr "Gemarkung" msgstr "Gemarkung"
#: compensation/tables.py:53 compensation/tables.py:236 ema/tables.py:50 #: compensation/tables.py:50 compensation/tables.py:225 ema/tables.py:50
#: intervention/tables.py:52 #: intervention/tables.py:50
msgid "Editable" msgid "Editable"
msgstr "Freigegeben" msgstr "Freigegeben"
#: compensation/tables.py:59 compensation/tables.py:242 ema/tables.py:56 #: compensation/tables.py:56 compensation/tables.py:231 ema/tables.py:56
#: intervention/tables.py:58 #: intervention/tables.py:56
msgid "Last edit" msgid "Last edit"
msgstr "Zuletzt bearbeitet" msgstr "Zuletzt bearbeitet"
#: compensation/tables.py:90 compensation/tables.py:274 ema/tables.py:89 #: compensation/tables.py:87 compensation/tables.py:263 ema/tables.py:89
#: intervention/tables.py:89 #: intervention/tables.py:87
msgid "Open {}" msgid "Open {}"
msgstr "Öffne {}" msgstr "Öffne {}"
#: compensation/tables.py:170 #: compensation/tables.py:163
#: compensation/templates/compensation/detail/compensation/view.html:81 #: compensation/templates/compensation/detail/compensation/view.html:86
#: compensation/templates/compensation/detail/eco_account/includes/deductions.html:58 #: compensation/templates/compensation/detail/eco_account/includes/deductions.html:58
#: compensation/templates/compensation/detail/eco_account/view.html:48 #: compensation/templates/compensation/detail/eco_account/view.html:48
#: ema/tables.py:131 ema/templates/ema/detail/view.html:38 #: ema/tables.py:130 ema/templates/ema/detail/view.html:38
#: intervention/tables.py:167 #: intervention/tables.py:161
#: intervention/templates/intervention/detail/view.html:85 #: intervention/templates/intervention/detail/view.html:90
msgid "Not recorded yet" msgid "Not recorded yet"
msgstr "Noch nicht verzeichnet" msgstr "Noch nicht verzeichnet"
#: compensation/tables.py:175 compensation/tables.py:334 ema/tables.py:136 #: compensation/tables.py:166 compensation/tables.py:321 ema/tables.py:133
#: intervention/tables.py:172 #: intervention/tables.py:164
msgid "Recorded on {} by {}" msgid "Recorded on {} by {}"
msgstr "Am {} von {} verzeichnet worden" msgstr "Am {} von {} verzeichnet worden"
#: compensation/tables.py:197 compensation/tables.py:356 ema/tables.py:157 #: compensation/tables.py:186 compensation/tables.py:343 ema/tables.py:154
#: intervention/tables.py:193 #: intervention/tables.py:185
msgid "Full access granted" msgid "Full access granted"
msgstr "Für Sie freigegeben - Datensatz kann bearbeitet werden" msgstr "Für Sie freigegeben - Datensatz kann bearbeitet werden"
#: compensation/tables.py:197 compensation/tables.py:356 ema/tables.py:157 #: compensation/tables.py:186 compensation/tables.py:343 ema/tables.py:154
#: intervention/tables.py:193 #: intervention/tables.py:185
msgid "Access not granted" msgid "Access not granted"
msgstr "Nicht freigegeben - Datensatz nur lesbar" msgstr "Nicht freigegeben - Datensatz nur lesbar"
#: compensation/tables.py:220 #: compensation/tables.py:209
#: compensation/templates/compensation/detail/eco_account/view.html:36 #: compensation/templates/compensation/detail/eco_account/view.html:36
#: konova/templates/konova/widgets/progressbar.html:3 #: konova/templates/konova/widgets/progressbar.html:3
msgid "Available" msgid "Available"
msgstr "Verfügbar" msgstr "Verfügbar"
#: compensation/tables.py:251 #: compensation/tables.py:240
msgid "Eco Accounts" msgid "Eco Accounts"
msgstr "Ökokonten" msgstr "Ökokonten"
#: compensation/tables.py:329 #: compensation/tables.py:318
msgid "Not recorded yet. Can not be used for deductions, yet." msgid "Not recorded yet. Can not be used for deductions, yet."
msgstr "" msgstr ""
"Noch nicht verzeichnet. Kann noch nicht für Abbuchungen genutzt werden." "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/documents.html:39
#: intervention/templates/intervention/detail/includes/payments.html:39 #: intervention/templates/intervention/detail/includes/payments.html:39
#: intervention/templates/intervention/detail/includes/revocation.html:43 #: 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" msgid "Action"
msgstr "Aktionen" msgstr "Aktionen"
@ -975,43 +975,43 @@ msgstr "Nein"
msgid "Is Coherence keeping compensation" msgid "Is Coherence keeping compensation"
msgstr "Ist Kohärenzsicherungsmaßnahme" msgstr "Ist Kohärenzsicherungsmaßnahme"
#: compensation/templates/compensation/detail/compensation/view.html:71 #: compensation/templates/compensation/detail/compensation/view.html:76
#: intervention/templates/intervention/detail/view.html:75 #: intervention/templates/intervention/detail/view.html:80
msgid "Checked on " msgid "Checked on "
msgstr "Geprüft am " msgstr "Geprüft am "
#: compensation/templates/compensation/detail/compensation/view.html:71 #: compensation/templates/compensation/detail/compensation/view.html:76
#: compensation/templates/compensation/detail/compensation/view.html:85 #: compensation/templates/compensation/detail/compensation/view.html:90
#: compensation/templates/compensation/detail/eco_account/includes/deductions.html:56 #: compensation/templates/compensation/detail/eco_account/includes/deductions.html:56
#: compensation/templates/compensation/detail/eco_account/view.html:52 #: compensation/templates/compensation/detail/eco_account/view.html:52
#: ema/templates/ema/detail/view.html:42 #: ema/templates/ema/detail/view.html:42
#: intervention/templates/intervention/detail/view.html:75 #: intervention/templates/intervention/detail/view.html:80
#: intervention/templates/intervention/detail/view.html:89 #: intervention/templates/intervention/detail/view.html:94
msgid "by" msgid "by"
msgstr "von" 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 #: compensation/templates/compensation/detail/eco_account/view.html:52
#: ema/templates/ema/detail/view.html:42 #: ema/templates/ema/detail/view.html:42
#: intervention/templates/intervention/detail/view.html:89 #: intervention/templates/intervention/detail/view.html:94
msgid "Recorded on " msgid "Recorded on "
msgstr "Verzeichnet am" 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/detail/eco_account/view.html:75
#: compensation/templates/compensation/report/compensation/report.html:24 #: compensation/templates/compensation/report/compensation/report.html:24
#: compensation/templates/compensation/report/eco_account/report.html:37 #: compensation/templates/compensation/report/eco_account/report.html:37
#: ema/templates/ema/detail/view.html:61 #: ema/templates/ema/detail/view.html:61
#: ema/templates/ema/report/report.html:24 #: 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 #: intervention/templates/intervention/report/report.html:87
msgid "Last modified" msgid "Last modified"
msgstr "Zuletzt bearbeitet" 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 #: compensation/templates/compensation/detail/eco_account/view.html:89
#: ema/templates/ema/detail/view.html:75 #: ema/templates/ema/detail/view.html:75
#: intervention/templates/intervention/detail/view.html:122 #: intervention/templates/intervention/detail/view.html:127
msgid "Shared with" msgid "Shared with"
msgstr "Freigegeben für" msgstr "Freigegeben für"
@ -1050,7 +1050,7 @@ msgstr "Eingriffskennung"
#: compensation/templates/compensation/detail/eco_account/includes/deductions.html:37 #: compensation/templates/compensation/detail/eco_account/includes/deductions.html:37
#: intervention/templates/intervention/detail/includes/deductions.html:34 #: intervention/templates/intervention/detail/includes/deductions.html:34
#: user/models/user_action.py:23 #: user/models/user_action.py:24
msgid "Created" msgid "Created"
msgstr "Erstellt" 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:55
#: intervention/templates/intervention/detail/view.html:59 #: intervention/templates/intervention/detail/view.html:59
#: intervention/templates/intervention/detail/view.html:63 #: intervention/templates/intervention/detail/view.html:63
#: intervention/templates/intervention/detail/view.html:95 #: intervention/templates/intervention/detail/view.html:100
#: intervention/templates/intervention/detail/view.html:99 #: intervention/templates/intervention/detail/view.html:104
msgid "Missing" msgid "Missing"
msgstr "fehlt" msgstr "fehlt"
@ -1142,17 +1142,17 @@ msgid "Compensation {} edited"
msgstr "Kompensation {} bearbeitet" msgstr "Kompensation {} bearbeitet"
#: compensation/views/compensation.py:182 compensation/views/eco_account.py:173 #: 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 {}" msgid "Edit {}"
msgstr "Bearbeite {}" msgstr "Bearbeite {}"
#: compensation/views/compensation.py:261 compensation/views/eco_account.py:359 #: compensation/views/compensation.py:268 compensation/views/eco_account.py:359
#: ema/views.py:194 intervention/views.py:536 #: ema/views.py:194 intervention/views.py:542
msgid "Log" msgid "Log"
msgstr "Log" msgstr "Log"
#: compensation/views/compensation.py:605 compensation/views/eco_account.py:727 #: compensation/views/compensation.py:612 compensation/views/eco_account.py:727
#: ema/views.py:558 intervention/views.py:682 #: ema/views.py:558 intervention/views.py:688
msgid "Report {}" msgid "Report {}"
msgstr "Bericht {}" msgstr "Bericht {}"
@ -1173,32 +1173,32 @@ msgid "Eco-account removed"
msgstr "Ökokonto entfernt" msgstr "Ökokonto entfernt"
#: compensation/views/eco_account.py:380 ema/views.py:282 #: compensation/views/eco_account.py:380 ema/views.py:282
#: intervention/views.py:635 #: intervention/views.py:641
msgid "{} unrecorded" msgid "{} unrecorded"
msgstr "{} entzeichnet" msgstr "{} entzeichnet"
#: compensation/views/eco_account.py:380 ema/views.py:282 #: compensation/views/eco_account.py:380 ema/views.py:282
#: intervention/views.py:635 #: intervention/views.py:641
msgid "{} recorded" msgid "{} recorded"
msgstr "{} verzeichnet" msgstr "{} verzeichnet"
#: compensation/views/eco_account.py:804 ema/views.py:628 #: 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" msgid "{} has already been shared with you"
msgstr "{} wurde bereits für Sie freigegeben" msgstr "{} wurde bereits für Sie freigegeben"
#: compensation/views/eco_account.py:809 ema/views.py:633 #: compensation/views/eco_account.py:809 ema/views.py:633
#: intervention/views.py:438 #: intervention/views.py:444
msgid "{} has been shared with you" msgid "{} has been shared with you"
msgstr "{} ist nun für Sie freigegeben" msgstr "{} ist nun für Sie freigegeben"
#: compensation/views/eco_account.py:816 ema/views.py:640 #: compensation/views/eco_account.py:816 ema/views.py:640
#: intervention/views.py:445 #: intervention/views.py:451
msgid "Share link invalid" msgid "Share link invalid"
msgstr "Freigabelink ungültig" msgstr "Freigabelink ungültig"
#: compensation/views/eco_account.py:839 ema/views.py:663 #: compensation/views/eco_account.py:839 ema/views.py:663
#: intervention/views.py:468 #: intervention/views.py:474
msgid "Share settings updated" msgid "Share settings updated"
msgstr "Freigabe Einstellungen aktualisiert" msgstr "Freigabe Einstellungen aktualisiert"
@ -1292,14 +1292,14 @@ msgid "Intervention handler detail"
msgstr "Detailangabe zum Eingriffsverursacher" msgstr "Detailangabe zum Eingriffsverursacher"
#: intervention/forms/forms.py:173 #: 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/templates/intervention/report/report.html:79
#: intervention/utils/quality.py:73 #: intervention/utils/quality.py:73
msgid "Registration date" msgid "Registration date"
msgstr "Datum Zulassung bzw. Satzungsbeschluss" msgstr "Datum Zulassung bzw. Satzungsbeschluss"
#: intervention/forms/forms.py:185 #: 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 #: intervention/templates/intervention/report/report.html:83
msgid "Binding on" msgid "Binding on"
msgstr "Datum Bestandskraft bzw. Rechtskraft" msgstr "Datum Bestandskraft bzw. Rechtskraft"
@ -1471,7 +1471,7 @@ msgid "Remove payment"
msgstr "Zahlung entfernen" msgstr "Zahlung entfernen"
#: intervention/templates/intervention/detail/includes/revocation.html:8 #: intervention/templates/intervention/detail/includes/revocation.html:8
#: intervention/templates/intervention/detail/view.html:104 #: intervention/templates/intervention/detail/view.html:109
msgid "Revocations" msgid "Revocations"
msgstr "Widersprüche" msgstr "Widersprüche"
@ -1493,7 +1493,7 @@ msgstr "Widerspruch entfernen"
msgid "Intervention handler" msgid "Intervention handler"
msgstr "Eingriffsverursacher" msgstr "Eingriffsverursacher"
#: intervention/templates/intervention/detail/view.html:103 #: intervention/templates/intervention/detail/view.html:108
msgid "Exists" msgid "Exists"
msgstr "vorhanden" msgstr "vorhanden"
@ -1532,19 +1532,19 @@ msgstr "Eingriffe - Übersicht"
msgid "Intervention {} added" msgid "Intervention {} added"
msgstr "Eingriff {} hinzugefügt" msgstr "Eingriff {} hinzugefügt"
#: intervention/views.py:320 #: intervention/views.py:326
msgid "Intervention {} edited" msgid "Intervention {} edited"
msgstr "Eingriff {} bearbeitet" msgstr "Eingriff {} bearbeitet"
#: intervention/views.py:356 #: intervention/views.py:362
msgid "{} removed" msgid "{} removed"
msgstr "{} entfernt" msgstr "{} entfernt"
#: intervention/views.py:489 #: intervention/views.py:495
msgid "Check performed" msgid "Check performed"
msgstr "Prüfung durchgeführt" msgstr "Prüfung durchgeführt"
#: intervention/views.py:640 #: intervention/views.py:646
msgid "There are errors on this intervention:" msgid "There are errors on this intervention:"
msgstr "Es liegen Fehler in diesem Eingriff vor:" msgstr "Es liegen Fehler in diesem Eingriff vor:"
@ -2058,7 +2058,8 @@ msgstr "Am {} von {} geprüft worden"
#: konova/utils/message_templates.py:87 #: konova/utils/message_templates.py:87
msgid "Data has changed since last check on {} by {}" 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 #: konova/utils/message_templates.py:88
msgid "Current data not checked yet" 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!" msgid "A new token needs to be validated by an administrator!"
msgstr "Neue Tokens müssen durch Administratoren freigeschaltet werden!" 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" msgid "Team name"
msgstr "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" msgid "Description"
msgstr "Beschreibung" msgstr "Beschreibung"
@ -2579,43 +2580,61 @@ msgstr ""
"Sie werden standardmäßig der Administrator dieses Teams. Sie müssen sich " "Sie werden standardmäßig der Administrator dieses Teams. Sie müssen sich "
"selbst nicht zur Liste der Mitglieder hinzufügen." "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." msgid "Name already taken. Try another."
msgstr "Name bereits vergeben. Probieren Sie einen anderen." msgstr "Name bereits vergeben. Probieren Sie einen anderen."
#: user/forms.py:249 #: user/forms.py:248
msgid "Admin" msgid "Admins"
msgstr "Administrator" msgstr "Administratoren"
#: user/forms.py:250 #: user/forms.py:250
msgid "Administrators manage team details and members" msgid "Administrators manage team details and members"
msgstr "Administratoren verwalten die Teamdaten und Mitglieder" msgstr "Administratoren verwalten die Teamdaten und Mitglieder"
#: user/forms.py:263 #: user/forms.py:273
msgid "Selected admin ({}) needs to be a member of this team." msgid "Selected admins need to be members of this team."
msgstr "Gewählter Administrator ({}) muss ein Mitglied des Teams sein." msgstr "Gewählte Administratoren müssen Teammitglieder sein."
#: 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:291 user/templates/user/team/index.html:54 #: user/forms.py:308 user/templates/user/team/index.html:60
msgid "Edit team" msgid "Edit team"
msgstr "Team bearbeiten" 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" msgid "Leave team"
msgstr "Team verlassen" msgstr "Team verlassen"
#: user/forms.py:356 #: user/forms.py:375
msgid "Team" msgid "Team"
msgstr "Team" msgstr "Team"
#: user/models/user_action.py:22 #: user/models/user_action.py:23
msgid "Unrecorded" msgid "Unrecorded"
msgstr "Entzeichnet" msgstr "Entzeichnet"
#: user/models/user_action.py:24 #: user/models/user_action.py:25
msgid "Edited" msgid "Edited"
msgstr "Bearbeitet" msgstr "Bearbeitet"
#: user/models/user_action.py:25 #: user/models/user_action.py:26
msgid "Deleted" msgid "Deleted"
msgstr "Gelöscht" msgstr "Gelöscht"
@ -2632,8 +2651,8 @@ msgid "Name"
msgstr "" msgstr ""
#: user/templates/user/index.html:21 #: user/templates/user/index.html:21
msgid "Groups" msgid "Permissions"
msgstr "Gruppen" msgstr "Berechtigungen"
#: user/templates/user/index.html:34 #: user/templates/user/index.html:34
msgid "" msgid ""
@ -2689,7 +2708,11 @@ msgstr "Neues Team hinzufügen"
msgid "Members" msgid "Members"
msgstr "Mitglieder" 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" msgid "Remove team"
msgstr "Team entfernen" msgstr "Team entfernen"
@ -2741,19 +2764,19 @@ msgstr "API Nutzer Token"
msgid "New team added" msgid "New team added"
msgstr "Neues Team hinzugefügt" msgstr "Neues Team hinzugefügt"
#: user/views.py:191 #: user/views.py:192
msgid "Team edited" msgid "Team edited"
msgstr "Team bearbeitet" msgstr "Team bearbeitet"
#: user/views.py:204 #: user/views.py:206
msgid "Team removed" msgid "Team removed"
msgstr "Team gelöscht" msgstr "Team gelöscht"
#: user/views.py:218 #: user/views.py:220
msgid "You are not a member of this team" msgid "You are not a member of this team"
msgstr "Sie sind kein Mitglied dieses Teams" msgstr "Sie sind kein Mitglied dieses Teams"
#: user/views.py:225 #: user/views.py:227
msgid "Left Team" msgid "Left Team"
msgstr "Team verlassen" msgstr "Team verlassen"
@ -4256,6 +4279,9 @@ msgstr ""
msgid "Unable to connect to qpid with SASL mechanism %s" msgid "Unable to connect to qpid with SASL mechanism %s"
msgstr "" msgstr ""
#~ msgid "Groups"
#~ msgstr "Gruppen"
#~ msgid "Show more..." #~ msgid "Show more..."
#~ msgstr "Mehr anzeigen..." #~ msgstr "Mehr anzeigen..."

@ -11,7 +11,7 @@
</h4> </h4>
{% if form.form_caption is not None %} {% if form.form_caption is not None %}
<small> <small>
{{ form.form_caption }} {{ form.form_caption|linebreaks }}
</small> </small>
{% endif %} {% endif %}
<form method="post" action="{{ form.action_url }}" {% for attr_key, attr_val in form.form_attrs.items %} {{attr_key}}="{{attr_val}}"{% endfor %}> <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"> <div class="modal-body">
<article> <article>
{{ form.form_caption }} {{ form.form_caption|linebreaks }}
</article> </article>
{% include 'form/table/generic_table_form_body.html' %} {% include 'form/table/generic_table_form_body.html' %}
</div> </div>

@ -1,6 +1,7 @@
from django.contrib import admin 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): class UserNotificationAdmin(admin.ModelAdmin):
@ -64,26 +65,28 @@ class UserActionLogEntryAdmin(admin.ModelAdmin):
] ]
class TeamAdmin(admin.ModelAdmin): class TeamAdmin(DeletableObjectMixinAdmin, admin.ModelAdmin):
list_display = [ list_display = [
"name", "name",
"description", "description",
"admin", "deleted",
] ]
search_fields = [ search_fields = [
"name", "name",
"description", "description",
] ]
filter_horizontal = [ filter_horizontal = [
"users" "users",
"admins",
] ]
def formfield_for_foreignkey(self, db_field, request, **kwargs): readonly_fields = [
if db_field.name == "admin": "deleted"
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)
actions = [
"restore_deleted_data"
]
admin.site.register(User, UserAdmin) admin.site.register(User, UserAdmin)
admin.site.register(Team, TeamAdmin) admin.site.register(Team, TeamAdmin)

@ -230,8 +230,8 @@ class NewTeamModalForm(BaseModalForm):
team = Team.objects.create( team = Team.objects.create(
name=self.cleaned_data.get("name", None), name=self.cleaned_data.get("name", None),
description=self.cleaned_data.get("description", 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()) members = self.cleaned_data.get("members", User.objects.none())
if self.user.id not in members: if self.user.id not in members:
members = members.union( members = members.union(
@ -244,23 +244,40 @@ class NewTeamModalForm(BaseModalForm):
class EditTeamModalForm(NewTeamModalForm): class EditTeamModalForm(NewTeamModalForm):
admin = forms.ModelChoiceField( admins = forms.ModelMultipleChoiceField(
label=_("Admins"),
label_suffix="", label_suffix="",
label=_("Admin"),
help_text=_("Administrators manage team details and members"), help_text=_("Administrators manage team details and members"),
queryset=User.objects.none(), required=True,
empty_label=None, 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): def __is_admins_valid(self):
admin = self.cleaned_data.get("admin", None) admins = set(self.cleaned_data.get("admins", {}))
members = self.cleaned_data.get("members", None) members = set(self.cleaned_data.get("members", {}))
_is_valid = admin in members _is_valid = admins.issubset(members)
if not _is_valid: if not _is_valid:
self.add_error( self.add_error(
"members", "admins",
_("Selected admin ({}) needs to be a member of this team.").format(admin.username) _("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 return _is_valid
@ -283,7 +300,7 @@ class EditTeamModalForm(NewTeamModalForm):
def is_valid(self): def is_valid(self):
super_valid = super().is_valid() super_valid = super().is_valid()
admin_valid = self.__is_admin_valid() admin_valid = self.__is_admins_valid()
return super_valid and admin_valid return super_valid and admin_valid
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -293,13 +310,12 @@ class EditTeamModalForm(NewTeamModalForm):
self.cancel_redirect = reverse("user:team-index") self.cancel_redirect = reverse("user:team-index")
members = self.instance.users.all() members = self.instance.users.all()
self.fields["admin"].queryset = members
form_data = { form_data = {
"members": members, "members": members,
"name": self.instance.name, "name": self.instance.name,
"description": self.instance.description, "description": self.instance.description,
"admin": self.instance.admin, "admins": self.instance.admins.all(),
} }
self.load_initial_data(form_data) self.load_initial_data(form_data)
@ -307,14 +323,20 @@ class EditTeamModalForm(NewTeamModalForm):
with transaction.atomic(): with transaction.atomic():
self.instance.name = self.cleaned_data.get("name", None) self.instance.name = self.cleaned_data.get("name", None)
self.instance.description = self.cleaned_data.get("description", None) self.instance.description = self.cleaned_data.get("description", None)
self.instance.admin = self.cleaned_data.get("admin", None)
self.instance.save() self.instance.save()
self.instance.users.set(self.cleaned_data.get("members", [])) self.instance.users.set(self.cleaned_data.get("members", []))
self.instance.admins.set(self.cleaned_data.get("admins", []))
return self.instance return self.instance
class RemoveTeamModalForm(RemoveModalForm): 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): class LeaveTeamModalForm(RemoveModalForm):

@ -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',
),
]

@ -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 django.db import models
from konova.models import UuidModel from konova.models import UuidModel, DeletableObjectMixin
from konova.utils.mailer import Mailer 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 """ Groups users in self managed teams. Can be used for multi-sharing of data
""" """
name = models.CharField(max_length=500, null=True, blank=True) name = models.CharField(max_length=500, null=True, blank=True)
description = models.TextField(null=True, blank=True) description = models.TextField(null=True, blank=True)
users = models.ManyToManyField("user.User", blank=True, related_name="teams") 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): def __str__(self):
return self.name 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): 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 """ Sends a mail to the team members in case of given shared access
@ -104,6 +118,19 @@ class Team(UuidModel):
""" """
self.users.remove(user) self.users.remove(user)
if self.admin == user: self.admins.remove(user)
self.admin = self.users.first()
self.save() 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: else:
token = self.api_token token = self.api_token
return 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> <td>{{user.email}}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">{% trans 'Groups' %}</th> <th scope="row">{% trans 'Permissions' %}</th>
<td> <td>
{% for group in user.groups.all %} {% for group in user.groups.all %}
<span class="badge badge-pill rlp-r">{% trans group.name %}</span> <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">{% trans 'Name' %}</th>
<th scope="col" class="align-middle w-20">{% trans 'Description' %}</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 'Members' %}</th>
<th scope="col" class="align-middle">{% trans 'Administrator' %}</th>
<th scope="col" class="align-middle">{% trans 'Action' %}</th> <th scope="col" class="align-middle">{% trans 'Action' %}</th>
</tr> </tr>
</thead> </thead>
@ -45,11 +46,16 @@
<span class="badge badge-pill rlp-r">{{member.username}}</span> <span class="badge badge-pill rlp-r">{{member.username}}</span>
{% endfor %} {% endfor %}
</td> </td>
<td>
{% for admin in team.admins.all %}
<span class="badge badge-pill rlp-r">{{admin.username}}</span>
{% endfor %}
</td>
<td> <td>
<button class="btn rlp-r btn-modal" data-form-url="{% url 'user:team-leave' team.id %}" title="{% trans 'Leave team' %}"> <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' %} {% fa5_icon 'sign-out-alt' %}
</button> </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' %}"> <button class="btn rlp-r btn-modal" data-form-url="{% url 'user:team-edit' team.id %}" title="{% trans 'Edit team' %}">
{% fa5_icon 'edit' %} {% fa5_icon 'edit' %}
</button> </button>

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

@ -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
"""

@ -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)

@ -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" template = "user/team/index.html"
user = request.user user = request.user
context = { context = {
"teams": user.teams.all(), "teams": user.shared_teams,
"tab_title": _("Teams"), "tab_title": _("Teams"),
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context
@ -183,7 +183,8 @@ def new_team_view(request: HttpRequest):
@login_required @login_required
def edit_team_view(request: HttpRequest, id: str): def edit_team_view(request: HttpRequest, id: str):
team = get_object_or_404(Team, id=id) 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() raise Http404()
form = EditTeamModalForm(request.POST or None, instance=team, request=request) form = EditTeamModalForm(request.POST or None, instance=team, request=request)
return form.process_request( return form.process_request(
@ -196,7 +197,8 @@ def edit_team_view(request: HttpRequest, id: str):
@login_required @login_required
def remove_team_view(request: HttpRequest, id: str): def remove_team_view(request: HttpRequest, id: str):
team = get_object_or_404(Team, id=id) 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() raise Http404()
form = RemoveTeamModalForm(request.POST or None, instance=team, request=request) form = RemoveTeamModalForm(request.POST or None, instance=team, request=request)
return form.process_request( return form.process_request(

Loading…
Cancel
Save