diff --git a/analysis/utils/report.py b/analysis/utils/report.py index 63a06b84..9932863a 100644 --- a/analysis/utils/report.py +++ b/analysis/utils/report.py @@ -17,6 +17,7 @@ from compensation.models import Compensation, Payment, EcoAccountDeduction, EcoA from intervention.models import Intervention from konova.models import Geometry from konova.sub_settings.django_settings import BASE_DIR, DEFAULT_DATE_FORMAT +from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP class TimespanReport: @@ -333,7 +334,7 @@ class TimespanReport: return Geometry.objects.filter( id__in=ids ).annotate( - geom_cast=Cast("geom", MultiPolygonField()) + geom_cast=Cast("geom", MultiPolygonField(srid=DEFAULT_SRID_RLP)) ).annotate( num=NumGeometries("geom_cast") ).aggregate( diff --git a/compensation/admin.py b/compensation/admin.py index 2821a146..c0186216 100644 --- a/compensation/admin.py +++ b/compensation/admin.py @@ -81,13 +81,15 @@ class EcoAccountAdmin(AbstractCompensationAdmin): ] filter_horizontal = [ - "users" + "users", + "teams", ] def get_fields(self, request, obj=None): return super().get_fields(request, obj) + [ "deductable_surface", - "users" + "users", + "teams", ] diff --git a/compensation/migrations/0012_auto_20221116_1322.py b/compensation/migrations/0012_auto_20221116_1322.py new file mode 100644 index 00000000..947352a3 --- /dev/null +++ b/compensation/migrations/0012_auto_20221116_1322.py @@ -0,0 +1,26 @@ +# Generated by Django 3.1.3 on 2022-11-16 12:22 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0006_auto_20220815_0759'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('compensation', '0011_ecoaccount_deductable_rest'), + ] + + operations = [ + migrations.AlterField( + model_name='ecoaccount', + name='teams', + field=models.ManyToManyField(blank=True, help_text='Teams having access (data shared with)', to='user.Team'), + ), + migrations.AlterField( + model_name='ecoaccount', + name='users', + field=models.ManyToManyField(blank=True, help_text='Users having access (data shared with)', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/compensation/migrations/0013_auto_20221117_0819.py b/compensation/migrations/0013_auto_20221117_0819.py new file mode 100644 index 00000000..defcfbc2 --- /dev/null +++ b/compensation/migrations/0013_auto_20221117_0819.py @@ -0,0 +1,35 @@ +# Generated by Django 3.1.3 on 2022-11-17 07:19 + +from django.db import migrations + +from compensation.models import UnitChoices + + +def harmonize_action_units(apps, schema_editor): + """ + CompensationAction units (based on UnitChoices) can be mixed up at this point where + * qm represents m² and + * m2 represents m² + + We drop qm in support of m2 + + """ + CompensationAction = apps.get_model("compensation", "CompensationAction") + actions = CompensationAction.objects.filter( + unit="qm" + ) + + for action in actions: + action.unit = UnitChoices.m2 + action.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('compensation', '0012_auto_20221116_1322'), + ] + + operations = [ + migrations.RunPython(harmonize_action_units), + ] diff --git a/compensation/models/action.py b/compensation/models/action.py index 443c4491..58e41c46 100644 --- a/compensation/models/action.py +++ b/compensation/models/action.py @@ -19,9 +19,9 @@ class UnitChoices(models.TextChoices): """ cm = "cm", _("cm") m = "m", _("m") - km = "km", _("km") m2 = "m2", _("m²") m3 = "m3", _("m³") + km = "km", _("km") ha = "ha", _("ha") st = "pcs", _("Pieces") # pieces diff --git a/compensation/templates/compensation/detail/compensation/view.html b/compensation/templates/compensation/detail/compensation/view.html index 759f7600..b40fc2f4 100644 --- a/compensation/templates/compensation/detail/compensation/view.html +++ b/compensation/templates/compensation/detail/compensation/view.html @@ -124,7 +124,7 @@ {% endfor %}
{% if has_access %} - {% for user in obj.users.all %} + {% for user in obj.intervention.shared_users %} {% include 'user/includes/contact_modal_button.html' %} {% endfor %} {% else %} diff --git a/ema/admin.py b/ema/admin.py index 7cd5a7ff..dbfff3ab 100644 --- a/ema/admin.py +++ b/ema/admin.py @@ -6,12 +6,14 @@ from ema.models import Ema class EmaAdmin(AbstractCompensationAdmin): filter_horizontal = [ - "users" + "users", + "teams", ] def get_fields(self, request, obj=None): return super().get_fields(request, obj) + [ - "users" + "users", + "teams", ] admin.site.register(Ema, EmaAdmin) diff --git a/ema/migrations/0008_auto_20221116_1322.py b/ema/migrations/0008_auto_20221116_1322.py new file mode 100644 index 00000000..c705358f --- /dev/null +++ b/ema/migrations/0008_auto_20221116_1322.py @@ -0,0 +1,26 @@ +# Generated by Django 3.1.3 on 2022-11-16 12:22 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0006_auto_20220815_0759'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('ema', '0007_auto_20220815_1030'), + ] + + operations = [ + migrations.AlterField( + model_name='ema', + name='teams', + field=models.ManyToManyField(blank=True, help_text='Teams having access (data shared with)', to='user.Team'), + ), + migrations.AlterField( + model_name='ema', + name='users', + field=models.ManyToManyField(blank=True, help_text='Users having access (data shared with)', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/intervention/admin.py b/intervention/admin.py index 932ddb93..46f7244d 100644 --- a/intervention/admin.py +++ b/intervention/admin.py @@ -14,7 +14,8 @@ class InterventionAdmin(BaseObjectAdmin): ] filter_horizontal = [ - "users" + "users", + "teams", ] def get_fields(self, request, obj=None): @@ -25,6 +26,7 @@ class InterventionAdmin(BaseObjectAdmin): "checked", "recorded", "users", + "teams", "geometry", ] diff --git a/intervention/forms/modals/document.py b/intervention/forms/modals/document.py index 212f3f02..17dfa560 100644 --- a/intervention/forms/modals/document.py +++ b/intervention/forms/modals/document.py @@ -10,4 +10,23 @@ from konova.forms.modals import NewDocumentModalForm class NewInterventionDocumentModalForm(NewDocumentModalForm): - document_model = InterventionDocument \ No newline at end of file + document_model = InterventionDocument + + def save(self, *args, **kwargs): + """ Extension of regular NewDocumentModalForm + + Checks whether payments exist on the intervention and sends the data to EGON + + Args: + *args (): + **kwargs (): + + Returns: + + """ + doc = super().save(*args, **kwargs) + + if self.instance.payments.exists(): + self.instance.send_data_to_egon() + + return doc diff --git a/intervention/migrations/0008_auto_20221116_1322.py b/intervention/migrations/0008_auto_20221116_1322.py new file mode 100644 index 00000000..f92b1489 --- /dev/null +++ b/intervention/migrations/0008_auto_20221116_1322.py @@ -0,0 +1,26 @@ +# Generated by Django 3.1.3 on 2022-11-16 12:22 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0006_auto_20220815_0759'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('intervention', '0007_auto_20220815_1030'), + ] + + operations = [ + migrations.AlterField( + model_name='intervention', + name='teams', + field=models.ManyToManyField(blank=True, help_text='Teams having access (data shared with)', to='user.Team'), + ), + migrations.AlterField( + model_name='intervention', + name='users', + field=models.ManyToManyField(blank=True, help_text='Users having access (data shared with)', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/intervention/templates/intervention/detail/includes/documents.html b/intervention/templates/intervention/detail/includes/documents.html index 9fccc4b7..5037ff8a 100644 --- a/intervention/templates/intervention/detail/includes/documents.html +++ b/intervention/templates/intervention/detail/includes/documents.html @@ -20,6 +20,11 @@ + {% if has_payment_without_document %} +
+ {% trans 'You entered a payment. Please upload the legal document which defines the payment`s amount.' %} +
+ {% endif %}
diff --git a/intervention/tests/test_workflow.py b/intervention/tests/test_workflow.py index 9124c3e8..be885c96 100644 --- a/intervention/tests/test_workflow.py +++ b/intervention/tests/test_workflow.py @@ -11,7 +11,7 @@ from django.core.exceptions import ObjectDoesNotExist from django.urls import reverse from compensation.models import Payment, EcoAccountDeduction -from intervention.models import Intervention +from intervention.models import Intervention, InterventionDocument from konova.settings import ETS_GROUP, ZB_GROUP from konova.tests.test_views import BaseWorkflowTestCase from user.models import UserActionLogEntry, UserAction @@ -153,13 +153,16 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase): payment = Payment.objects.create(amount=10.00, due_on=None, comment="No due date because test") self.intervention.payments.add(payment) + # Since there is a payment, we need to add a dummy document (mocking a legal document for payment info) + document = self.create_dummy_document(InterventionDocument, self.intervention) + # Run request again self.client_user.post(check_url, post_data) # Update intervention from db self.intervention.refresh_from_db() - # We expect the intervention to be checked now and contains the proper data + # We expect the intervention to be checked now and contain the proper data # Attention: We check the timestamp only on the date, not the time, since the microseconds delay would result # in an unwanted assertion error checked = self.intervention.checked @@ -209,6 +212,9 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase): payment = Payment.objects.create(amount=10.00, due_on=None, comment="No due date because test") self.intervention.payments.add(payment) + # Since there is a payment, we need to add a dummy document (mocking a legal document for payment info) + document = self.create_dummy_document(InterventionDocument, self.intervention) + # Run request again self.client_user.post(record_url, post_data) diff --git a/intervention/utils/quality.py b/intervention/utils/quality.py index 6135b403..bae886ee 100644 --- a/intervention/utils/quality.py +++ b/intervention/utils/quality.py @@ -21,8 +21,21 @@ class InterventionQualityChecker(AbstractQualityChecker): self._check_legal_data() self._check_compensations() self._check_geometry() + self._check_payment_documents() self.valid = len(self.messages) == 0 + def _check_payment_documents(self): + """ Checks existence of documents in case of payments + + There should be at least one legal document which defines the payment's total amount. + + Returns: + + """ + has_payment_without_document = self.obj.payments.exists() and not self.obj.get_documents()[1].exists() + if has_payment_without_document: + self._add_missing_attr_name(_("Documents")) + def _check_responsible_data(self): """ Checks data quality of related Responsibility diff --git a/intervention/views/intervention.py b/intervention/views/intervention.py index 524c2c75..1bca8968 100644 --- a/intervention/views/intervention.py +++ b/intervention/views/intervention.py @@ -157,6 +157,8 @@ def detail_view(request: HttpRequest, id: str): if last_checked: last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(last_checked.get_timestamp_str_formatted(), last_checked.user) + has_payment_without_document = intervention.payments.exists() and not intervention.get_documents()[1].exists() + context = { "obj": intervention, "last_checked": last_checked, @@ -168,6 +170,7 @@ def detail_view(request: HttpRequest, id: str): "is_zb_member": in_group(_user, ZB_GROUP), "is_ets_member": in_group(_user, ETS_GROUP), "LANIS_LINK": intervention.get_LANIS_link(), + "has_payment_without_document": has_payment_without_document, TAB_TITLE_IDENTIFIER: f"{intervention.identifier} - {intervention.title}", } diff --git a/konova/filters/mixins/geo_reference.py b/konova/filters/mixins/geo_reference.py index eacf5c5b..71fa41f5 100644 --- a/konova/filters/mixins/geo_reference.py +++ b/konova/filters/mixins/geo_reference.py @@ -46,7 +46,7 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet): ), ) # Parcel - p = django_filters.CharFilter( + p = django_filters.NumberFilter( method="filter_parcel", label=_(""), label_suffix=_(""), @@ -59,7 +59,7 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet): ), ) # Parcel counter - pc = django_filters.CharFilter( + pc = django_filters.NumberFilter( method="filter_parcel_counter", label=_(""), label_suffix=_(""), @@ -73,7 +73,7 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet): ) # Parcel counter - pn = django_filters.CharFilter( + pn = django_filters.NumberFilter( method="filter_parcel_number", label=_(""), label_suffix=_(""), @@ -165,7 +165,6 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet): Returns: """ - value = value.replace("-", "") queryset = self._filter_parcel_reference( queryset, Q(flr=value), @@ -183,7 +182,6 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet): Returns: """ - value = value.replace("-", "") queryset = self._filter_parcel_reference( queryset, Q(flrstck_zhlr=value) @@ -201,7 +199,6 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet): Returns: """ - value = value.replace("-", "") queryset = self._filter_parcel_reference( queryset, Q(flrstck_nnr=value), diff --git a/konova/forms/geometry_form.py b/konova/forms/geometry_form.py index 5d25a85d..f76b907f 100644 --- a/konova/forms/geometry_form.py +++ b/konova/forms/geometry_form.py @@ -10,6 +10,7 @@ import json from django.contrib.gis import gdal from django.contrib.gis.forms import MultiPolygonField from django.contrib.gis.geos import MultiPolygon, Polygon +from django.contrib.gis.geos.prototypes.io import WKTWriter from django.utils.translation import gettext_lazy as _ from konova.forms.base_form import BaseForm @@ -74,9 +75,21 @@ class SimpleGeomForm(BaseForm): # this case) features = [] features_json = geom.get("features", []) + accepted_ogr_types = [ + "Polygon", + "Polygon25D", + "MultiPolygon", + "MultiPolygon25D", + ] for feature in features_json: - g = gdal.OGRGeometry(json.dumps(feature.get("geometry", feature)), srs=DEFAULT_SRID_RLP) - if g.geom_type not in ["Polygon", "MultiPolygon"]: + feature_geom = json.dumps(feature.get("geometry", feature)) + g = gdal.OGRGeometry(feature_geom, srs=DEFAULT_SRID_RLP) + + flatten_geometry = g.coord_dim > 2 + if flatten_geometry: + g = self.__flatten_geom_to_2D(g) + + if g.geom_type not in accepted_ogr_types: self.add_error("geom", _("Only surfaces allowed. Points or lines must be buffered.")) is_valid = False return is_valid @@ -131,3 +144,13 @@ class SimpleGeomForm(BaseForm): # Start the parcel update procedure in a background process celery_update_parcels.delay(geometry.id) return geometry + + def __flatten_geom_to_2D(self, geom): + """ + Enforces a given OGRGeometry from higher dimensions into 2D + + """ + wkt_w = WKTWriter(dim=2) + g_wkt = wkt_w.write(geom.geos).decode("utf-8") + geom = gdal.OGRGeometry(g_wkt) + return geom diff --git a/konova/models/object.py b/konova/models/object.py index 015d34ef..a1c8c9c2 100644 --- a/konova/models/object.py +++ b/konova/models/object.py @@ -434,8 +434,16 @@ class CheckableObjectMixin(models.Model): class ShareableObjectMixin(models.Model): # Users having access on this object - users = models.ManyToManyField("user.User", help_text="Users having access (data shared with)") - teams = models.ManyToManyField("user.Team", help_text="Teams having access (data shared with)") + users = models.ManyToManyField( + "user.User", + help_text="Users having access (data shared with)", + blank=True + ) + teams = models.ManyToManyField( + "user.Team", + help_text="Teams having access (data shared with)", + blank=True + ) access_token = models.CharField( max_length=255, null=True, diff --git a/konova/tests/test_views.py b/konova/tests/test_views.py index 84a4238b..437f114a 100644 --- a/konova/tests/test_views.py +++ b/konova/tests/test_views.py @@ -115,6 +115,19 @@ class BaseTestCase(TestCase): """ return f"{prefix}{generate_random_string(3, True)}" + def create_dummy_document(self, DocumentModel, instance): + """ Creates a document db entry which can be used for tests + + """ + doc = DocumentModel.objects.create( + title="TEST_doc", + comment="", + file=None, + date_of_creation="1970-01-01", + instance=instance, + ) + return doc + def create_dummy_intervention(self): """ Creates an intervention which can be used for tests diff --git a/konova/utils/mailer.py b/konova/utils/mailer.py index 8de91198..65bbbe61 100644 --- a/konova/utils/mailer.py +++ b/konova/utils/mailer.py @@ -92,11 +92,14 @@ class Mailer: msg ) - def send_mail_shared_access_given_team(self, obj_identifier, obj_title, team): + def send_mail_shared_access_given_team(self, obj_identifier, obj_title, team, users_to_notify): """ Send a mail if a team just got access to the object Args: obj_identifier (str): The object identifier + obj_title (str): Title of the main object + team (Team): Team to be notified + users_to_notify (QueryDict): Contains the team users which should be notified Returns: @@ -108,18 +111,21 @@ class Mailer: "EMAIL_REPLY_TO": EMAIL_REPLY_TO, } msg = render_to_string("email/sharing/shared_access_given_team.html", context) - user_mail_address = team.users.values_list("email", flat=True) + user_mail_address = users_to_notify.values_list("email", flat=True) self.send( user_mail_address, _("{} - Shared access given").format(obj_identifier), msg ) - def send_mail_shared_access_removed_team(self, obj_identifier, obj_title, team): + def send_mail_shared_access_removed_team(self, obj_identifier, obj_title, team, users_to_notify): """ Send a mail if a team just lost access to the object Args: obj_identifier (str): The object identifier + obj_title (str): Title of the main object + team (Team): Team to be notified + users_to_notify (QueryDict): Contains the team users which should be notified Returns: @@ -131,18 +137,21 @@ class Mailer: "EMAIL_REPLY_TO": EMAIL_REPLY_TO, } msg = render_to_string("email/sharing/shared_access_removed_team.html", context) - user_mail_address = team.users.values_list("email", flat=True) + user_mail_address = users_to_notify.values_list("email", flat=True) self.send( user_mail_address, _("{} - Shared access removed").format(obj_identifier), msg ) - def send_mail_shared_data_unrecorded_team(self, obj_identifier, obj_title, team): + def send_mail_shared_data_unrecorded_team(self, obj_identifier, obj_title, team, users_to_notify): """ Send a mail if data has just been unrecorded Args: obj_identifier (str): The object identifier + obj_title (str): Title of the main object + team (Team): Team to be notified + users_to_notify (QueryDict): Contains the team users which should be notified Returns: @@ -154,18 +163,21 @@ class Mailer: "EMAIL_REPLY_TO": EMAIL_REPLY_TO, } msg = render_to_string("email/recording/shared_data_unrecorded_team.html", context) - user_mail_address = team.users.values_list("email", flat=True) + user_mail_address = users_to_notify.values_list("email", flat=True) self.send( user_mail_address, _("{} - Shared data unrecorded").format(obj_identifier), msg ) - def send_mail_shared_data_recorded_team(self, obj_identifier, obj_title, team): + def send_mail_shared_data_recorded_team(self, obj_identifier, obj_title, team, users_to_notify): """ Send a mail if data has just been recorded Args: obj_identifier (str): The object identifier + obj_title (str): Title of the main object + team (Team): Team to be notified + users_to_notify (QueryDict): Contains the team users which should be notified Returns: @@ -177,18 +189,21 @@ class Mailer: "EMAIL_REPLY_TO": EMAIL_REPLY_TO, } msg = render_to_string("email/recording/shared_data_recorded_team.html", context) - user_mail_address = team.users.values_list("email", flat=True) + user_mail_address = users_to_notify.values_list("email", flat=True) self.send( user_mail_address, _("{} - Shared data recorded").format(obj_identifier), msg ) - def send_mail_shared_data_checked_team(self, obj_identifier, obj_title, team): + def send_mail_shared_data_checked_team(self, obj_identifier, obj_title, team, users_to_notify): """ Send a mail if data has just been checked Args: obj_identifier (str): The object identifier + obj_title (str): Title of the main object + team (Team): Team to be notified + users_to_notify (QueryDict): Contains the team users which should be notified Returns: @@ -200,14 +215,14 @@ class Mailer: "EMAIL_REPLY_TO": EMAIL_REPLY_TO, } msg = render_to_string("email/checking/shared_data_checked_team.html", context) - user_mail_address = team.users.values_list("email", flat=True) + user_mail_address = users_to_notify.values_list("email", flat=True) self.send( user_mail_address, _("{} - Shared data checked").format(obj_identifier), msg ) - def send_mail_deduction_changed_team(self, obj_identifier, obj_title, team, data_changes): + def send_mail_deduction_changed_team(self, obj_identifier, obj_title, team, data_changes, users_to_notify): """ Send a mail if deduction has been changed Args: @@ -215,7 +230,7 @@ class Mailer: obj_title (str): Title of the main object team (Team): Team to be notified data_changes (dict): Contains the old|new changes of the deduction changes - + users_to_notify (QueryDict): Contains the team users which should be notified Returns: """ @@ -227,14 +242,14 @@ class Mailer: "data_changes": data_changes, } msg = render_to_string("email/other/deduction_changed_team.html", context) - user_mail_address = team.users.values_list("email", flat=True) + user_mail_address = users_to_notify.values_list("email", flat=True) self.send( user_mail_address, _("{} - Deduction changed").format(obj_identifier), msg ) - def send_mail_shared_data_deleted_team(self, obj_identifier, obj_title, team): + def send_mail_shared_data_deleted_team(self, obj_identifier, obj_title, team, users_to_notify): """ Send a mail if data has just been deleted Args: @@ -250,7 +265,7 @@ class Mailer: "EMAIL_REPLY_TO": EMAIL_REPLY_TO, } msg = render_to_string("email/deleting/shared_data_deleted_team.html", context) - user_mail_address = team.users.values_list("email", flat=True) + user_mail_address = users_to_notify.values_list("email", flat=True) self.send( user_mail_address, _("{} - Shared data deleted").format(obj_identifier), diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index 9f50fc49..422ac7b6 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 9e41e2b3..96bfa072 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -43,7 +43,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-10-11 16:39+0200\n" +"POT-Creation-Date: 2022-11-16 13:36+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -204,7 +204,7 @@ msgstr "Geprüft" #: compensation/templates/compensation/detail/compensation/view.html:93 #: 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 +#: ema/tables.py:41 ema/templates/ema/detail/view.html:35 #: intervention/tables.py:44 #: intervention/templates/intervention/detail/view.html:87 #: user/models/user_action.py:22 @@ -357,7 +357,7 @@ msgid "Show only unrecorded" msgstr "Nur unverzeichnete anzeigen" #: compensation/forms/compensation.py:30 compensation/tables/compensation.py:23 -#: compensation/tables/eco_account.py:23 ema/tables.py:29 +#: compensation/tables/eco_account.py:23 ema/tables.py:26 #: intervention/forms/intervention.py:29 intervention/tables.py:23 #: intervention/templates/intervention/detail/includes/compensations.html:30 msgid "Identifier" @@ -376,12 +376,12 @@ msgstr "Automatisch generiert" #: compensation/templates/compensation/detail/eco_account/view.html:32 #: compensation/templates/compensation/report/compensation/report.html:12 #: compensation/templates/compensation/report/eco_account/report.html:12 -#: ema/tables.py:34 ema/templates/ema/detail/includes/documents.html:28 +#: ema/tables.py:31 ema/templates/ema/detail/includes/documents.html:28 #: ema/templates/ema/detail/view.html:31 #: ema/templates/ema/report/report.html:12 #: intervention/forms/intervention.py:41 intervention/tables.py:28 #: intervention/templates/intervention/detail/includes/compensations.html:33 -#: intervention/templates/intervention/detail/includes/documents.html:28 +#: intervention/templates/intervention/detail/includes/documents.html:33 #: intervention/templates/intervention/detail/view.html:31 #: intervention/templates/intervention/report/report.html:12 #: konova/forms/modals/document_form.py:24 @@ -411,7 +411,7 @@ msgstr "Kompensation XY; Flur ABC" #: ema/templates/ema/detail/includes/documents.html:34 #: intervention/forms/intervention.py:199 #: intervention/forms/modals/revocation.py:45 -#: intervention/templates/intervention/detail/includes/documents.html:34 +#: intervention/templates/intervention/detail/includes/documents.html:39 #: intervention/templates/intervention/detail/includes/payments.html:34 #: intervention/templates/intervention/detail/includes/revocation.html:38 #: konova/forms/modals/document_form.py:59 @@ -473,13 +473,15 @@ msgstr "Neues Ökokonto" msgid "Eco-Account XY; Location ABC" msgstr "Ökokonto XY; Flur ABC" -#: compensation/forms/eco_account.py:141 +#: compensation/forms/eco_account.py:143 msgid "Edit Eco-Account" msgstr "Ökokonto bearbeiten" -#: compensation/forms/eco_account.py:224 +#: compensation/forms/eco_account.py:228 msgid "The account can not be removed, since there are still deductions." -msgstr "Das Ökokonto kann nicht entfernt werden, da hierzu noch Abbuchungen vorliegen." +msgstr "" +"Das Ökokonto kann nicht entfernt werden, da hierzu noch Abbuchungen " +"vorliegen." #: compensation/forms/mixins.py:37 #: compensation/templates/compensation/detail/eco_account/view.html:63 @@ -754,54 +756,42 @@ msgstr "" "wollen. Kontaktieren Sie die für die Abbuchungen verantwortlichen Nutzer!" #: compensation/tables/compensation.py:33 compensation/tables/eco_account.py:33 -#: ema/tables.py:39 intervention/tables.py:33 +#: ema/tables.py:36 intervention/tables.py:33 #: konova/filters/mixins/geo_reference.py:42 msgid "Parcel gmrkng" msgstr "Gemarkung" #: compensation/tables/compensation.py:50 compensation/tables/eco_account.py:54 -#: ema/tables.py:50 intervention/tables.py:50 +#: ema/tables.py:47 intervention/tables.py:50 msgid "Editable" msgstr "Freigegeben" #: compensation/tables/compensation.py:56 compensation/tables/eco_account.py:60 -#: ema/tables.py:56 intervention/tables.py:56 +#: ema/tables.py:53 intervention/tables.py:56 msgid "Last edit" msgstr "Zuletzt bearbeitet" #: compensation/tables/compensation.py:87 compensation/tables/eco_account.py:92 -#: ema/tables.py:89 intervention/tables.py:87 +#: ema/tables.py:86 intervention/tables.py:87 msgid "Open {}" msgstr "Öffne {}" -#: compensation/tables/compensation.py:163 +#: compensation/tables/compensation.py:141 #: compensation/templates/compensation/detail/compensation/view.html:96 #: compensation/templates/compensation/detail/eco_account/includes/deductions.html:58 #: compensation/templates/compensation/detail/eco_account/view.html:48 -#: ema/tables.py:130 ema/templates/ema/detail/view.html:38 -#: intervention/tables.py:161 +#: ema/tables.py:105 ema/templates/ema/detail/view.html:38 +#: intervention/tables.py:139 #: intervention/templates/intervention/detail/view.html:90 msgid "Not recorded yet" msgstr "Noch nicht verzeichnet" -#: compensation/tables/compensation.py:166 -#: compensation/tables/eco_account.py:153 ema/tables.py:133 -#: intervention/tables.py:164 +#: compensation/tables/compensation.py:144 +#: compensation/tables/eco_account.py:131 ema/tables.py:108 +#: intervention/tables.py:142 msgid "Recorded on {} by {}" msgstr "Am {} von {} verzeichnet worden" -#: compensation/tables/compensation.py:186 -#: compensation/tables/eco_account.py:175 ema/tables.py:154 -#: intervention/tables.py:185 -msgid "Full access granted" -msgstr "Für Sie freigegeben - Datensatz kann bearbeitet werden" - -#: compensation/tables/compensation.py:186 -#: compensation/tables/eco_account.py:175 ema/tables.py:154 -#: intervention/tables.py:185 -msgid "Access not granted" -msgstr "Nicht freigegeben - Datensatz nur lesbar" - #: compensation/tables/eco_account.py:38 #: compensation/templates/compensation/detail/eco_account/view.html:36 #: konova/templates/konova/widgets/progressbar.html:3 @@ -812,7 +802,7 @@ msgstr "Verfügbar" msgid "Eco Accounts" msgstr "Ökokonten" -#: compensation/tables/eco_account.py:150 +#: compensation/tables/eco_account.py:128 msgid "Not recorded yet. Can not be used for deductions, yet." msgstr "" "Noch nicht verzeichnet. Kann noch nicht für Abbuchungen genutzt werden." @@ -861,7 +851,7 @@ msgstr "Menge" #: ema/templates/ema/detail/includes/states-before.html:40 #: intervention/templates/intervention/detail/includes/compensations.html:38 #: intervention/templates/intervention/detail/includes/deductions.html:39 -#: intervention/templates/intervention/detail/includes/documents.html:39 +#: intervention/templates/intervention/detail/includes/documents.html:44 #: 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:33 @@ -995,7 +985,7 @@ msgstr "Neues Dokument hinzufügen" #: compensation/templates/compensation/detail/compensation/includes/documents.html:31 #: compensation/templates/compensation/detail/eco_account/includes/documents.html:31 #: ema/templates/ema/detail/includes/documents.html:31 -#: intervention/templates/intervention/detail/includes/documents.html:31 +#: intervention/templates/intervention/detail/includes/documents.html:36 #: konova/forms/modals/document_form.py:34 msgid "Created on" msgstr "Erstellt" @@ -1003,7 +993,7 @@ msgstr "Erstellt" #: compensation/templates/compensation/detail/compensation/includes/documents.html:63 #: compensation/templates/compensation/detail/eco_account/includes/documents.html:61 #: ema/templates/ema/detail/includes/documents.html:61 -#: intervention/templates/intervention/detail/includes/documents.html:65 +#: intervention/templates/intervention/detail/includes/documents.html:70 #: konova/forms/modals/document_form.py:139 msgid "Edit document" msgstr "Dokument bearbeiten" @@ -1011,7 +1001,7 @@ msgstr "Dokument bearbeiten" #: compensation/templates/compensation/detail/compensation/includes/documents.html:66 #: compensation/templates/compensation/detail/eco_account/includes/documents.html:64 #: ema/templates/ema/detail/includes/documents.html:64 -#: intervention/templates/intervention/detail/includes/documents.html:68 +#: intervention/templates/intervention/detail/includes/documents.html:73 msgid "Remove document" msgstr "Dokument löschen" @@ -1287,8 +1277,8 @@ msgid "Compensation {} edited" msgstr "Kompensation {} bearbeitet" #: compensation/views/compensation/compensation.py:185 -#: compensation/views/eco_account/eco_account.py:159 ema/views/ema.py:211 -#: intervention/views/intervention.py:225 +#: compensation/views/eco_account/eco_account.py:159 ema/views/ema.py:210 +#: intervention/views/intervention.py:228 msgid "Edit {}" msgstr "Bearbeite {}" @@ -1314,7 +1304,7 @@ msgstr "Ökokonto {} bearbeitet" msgid "Eco-account removed" msgstr "Ökokonto entfernt" -#: ema/forms.py:42 ema/views/ema.py:94 +#: ema/forms.py:42 ema/views/ema.py:93 msgid "New EMA" msgstr "Neue EMA hinzufügen" @@ -1322,11 +1312,11 @@ msgstr "Neue EMA hinzufügen" msgid "Edit EMA" msgstr "Bearbeite EMA" -#: ema/tables.py:65 templates/navbars/navbar.html:43 +#: ema/tables.py:62 templates/navbars/navbar.html:43 msgid "Payment funded compensations" msgstr "Ersatzzahlungsmaßnahmen (EMA)" -#: ema/tables.py:66 +#: ema/tables.py:63 msgid "EMA explanation" msgstr "" "EMA sind Kompensationen, die durch Ersatzzahlungen finanziert wurden. " @@ -1334,7 +1324,7 @@ msgstr "" "Maßnahmen aus Ersatzzahlungen, die nach 2015 rechtskräftig wurden, werden " "durch die Stiftung Natur und Umwelt verwaltet." -#: ema/tables.py:89 templates/navbars/navbar.html:43 +#: ema/tables.py:86 templates/navbars/navbar.html:43 msgid "EMA" msgstr "" @@ -1342,19 +1332,19 @@ msgstr "" msgid "Payment funded compensation" msgstr "Ersatzzahlungsmaßnahme" -#: ema/views/ema.py:51 +#: ema/views/ema.py:50 msgid "EMAs - Overview" msgstr "EMAs - Übersicht" -#: ema/views/ema.py:84 +#: ema/views/ema.py:83 msgid "EMA {} added" msgstr "EMA {} hinzugefügt" -#: ema/views/ema.py:201 +#: ema/views/ema.py:200 msgid "EMA {} edited" msgstr "EMA {} bearbeitet" -#: ema/views/ema.py:235 +#: ema/views/ema.py:234 msgid "EMA removed" msgstr "EMA entfernt" @@ -1564,6 +1554,13 @@ msgstr "Ökokonto gelöscht! Abbuchung ungültig!" msgid "Eco-account not recorded! Deduction invalid!" msgstr "Ökokonto nicht verzeichnet! Abbuchung ungültig!" +#: intervention/templates/intervention/detail/includes/documents.html:25 +msgid "" +"You entered a payment. Please upload the legal document which defines the " +"payment`s amount." +msgstr "" +"Sie haben Ersatzzahlungen angegeben. Laden Sie bitte den Zahlungsbescheid als Dokument hoch." + #: intervention/templates/intervention/detail/includes/payments.html:8 #: intervention/templates/intervention/report/report.html:69 msgid "Payments" @@ -1648,11 +1645,11 @@ msgstr "Eingriffe - Übersicht" msgid "Intervention {} added" msgstr "Eingriff {} hinzugefügt" -#: intervention/views/intervention.py:213 +#: intervention/views/intervention.py:216 msgid "Intervention {} edited" msgstr "Eingriff {} bearbeitet" -#: intervention/views/intervention.py:250 +#: intervention/views/intervention.py:253 msgid "{} removed" msgstr "{} entfernt" @@ -1686,7 +1683,7 @@ msgid "Search for file number" msgstr "Nach Aktenzeichen suchen" #: konova/filters/mixins/geo_reference.py:29 -#: konova/templates/konova/includes/parcels/parcel_table_frame.html:18 +#: konova/templates/konova/includes/parcels/parcel_table_frame.html:19 msgid "District" msgstr "Kreis" @@ -1699,7 +1696,7 @@ msgid "Search for parcel gmrkng" msgstr "Nach Gemarkung suchen" #: konova/filters/mixins/geo_reference.py:55 -#: konova/templates/konova/includes/parcels/parcel_table_frame.html:39 +#: konova/templates/konova/includes/parcels/parcel_table_frame.html:40 msgid "Parcel" msgstr "Flur" @@ -1708,7 +1705,7 @@ msgid "Search for parcel" msgstr "Nach Flur suchen" #: konova/filters/mixins/geo_reference.py:68 -#: konova/templates/konova/includes/parcels/parcel_table_frame.html:40 +#: konova/templates/konova/includes/parcels/parcel_table_frame.html:41 msgid "Parcel counter" msgstr "Flurstückzähler" @@ -1717,7 +1714,7 @@ msgid "Search for parcel counter" msgstr "Nach Flurstückzähler suchen" #: konova/filters/mixins/geo_reference.py:82 -#: konova/templates/konova/includes/parcels/parcel_table_frame.html:41 +#: konova/templates/konova/includes/parcels/parcel_table_frame.html:42 msgid "Parcel number" msgstr "Flurstücknenner" @@ -1873,37 +1870,37 @@ msgstr "" msgid "English" msgstr "" -#: konova/templates/konova/includes/parcels/parcel_table_frame.html:5 +#: konova/templates/konova/includes/parcels/parcel_table_frame.html:6 msgid "Parcels can not be calculated, since no geometry is given." msgstr "" "Flurstücke können nicht berechnet werden, da keine Geometrie eingegeben " "wurde." -#: konova/templates/konova/includes/parcels/parcel_table_frame.html:11 +#: konova/templates/konova/includes/parcels/parcel_table_frame.html:12 msgid "Parcels found" msgstr "Flurstücke" -#: konova/templates/konova/includes/parcels/parcel_table_frame.html:16 +#: konova/templates/konova/includes/parcels/parcel_table_frame.html:17 msgid "Municipal" msgstr "Gemeinde" -#: konova/templates/konova/includes/parcels/parcel_table_frame.html:17 +#: konova/templates/konova/includes/parcels/parcel_table_frame.html:18 msgid "Municipal key" msgstr "Gemeindeschlüssel" -#: konova/templates/konova/includes/parcels/parcel_table_frame.html:19 +#: konova/templates/konova/includes/parcels/parcel_table_frame.html:20 msgid "District key" msgstr "Kreisschlüssel" -#: konova/templates/konova/includes/parcels/parcel_table_frame.html:37 +#: konova/templates/konova/includes/parcels/parcel_table_frame.html:38 msgid "Parcel group" msgstr "Gemarkung" -#: konova/templates/konova/includes/parcels/parcel_table_frame.html:38 +#: konova/templates/konova/includes/parcels/parcel_table_frame.html:39 msgid "Parcel group key" msgstr "Gemarkungsschlüssel" -#: konova/templates/konova/includes/parcels/parcels.html:7 +#: konova/templates/konova/includes/parcels/parcels.html:9 msgid "Spatial reference" msgstr "Raumreferenz" @@ -1951,39 +1948,39 @@ msgstr "In Zwischenablage kopiert" msgid "Search" msgstr "Suchen" -#: konova/utils/mailer.py:68 konova/utils/mailer.py:137 +#: konova/utils/mailer.py:68 konova/utils/mailer.py:143 msgid "{} - Shared access removed" msgstr "{} - Zugriff entzogen" -#: konova/utils/mailer.py:91 konova/utils/mailer.py:114 +#: konova/utils/mailer.py:91 konova/utils/mailer.py:117 msgid "{} - Shared access given" msgstr "{} - Zugriff freigegeben" -#: konova/utils/mailer.py:160 konova/utils/mailer.py:302 +#: konova/utils/mailer.py:169 konova/utils/mailer.py:317 msgid "{} - Shared data unrecorded" msgstr "{} - Freigegebene Daten entzeichnet" -#: konova/utils/mailer.py:183 konova/utils/mailer.py:279 +#: konova/utils/mailer.py:195 konova/utils/mailer.py:294 msgid "{} - Shared data recorded" msgstr "{} - Freigegebene Daten verzeichnet" -#: konova/utils/mailer.py:206 konova/utils/mailer.py:348 +#: konova/utils/mailer.py:221 konova/utils/mailer.py:363 msgid "{} - Shared data checked" msgstr "{} - Freigegebene Daten geprüft" -#: konova/utils/mailer.py:233 konova/utils/mailer.py:376 +#: konova/utils/mailer.py:248 konova/utils/mailer.py:391 msgid "{} - Deduction changed" msgstr "{} - Abbuchung geändert" -#: konova/utils/mailer.py:256 konova/utils/mailer.py:325 +#: konova/utils/mailer.py:271 konova/utils/mailer.py:340 msgid "{} - Shared data deleted" msgstr "{} - Freigegebene Daten gelöscht" -#: konova/utils/mailer.py:397 templates/email/api/verify_token.html:4 +#: konova/utils/mailer.py:412 templates/email/api/verify_token.html:4 msgid "Request for new API token" msgstr "Anfrage für neuen API Token" -#: konova/utils/mailer.py:420 +#: konova/utils/mailer.py:435 msgid "Resubmission - {}" msgstr "Wiedervorlage - {}" @@ -2225,7 +2222,15 @@ msgstr "{} wurde erfolgreich vom Nutzer {} geprüft! {}" msgid "missing" msgstr "fehlend" -#: konova/views/home.py:78 templates/navbars/navbar.html:16 +#: konova/utils/tables.py:218 +msgid "Full access granted" +msgstr "Für Sie freigegeben - Datensatz kann bearbeitet werden" + +#: konova/utils/tables.py:218 +msgid "Access not granted" +msgstr "Nicht freigegeben - Datensatz nur lesbar" + +#: konova/views/home.py:79 templates/navbars/navbar.html:16 msgid "Home" msgstr "Home" @@ -2670,7 +2675,7 @@ msgstr "" msgid "User" msgstr "Nutzer" -#: templates/map/geom_form.html:9 +#: templates/map/geom_form.html:11 templates/table/gmrkng_col.html:4 msgid "No geometry added, yet." msgstr "Keine Geometrie vorhanden" @@ -2725,7 +2730,7 @@ msgstr "" "vorbei. \n" " " -#: templates/table/gmrkng_col.html:6 +#: templates/table/gmrkng_col.html:12 msgid "" "If the geometry is not empty, the parcels are currently recalculated. Please " "refresh this page in a few moments." @@ -2897,35 +2902,27 @@ msgstr "" " " #: user/templates/user/index.html:42 -msgid "Change default configuration for your KSP map" -msgstr "Karteneinstellungen ändern" - -#: user/templates/user/index.html:45 -msgid "Map settings" -msgstr "Karte" - -#: user/templates/user/index.html:50 msgid "Change notification configurations" msgstr "Benachrichtigungseinstellungen ändern" -#: user/templates/user/index.html:53 +#: user/templates/user/index.html:45 msgid "Notification settings" msgstr "Benachrichtigungen" -#: user/templates/user/index.html:58 +#: user/templates/user/index.html:50 msgid "Manage teams" msgstr "" -#: user/templates/user/index.html:61 user/templates/user/team/index.html:19 +#: user/templates/user/index.html:53 user/templates/user/team/index.html:19 #: user/views.py:171 msgid "Teams" msgstr "" -#: user/templates/user/index.html:66 +#: user/templates/user/index.html:58 msgid "See or edit your API token" msgstr "API token einsehen oder neu generieren" -#: user/templates/user/index.html:69 +#: user/templates/user/index.html:61 msgid "API" msgstr "" @@ -4508,6 +4505,12 @@ msgstr "" msgid "Unable to connect to qpid with SASL mechanism %s" msgstr "" +#~ msgid "Change default configuration for your KSP map" +#~ msgstr "Karteneinstellungen ändern" + +#~ msgid "Map settings" +#~ msgstr "Karte" + #~ msgid "There are errors on this intervention:" #~ msgstr "Es liegen Fehler in diesem Eingriff vor:" diff --git a/user/autocomplete/team.py b/user/autocomplete/team.py index 377c693e..c78053c8 100644 --- a/user/autocomplete/team.py +++ b/user/autocomplete/team.py @@ -17,15 +17,15 @@ class TeamAdminAutocomplete(Select2QuerySetView): 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 + username__icontains=self.q ) qs = qs.order_by( "username" diff --git a/user/models/team.py b/user/models/team.py index 7162e977..b4bc26c1 100644 --- a/user/models/team.py +++ b/user/models/team.py @@ -2,6 +2,7 @@ from django.db import models from konova.models import UuidModel, DeletableObjectMixin from konova.utils.mailer import Mailer +from user.enums import UserNotificationEnum from user.models import UserActionLogEntry @@ -41,7 +42,10 @@ class Team(UuidModel, DeletableObjectMixin): """ mailer = Mailer() - mailer.send_mail_shared_access_given_team(obj_identifier, obj_title, self) + users_to_notify = self.users.filter( + notifications__in=[UserNotificationEnum.NOTIFY_ON_SHARED_ACCESS_GAINED.value] + ) + mailer.send_mail_shared_access_given_team(obj_identifier, obj_title, self, users_to_notify) def send_mail_shared_access_removed(self, obj_identifier, obj_title): """ Sends a mail to the team members in case of removed shared access @@ -54,7 +58,10 @@ class Team(UuidModel, DeletableObjectMixin): """ mailer = Mailer() - mailer.send_mail_shared_access_removed_team(obj_identifier, obj_title, self) + users_to_notify = self.users.filter( + notifications__in=[UserNotificationEnum.NOTIFY_ON_SHARED_ACCESS_REMOVED.value] + ) + mailer.send_mail_shared_access_removed_team(obj_identifier, obj_title, self, users_to_notify) def send_mail_shared_data_unrecorded(self, obj_identifier, obj_title): """ Sends a mail to the team members in case of unrecorded data @@ -67,7 +74,10 @@ class Team(UuidModel, DeletableObjectMixin): """ mailer = Mailer() - mailer.send_mail_shared_data_unrecorded_team(obj_identifier, obj_title, self) + users_to_notify = self.users.filter( + notifications__in=[UserNotificationEnum.NOTIFY_ON_SHARED_DATA_RECORDED.value] + ) + mailer.send_mail_shared_data_unrecorded_team(obj_identifier, obj_title, self, users_to_notify) def send_mail_shared_data_recorded(self, obj_identifier, obj_title): """ Sends a mail to the team members in case of unrecorded data @@ -80,7 +90,10 @@ class Team(UuidModel, DeletableObjectMixin): """ mailer = Mailer() - mailer.send_mail_shared_data_recorded_team(obj_identifier, obj_title, self) + users_to_notify = self.users.filter( + notifications__in=[UserNotificationEnum.NOTIFY_ON_SHARED_DATA_RECORDED.value] + ) + mailer.send_mail_shared_data_recorded_team(obj_identifier, obj_title, self, users_to_notify) def send_mail_shared_data_checked(self, obj_identifier, obj_title): """ Sends a mail to the team members in case of checked data @@ -93,7 +106,10 @@ class Team(UuidModel, DeletableObjectMixin): """ mailer = Mailer() - mailer.send_mail_shared_data_checked_team(obj_identifier, obj_title, self) + users_to_notify = self.users.filter( + notifications__in=[UserNotificationEnum.NOTIFY_ON_SHARED_DATA_CHECKED.value] + ) + mailer.send_mail_shared_data_checked_team(obj_identifier, obj_title, self, users_to_notify) def send_mail_deduction_changed(self, obj_identifier, obj_title, data_changes): """ Sends a mail to the team members in case of changed deduction values @@ -107,7 +123,10 @@ class Team(UuidModel, DeletableObjectMixin): """ mailer = Mailer() - mailer.send_mail_deduction_changed_team(obj_identifier, obj_title, self, data_changes) + users_to_notify = self.users.filter( + notifications__in=[UserNotificationEnum.NOTIFY_ON_DEDUCTION_CHANGES.value] + ) + mailer.send_mail_deduction_changed_team(obj_identifier, obj_title, self, data_changes, users_to_notify) def send_mail_shared_data_deleted(self, obj_identifier, obj_title): """ Sends a mail to the team members in case of deleted data @@ -120,7 +139,10 @@ class Team(UuidModel, DeletableObjectMixin): """ mailer = Mailer() - mailer.send_mail_shared_data_deleted_team(obj_identifier, obj_title, self) + users_to_notify = self.users.filter( + notifications__in=[UserNotificationEnum.NOTIFY_ON_SHARED_DATA_DELETED.value] + ) + mailer.send_mail_shared_data_deleted_team(obj_identifier, obj_title, self, users_to_notify) def remove_user(self, user): """ Removes a user from the team