169_Unknown_admin_on_teams #170
@ -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…
Reference in New Issue
Block a user