account: member deletion, with post anonymization (#57)

This commit is contained in:
Lephe 2021-07-08 10:39:22 +02:00
parent bee912f88c
commit cc5f4e481b
Signed by untrusted user: Lephenixnoir
GPG Key ID: 1BBA026E13FC0495
7 changed files with 115 additions and 37 deletions

View File

@ -125,14 +125,24 @@ class UpdateAccountForm(FlaskForm):
submit = SubmitField('Mettre à jour')
class DeleteAccountForm(FlaskForm):
class DeleteAccountBaseForm(FlaskForm):
transfer = BooleanField(
'Conserver les posts sous forme anonyme',
description="Aucune information personnelle n'est conservée ; seul le texte de posts reste. Cela permet de garder un historique fidèle des échanges.",
default=True
)
delete = BooleanField(
'Confirmer la suppression',
validators=[
InputRequired(),
],
description='Attention, cette opération est irréversible!'
description='Attention, cette opération est irréversible !'
)
submit = SubmitField(
'Supprimer le compte',
)
class DeleteAccountForm(DeleteAccountBaseForm):
old_password = PasswordField(
'Mot de passe',
validators=[
@ -140,10 +150,6 @@ class DeleteAccountForm(FlaskForm):
vd.password.old_password,
],
)
submit = SubmitField(
'Supprimer le compte',
)
class AskResetPasswordForm(FlaskForm):
email = EmailField(
@ -267,14 +273,5 @@ class AdminAccountEditGroupForm(FlaskForm):
)
class AdminDeleteAccountForm(FlaskForm):
delete = BooleanField(
'Confirmer la suppression',
validators=[
InputRequired(),
],
description='Attention, cette opération est irréversible!',
)
submit = SubmitField(
'Supprimer le compte',
)
class AdminDeleteAccountForm(DeleteAccountBaseForm):
pass

View File

@ -29,8 +29,9 @@ class Trophy(db.Model):
self.hidden = hidden
def delete(self):
for tm in TrophyMember.query.filter_by(tid=self.id).all():
db.session.delete(tm)
for owner in owners:
owner.del_trophy(self)
db.session.add(owner)
db.session.commit()
db.session.delete(self)

View File

@ -135,18 +135,50 @@ class Member(User):
self.signature = ""
self.birthday = None
def generate_guest_name(self):
"""Generates a unique guest name to transfer contents to."""
count = 0
while Guest.query.filter_by(name=f"{self.name}_{count}").first():
count += 1
return f"{self.name}_{count}"
def transfer_posts(self, other):
"""
Transfers all the posts to another user. This is generally used to
transfer ownership to a newly-created Guest before deleting an account.
"""
for t in self.topics:
t.author = other
db.session.add(t)
for p in self.programs:
p.author = other
db.session.add(p)
for c in self.comments:
c.author = other
db.session.add(c)
def delete_posts(self):
"""Deletes the user's posts."""
for t in self.topics:
t.delete()
for p in self.programs:
p.delete()
for c in self.comments:
c.delete()
def delete(self):
"""
Deletes the user and the associated information:
* Special privileges
Deletes the user, but not the posts; use either transfer_posts() or
delete_posts() before calling this.
"""
for sp in SpecialPrivilege.query.filter_by(mid=self.id).all():
db.session.delete(sp)
self.trophies = []
db.session.add(self)
db.session.commit()
db.session.delete(self)
db.session.commit()
# Privilege checks

View File

@ -3,7 +3,7 @@ from flask_login import login_required, current_user, logout_user
from app import app, db
from app.forms.account import UpdateAccountForm, RegistrationForm, \
DeleteAccountForm, AskResetPasswordForm, ResetPasswordForm
from app.models.user import Member
from app.models.user import Guest, Member
from app.models.trophy import Title
from app.utils.render import render
from app.utils.send_mail import send_validation_mail, send_reset_password_mail
@ -88,9 +88,20 @@ def reset_password(token):
@login_required
def delete_account():
del_form = DeleteAccountForm()
if del_form.submit.data:
if del_form.validate_on_submit():
db.session.delete(current_user)
if del_form.transfer.data:
guest = Guest(current_user.generate_guest_name())
db.session.add(guest)
db.session.commit()
current_user.transfer_posts(guest)
db.session.commit()
else:
current_user.delete_posts()
db.session.commit()
current_user.delete()
logout_user()
db.session.commit()
flash('Compte supprimé', 'ok')
@ -98,6 +109,7 @@ def delete_account():
else:
flash('Erreur lors de la suppression du compte', 'error')
del_form.delete.data = False # Force to tick to delete the account
return render('account/delete_account.html', del_form=del_form)

View File

@ -2,7 +2,7 @@ from flask import flash, redirect, url_for, request
from flask_login import current_user
from wtforms import BooleanField
from app.utils.priv_required import priv_required
from app.models.user import Member
from app.models.user import Guest, Member
from app.models.trophy import Trophy, Title
from app.models.priv import Group
from app.forms.account import AdminUpdateAccountForm, AdminDeleteAccountForm, \
@ -123,22 +123,38 @@ def adm_edit_account(user_id):
@app.route('/admin/compte/<user_id>/supprimer', methods=['GET', 'POST'])
@priv_required('misc.admin-panel', 'delete.accounts')
def adm_delete_account(user_id):
# A user deleting their own account will be disconnected
user = Member.query.filter_by(id=user_id).first_or_404()
# Note: A user deleting their own account will be disconnected.
# TODO: Number of comments by *other* members which will be deleted
stats = {
'comments': len(user.comments),
'topics': len(user.topics),
'programs': len(user.programs),
'groups': len(user.groups),
'privs': len(user.special_privileges()),
}
# TODO: Add an overview of what will be deleted.
# * How many posts will be turned into guest posts
# * Option: purely delete the posts in question
# * How many PMs will be deleted (can't unassign PMs)
# * etc.
del_form = AdminDeleteAccountForm()
if del_form.submit.data:
if del_form.validate_on_submit():
if del_form.transfer.data:
guest = Guest(user.generate_guest_name())
db.session.add(guest)
db.session.commit()
user.transfer_posts(guest)
db.session.commit()
else:
user.delete_posts()
db.session.commit()
user.delete()
db.session.commit()
flash('Compte supprimé', 'ok')
return redirect(url_for('adm'))
else:
flash('Erreur lors de la suppression du compte', 'error')
del_form.delete.data = False # Force to tick to delete the account
return render('admin/delete_account.html', user=user, del_form=del_form)
return render('admin/delete_account.html', user=user, stats=stats,
del_form=del_form)

View File

@ -6,6 +6,13 @@
<form action="{{ url_for('delete_account') }}" method="post">
{{ del_form.hidden_tag() }}
<div>
{{ del_form.transfer.label }}
{{ del_form.transfer() }}
<div style="font-size: 80%; color: gray">{{ del_form.transfer.description }}</div>
{% for error in del_form.transfer.errors %}
<span class=msgerror>{{ error }}</span>
{% endfor %}
<br>
{{ del_form.delete.label }}
{{ del_form.delete(checked=False) }}
<div style="font-size:80%;color:rgba(0,0,0,.5)">{{ del_form.delete.description }}</div>

View File

@ -7,16 +7,29 @@
{% block content %}
<section class="form">
<h2>Confirmer la suppression du compte</h2>
<p>Le compte '{{ user.name }}' que vous allez supprimer est lié à :</p>
<p>Le compte '{{ user.name }}' possède les posts (migrables) suivants :</p>
<ul>
<li>{{ user.groups | length }} groupe{{ user.groups|length|pluralize }}</li>
<li>{% set sp = user.special_privileges() | length %}
{{- sp }} privilège{{sp|pluralize}} spéci{{sp|pluralize("al","aux")}}</li>
<li>{{ stats.comments }} commentaire{{ stats.comments | pluralize }}</li>
<li>{{ stats.topics }} topic{{ stats.topics | pluralize }}</li>
<li>{{ stats.programs }} topic{{ stats.programs | pluralize }}</li>
</ul>
<p>Les propriétés suivantes seront supprimées :</p>
<ul>
<li>{{ stats.groups }} groupe{{ stats.groups | pluralize }}</li>
<li>{{ stats.privs }} privilège{{ stats.privs | pluralize }}
spéci{{ stats.privs | pluralize("al","aux") }}</li>
</ul>
<form action="{{ url_for('adm_delete_account', user_id=user.id) }}" method=post>
{{ del_form.hidden_tag() }}
<div>
{{ del_form.transfer.label }}
{{ del_form.transfer() }}
<div style="font-size: 80%; color: gray">{{ del_form.transfer.description }}</div>
{% for error in del_form.transfer.errors %}
<span class=msgerror>{{ error }}</span>
{% endfor %}
<br>
{{ del_form.delete.label }}
{{ del_form.delete(checked=False) }}
<div style="font-size: 80%; color: gray">{{ del_form.delete.description }}</div>