Delete account #456

This commit is contained in:
Daniel Grams 2023-05-01 19:53:01 +02:00
parent df99e1d281
commit 235c41531e
44 changed files with 1457 additions and 177 deletions

View File

@ -2,16 +2,11 @@
"editor.formatOnSave": true,
"python.pythonPath": "./env/bin/python3",
"python.formatting.provider": "black",
"python.sortImports.args": [
"-sp .isort.cfg"
],
"isort.args": ["-sp", ".isort.cfg"],
"python.linting.enabled": true,
"python.linting.pylintEnabled": false,
"python.linting.flake8Enabled": true,
"python.testing.pytestArgs": [
"tests",
"--capture=sys"
],
"python.testing.pytestArgs": ["tests", "--capture=sys"],
"python.testing.unittestEnabled": false,
"python.testing.nosetestsEnabled": false,
"python.testing.pytestEnabled": true,

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2023-04-27 15:10+0200\n"
"POT-Creation-Date: 2023-05-01 18:11+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -194,7 +194,7 @@ msgid "message"
msgstr ""
#: project/api/organization/resources.py:401
#: project/views/admin_unit_member_invitation.py:85
#: project/views/admin_unit_member_invitation.py:89
msgid "You have received an invitation"
msgstr ""
@ -211,7 +211,7 @@ msgstr ""
#: project/forms/admin.py:13 project/templates/_macros.html:1473
#: project/templates/layout.html:311
#: project/templates/widget/event_suggestion/create.html:204
#: project/views/admin_unit.py:82 project/views/root.py:71
#: project/views/admin_unit.py:83 project/views/root.py:71
msgid "Contact"
msgstr ""
@ -247,6 +247,7 @@ msgstr ""
#: project/forms/admin_unit_member.py:11 project/forms/admin_unit_member.py:23
#: project/forms/admin_unit_member.py:28 project/forms/event.py:107
#: project/forms/event_suggestion.py:38 project/forms/organizer.py:27
#: project/forms/user.py:18 project/forms/user.py:23
#: project/templates/_macros.html:237 project/templates/_macros.html:1569
#: project/templates/admin/admin.html:27 project/templates/admin/email.html:4
#: project/templates/admin/email.html:66 project/templates/admin/users.html:19
@ -474,14 +475,15 @@ msgstr ""
msgid "Link Color"
msgstr ""
#: project/forms/admin_unit.py:133
#: project/forms/admin_unit.py:133 project/forms/user.py:17
msgid "Request deletion"
msgstr ""
#: project/forms/admin_unit.py:138
#: project/forms/admin_unit.py:138 project/forms/user.py:22
#: project/templates/admin_unit/cancel_deletion.html:6
#: project/templates/admin_unit/update.html:26
#: project/templates/manage/events.html:49
#: project/templates/manage/events.html:49 project/templates/profile.html:13
#: project/templates/user/cancel_deletion.html:6
msgid "Cancel deletion"
msgstr ""
@ -1324,7 +1326,7 @@ msgstr ""
#: project/templates/_macros.html:590 project/templates/_macros.html:633
#: project/templates/_macros.html:765
#: project/templates/admin/admin_units.html:36
#: project/templates/admin/users.html:32
#: project/templates/admin/users.html:34
#: project/templates/manage/events.html:116
#: project/templates/manage/members.html:35
#: project/templates/manage/organizers.html:33
@ -1539,7 +1541,7 @@ msgstr ""
#: project/templates/oauth2_client/list.html:10
#: project/templates/oauth2_client/read.html:10
#: project/templates/oauth2_token/list.html:10 project/templates/profile.html:4
#: project/templates/profile.html:10
#: project/templates/profile.html:17
msgid "Profile"
msgstr ""
@ -1641,7 +1643,7 @@ msgstr ""
#: project/templates/admin_unit/update.html:6
#: project/templates/admin_unit/update.html:30
#: project/templates/layout.html:259 project/templates/manage/widgets.html:11
#: project/templates/manage/widgets.html:15 project/templates/profile.html:19
#: project/templates/manage/widgets.html:15 project/templates/profile.html:32
msgid "Settings"
msgstr ""
@ -1667,24 +1669,34 @@ msgstr ""
#: project/templates/developer/read.html:4
#: project/templates/developer/read.html:8 project/templates/layout.html:319
#: project/templates/profile.html:33
#: project/templates/profile.html:46
msgid "Developer"
msgstr ""
#: project/templates/profile.html:23
#: project/templates/profile.html:12 project/views/admin_unit.py:89
#: project/views/admin_unit_member_invitation.py:39
msgid "Your account is scheduled for deletion."
msgstr ""
#: project/templates/profile.html:26
#: project/templates/user/request_deletion.html:6
msgid "Delete account"
msgstr ""
#: project/templates/profile.html:36
#: project/templates/user/notifications.html:4
#: project/templates/user/notifications.html:8
msgid "Notifications"
msgstr ""
#: project/templates/profile.html:27
#: project/templates/profile.html:40
msgid "Applications"
msgstr ""
#: project/templates/oauth2_client/list.html:4
#: project/templates/oauth2_client/list.html:11
#: project/templates/oauth2_client/read.html:11
#: project/templates/profile.html:37
#: project/templates/profile.html:50
msgid "OAuth2 clients"
msgstr ""
@ -1702,7 +1714,7 @@ msgid "View"
msgstr ""
#: project/templates/admin/admin_units.html:37
#: project/templates/admin/users.html:33
#: project/templates/admin/users.html:35
#: project/templates/manage/events.html:117
#: project/templates/manage/members.html:21
#: project/templates/manage/members.html:36
@ -1714,6 +1726,8 @@ msgid "Delete"
msgstr ""
#: project/templates/admin/delete_user.html:13
#: project/templates/user/cancel_deletion.html:13
#: project/templates/user/request_deletion.html:15
msgid "User"
msgstr ""
@ -1800,7 +1814,6 @@ msgid "You have been invited to join %(admin_unit_name)s."
msgstr ""
#: project/templates/email/invitation_notice.html:5
#: project/templates/email/organization_deletion_requested_notice.html:5
#: project/templates/email/organization_invitation_notice.html:5
msgid "Click here to view the invitation"
msgstr ""
@ -1823,6 +1836,10 @@ msgstr ""
msgid "%(admin_unit_name)s is scheduled for deletion."
msgstr ""
#: project/templates/email/organization_deletion_requested_notice.html:5
msgid "Click here below to cancel the deletion"
msgstr ""
#: project/templates/email/organization_invitation_accepted_notice.html:4
#, python-format
msgid ""
@ -1889,6 +1906,15 @@ msgstr ""
msgid "Click here to open the site"
msgstr ""
#: project/templates/email/user_deletion_requested_notice.html:4
#, python-format
msgid "%(user_email)s is scheduled for deletion."
msgstr ""
#: project/templates/email/user_deletion_requested_notice.html:5
msgid "Click the link below to cancel the deletion"
msgstr ""
#: project/templates/event/actions.html:5
#: project/templates/event/actions.html:22
msgid "Actions for event"
@ -2205,6 +2231,12 @@ msgstr ""
msgid "Register for free"
msgstr ""
#: project/templates/user/request_deletion.html:8
msgid ""
"The account is not deleted immediately. After a period of time, the "
"account will be deleted. Until then, the deletion can be canceled."
msgstr ""
#: project/templates/widget/event_date/list.html:5
msgid "Widget"
msgstr ""
@ -2229,8 +2261,8 @@ msgstr ""
msgid "Organization successfully updated"
msgstr ""
#: project/views/admin.py:85 project/views/admin_unit.py:182
#: project/views/admin_unit.py:215
#: project/views/admin.py:85 project/views/admin_unit.py:187
#: project/views/admin_unit.py:220
msgid "Entered name does not match organization name"
msgstr ""
@ -2239,7 +2271,7 @@ msgid "Organization successfully deleted"
msgstr ""
#: project/views/admin.py:113 project/views/manage.py:432
#: project/views/user.py:28
#: project/views/user.py:40
msgid "Settings successfully updated"
msgstr ""
@ -2261,29 +2293,29 @@ msgstr ""
msgid "Entered email does not match user email"
msgstr ""
#: project/views/admin.py:237
#: project/views/admin.py:236
msgid "User successfully deleted"
msgstr ""
#: project/views/admin_unit.py:78
#: project/views/admin_unit.py:79
msgid ""
"Organizations cannot currently be created. The project is in a closed "
"test phase. If you are interested, you can contact us."
msgstr ""
#: project/views/admin_unit.py:128
#: project/views/admin_unit.py:133
msgid "Organization successfully created"
msgstr ""
#: project/views/admin_unit.py:158
#: project/views/admin_unit.py:163
msgid "AdminUnit successfully updated"
msgstr ""
#: project/views/admin_unit.py:245
#: project/views/admin_unit.py:250
msgid "Organization invitation accepted"
msgstr ""
#: project/views/admin_unit.py:259
#: project/views/admin_unit.py:264
msgid "Organization deletion requested"
msgstr ""
@ -2299,23 +2331,23 @@ msgstr ""
msgid "Member successfully deleted"
msgstr ""
#: project/views/admin_unit_member_invitation.py:38
#: project/views/admin_unit_member_invitation.py:42
msgid "Invitation successfully accepted"
msgstr ""
#: project/views/admin_unit_member_invitation.py:45
#: project/views/admin_unit_member_invitation.py:49
msgid "Invitation successfully declined"
msgstr ""
#: project/views/admin_unit_member_invitation.py:90
#: project/views/admin_unit_member_invitation.py:94
msgid "Invitation successfully sent"
msgstr ""
#: project/views/admin_unit_member_invitation.py:113
#: project/views/admin_unit_member_invitation.py:117
msgid "Entered email does not match invitation email"
msgstr ""
#: project/views/admin_unit_member_invitation.py:118
#: project/views/admin_unit_member_invitation.py:122
msgid "Invitation successfully deleted"
msgstr ""
@ -2470,6 +2502,14 @@ msgid ""
"verified automatically."
msgstr ""
#: project/views/user.py:84 project/views/user.py:111
msgid "Entered email does not match your email"
msgstr ""
#: project/views/user.py:130
msgid "User deletion requested"
msgstr ""
#: project/views/utils.py:71
msgid ""
"An entry with the entered values already exists. Duplicate entries are "

View File

@ -50,4 +50,5 @@ format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
[alembic:exclude]
tables = spatial_ref_sys
tables = spatial_ref_sys,layer,topology
indexes = idx_event_description,idx_event_fulltext,idx_event_name,idx_event_tags,idx_location_coordinate

View File

@ -44,19 +44,23 @@ def get_metadata():
return target_db.metadata
def exclude_tables_from_config(config_):
tables_ = config_.get("tables", None)
if tables_ is not None:
tables = tables_.split(",")
return tables
def exclude_items_from_config(section, key):
items_ = section.get(key, None)
if items_ is not None:
items = items_.split(",")
return items
exclude_tables = exclude_tables_from_config(config.get_section("alembic:exclude"))
config_exclude_section = config.get_section("alembic:exclude")
exclude_tables = exclude_items_from_config(config_exclude_section, "tables")
exclude_indexes = exclude_items_from_config(config_exclude_section, "indexes")
def include_object(object, name, type_, reflected, compare_to):
if type_ == "table" and name in exclude_tables:
return False
elif type_ == "index" and name in exclude_indexes:
return False
else:
return True

View File

@ -0,0 +1,65 @@
"""empty message
Revision ID: 58d8aae621e6
Revises: cbac4166f9c0
Create Date: 2023-04-28 11:26:33.237832
"""
import sqlalchemy as sa
import sqlalchemy_utils
from alembic import op
from project import dbtypes
# revision identifiers, used by Alembic.
revision = "58d8aae621e6"
down_revision = "cbac4166f9c0"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_unique_constraint(
op.f("uq_eventlist_name"), "eventlist", ["name", "admin_unit_id"]
)
op.add_column(
"oauth2_token",
sa.Column(
"access_token_revoked_at", sa.Integer(), nullable=False, server_default="0"
),
)
op.add_column(
"oauth2_token",
sa.Column(
"refresh_token_revoked_at", sa.Integer(), nullable=False, server_default="0"
),
)
op.drop_column("oauth2_token", "revoked")
op.alter_column(
"user",
"newsletter_enabled",
existing_type=sa.BOOLEAN(),
nullable=True,
existing_server_default=sa.text("true"),
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column(
"user",
"newsletter_enabled",
existing_type=sa.BOOLEAN(),
nullable=False,
existing_server_default=sa.text("true"),
)
op.add_column(
"oauth2_token",
sa.Column("revoked", sa.BOOLEAN(), autoincrement=False, nullable=True),
)
op.drop_column("oauth2_token", "refresh_token_revoked_at")
op.drop_column("oauth2_token", "access_token_revoked_at")
op.drop_constraint(op.f("uq_eventlist_name"), "eventlist", type_="unique")
# ### end Alembic commands ###

View File

@ -0,0 +1,56 @@
"""empty message
Revision ID: 5fde26e1904d
Revises: 58d8aae621e6
Create Date: 2023-04-28 13:04:22.142011
"""
import sqlalchemy as sa
from alembic import op
from sqlalchemy.sql.elements import conv
from sqlalchemy.sql.naming import ConventionDict, _get_convention
from project import dbtypes
# revision identifiers, used by Alembic.
revision = "5fde26e1904d"
down_revision = "58d8aae621e6"
branch_labels = None
depends_on = None
def upgrade():
conn = op.get_bind()
ctx = op.get_context()
existing_metadata = sa.schema.MetaData()
existing_metadata.reflect(bind=conn)
target_metadata = ctx.opts["target_metadata"]
for table_name, table in existing_metadata.tables.items():
if table_name not in target_metadata.tables:
continue
for c in table.constraints:
existing_name = c.name
if not existing_name:
continue
convention = _get_convention(target_metadata.naming_convention, type(c))
if not convention:
continue
target_c_name = conv(
convention % ConventionDict(c, table, target_metadata.naming_convention)
)
if not target_c_name:
continue
if existing_name != target_c_name:
op.execute(
f"ALTER TABLE public.{table_name} RENAME CONSTRAINT {existing_name} to {target_c_name};"
)
def downgrade():
pass

View File

@ -0,0 +1,596 @@
"""empty message
Revision ID: cceaf9b28134
Revises: d952cd5df596
Create Date: 2023-05-01 18:02:53.515904
"""
import sqlalchemy as sa
import sqlalchemy_utils
from alembic import op
from project import dbtypes
# revision identifiers, used by Alembic.
revision = "cceaf9b28134"
down_revision = "d952cd5df596"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(
"fk_adminunit_created_by_id_user", "adminunit", type_="foreignkey"
)
op.drop_constraint(
"fk_adminunit_updated_by_id_user", "adminunit", type_="foreignkey"
)
op.create_foreign_key(
op.f("fk_adminunit_updated_by_id_user"),
"adminunit",
"user",
["updated_by_id"],
["id"],
ondelete="SET NULL",
)
op.create_foreign_key(
op.f("fk_adminunit_created_by_id_user"),
"adminunit",
"user",
["created_by_id"],
["id"],
ondelete="SET NULL",
)
op.drop_constraint(
"fk_adminunitinvitation_updated_by_id_user",
"adminunitinvitation",
type_="foreignkey",
)
op.drop_constraint(
"fk_adminunitinvitation_created_by_id_user",
"adminunitinvitation",
type_="foreignkey",
)
op.create_foreign_key(
op.f("fk_adminunitinvitation_updated_by_id_user"),
"adminunitinvitation",
"user",
["updated_by_id"],
["id"],
ondelete="SET NULL",
)
op.create_foreign_key(
op.f("fk_adminunitinvitation_created_by_id_user"),
"adminunitinvitation",
"user",
["created_by_id"],
["id"],
ondelete="SET NULL",
)
op.drop_constraint(
"fk_adminunitrelation_updated_by_id_user",
"adminunitrelation",
type_="foreignkey",
)
op.drop_constraint(
"fk_adminunitrelation_created_by_id_user",
"adminunitrelation",
type_="foreignkey",
)
op.create_foreign_key(
op.f("fk_adminunitrelation_updated_by_id_user"),
"adminunitrelation",
"user",
["updated_by_id"],
["id"],
ondelete="SET NULL",
)
op.create_foreign_key(
op.f("fk_adminunitrelation_created_by_id_user"),
"adminunitrelation",
"user",
["created_by_id"],
["id"],
ondelete="SET NULL",
)
op.drop_constraint(
"fk_customwidget_updated_by_id_user", "customwidget", type_="foreignkey"
)
op.drop_constraint(
"fk_customwidget_created_by_id_user", "customwidget", type_="foreignkey"
)
op.create_foreign_key(
op.f("fk_customwidget_updated_by_id_user"),
"customwidget",
"user",
["updated_by_id"],
["id"],
ondelete="SET NULL",
)
op.create_foreign_key(
op.f("fk_customwidget_created_by_id_user"),
"customwidget",
"user",
["created_by_id"],
["id"],
ondelete="SET NULL",
)
op.drop_constraint("fk_event_updated_by_id_user", "event", type_="foreignkey")
op.drop_constraint("fk_event_created_by_id_user", "event", type_="foreignkey")
op.create_foreign_key(
op.f("fk_event_created_by_id_user"),
"event",
"user",
["created_by_id"],
["id"],
ondelete="SET NULL",
)
op.create_foreign_key(
op.f("fk_event_updated_by_id_user"),
"event",
"user",
["updated_by_id"],
["id"],
ondelete="SET NULL",
)
op.drop_constraint(
"fk_eventlist_created_by_id_user", "eventlist", type_="foreignkey"
)
op.drop_constraint(
"fk_eventlist_updated_by_id_user", "eventlist", type_="foreignkey"
)
op.create_foreign_key(
op.f("fk_eventlist_updated_by_id_user"),
"eventlist",
"user",
["updated_by_id"],
["id"],
ondelete="SET NULL",
)
op.create_foreign_key(
op.f("fk_eventlist_created_by_id_user"),
"eventlist",
"user",
["created_by_id"],
["id"],
ondelete="SET NULL",
)
op.drop_constraint(
"fk_eventorganizer_updated_by_id_user", "eventorganizer", type_="foreignkey"
)
op.drop_constraint(
"fk_eventorganizer_created_by_id_user", "eventorganizer", type_="foreignkey"
)
op.create_foreign_key(
op.f("fk_eventorganizer_updated_by_id_user"),
"eventorganizer",
"user",
["updated_by_id"],
["id"],
ondelete="SET NULL",
)
op.create_foreign_key(
op.f("fk_eventorganizer_created_by_id_user"),
"eventorganizer",
"user",
["created_by_id"],
["id"],
ondelete="SET NULL",
)
op.drop_constraint(
"fk_eventplace_created_by_id_user", "eventplace", type_="foreignkey"
)
op.drop_constraint(
"fk_eventplace_updated_by_id_user", "eventplace", type_="foreignkey"
)
op.create_foreign_key(
op.f("fk_eventplace_created_by_id_user"),
"eventplace",
"user",
["created_by_id"],
["id"],
ondelete="SET NULL",
)
op.create_foreign_key(
op.f("fk_eventplace_updated_by_id_user"),
"eventplace",
"user",
["updated_by_id"],
["id"],
ondelete="SET NULL",
)
op.drop_constraint(
"fk_eventreference_created_by_id_user", "eventreference", type_="foreignkey"
)
op.drop_constraint(
"fk_eventreference_updated_by_id_user", "eventreference", type_="foreignkey"
)
op.create_foreign_key(
op.f("fk_eventreference_updated_by_id_user"),
"eventreference",
"user",
["updated_by_id"],
["id"],
ondelete="SET NULL",
)
op.create_foreign_key(
op.f("fk_eventreference_created_by_id_user"),
"eventreference",
"user",
["created_by_id"],
["id"],
ondelete="SET NULL",
)
op.drop_constraint(
"fk_eventreferencerequest_updated_by_id_user",
"eventreferencerequest",
type_="foreignkey",
)
op.drop_constraint(
"fk_eventreferencerequest_created_by_id_user",
"eventreferencerequest",
type_="foreignkey",
)
op.create_foreign_key(
op.f("fk_eventreferencerequest_created_by_id_user"),
"eventreferencerequest",
"user",
["created_by_id"],
["id"],
ondelete="SET NULL",
)
op.create_foreign_key(
op.f("fk_eventreferencerequest_updated_by_id_user"),
"eventreferencerequest",
"user",
["updated_by_id"],
["id"],
ondelete="SET NULL",
)
op.drop_constraint(
"fk_eventsuggestion_created_by_id_user", "eventsuggestion", type_="foreignkey"
)
op.drop_constraint(
"fk_eventsuggestion_updated_by_id_user", "eventsuggestion", type_="foreignkey"
)
op.create_foreign_key(
op.f("fk_eventsuggestion_created_by_id_user"),
"eventsuggestion",
"user",
["created_by_id"],
["id"],
ondelete="SET NULL",
)
op.create_foreign_key(
op.f("fk_eventsuggestion_updated_by_id_user"),
"eventsuggestion",
"user",
["updated_by_id"],
["id"],
ondelete="SET NULL",
)
op.drop_constraint("fk_image_updated_by_id_user", "image", type_="foreignkey")
op.drop_constraint("fk_image_created_by_id_user", "image", type_="foreignkey")
op.create_foreign_key(
op.f("fk_image_updated_by_id_user"),
"image",
"user",
["updated_by_id"],
["id"],
ondelete="SET NULL",
)
op.create_foreign_key(
op.f("fk_image_created_by_id_user"),
"image",
"user",
["created_by_id"],
["id"],
ondelete="SET NULL",
)
op.drop_constraint("fk_location_created_by_id_user", "location", type_="foreignkey")
op.drop_constraint("fk_location_updated_by_id_user", "location", type_="foreignkey")
op.create_foreign_key(
op.f("fk_location_created_by_id_user"),
"location",
"user",
["created_by_id"],
["id"],
ondelete="SET NULL",
)
op.create_foreign_key(
op.f("fk_location_updated_by_id_user"),
"location",
"user",
["updated_by_id"],
["id"],
ondelete="SET NULL",
)
op.drop_constraint("fk_settings_updated_by_id_user", "settings", type_="foreignkey")
op.drop_constraint("fk_settings_created_by_id_user", "settings", type_="foreignkey")
op.create_foreign_key(
op.f("fk_settings_created_by_id_user"),
"settings",
"user",
["created_by_id"],
["id"],
ondelete="SET NULL",
)
op.create_foreign_key(
op.f("fk_settings_updated_by_id_user"),
"settings",
"user",
["updated_by_id"],
["id"],
ondelete="SET NULL",
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(
op.f("fk_settings_updated_by_id_user"), "settings", type_="foreignkey"
)
op.drop_constraint(
op.f("fk_settings_created_by_id_user"), "settings", type_="foreignkey"
)
op.create_foreign_key(
"fk_settings_created_by_id_user", "settings", "user", ["created_by_id"], ["id"]
)
op.create_foreign_key(
"fk_settings_updated_by_id_user", "settings", "user", ["updated_by_id"], ["id"]
)
op.drop_constraint(
op.f("fk_location_updated_by_id_user"), "location", type_="foreignkey"
)
op.drop_constraint(
op.f("fk_location_created_by_id_user"), "location", type_="foreignkey"
)
op.create_foreign_key(
"fk_location_updated_by_id_user", "location", "user", ["updated_by_id"], ["id"]
)
op.create_foreign_key(
"fk_location_created_by_id_user", "location", "user", ["created_by_id"], ["id"]
)
op.drop_constraint(op.f("fk_image_created_by_id_user"), "image", type_="foreignkey")
op.drop_constraint(op.f("fk_image_updated_by_id_user"), "image", type_="foreignkey")
op.create_foreign_key(
"fk_image_created_by_id_user", "image", "user", ["created_by_id"], ["id"]
)
op.create_foreign_key(
"fk_image_updated_by_id_user", "image", "user", ["updated_by_id"], ["id"]
)
op.drop_constraint(
op.f("fk_eventsuggestion_updated_by_id_user"),
"eventsuggestion",
type_="foreignkey",
)
op.drop_constraint(
op.f("fk_eventsuggestion_created_by_id_user"),
"eventsuggestion",
type_="foreignkey",
)
op.create_foreign_key(
"fk_eventsuggestion_updated_by_id_user",
"eventsuggestion",
"user",
["updated_by_id"],
["id"],
)
op.create_foreign_key(
"fk_eventsuggestion_created_by_id_user",
"eventsuggestion",
"user",
["created_by_id"],
["id"],
)
op.drop_constraint(
op.f("fk_eventreferencerequest_updated_by_id_user"),
"eventreferencerequest",
type_="foreignkey",
)
op.drop_constraint(
op.f("fk_eventreferencerequest_created_by_id_user"),
"eventreferencerequest",
type_="foreignkey",
)
op.create_foreign_key(
"fk_eventreferencerequest_created_by_id_user",
"eventreferencerequest",
"user",
["created_by_id"],
["id"],
)
op.create_foreign_key(
"fk_eventreferencerequest_updated_by_id_user",
"eventreferencerequest",
"user",
["updated_by_id"],
["id"],
)
op.drop_constraint(
op.f("fk_eventreference_created_by_id_user"),
"eventreference",
type_="foreignkey",
)
op.drop_constraint(
op.f("fk_eventreference_updated_by_id_user"),
"eventreference",
type_="foreignkey",
)
op.create_foreign_key(
"fk_eventreference_updated_by_id_user",
"eventreference",
"user",
["updated_by_id"],
["id"],
)
op.create_foreign_key(
"fk_eventreference_created_by_id_user",
"eventreference",
"user",
["created_by_id"],
["id"],
)
op.drop_constraint(
op.f("fk_eventplace_updated_by_id_user"), "eventplace", type_="foreignkey"
)
op.drop_constraint(
op.f("fk_eventplace_created_by_id_user"), "eventplace", type_="foreignkey"
)
op.create_foreign_key(
"fk_eventplace_updated_by_id_user",
"eventplace",
"user",
["updated_by_id"],
["id"],
)
op.create_foreign_key(
"fk_eventplace_created_by_id_user",
"eventplace",
"user",
["created_by_id"],
["id"],
)
op.drop_constraint(
op.f("fk_eventorganizer_created_by_id_user"),
"eventorganizer",
type_="foreignkey",
)
op.drop_constraint(
op.f("fk_eventorganizer_updated_by_id_user"),
"eventorganizer",
type_="foreignkey",
)
op.create_foreign_key(
"fk_eventorganizer_created_by_id_user",
"eventorganizer",
"user",
["created_by_id"],
["id"],
)
op.create_foreign_key(
"fk_eventorganizer_updated_by_id_user",
"eventorganizer",
"user",
["updated_by_id"],
["id"],
)
op.drop_constraint(
op.f("fk_eventlist_created_by_id_user"), "eventlist", type_="foreignkey"
)
op.drop_constraint(
op.f("fk_eventlist_updated_by_id_user"), "eventlist", type_="foreignkey"
)
op.create_foreign_key(
"fk_eventlist_updated_by_id_user",
"eventlist",
"user",
["updated_by_id"],
["id"],
)
op.create_foreign_key(
"fk_eventlist_created_by_id_user",
"eventlist",
"user",
["created_by_id"],
["id"],
)
op.drop_constraint(op.f("fk_event_updated_by_id_user"), "event", type_="foreignkey")
op.drop_constraint(op.f("fk_event_created_by_id_user"), "event", type_="foreignkey")
op.create_foreign_key(
"fk_event_created_by_id_user", "event", "user", ["created_by_id"], ["id"]
)
op.create_foreign_key(
"fk_event_updated_by_id_user", "event", "user", ["updated_by_id"], ["id"]
)
op.drop_constraint(
op.f("fk_customwidget_created_by_id_user"), "customwidget", type_="foreignkey"
)
op.drop_constraint(
op.f("fk_customwidget_updated_by_id_user"), "customwidget", type_="foreignkey"
)
op.create_foreign_key(
"fk_customwidget_created_by_id_user",
"customwidget",
"user",
["created_by_id"],
["id"],
)
op.create_foreign_key(
"fk_customwidget_updated_by_id_user",
"customwidget",
"user",
["updated_by_id"],
["id"],
)
op.drop_constraint(
op.f("fk_adminunitrelation_created_by_id_user"),
"adminunitrelation",
type_="foreignkey",
)
op.drop_constraint(
op.f("fk_adminunitrelation_updated_by_id_user"),
"adminunitrelation",
type_="foreignkey",
)
op.create_foreign_key(
"fk_adminunitrelation_created_by_id_user",
"adminunitrelation",
"user",
["created_by_id"],
["id"],
)
op.create_foreign_key(
"fk_adminunitrelation_updated_by_id_user",
"adminunitrelation",
"user",
["updated_by_id"],
["id"],
)
op.drop_constraint(
op.f("fk_adminunitinvitation_created_by_id_user"),
"adminunitinvitation",
type_="foreignkey",
)
op.drop_constraint(
op.f("fk_adminunitinvitation_updated_by_id_user"),
"adminunitinvitation",
type_="foreignkey",
)
op.create_foreign_key(
"fk_adminunitinvitation_created_by_id_user",
"adminunitinvitation",
"user",
["created_by_id"],
["id"],
)
op.create_foreign_key(
"fk_adminunitinvitation_updated_by_id_user",
"adminunitinvitation",
"user",
["updated_by_id"],
["id"],
)
op.drop_constraint(
op.f("fk_adminunit_created_by_id_user"), "adminunit", type_="foreignkey"
)
op.drop_constraint(
op.f("fk_adminunit_updated_by_id_user"), "adminunit", type_="foreignkey"
)
op.create_foreign_key(
"fk_adminunit_updated_by_id_user",
"adminunit",
"user",
["updated_by_id"],
["id"],
)
op.create_foreign_key(
"fk_adminunit_created_by_id_user",
"adminunit",
"user",
["created_by_id"],
["id"],
)
# ### end Alembic commands ###

View File

@ -0,0 +1,53 @@
"""empty message
Revision ID: d952cd5df596
Revises: 5fde26e1904d
Create Date: 2023-05-01 17:27:48.768633
"""
import sqlalchemy as sa
import sqlalchemy_utils
from alembic import op
from project import dbtypes
# revision identifiers, used by Alembic.
revision = "d952cd5df596"
down_revision = "5fde26e1904d"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(
"fk_adminunitmember_user_id_user", "adminunitmember", type_="foreignkey"
)
op.create_foreign_key(
op.f("fk_adminunitmember_user_id_user"),
"adminunitmember",
"user",
["user_id"],
["id"],
ondelete="CASCADE",
)
op.add_column(
"user", sa.Column("deletion_requested_at", sa.DateTime(), nullable=True)
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("user", "deletion_requested_at")
op.drop_constraint(
op.f("fk_adminunitmember_user_id_user"), "adminunitmember", type_="foreignkey"
)
op.create_foreign_key(
"fk_adminunitmember_user_id_user",
"adminunitmember",
"user",
["user_id"],
["id"],
)
# ### end Alembic commands ###

View File

@ -193,7 +193,7 @@ convention = {
}
metadata = MetaData(naming_convention=convention)
db = SQLAlchemy(app, metadata=metadata)
migrate = Migrate(app, db)
migrate = Migrate(app, db, render_as_batch=False)
# Celery tasks
from project import celery_tasks

View File

@ -11,6 +11,9 @@ def setup_periodic_tasks(sender, **kwargs):
sender.add_periodic_task(
crontab(hour=0, minute=30), delete_admin_units_with_due_request_task
)
sender.add_periodic_task(
crontab(hour=0, minute=40), delete_user_with_due_request_task
)
sender.add_periodic_task(crontab(hour=1, minute=0), update_recurring_dates_task)
sender.add_periodic_task(crontab(hour=2, minute=0), dump_all_task)
sender.add_periodic_task(crontab(hour=3, minute=0), seo_generate_sitemap_task)
@ -97,6 +100,36 @@ def delete_admin_unit_task(admin_unit_id):
delete_admin_unit(admin_unit)
@celery.task(
acks_late=True,
reject_on_worker_lost=True,
)
def delete_user_with_due_request_task():
from project.services.user import get_users_with_due_delete_request
users = get_users_with_due_delete_request()
if not users:
return
group(delete_user_task.s(user.id) for user in users).delay()
@celery.task(
acks_late=True,
reject_on_worker_lost=True,
)
def delete_user_task(user_id):
from project.services.user import delete_user, get_user
user = get_user(user_id)
if not user:
return
delete_user(user)
@celery.task(
acks_late=True,
reject_on_worker_lost=True,

View File

@ -1,7 +1,7 @@
from flask_babel import lazy_gettext
from flask_wtf import FlaskForm
from wtforms import BooleanField, SubmitField
from wtforms.validators import Optional
from wtforms import BooleanField, EmailField, SubmitField
from wtforms.validators import DataRequired, Optional
class NotificationForm(FlaskForm):
@ -11,3 +11,13 @@ class NotificationForm(FlaskForm):
validators=[Optional()],
)
submit = SubmitField(lazy_gettext("Save"))
class RequestUserDeletionForm(FlaskForm):
submit = SubmitField(lazy_gettext("Request deletion"))
email = EmailField(lazy_gettext("Email"), validators=[DataRequired()])
class CancelUserDeletionForm(FlaskForm):
submit = SubmitField(lazy_gettext("Cancel deletion"))
email = EmailField(lazy_gettext("Email"), validators=[DataRequired()])

View File

@ -48,6 +48,13 @@ class AdminUnitMember(db.Model):
admin_unit_id = db.Column(db.Integer, db.ForeignKey("adminunit.id"), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
user = db.relationship("User", backref=db.backref("adminunitmembers", lazy=True))
user_id = db.Column(
db.Integer, db.ForeignKey("user.id", ondelete="CASCADE"), nullable=False
)
user = db.relationship(
"User",
backref=db.backref("adminunitmembers", cascade="all, delete-orphan", lazy=True),
)
roles = relationship(
"AdminUnitMemberRole",
secondary="adminunitmemberroles_members",

View File

@ -6,11 +6,7 @@ from project.models.trackable_mixin import TrackableMixin
class EventList(db.Model, TrackableMixin):
__tablename__ = "eventlist"
__table_args__ = (
UniqueConstraint(
"name", "admin_unit_id", name="eventreference_name_admin_unit_id"
),
)
__table_args__ = (UniqueConstraint("name", "admin_unit_id"),)
id = Column(Integer(), primary_key=True)
name = Column(Unicode(255))
admin_unit_id = db.Column(db.Integer, db.ForeignKey("adminunit.id"), nullable=False)

View File

@ -6,11 +6,7 @@ from project.models.trackable_mixin import TrackableMixin
class EventReference(db.Model, TrackableMixin):
__tablename__ = "eventreference"
__table_args__ = (
UniqueConstraint(
"event_id", "admin_unit_id", name="eventreference_event_id_admin_unit_id"
),
)
__table_args__ = (UniqueConstraint("event_id", "admin_unit_id"),)
id = Column(Integer(), primary_key=True)
event_id = db.Column(db.Integer, db.ForeignKey("event.id"), nullable=False)
admin_unit_id = db.Column(db.Integer, db.ForeignKey("adminunit.id"), nullable=False)

View File

@ -27,7 +27,6 @@ class EventReferenceRequest(db.Model, TrackableMixin):
UniqueConstraint(
"event_id",
"admin_unit_id",
name="eventreferencerequest_event_id_admin_unit_id",
),
)
id = Column(Integer(), primary_key=True)

View File

@ -1,7 +1,7 @@
import datetime
from sqlalchemy import Column, DateTime, ForeignKey
from sqlalchemy.orm import declared_attr, deferred, relationship
from sqlalchemy.orm import backref, declared_attr, deferred, relationship
from project.models.functions import _current_user_id_or_none
@ -29,7 +29,7 @@ class TrackableMixin(object):
return deferred(
Column(
"created_by_id",
ForeignKey("user.id"),
ForeignKey("user.id", ondelete="SET NULL"),
default=_current_user_id_or_none,
),
group="trackable",
@ -41,6 +41,7 @@ class TrackableMixin(object):
"User",
primaryjoin="User.id == %s.created_by_id" % cls.__name__,
remote_side="User.id",
backref=backref("created_%s" % cls.__tablename__, lazy=True),
)
@declared_attr
@ -48,7 +49,7 @@ class TrackableMixin(object):
return deferred(
Column(
"updated_by_id",
ForeignKey("user.id"),
ForeignKey("user.id", ondelete="SET NULL"),
default=_current_user_id_or_none,
onupdate=_current_user_id_or_none,
),
@ -61,4 +62,5 @@ class TrackableMixin(object):
"User",
primaryjoin="User.id == %s.updated_by_id" % cls.__name__,
remote_side="User.id",
backref=backref("updated_%s" % cls.__tablename__, lazy=True),
)

View File

@ -60,6 +60,8 @@ class User(db.Model, UserMixin):
)
)
created_at = Column(DateTime, default=datetime.datetime.utcnow)
created_at = deferred(Column(DateTime, default=datetime.datetime.utcnow))
deletion_requested_at = deferred(Column(DateTime, nullable=True))
def get_user_id(self):
return self.id

View File

@ -1,6 +1,8 @@
import datetime
from flask_security import hash_password
from project import user_datastore
from project import db, user_datastore
from project.models import Event, Role, User, UserFavoriteEvents
@ -99,3 +101,13 @@ def remove_favorite_event(user_id: int, event_id: int):
db.session.delete(favorite)
return True
def get_users_with_due_delete_request():
due = datetime.datetime.utcnow() - datetime.timedelta(days=3)
return User.query.filter(User.deletion_requested_at < due).all()
def delete_user(user):
user_datastore.delete_user(user)
db.session.commit()

View File

@ -19,6 +19,7 @@
<th>{{ _('Email') }}</th>
<th>created_at</th>
<th>confirmed_at</th>
<th>deletion_requested_at</th>
<th></th>
</tr>
</thead>
@ -28,6 +29,7 @@
<td>{{ user.email }}</td>
<td>{% if user.created_at %}{{ user.created_at | dateformat }}{% endif %}</td>
<td>{% if user.confirmed_at %}{{ user.confirmed_at | dateformat }}{% endif %}</td>
<td>{% if user.deletion_requested_at %}{{ user.deletion_requested_at | dateformat }}{% endif %}</td>
<td>
<a href="{{ url_for('admin_user_update', id=user.id) }}">{{ _('Edit') }}</a>
<a href="{{ url_for('admin_user_delete', id=user.id) }}">{{ _('Delete') }}</a>

View File

@ -2,5 +2,5 @@
{% from "_macros.html" import render_email_button %}
{% block content %}
<p>{{ _('%(admin_unit_name)s is scheduled for deletion.', admin_unit_name=admin_unit.name) }}</p>
{{ render_email_button(url_for('admin_unit_cancel_deletion', id=admin_unit.id, _external=True), _('Click here to view the invitation')) }}
{{ render_email_button(url_for('admin_unit_cancel_deletion', id=admin_unit.id, _external=True), _('Click here below to cancel the deletion')) }}
{% endblock %}

View File

@ -0,0 +1,6 @@
{% extends "email/layout.html" %}
{% from "_macros.html" import render_email_button %}
{% block content %}
<p>{{ _('%(user_email)s is scheduled for deletion.', user_email=user.email) }}</p>
{{ render_email_button(url_for('user_cancel_deletion', _external=True), _('Click the link below to cancel the deletion')) }}
{% endblock %}

View File

@ -0,0 +1,3 @@
{{ _('%(user_email)s is scheduled for deletion.', user_email=user.email) }}
{{ _('Click the link below to cancel the deletion') }}
{{ url_for('user_cancel_deletion', _external=True) }}

View File

@ -7,6 +7,13 @@
<h1>{{ current_user.email }}</h1>
{% if current_user.deletion_requested_at %}
<div class="alert alert-danger" role="alert">
{{ _('Your account is scheduled for deletion.') }}
<a class="alert-link" href="{{ url_for('user_cancel_deletion') }}" role="button">{{ _('Cancel deletion') }}</a>
</div>
{% endif %}
<h2>{{ _('Profile') }}</h2>
<div class="list-group">
@ -14,6 +21,12 @@
{{ _fsdomain('Change password') }}
<i class="fa fa-caret-right"></i>
</a>
{% if not current_user.deletion_requested_at %}
<a href="{{ url_for('user_request_deletion') }}" class="list-group-item text-danger">
{{ _('Delete account') }}
<i class="fa fa-caret-right"></i>
</a>
{% endif %}
</div>
<h2>{{ _('Settings') }}</h2>

View File

@ -0,0 +1,24 @@
{% extends "layout.html" %}
{% from "_macros.html" import render_field_with_errors, render_field %}
{% block content %}
<h1>{{ _('Cancel deletion') }} &quot;{{ current_user.email }}&quot;</h1>
<form action="" method="POST">
{{ form.hidden_tag() }}
<div class="card mb-4">
<div class="card-header">
{{ _('User') }}
</div>
<div class="card-body">
{{ render_field_with_errors(form.email) }}
</div>
</div>
{{ render_field(form.submit) }}
</form>
{% endblock %}

View File

@ -0,0 +1,26 @@
{% extends "layout.html" %}
{% from "_macros.html" import render_field_with_errors, render_field %}
{% block content %}
<h1>{{ _('Delete account') }} &quot;{{ current_user.email }}&quot;</h1>
<p>{{ _('The account is not deleted immediately. After a period of time, the account will be deleted. Until then, the deletion can be canceled.') }}</p>
<form action="" method="POST">
{{ form.hidden_tag() }}
<div class="card mb-4">
<div class="card-header">
{{ _('User') }}
</div>
<div class="card-body">
{{ render_field_with_errors(form.email) }}
</div>
</div>
{{ render_field(form.submit) }}
</form>
{% endblock %}

View File

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2023-04-27 15:10+0200\n"
"POT-Creation-Date: 2023-05-01 18:11+0200\n"
"PO-Revision-Date: 2020-06-07 18:51+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: de\n"
@ -195,7 +195,7 @@ msgid "message"
msgstr "message"
#: project/api/organization/resources.py:401
#: project/views/admin_unit_member_invitation.py:85
#: project/views/admin_unit_member_invitation.py:89
msgid "You have received an invitation"
msgstr "Du hast eine Einladung erhalten"
@ -212,7 +212,7 @@ msgstr "Impressum"
#: project/forms/admin.py:13 project/templates/_macros.html:1473
#: project/templates/layout.html:311
#: project/templates/widget/event_suggestion/create.html:204
#: project/views/admin_unit.py:82 project/views/root.py:71
#: project/views/admin_unit.py:83 project/views/root.py:71
msgid "Contact"
msgstr "Kontakt"
@ -248,6 +248,7 @@ msgstr "Nutzer löschen"
#: project/forms/admin_unit_member.py:11 project/forms/admin_unit_member.py:23
#: project/forms/admin_unit_member.py:28 project/forms/event.py:107
#: project/forms/event_suggestion.py:38 project/forms/organizer.py:27
#: project/forms/user.py:18 project/forms/user.py:23
#: project/templates/_macros.html:237 project/templates/_macros.html:1569
#: project/templates/admin/admin.html:27 project/templates/admin/email.html:4
#: project/templates/admin/email.html:66 project/templates/admin/users.html:19
@ -494,14 +495,15 @@ msgstr "Hauptfarbe"
msgid "Link Color"
msgstr "Linkfarbe"
#: project/forms/admin_unit.py:133
#: project/forms/admin_unit.py:133 project/forms/user.py:17
msgid "Request deletion"
msgstr "Löschung beantragen"
#: project/forms/admin_unit.py:138
#: project/forms/admin_unit.py:138 project/forms/user.py:22
#: project/templates/admin_unit/cancel_deletion.html:6
#: project/templates/admin_unit/update.html:26
#: project/templates/manage/events.html:49
#: project/templates/manage/events.html:49 project/templates/profile.html:13
#: project/templates/user/cancel_deletion.html:6
msgid "Cancel deletion"
msgstr "Löschen abbrechen"
@ -1381,7 +1383,7 @@ msgstr "Merkzettel"
#: project/templates/_macros.html:590 project/templates/_macros.html:633
#: project/templates/_macros.html:765
#: project/templates/admin/admin_units.html:36
#: project/templates/admin/users.html:32
#: project/templates/admin/users.html:34
#: project/templates/manage/events.html:116
#: project/templates/manage/members.html:35
#: project/templates/manage/organizers.html:33
@ -1596,7 +1598,7 @@ msgstr "Planung"
#: project/templates/oauth2_client/list.html:10
#: project/templates/oauth2_client/read.html:10
#: project/templates/oauth2_token/list.html:10 project/templates/profile.html:4
#: project/templates/profile.html:10
#: project/templates/profile.html:17
msgid "Profile"
msgstr "Profil"
@ -1698,7 +1700,7 @@ msgstr "Organisationseinladungen"
#: project/templates/admin_unit/update.html:6
#: project/templates/admin_unit/update.html:30
#: project/templates/layout.html:259 project/templates/manage/widgets.html:11
#: project/templates/manage/widgets.html:15 project/templates/profile.html:19
#: project/templates/manage/widgets.html:15 project/templates/profile.html:32
msgid "Settings"
msgstr "Einstellungen"
@ -1724,24 +1726,34 @@ msgstr "Organisation wechseln"
#: project/templates/developer/read.html:4
#: project/templates/developer/read.html:8 project/templates/layout.html:319
#: project/templates/profile.html:33
#: project/templates/profile.html:46
msgid "Developer"
msgstr "Entwickler"
#: project/templates/profile.html:23
#: project/templates/profile.html:12 project/views/admin_unit.py:89
#: project/views/admin_unit_member_invitation.py:39
msgid "Your account is scheduled for deletion."
msgstr "Dein Account ist zur Löschung vorgesehen."
#: project/templates/profile.html:26
#: project/templates/user/request_deletion.html:6
msgid "Delete account"
msgstr "Account löschen"
#: project/templates/profile.html:36
#: project/templates/user/notifications.html:4
#: project/templates/user/notifications.html:8
msgid "Notifications"
msgstr "Benachrichtigungen"
#: project/templates/profile.html:27
#: project/templates/profile.html:40
msgid "Applications"
msgstr "Apps"
#: project/templates/oauth2_client/list.html:4
#: project/templates/oauth2_client/list.html:11
#: project/templates/oauth2_client/read.html:11
#: project/templates/profile.html:37
#: project/templates/profile.html:50
msgid "OAuth2 clients"
msgstr "OAuth2 Clients"
@ -1759,7 +1771,7 @@ msgid "View"
msgstr "Anzeigen"
#: project/templates/admin/admin_units.html:37
#: project/templates/admin/users.html:33
#: project/templates/admin/users.html:35
#: project/templates/manage/events.html:117
#: project/templates/manage/members.html:21
#: project/templates/manage/members.html:36
@ -1771,6 +1783,8 @@ msgid "Delete"
msgstr "Löschen"
#: project/templates/admin/delete_user.html:13
#: project/templates/user/cancel_deletion.html:13
#: project/templates/user/request_deletion.html:15
msgid "User"
msgstr "Nutzer"
@ -1815,7 +1829,9 @@ msgstr "Nutzer:in einladen"
msgid ""
"The organization is not deleted immediately. After a period of time, the "
"organization will be deleted. Until then, the deletion can be canceled."
msgstr "Die Organisation wird nicht sofort gelöscht. Nach einer Frist wird die Organisation gelöscht. Bis dahin kann die Löschung abgebrochen werden."
msgstr ""
"Die Organisation wird nicht sofort gelöscht. Nach einer Frist wird die "
"Organisation gelöscht. Bis dahin kann die Löschung abgebrochen werden."
#: project/templates/admin_unit/update.html:25
#: project/templates/manage/events.html:48
@ -1857,7 +1873,6 @@ msgid "You have been invited to join %(admin_unit_name)s."
msgstr "Du wurdest eingeladen, %(admin_unit_name)s beizutreten."
#: project/templates/email/invitation_notice.html:5
#: project/templates/email/organization_deletion_requested_notice.html:5
#: project/templates/email/organization_invitation_notice.html:5
msgid "Click here to view the invitation"
msgstr "Klicke hier, um die Einladung anzunehmen."
@ -1880,6 +1895,10 @@ msgstr "Benachrichtigungseinstellungen"
msgid "%(admin_unit_name)s is scheduled for deletion."
msgstr "%(admin_unit_name)s ist zur Löschung vorgesehen."
#: project/templates/email/organization_deletion_requested_notice.html:5
msgid "Click here below to cancel the deletion"
msgstr "Klicke hier, um das Löschen abzubrechen"
#: project/templates/email/organization_invitation_accepted_notice.html:4
#, python-format
msgid ""
@ -1948,6 +1967,15 @@ msgstr "Das ist eine Test-Mail"
msgid "Click here to open the site"
msgstr "Klicke hier, um die Seite zu öffnen"
#: project/templates/email/user_deletion_requested_notice.html:4
#, python-format
msgid "%(user_email)s is scheduled for deletion."
msgstr "%(user_email)s ist zur Löschung vorgesehen."
#: project/templates/email/user_deletion_requested_notice.html:5
msgid "Click the link below to cancel the deletion"
msgstr "Klicke den Link, um das Löschen abzubrechen"
#: project/templates/event/actions.html:5
#: project/templates/event/actions.html:22
msgid "Actions for event"
@ -2273,6 +2301,14 @@ msgstr "Du hast noch keinen Account? Kein Problem!"
msgid "Register for free"
msgstr "Kostenlos registrieren"
#: project/templates/user/request_deletion.html:8
msgid ""
"The account is not deleted immediately. After a period of time, the "
"account will be deleted. Until then, the deletion can be canceled."
msgstr ""
"Dein Account wird nicht sofort gelöscht. Nach einer Frist wird dein "
"Account gelöscht. Bis dahin kann die Löschung abgebrochen werden."
#: project/templates/widget/event_date/list.html:5
msgid "Widget"
msgstr "Widget"
@ -2297,8 +2333,8 @@ msgstr "Vorschau"
msgid "Organization successfully updated"
msgstr "Organisation erfolgreich aktualisiert"
#: project/views/admin.py:85 project/views/admin_unit.py:182
#: project/views/admin_unit.py:215
#: project/views/admin.py:85 project/views/admin_unit.py:187
#: project/views/admin_unit.py:220
msgid "Entered name does not match organization name"
msgstr "Der eingegebene Name entspricht nicht dem Namen der Organisation"
@ -2307,7 +2343,7 @@ msgid "Organization successfully deleted"
msgstr "Organisation erfolgreich gelöscht"
#: project/views/admin.py:113 project/views/manage.py:432
#: project/views/user.py:28
#: project/views/user.py:40
msgid "Settings successfully updated"
msgstr "Einstellungen erfolgreich aktualisiert"
@ -2329,11 +2365,11 @@ msgstr "Nutzer erfolgreich aktualisiert"
msgid "Entered email does not match user email"
msgstr "Die eingegebene Email passt nicht zur Email des Nutzers"
#: project/views/admin.py:237
#: project/views/admin.py:236
msgid "User successfully deleted"
msgstr "Nutzer erfolgreich gelöscht"
#: project/views/admin_unit.py:78
#: project/views/admin_unit.py:79
msgid ""
"Organizations cannot currently be created. The project is in a closed "
"test phase. If you are interested, you can contact us."
@ -2342,19 +2378,19 @@ msgstr ""
" sich in einer geschlossenen Test-Phase. Bei Interesse kannst du uns "
"kontaktieren."
#: project/views/admin_unit.py:128
#: project/views/admin_unit.py:133
msgid "Organization successfully created"
msgstr "Organisation erfolgreich erstellt"
#: project/views/admin_unit.py:158
#: project/views/admin_unit.py:163
msgid "AdminUnit successfully updated"
msgstr "Organisation erfolgreich aktualisiert"
#: project/views/admin_unit.py:245
#: project/views/admin_unit.py:250
msgid "Organization invitation accepted"
msgstr "Organisationseinladung akzeptiert"
#: project/views/admin_unit.py:259
#: project/views/admin_unit.py:264
msgid "Organization deletion requested"
msgstr "Löschung der Organisation beantragt"
@ -2370,23 +2406,23 @@ msgstr "Die eingegebene Email passt nicht zur Email des Mitglieds"
msgid "Member successfully deleted"
msgstr "Mitglied erfolgreich gelöscht"
#: project/views/admin_unit_member_invitation.py:38
#: project/views/admin_unit_member_invitation.py:42
msgid "Invitation successfully accepted"
msgstr "Einladung erfolgreich akzeptiert"
#: project/views/admin_unit_member_invitation.py:45
#: project/views/admin_unit_member_invitation.py:49
msgid "Invitation successfully declined"
msgstr "Einladung erfolgreich abgelehnt"
#: project/views/admin_unit_member_invitation.py:90
#: project/views/admin_unit_member_invitation.py:94
msgid "Invitation successfully sent"
msgstr "Einladung erfolgreich gesendet"
#: project/views/admin_unit_member_invitation.py:113
#: project/views/admin_unit_member_invitation.py:117
msgid "Entered email does not match invitation email"
msgstr "Die eingegebene Email passt nicht zur Email der Einladung"
#: project/views/admin_unit_member_invitation.py:118
#: project/views/admin_unit_member_invitation.py:122
msgid "Invitation successfully deleted"
msgstr "Einladung erfolgreich gelöscht"
@ -2545,6 +2581,14 @@ msgstr ""
"Ob alle zukünftigen Empfehlungsanfragen von %(admin_unit_name)s "
"automatisch verifiziert werden sollen."
#: project/views/user.py:84 project/views/user.py:111
msgid "Entered email does not match your email"
msgstr "Die eingegebene Email entspricht nicht deiner Email"
#: project/views/user.py:130
msgid "User deletion requested"
msgstr "Löschung des Nutzers beantragt"
#: project/views/utils.py:71
msgid ""
"An entry with the entered values already exists. Duplicate entries are "

View File

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2023-04-27 15:10+0200\n"
"POT-Creation-Date: 2023-05-01 18:11+0200\n"
"PO-Revision-Date: 2021-04-30 15:04+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: en\n"
@ -195,7 +195,7 @@ msgid "message"
msgstr ""
#: project/api/organization/resources.py:401
#: project/views/admin_unit_member_invitation.py:85
#: project/views/admin_unit_member_invitation.py:89
msgid "You have received an invitation"
msgstr ""
@ -212,7 +212,7 @@ msgstr ""
#: project/forms/admin.py:13 project/templates/_macros.html:1473
#: project/templates/layout.html:311
#: project/templates/widget/event_suggestion/create.html:204
#: project/views/admin_unit.py:82 project/views/root.py:71
#: project/views/admin_unit.py:83 project/views/root.py:71
msgid "Contact"
msgstr ""
@ -248,6 +248,7 @@ msgstr ""
#: project/forms/admin_unit_member.py:11 project/forms/admin_unit_member.py:23
#: project/forms/admin_unit_member.py:28 project/forms/event.py:107
#: project/forms/event_suggestion.py:38 project/forms/organizer.py:27
#: project/forms/user.py:18 project/forms/user.py:23
#: project/templates/_macros.html:237 project/templates/_macros.html:1569
#: project/templates/admin/admin.html:27 project/templates/admin/email.html:4
#: project/templates/admin/email.html:66 project/templates/admin/users.html:19
@ -475,14 +476,15 @@ msgstr ""
msgid "Link Color"
msgstr ""
#: project/forms/admin_unit.py:133
#: project/forms/admin_unit.py:133 project/forms/user.py:17
msgid "Request deletion"
msgstr ""
#: project/forms/admin_unit.py:138
#: project/forms/admin_unit.py:138 project/forms/user.py:22
#: project/templates/admin_unit/cancel_deletion.html:6
#: project/templates/admin_unit/update.html:26
#: project/templates/manage/events.html:49
#: project/templates/manage/events.html:49 project/templates/profile.html:13
#: project/templates/user/cancel_deletion.html:6
msgid "Cancel deletion"
msgstr ""
@ -1332,7 +1334,7 @@ msgstr ""
#: project/templates/_macros.html:590 project/templates/_macros.html:633
#: project/templates/_macros.html:765
#: project/templates/admin/admin_units.html:36
#: project/templates/admin/users.html:32
#: project/templates/admin/users.html:34
#: project/templates/manage/events.html:116
#: project/templates/manage/members.html:35
#: project/templates/manage/organizers.html:33
@ -1547,7 +1549,7 @@ msgstr ""
#: project/templates/oauth2_client/list.html:10
#: project/templates/oauth2_client/read.html:10
#: project/templates/oauth2_token/list.html:10 project/templates/profile.html:4
#: project/templates/profile.html:10
#: project/templates/profile.html:17
msgid "Profile"
msgstr ""
@ -1649,7 +1651,7 @@ msgstr ""
#: project/templates/admin_unit/update.html:6
#: project/templates/admin_unit/update.html:30
#: project/templates/layout.html:259 project/templates/manage/widgets.html:11
#: project/templates/manage/widgets.html:15 project/templates/profile.html:19
#: project/templates/manage/widgets.html:15 project/templates/profile.html:32
msgid "Settings"
msgstr ""
@ -1675,24 +1677,34 @@ msgstr ""
#: project/templates/developer/read.html:4
#: project/templates/developer/read.html:8 project/templates/layout.html:319
#: project/templates/profile.html:33
#: project/templates/profile.html:46
msgid "Developer"
msgstr ""
#: project/templates/profile.html:23
#: project/templates/profile.html:12 project/views/admin_unit.py:89
#: project/views/admin_unit_member_invitation.py:39
msgid "Your account is scheduled for deletion."
msgstr ""
#: project/templates/profile.html:26
#: project/templates/user/request_deletion.html:6
msgid "Delete account"
msgstr ""
#: project/templates/profile.html:36
#: project/templates/user/notifications.html:4
#: project/templates/user/notifications.html:8
msgid "Notifications"
msgstr ""
#: project/templates/profile.html:27
#: project/templates/profile.html:40
msgid "Applications"
msgstr ""
#: project/templates/oauth2_client/list.html:4
#: project/templates/oauth2_client/list.html:11
#: project/templates/oauth2_client/read.html:11
#: project/templates/profile.html:37
#: project/templates/profile.html:50
msgid "OAuth2 clients"
msgstr ""
@ -1710,7 +1722,7 @@ msgid "View"
msgstr ""
#: project/templates/admin/admin_units.html:37
#: project/templates/admin/users.html:33
#: project/templates/admin/users.html:35
#: project/templates/manage/events.html:117
#: project/templates/manage/members.html:21
#: project/templates/manage/members.html:36
@ -1722,6 +1734,8 @@ msgid "Delete"
msgstr ""
#: project/templates/admin/delete_user.html:13
#: project/templates/user/cancel_deletion.html:13
#: project/templates/user/request_deletion.html:15
msgid "User"
msgstr ""
@ -1808,7 +1822,6 @@ msgid "You have been invited to join %(admin_unit_name)s."
msgstr ""
#: project/templates/email/invitation_notice.html:5
#: project/templates/email/organization_deletion_requested_notice.html:5
#: project/templates/email/organization_invitation_notice.html:5
msgid "Click here to view the invitation"
msgstr ""
@ -1831,6 +1844,10 @@ msgstr ""
msgid "%(admin_unit_name)s is scheduled for deletion."
msgstr ""
#: project/templates/email/organization_deletion_requested_notice.html:5
msgid "Click here below to cancel the deletion"
msgstr ""
#: project/templates/email/organization_invitation_accepted_notice.html:4
#, python-format
msgid ""
@ -1897,6 +1914,15 @@ msgstr ""
msgid "Click here to open the site"
msgstr ""
#: project/templates/email/user_deletion_requested_notice.html:4
#, python-format
msgid "%(user_email)s is scheduled for deletion."
msgstr ""
#: project/templates/email/user_deletion_requested_notice.html:5
msgid "Click the link below to cancel the deletion"
msgstr ""
#: project/templates/event/actions.html:5
#: project/templates/event/actions.html:22
msgid "Actions for event"
@ -2213,6 +2239,12 @@ msgstr ""
msgid "Register for free"
msgstr ""
#: project/templates/user/request_deletion.html:8
msgid ""
"The account is not deleted immediately. After a period of time, the "
"account will be deleted. Until then, the deletion can be canceled."
msgstr ""
#: project/templates/widget/event_date/list.html:5
msgid "Widget"
msgstr ""
@ -2237,8 +2269,8 @@ msgstr ""
msgid "Organization successfully updated"
msgstr ""
#: project/views/admin.py:85 project/views/admin_unit.py:182
#: project/views/admin_unit.py:215
#: project/views/admin.py:85 project/views/admin_unit.py:187
#: project/views/admin_unit.py:220
msgid "Entered name does not match organization name"
msgstr ""
@ -2247,7 +2279,7 @@ msgid "Organization successfully deleted"
msgstr ""
#: project/views/admin.py:113 project/views/manage.py:432
#: project/views/user.py:28
#: project/views/user.py:40
msgid "Settings successfully updated"
msgstr ""
@ -2269,29 +2301,29 @@ msgstr ""
msgid "Entered email does not match user email"
msgstr ""
#: project/views/admin.py:237
#: project/views/admin.py:236
msgid "User successfully deleted"
msgstr ""
#: project/views/admin_unit.py:78
#: project/views/admin_unit.py:79
msgid ""
"Organizations cannot currently be created. The project is in a closed "
"test phase. If you are interested, you can contact us."
msgstr ""
#: project/views/admin_unit.py:128
#: project/views/admin_unit.py:133
msgid "Organization successfully created"
msgstr ""
#: project/views/admin_unit.py:158
#: project/views/admin_unit.py:163
msgid "AdminUnit successfully updated"
msgstr ""
#: project/views/admin_unit.py:245
#: project/views/admin_unit.py:250
msgid "Organization invitation accepted"
msgstr ""
#: project/views/admin_unit.py:259
#: project/views/admin_unit.py:264
msgid "Organization deletion requested"
msgstr ""
@ -2307,23 +2339,23 @@ msgstr ""
msgid "Member successfully deleted"
msgstr ""
#: project/views/admin_unit_member_invitation.py:38
#: project/views/admin_unit_member_invitation.py:42
msgid "Invitation successfully accepted"
msgstr ""
#: project/views/admin_unit_member_invitation.py:45
#: project/views/admin_unit_member_invitation.py:49
msgid "Invitation successfully declined"
msgstr ""
#: project/views/admin_unit_member_invitation.py:90
#: project/views/admin_unit_member_invitation.py:94
msgid "Invitation successfully sent"
msgstr ""
#: project/views/admin_unit_member_invitation.py:113
#: project/views/admin_unit_member_invitation.py:117
msgid "Entered email does not match invitation email"
msgstr ""
#: project/views/admin_unit_member_invitation.py:118
#: project/views/admin_unit_member_invitation.py:122
msgid "Invitation successfully deleted"
msgstr ""
@ -2478,6 +2510,14 @@ msgid ""
"verified automatically."
msgstr ""
#: project/views/user.py:84 project/views/user.py:111
msgid "Entered email does not match your email"
msgstr ""
#: project/views/user.py:130
msgid "User deletion requested"
msgstr ""
#: project/views/utils.py:71
msgid ""
"An entry with the entered values already exists. Duplicate entries are "

View File

@ -5,7 +5,7 @@ from flask_security import roles_required
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.sql import func
from project import app, db, user_datastore
from project import app, db
from project.base_tasks import send_mail_task
from project.forms.admin import (
AdminNewsletterForm,
@ -19,7 +19,7 @@ from project.forms.admin import (
from project.models import AdminUnit, Role, User
from project.services.admin import upsert_settings
from project.services.admin_unit import delete_admin_unit
from project.services.user import set_roles_for_user
from project.services.user import delete_user, set_roles_for_user
from project.views.utils import (
flash_errors,
get_celery_poll_group_result,
@ -232,8 +232,7 @@ def admin_user_delete(id):
flash(gettext("Entered email does not match user email"), "danger")
else:
try:
user_datastore.delete_user(user)
db.session.commit()
delete_user(user)
flash(gettext("User successfully deleted"), "success")
return redirect(url_for("admin_users"))
except SQLAlchemyError as e:

View File

@ -73,7 +73,8 @@ def admin_unit_create():
if not strings_are_equal_ignoring_case(invitation.email, current_user.email):
return permission_missing(url_for("manage_admin_units"))
if not invitation and not can_create_admin_unit():
if not invitation:
if not can_create_admin_unit():
flash_message(
gettext(
"Organizations cannot currently be created. The project is in a closed test phase. If you are interested, you can contact us."
@ -84,6 +85,10 @@ def admin_unit_create():
)
return redirect(url_for("manage_admin_units"))
if current_user.deletion_requested_at: # pragma: no cover
flash(gettext("Your account is scheduled for deletion."), "danger")
return redirect(url_for("profile"))
form = CreateAdminUnitForm()
if invitation and not form.is_submitted():

View File

@ -35,6 +35,10 @@ def admin_unit_member_invitation(id):
if form.validate_on_submit():
try:
if form.accept.data:
if current_user.deletion_requested_at: # pragma: no cover
flash(gettext("Your account is scheduled for deletion."), "danger")
return redirect(url_for("profile"))
message = gettext("Invitation successfully accepted")
roles = invitation.roles.split(",")
add_user_to_admin_unit_with_roles(

View File

@ -1,12 +1,24 @@
import datetime
from flask import flash, redirect, render_template, url_for
from flask_babel import gettext
from flask_security import auth_required, current_user
from sqlalchemy.exc import SQLAlchemyError
from project import app, db
from project.forms.user import NotificationForm
from project.forms.user import (
CancelUserDeletionForm,
NotificationForm,
RequestUserDeletionForm,
)
from project.models import AdminUnitInvitation, User
from project.views.utils import get_invitation_access_result, handleSqlError
from project.views.utils import (
flash_errors,
get_invitation_access_result,
handleSqlError,
non_match_for_deletion,
send_mail,
)
@app.route("/profile")
@ -56,3 +68,66 @@ def user_organization_invitations(path=None):
@auth_required()
def user_favorite_events():
return render_template("user/favorite_events.html")
@app.route("/user/request-deletion", methods=("GET", "POST"))
@auth_required()
def user_request_deletion():
if current_user.deletion_requested_at: # pragma: no cover
return redirect(url_for("user_cancel_deletion"))
form = None
form = RequestUserDeletionForm()
if form.validate_on_submit():
if non_match_for_deletion(form.email.data, current_user.email):
flash(gettext("Entered email does not match your email"), "danger")
else:
current_user.deletion_requested_at = datetime.datetime.utcnow()
try:
db.session.commit()
send_user_deletion_requested_mail(current_user)
return redirect(url_for("profile"))
except SQLAlchemyError as e:
db.session.rollback()
flash(handleSqlError(e), "danger")
else:
flash_errors(form)
return render_template("user/request_deletion.html", form=form)
@app.route("/user/cancel-deletion", methods=("GET", "POST"))
@auth_required()
def user_cancel_deletion():
if not current_user.deletion_requested_at: # pragma: no cover
return redirect(url_for("user_request_deletion"))
form = CancelUserDeletionForm()
if form.validate_on_submit():
if non_match_for_deletion(form.email.data, current_user.email):
flash(gettext("Entered email does not match your email"), "danger")
else:
current_user.deletion_requested_at = None
try:
db.session.commit()
return redirect(url_for("profile"))
except SQLAlchemyError as e:
db.session.rollback()
flash(handleSqlError(e), "danger")
else:
flash_errors(form)
return render_template("user/cancel_deletion.html", form=form)
def send_user_deletion_requested_mail(user):
send_mail(
user.email,
gettext("User deletion requested"),
"user_deletion_requested_notice",
user=user,
)

View File

@ -170,7 +170,7 @@ def send_mail_message(msg):
app.logger.info(msg.body)
return
mail.send(msg)
mail.send(msg) # pragma: no cover
def non_match_for_deletion(str1: str, str2: str) -> bool:

View File

@ -1,9 +1,8 @@
import pytest
from project.api import RestApi
def test_handle_error_unique(app):
from project.api import RestApi
from project.utils import make_unique_violation
error = make_unique_violation()
@ -17,6 +16,7 @@ def test_handle_error_unique(app):
def test_handle_error_checkViolation(app):
from project.api import RestApi
from project.utils import make_check_violation
error = make_check_violation()
@ -30,6 +30,7 @@ def test_handle_error_checkViolation(app):
def test_handle_error_integrity(app):
from project.api import RestApi
from project.utils import make_integrity_error
error = make_integrity_error("custom")
@ -45,6 +46,8 @@ def test_handle_error_integrity(app):
def test_handle_error_httpException(app):
from werkzeug.exceptions import InternalServerError
from project.api import RestApi
error = InternalServerError()
with app.app_context():
@ -58,6 +61,8 @@ def test_handle_error_unprocessableEntity(app):
from marshmallow import ValidationError
from werkzeug.exceptions import UnprocessableEntity
from project.api import RestApi
args = {"name": ["Required"]}
validation_error = ValidationError(args)
@ -76,6 +81,8 @@ def test_handle_error_unprocessableEntity(app):
def test_handle_error_validationError(app):
from marshmallow import ValidationError
from project.api import RestApi
args = {"name": ["Required"]}
validation_error = ValidationError(args)
@ -90,6 +97,7 @@ def test_handle_error_validationError(app):
def test_handle_error_unspecificRaises(app):
error = Exception()
from project.api import RestApi
with app.app_context():
app.config["PROPAGATE_EXCEPTIONS"] = False

View File

@ -2,8 +2,6 @@ import base64
import pytest
from project.models import PublicStatus
def test_read(client, app, db, seeder, utils):
user_id, admin_unit_id = seeder.setup_base()
@ -225,6 +223,7 @@ def test_put(client, seeder, utils, app, db, mocker, variant):
EventAttendanceMode,
EventStatus,
EventTargetGroupOrigin,
PublicStatus,
)
event = db.session.get(Event, event_id)

View File

@ -333,6 +333,7 @@ class Seeder(object):
description="Beschreibung",
tags="",
place_id=None,
**kwargs
):
from project.models import (
Event,
@ -344,6 +345,7 @@ class Seeder(object):
with self._app.app_context():
event = Event()
event.__dict__.update(kwargs)
event.admin_unit_id = admin_unit_id
event.categories = [upsert_event_category("Other")]
event.name = name

View File

@ -40,3 +40,22 @@ def test_remove_favorite_event(client, seeder, utils, app):
assert remove_favorite_event(user_id, event_id)
assert remove_favorite_event(user_id, event_id) is False
assert has_favorite_event(user_id, event_id) is False
def test_get_users_with_due_delete_request(client, seeder, db, utils, app):
user_id, admin_unit_id = seeder.setup_base()
with app.app_context():
import datetime
from project.models import User
from project.services.user import get_users_with_due_delete_request
user = db.session.get(User, user_id)
user.deletion_requested_at = datetime.datetime.utcnow() - datetime.timedelta(
days=4
)
db.session.commit()
due_users = get_users_with_due_delete_request()
assert len(due_users) == 1

View File

@ -40,6 +40,25 @@ END $$;
conn.execute(sqlalchemy.text(sql).execution_options(autocommit=True))
def migrate_with_result(app):
from alembic import command
config = app.extensions["migrate"].migrate.get_config(
None, opts=["autogenerate"], x_arg=None
)
return command.revision(
config,
None,
autogenerate=True,
sql=False,
head="head",
splice=False,
branch_label=None,
version_path=None,
rev_id=None,
)
def test_migrations(app, seeder):
from flask_migrate import downgrade, upgrade
@ -49,6 +68,8 @@ def test_migrations(app, seeder):
with app.app_context():
drop_db(db)
upgrade()
migrate_result = migrate_with_result(app)
assert not migrate_result
create_initial_data()
user_id, admin_unit_id = seeder.setup_base()
seeder.upsert_default_event_place(admin_unit_id)

View File

@ -1,6 +1,6 @@
import pytest
from project.models import EventDateDefinition
from tests.seeder import Seeder
def test_location_update_coordinate(client, app, db):
@ -150,7 +150,7 @@ def test_event_date_defintion_deletion(client, app, db, seeder):
event_id = seeder.create_event(admin_unit_id)
with app.app_context():
from project.models import Event
from project.models import Event, EventDateDefinition
# Initial eine Definition
event = db.session.get(Event, event_id)
@ -568,3 +568,26 @@ def test_delete_admin_unit(client, app, db, seeder):
location = db.session.get(Location, location_id)
assert admin_unit is location
def test_delete_user(client, app, db, seeder: Seeder):
user_id, admin_unit_id = seeder.setup_base(log_in=False)
event_id = seeder.create_event(admin_unit_id, created_by_id=user_id)
with app.app_context():
from project.models import AdminUnit, AdminUnitMember, Event, User
from project.services.user import delete_user
user = db.session.get(User, user_id)
member_id = user.adminunitmembers[0].id
delete_user(user)
# User and membership should be gone
assert db.session.get(User, user_id) is None
assert db.session.get(AdminUnitMember, member_id) is None
# Admin unit and event should still be there
assert db.session.get(AdminUnit, admin_unit_id) is not None
event = db.session.get(Event, event_id)
event is not None
event.created_by_id is None

View File

@ -1,7 +1,6 @@
from project.services.user import find_user_by_email
def test_register(client, app, utils):
from project.services.user import find_user_by_email
utils.register("test@test.de", "MeinPasswortIstDasBeste")
with app.app_context():

View File

@ -2,11 +2,11 @@ import shutil
import pytest
from project import img_path
@pytest.mark.parametrize("size", [None, 100])
def test_read(app, seeder, utils, size):
from project import img_path
user_id, admin_unit_id = seeder.setup_base()
image_id = seeder.upsert_default_image()

View File

@ -1,7 +1,5 @@
import os
from project import dump_path
def test_home(client, seeder, utils):
url = utils.get_url("home")
@ -69,6 +67,8 @@ def test_privacy(app, db, utils):
def test_developer(client, seeder, utils):
from project import dump_path
file_name = "all.zip"
all_path = os.path.join(dump_path, file_name)

View File

@ -1,5 +1,7 @@
import pytest
from tests.seeder import Seeder
def test_profile(client, seeder, utils):
user_id, admin_unit_id = seeder.setup_base()
@ -110,3 +112,102 @@ def test_login_flash(client, seeder, utils):
utils.assert_response_error_message(
response, "Beachte, dass du deine E-Mail-Adresse bestätigen muss."
)
@pytest.mark.parametrize("db_error", [True, False])
@pytest.mark.parametrize("non_match", [True, False])
def test_user_request_deletion(
client, seeder: Seeder, utils, app, db, mocker, db_error, non_match
):
user_id, admin_unit_id = seeder.setup_base()
url = utils.get_url("user_request_deletion", id=user_id)
response = utils.get_ok(url)
if db_error:
utils.mock_db_commit(mocker)
form_email = "test@test.de"
if non_match:
form_email = "wrong"
response = utils.post_form(
url,
response,
{
"email": form_email,
},
)
if non_match:
utils.assert_response_error_message(
response, "Die eingegebene Email entspricht nicht deiner Email"
)
return
if db_error:
utils.assert_response_db_error(response)
return
utils.assert_response_redirect(response, "profile")
with app.app_context():
from project.models import User
user = db.session.get(User, user_id)
assert user.deletion_requested_at is not None
@pytest.mark.parametrize("db_error", [True, False])
@pytest.mark.parametrize("non_match", [True, False])
def test_user_cancel_deletion(
client, seeder, utils, app, db, mocker, db_error, non_match
):
user_id, admin_unit_id = seeder.setup_base()
with app.app_context():
import datetime
from project.models import User
user = db.session.get(User, user_id)
user.deletion_requested_at = datetime.datetime.utcnow()
db.session.commit()
url = utils.get_url("user_cancel_deletion", id=user_id)
response = utils.get_ok(url)
if db_error:
utils.mock_db_commit(mocker)
form_email = "test@test.de"
if non_match:
form_email = "wrong"
response = utils.post_form(
url,
response,
{
"email": form_email,
},
)
if non_match:
utils.assert_response_error_message(
response, "Die eingegebene Email entspricht nicht deiner Email"
)
return
if db_error:
utils.assert_response_db_error(response)
return
utils.assert_response_redirect(response, "profile")
with app.app_context():
from project.models import User
user = db.session.get(User, user_id)
assert user.deletion_requested_at is None