diff --git a/compensation/templates/compensation/detail/compensation/view.html b/compensation/templates/compensation/detail/compensation/view.html index be8dd3b9..4a1177a5 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 aa76fbf5..f4e8da4e 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 adc120f3..7d604731 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 55d57f68..1a596bb0 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 213120ea..b30f4b14 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 e6036f02..fbd92f75 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( @@ -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 diff --git a/konova/forms.py b/konova/forms.py index 72a44681..0a341e0c 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/models/object.py b/konova/models/object.py index 325762f4..b468932a 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/konova/tests/test_autocompletes.py b/konova/tests/test_autocompletes.py index 95a3508d..1533d572 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 afc381df..73029f96 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", - admin=self.superuser, )[0] team.users.add(self.superuser) diff --git a/konova/urls.py b/konova/urls.py index 75ac0113..e0126832 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 5e1ebe07..7cf9a79c 100644 Binary files a/locale/de/LC_MESSAGES/django.mo and b/locale/de/LC_MESSAGES/django.mo differ diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index e735ce6c..27578e3f 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -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 \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: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..." diff --git a/templates/form/table/generic_table_form.html b/templates/form/table/generic_table_form.html index a89ee4bf..5c00b89a 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 6f47b127..741b5392 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 f4ef9fce..bf5f5f86 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,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) diff --git a/user/forms.py b/user/forms.py index 4a657afb..a92c6b00 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), - 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): diff --git a/user/migrations/0004_auto_20220530_1105.py b/user/migrations/0004_auto_20220530_1105.py new file mode 100644 index 00000000..1c6ea830 --- /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/migrations/0005_team_deleted.py b/user/migrations/0005_team_deleted.py new file mode 100644 index 00000000..b3a5c68d --- /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 f14c7e0f..5e728e71 100644 --- a/user/models/team.py +++ b/user/models/team.py @@ -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 diff --git a/user/models/user.py b/user/models/user.py index df63dd76..b40a3b1b 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/templates/user/index.html b/user/templates/user/index.html index f8fd616d..cfe3bd05 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 3cd08e74..63560516 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/tests.py b/user/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- 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 00000000..2a3d47be --- /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 00000000..fe4c854f --- /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 00000000..a30fd9fb --- /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) diff --git a/user/views.py b/user/views.py index 13c974ac..2c5b86a1 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 @@ -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(