From 170e5798ecd82c293bddd041081628d44711a3f8 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 30 May 2022 14:35:31 +0200 Subject: [PATCH 1/4] #169 Admin on teams * adds admin column on team index view * refactors Team model, so multiple members can become admins * adds team migration for switch from fkey->m2m structure * renames 'Group' to 'Permission' on user index view to avoid confusion between 'Groups' and Teams * adds new autocomplete route for team-admin selection based on already selected members of the TeamForm --- konova/autocompletes.py | 23 ++ konova/forms.py | 2 +- konova/tests/test_autocompletes.py | 1 + konova/tests/test_views.py | 2 +- konova/urls.py | 3 +- locale/de/LC_MESSAGES/django.mo | Bin 42537 -> 43014 bytes locale/de/LC_MESSAGES/django.po | 228 +++++++++++-------- templates/form/table/generic_table_form.html | 2 +- templates/modal/modal_form.html | 2 +- user/admin.py | 10 +- user/forms.py | 51 +++-- user/migrations/0004_auto_20220530_1105.py | 35 +++ user/models/team.py | 21 +- user/templates/user/index.html | 2 +- user/templates/user/team/index.html | 8 +- user/views.py | 6 +- 16 files changed, 258 insertions(+), 138 deletions(-) create mode 100644 user/migrations/0004_auto_20220530_1105.py diff --git a/konova/autocompletes.py b/konova/autocompletes.py index e6036f0..288ee02 100644 --- a/konova/autocompletes.py +++ b/konova/autocompletes.py @@ -108,6 +108,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 diff --git a/konova/forms.py b/konova/forms.py index 72a4468..0a341e0 100644 --- a/konova/forms.py +++ b/konova/forms.py @@ -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 diff --git a/konova/tests/test_autocompletes.py b/konova/tests/test_autocompletes.py index 95a3508..1533d57 100644 --- a/konova/tests/test_autocompletes.py +++ b/konova/tests/test_autocompletes.py @@ -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) diff --git a/konova/tests/test_views.py b/konova/tests/test_views.py index afc381d..1ad90dd 100644 --- a/konova/tests/test_views.py +++ b/konova/tests/test_views.py @@ -274,7 +274,7 @@ class BaseTestCase(TestCase): team = Team.objects.get_or_create( name="Testteam", description="Testdescription", - admin=self.superuser, + admins__in=[self.superuser], )[0] team.users.add(self.superuser) diff --git a/konova/urls.py b/konova/urls.py index 75ac011..e012683 100644 --- a/konova/urls.py +++ b/konova/urls.py @@ -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: diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index 5e1ebe07b800b2394d8be056876cb8984367e376..7cf9a79c50f1eaaa779bd7b4ac4a2bc9a8d2b6d0 100644 GIT binary patch delta 12331 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 b9%}xr1xNbs?M%@5zXfDkjfz?oUJLp!GSG=l delta 11862 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\n" "Language-Team: LANGUAGE \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: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" 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..." diff --git a/templates/form/table/generic_table_form.html b/templates/form/table/generic_table_form.html index a89ee4b..5c00b89 100644 --- a/templates/form/table/generic_table_form.html +++ b/templates/form/table/generic_table_form.html @@ -11,7 +11,7 @@ {% if form.form_caption is not None %} - {{ form.form_caption }} + {{ form.form_caption|linebreaks }} {% endif %}
diff --git a/templates/modal/modal_form.html b/templates/modal/modal_form.html index 6f47b12..741b539 100644 --- a/templates/modal/modal_form.html +++ b/templates/modal/modal_form.html @@ -16,7 +16,7 @@ diff --git a/user/admin.py b/user/admin.py index f4ef9fc..d564066 100644 --- a/user/admin.py +++ b/user/admin.py @@ -68,22 +68,16 @@ class TeamAdmin(admin.ModelAdmin): list_display = [ "name", "description", - "admin", ] 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) - admin.site.register(User, UserAdmin) admin.site.register(Team, TeamAdmin) diff --git a/user/forms.py b/user/forms.py index 4a657af..f6831ff 100644 --- a/user/forms.py +++ b/user/forms.py @@ -230,7 +230,7 @@ class NewTeamModalForm(BaseModalForm): team = Team.objects.create( name=self.cleaned_data.get("name", None), description=self.cleaned_data.get("description", None), - admin=self.user, + admins__in=[self.user], ) members = self.cleaned_data.get("members", User.objects.none()) if self.user.id not in members: @@ -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,13 @@ class EditTeamModalForm(NewTeamModalForm): self.cancel_redirect = reverse("user:team-index") members = self.instance.users.all() - self.fields["admin"].queryset = members + #self.fields["admins"].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 +324,16 @@ 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?") class LeaveTeamModalForm(RemoveModalForm): diff --git a/user/migrations/0004_auto_20220530_1105.py b/user/migrations/0004_auto_20220530_1105.py new file mode 100644 index 0000000..1c6ea83 --- /dev/null +++ b/user/migrations/0004_auto_20220530_1105.py @@ -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', + ), + ] diff --git a/user/models/team.py b/user/models/team.py index f14c7e0..7908115 100644 --- a/user/models/team.py +++ b/user/models/team.py @@ -11,7 +11,7 @@ class Team(UuidModel): 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 @@ -104,6 +104,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 diff --git a/user/templates/user/index.html b/user/templates/user/index.html index f8fd616..cfe3bd0 100644 --- a/user/templates/user/index.html +++ b/user/templates/user/index.html @@ -18,7 +18,7 @@ {{user.email}} - {% trans 'Groups' %} + {% trans 'Permissions' %} {% for group in user.groups.all %} {% trans group.name %} diff --git a/user/templates/user/team/index.html b/user/templates/user/team/index.html index 3cd08e7..6356051 100644 --- a/user/templates/user/team/index.html +++ b/user/templates/user/team/index.html @@ -28,6 +28,7 @@ {% trans 'Name' %} {% trans 'Description' %} {% trans 'Members' %} + {% trans 'Administrator' %} {% trans 'Action' %} @@ -45,11 +46,16 @@ {{member.username}} {% endfor %} + + {% for admin in team.admins.all %} + {{admin.username}} + {% endfor %} + - {% if team.admin == user %} + {% if user in team.admins.all %} diff --git a/user/views.py b/user/views.py index 13c974a..b6e7f09 100644 --- a/user/views.py +++ b/user/views.py @@ -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( From 5de3f4c24ec2ba43174813540cd495e77d2fb5e0 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 30 May 2022 15:38:16 +0200 Subject: [PATCH 2/4] #169 Team delete-restore * adds restorable delete functionality to Team model * refactors minor code model parts by introducing DeletableObjectMixin * only non-deleted Teams can be chosen for sharing * deleted Teams can be restored using the proper function on the backend admin * deleted Teams do not provide * adds migration --- .../detail/compensation/view.html | 2 +- .../compensation/detail/eco_account/view.html | 2 +- ema/templates/ema/detail/view.html | 4 +- .../templates/intervention/detail/view.html | 2 +- konova/admin.py | 21 ++++++---- konova/autocompletes.py | 4 +- konova/models/object.py | 41 ++++++++++++------- user/admin.py | 13 +++++- user/forms.py | 6 ++- user/migrations/0005_team_deleted.py | 19 +++++++++ user/models/team.py | 18 +++++++- user/models/user.py | 12 ++++++ user/views.py | 2 +- 13 files changed, 111 insertions(+), 35 deletions(-) create mode 100644 user/migrations/0005_team_deleted.py diff --git a/compensation/templates/compensation/detail/compensation/view.html b/compensation/templates/compensation/detail/compensation/view.html index be8dd3b..4a1177a 100644 --- a/compensation/templates/compensation/detail/compensation/view.html +++ b/compensation/templates/compensation/detail/compensation/view.html @@ -109,7 +109,7 @@ {% trans 'Shared with' %} - {% for team in obj.intervention.teams.all %} + {% for team in obj.intervention.shared_teams %} {% include 'user/includes/team_data_modal_button.html' %} {% endfor %}
diff --git a/compensation/templates/compensation/detail/eco_account/view.html b/compensation/templates/compensation/detail/eco_account/view.html index aa76fbf..f4e8da4 100644 --- a/compensation/templates/compensation/detail/eco_account/view.html +++ b/compensation/templates/compensation/detail/eco_account/view.html @@ -87,7 +87,7 @@ {% trans 'Shared with' %} - {% for team in obj.teams.all %} + {% for team in obj.shared_teams %} {% include 'user/includes/team_data_modal_button.html' %} {% endfor %}
diff --git a/ema/templates/ema/detail/view.html b/ema/templates/ema/detail/view.html index adc120f..7d60473 100644 --- a/ema/templates/ema/detail/view.html +++ b/ema/templates/ema/detail/view.html @@ -73,11 +73,11 @@ {% trans 'Shared with' %} - {% for team in obj.teams.all %} + {% for team in obj.shared_teams %} {% include 'user/includes/team_data_modal_button.html' %} {% endfor %}
- {% for user in obj.users.all %} + {% for user in obj.user.all %} {% include 'user/includes/contact_modal_button.html' %} {% endfor %} diff --git a/intervention/templates/intervention/detail/view.html b/intervention/templates/intervention/detail/view.html index 55d57f6..1a596bb 100644 --- a/intervention/templates/intervention/detail/view.html +++ b/intervention/templates/intervention/detail/view.html @@ -125,7 +125,7 @@ {% trans 'Shared with' %} - {% for team in obj.teams.all %} + {% for team in obj.shared_teams %} {% include 'user/includes/team_data_modal_button.html' %} {% endfor %}
diff --git a/konova/admin.py b/konova/admin.py index 213120e..b30f4b1 100644 --- a/konova/admin.py +++ b/konova/admin.py @@ -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 diff --git a/konova/autocompletes.py b/konova/autocompletes.py index 288ee02..fbd92f7 100644 --- a/konova/autocompletes.py +++ b/konova/autocompletes.py @@ -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( diff --git a/konova/models/object.py b/konova/models/object.py index 325762f..b468932 100644 --- a/konova/models/object.py +++ b/konova/models/object.py @@ -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): diff --git a/user/admin.py b/user/admin.py index d564066..bf5f5f8 100644 --- a/user/admin.py +++ b/user/admin.py @@ -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,10 +65,11 @@ class UserActionLogEntryAdmin(admin.ModelAdmin): ] -class TeamAdmin(admin.ModelAdmin): +class TeamAdmin(DeletableObjectMixinAdmin, admin.ModelAdmin): list_display = [ "name", "description", + "deleted", ] search_fields = [ "name", @@ -78,6 +80,13 @@ class TeamAdmin(admin.ModelAdmin): "admins", ] + readonly_fields = [ + "deleted" + ] + + actions = [ + "restore_deleted_data" + ] admin.site.register(User, UserAdmin) admin.site.register(Team, TeamAdmin) diff --git a/user/forms.py b/user/forms.py index f6831ff..cfb6d72 100644 --- a/user/forms.py +++ b/user/forms.py @@ -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), - admins__in=[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( @@ -335,6 +335,10 @@ class RemoveTeamModalForm(RemoveModalForm): 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): def __init__(self, *args, **kwargs): diff --git a/user/migrations/0005_team_deleted.py b/user/migrations/0005_team_deleted.py new file mode 100644 index 0000000..b3a5c68 --- /dev/null +++ b/user/migrations/0005_team_deleted.py @@ -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'), + ), + ] diff --git a/user/models/team.py b/user/models/team.py index 7908115..5e728e7 100644 --- a/user/models/team.py +++ b/user/models/team.py @@ -1,10 +1,11 @@ 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 """ @@ -16,6 +17,19 @@ class Team(UuidModel): 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 diff --git a/user/models/user.py b/user/models/user.py index df63dd7..b40a3b1 100644 --- a/user/models/user.py +++ b/user/models/user.py @@ -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 \ No newline at end of file diff --git a/user/views.py b/user/views.py index b6e7f09..2c5b86a 100644 --- a/user/views.py +++ b/user/views.py @@ -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 From 4ec8e1ae070075327df7e7edf277bcce07c78376 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 31 May 2022 09:10:44 +0200 Subject: [PATCH 3/4] #169 Team delete-restore * adds tests for user app --- user/tests.py | 3 - user/tests/__init__.py | 7 ++ user/tests/test_views.py | 112 +++++++++++++++++++++++++ user/tests/test_workflow.py | 158 ++++++++++++++++++++++++++++++++++++ 4 files changed, 277 insertions(+), 3 deletions(-) delete mode 100644 user/tests.py create mode 100644 user/tests/__init__.py create mode 100644 user/tests/test_views.py create mode 100644 user/tests/test_workflow.py diff --git a/user/tests.py b/user/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/user/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/user/tests/__init__.py b/user/tests/__init__.py new file mode 100644 index 0000000..2a3d47b --- /dev/null +++ b/user/tests/__init__.py @@ -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 + +""" diff --git a/user/tests/test_views.py b/user/tests/test_views.py new file mode 100644 index 0000000..fe4c854 --- /dev/null +++ b/user/tests/test_views.py @@ -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) + diff --git a/user/tests/test_workflow.py b/user/tests/test_workflow.py new file mode 100644 index 0000000..a30fd9f --- /dev/null +++ b/user/tests/test_workflow.py @@ -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) From c911276cb498ba351e8ce9aca4106638e4a07ca1 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 31 May 2022 09:47:32 +0200 Subject: [PATCH 4/4] #169 Team delete-restore * removes unused code snippets --- konova/tests/test_views.py | 1 - user/forms.py | 1 - 2 files changed, 2 deletions(-) diff --git a/konova/tests/test_views.py b/konova/tests/test_views.py index 1ad90dd..73029f9 100644 --- a/konova/tests/test_views.py +++ b/konova/tests/test_views.py @@ -274,7 +274,6 @@ class BaseTestCase(TestCase): team = Team.objects.get_or_create( name="Testteam", description="Testdescription", - admins__in=[self.superuser], )[0] team.users.add(self.superuser) diff --git a/user/forms.py b/user/forms.py index cfb6d72..a92c6b0 100644 --- a/user/forms.py +++ b/user/forms.py @@ -310,7 +310,6 @@ class EditTeamModalForm(NewTeamModalForm): self.cancel_redirect = reverse("user:team-index") members = self.instance.users.all() - #self.fields["admins"].queryset = members form_data = { "members": members,