Skip to content

Commit b858a2e

Browse files
authored
Merge pull request #316 from DasSkelett/feature/delete-account
[WIP] Add cascade delete constraints, allow users to delete their accounts
2 parents ccc0382 + 309896a commit b858a2e

File tree

8 files changed

+336
-72
lines changed

8 files changed

+336
-72
lines changed

KerbalStuff/blueprints/api.py

+36-1
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
import zipfile
66
from datetime import datetime
77
from functools import wraps
8+
from shutil import rmtree
89
from typing import Dict, Any, Callable, Optional, Tuple, Iterable, List, Union
910

1011
from flask import Blueprint, url_for, current_app, request, abort
11-
from flask_login import login_user, current_user
12+
from flask_login import login_user, current_user, logout_user
1213
from sqlalchemy import desc, asc
1314
from sqlalchemy.orm import Query
1415
from werkzeug.utils import secure_filename
@@ -551,6 +552,40 @@ def change_password(username: str) -> Union[Dict[str, Any], Tuple[Union[str, Any
551552
return {'error': True, 'reason': pw_message}
552553

553554

555+
@api.route("/api/user/<username>/delete", methods=['POST'])
556+
@with_session
557+
@user_required
558+
@json_output
559+
def delete(username: str) -> Tuple[Dict[str, Any], int]:
560+
deletable = False
561+
if current_user:
562+
if current_user.admin:
563+
deletable = True
564+
if current_user.username == username:
565+
deletable = True
566+
if not deletable:
567+
return {'error': True, 'reason': 'Unauthorized'}, 401
568+
569+
form_username = request.form.get('username')
570+
if form_username != username:
571+
return {'error': True, 'reason': 'Wrong username'}, 403
572+
573+
user = User.query.filter(User.username == username).one_or_none()
574+
if not user:
575+
return {'error': True, 'reason': 'User does not exist'}, 404
576+
577+
storage = _cfg('storage')
578+
if storage:
579+
full_path = os.path.join(storage, user.base_path())
580+
rmtree(full_path, ignore_errors=True)
581+
582+
db.delete(user)
583+
if user == current_user:
584+
logout_user()
585+
586+
return {"error": False}, 400
587+
588+
554589
@api.route('/api/mod/<int:mod_id>/update-bg', methods=['POST'])
555590
@with_session
556591
@json_output

KerbalStuff/blueprints/mods.py

+25-26
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@
22
import os
33
import random
44
import re
5-
import sys
65
from datetime import datetime, timedelta
76
from shutil import rmtree
8-
from typing import Any, Dict, Tuple, Optional, Union, List
7+
from typing import Any, Dict, Tuple, Optional, Union
98

109
import werkzeug.wrappers
1110
import user_agents
1211

13-
from flask import Blueprint, render_template, send_file, make_response, url_for, abort, session, \
12+
from flask import Blueprint, render_template, make_response, url_for, abort, session, \
1413
redirect, request
1514
from flask_login import current_user
1615
from urllib.parse import urlparse
@@ -24,9 +23,8 @@
2423
from ..database import db
2524
from ..email import send_autoupdate_notification, send_mod_locked
2625
from ..objects import Mod, ModVersion, DownloadEvent, FollowEvent, ReferralEvent, \
27-
Featured, Media, GameVersion, Game, Following
26+
Featured, GameVersion, Game, Following
2827
from ..search import get_mod_score
29-
from ..thumbnail import thumb_path_from_background_path
3028
from ..purge import purge_download
3129

3230
mods = Blueprint('mods', __name__)
@@ -52,10 +50,11 @@ def _restore_game_info() -> Optional[Game]:
5250
game_id = session.get('gameid')
5351

5452
if game_id:
55-
game = Game.query.filter(Game.active == True, Game.id == game_id).one()
53+
game = Game.query.filter(Game.active == True, Game.id == game_id).one_or_none()
5654
# Make sure it's fully set in the session cookie.
57-
set_game_info(game)
58-
return game
55+
if game:
56+
set_game_info(game)
57+
return game
5958

6059
return None
6160

@@ -393,7 +392,7 @@ def export_referrals(mod_id: int, mod_name: str) -> werkzeug.wrappers.Response:
393392
@loginrequired
394393
@with_session
395394
def delete(mod_id: int) -> werkzeug.wrappers.Response:
396-
mod, game = _get_mod_game_info(mod_id)
395+
mod, _ = _get_mod_game_info(mod_id)
397396
editable = False
398397
if current_user:
399398
if current_user.admin:
@@ -402,21 +401,15 @@ def delete(mod_id: int) -> werkzeug.wrappers.Response:
402401
editable = True
403402
if not editable:
404403
abort(403)
405-
for featured in Featured.query.filter(Featured.mod_id == mod.id).all():
406-
db.delete(featured)
407-
for media in Media.query.filter(Media.mod_id == mod.id).all():
408-
db.delete(media)
409-
for referral in ReferralEvent.query.filter(ReferralEvent.mod_id == mod.id).all():
410-
db.delete(referral)
411-
for version in ModVersion.query.filter(ModVersion.mod_id == mod.id).all():
412-
db.delete(version)
413-
db.delete(mod)
414-
db.commit()
415-
notify_ckan(mod, 'delete', True)
404+
416405
storage = _cfg('storage')
417406
if storage:
418407
full_path = os.path.join(storage, mod.base_path())
419-
rmtree(full_path)
408+
rmtree(full_path, ignore_errors=True)
409+
db.delete(mod)
410+
db.commit()
411+
notify_ckan(mod, 'delete', True)
412+
420413
return redirect("/profile/" + current_user.username)
421414

422415

@@ -632,18 +625,24 @@ def download(mod_id: int, mod_name: Optional[str], version: Optional[str]) -> Op
632625
def delete_version(mod_id: int, version_id: str) -> werkzeug.wrappers.Response:
633626
mod, game = _get_mod_game_info(mod_id)
634627
check_mod_editable(mod)
635-
version = [v for v in mod.versions if v.id == int(version_id)]
628+
version = ModVersion.query.get(version_id)
636629
if len(mod.versions) == 1:
637630
abort(400)
638-
if len(version) == 0:
631+
if not version:
639632
abort(404)
640-
if version[0].id == mod.default_version_id:
633+
if version.id == mod.default_version_id:
634+
abort(400)
635+
if version.mod != mod:
641636
abort(400)
642637

643638
purge_download(version[0].download_path)
644639

645-
db.delete(version[0])
646-
mod.versions = [v for v in mod.versions if v.id != int(version_id)]
640+
storage = _cfg('storage')
641+
if storage:
642+
full_path = os.path.join(storage, version.download_path)
643+
os.remove(full_path)
644+
645+
db.delete(version)
647646
db.commit()
648647
return redirect(url_for("mods.mod", _anchor='changelog', mod_id=mod.id, mod_name=mod.name))
649648

KerbalStuff/objects.py

+43-37
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@
1919
class Following(Base): # type: ignore
2020
__tablename__ = 'mod_followers'
2121
__table_args__ = (PrimaryKeyConstraint('user_id', 'mod_id', name='pk_mod_followers'), )
22-
mod_id = Column(Integer, ForeignKey('mod.id'), index=True)
23-
mod = relationship('Mod', back_populates='followings')
24-
user_id = Column(Integer, ForeignKey('user.id'), index=True)
25-
user = relationship('User', back_populates='followings')
22+
mod_id = Column(Integer, ForeignKey('mod.id', ondelete='CASCADE'), index=True)
23+
mod = relationship('Mod', back_populates='followings', passive_deletes=True)
24+
user_id = Column(Integer, ForeignKey('user.id', ondelete='CASCADE'), index=True)
25+
user = relationship('User', back_populates='followings', passive_deletes=True)
2626
send_update = Column(Boolean(), default=True, nullable=False)
2727
send_autoupdate = Column(Boolean(), default=True, nullable=False)
2828

@@ -37,8 +37,8 @@ def __init__(self, mod: Optional['Mod'] = None, user: Optional['User'] = None,
3737
class Featured(Base): # type: ignore
3838
__tablename__ = 'featured'
3939
id = Column(Integer, primary_key=True)
40-
mod_id = Column(Integer, ForeignKey('mod.id'))
41-
mod = relationship('Mod', backref=backref('featured', order_by=id))
40+
mod_id = Column(Integer, ForeignKey('mod.id', ondelete='CASCADE'))
41+
mod = relationship('Mod', backref=backref('featured', passive_deletes=True, order_by=id))
4242
created = Column(DateTime, default=datetime.now, index=True)
4343

4444
def __repr__(self) -> str:
@@ -85,7 +85,7 @@ class User(Base): # type: ignore
8585
bgOffsetX = Column(Integer, default=0)
8686
bgOffsetY = Column(Integer, default=0)
8787
# List of Following objects
88-
followings = relationship('Following', back_populates='user')
88+
followings = relationship('Following', back_populates='user', passive_deletes=True)
8989
# List of mods the user follows
9090
following = association_proxy('followings', 'mod')
9191
dark_theme = Column(Boolean, default=False)
@@ -178,8 +178,8 @@ class Game(Base): # type: ignore
178178
rating = Column(Float())
179179
releasedate = Column(DateTime)
180180
short = Column(Unicode(1024))
181-
publisher_id = Column(Integer, ForeignKey('publisher.id'))
182-
publisher = relationship('Publisher', backref='games')
181+
publisher_id = Column(Integer, ForeignKey('publisher.id', ondelete='CASCADE'))
182+
publisher = relationship('Publisher', backref=backref('games', passive_deletes=True))
183183
description = Column(Unicode(100000))
184184
short_description = Column(Unicode(1000))
185185
created = Column(DateTime, default=datetime.now, index=True)
@@ -226,10 +226,11 @@ class Mod(Base): # type: ignore
226226
id = Column(Integer, primary_key=True)
227227
created = Column(DateTime, default=datetime.now, index=True)
228228
updated = Column(DateTime, default=datetime.now, index=True)
229-
user_id = Column(Integer, ForeignKey('user.id'))
230-
user = relationship('User', backref=backref('mods', order_by=created), foreign_keys=user_id)
231-
game_id = Column(Integer, ForeignKey('game.id'))
232-
game = relationship('Game', backref='mods')
229+
user_id = Column(Integer, ForeignKey('user.id', ondelete='CASCADE'))
230+
user = relationship('User', backref=backref('mods', passive_deletes=True, order_by=created),
231+
foreign_keys=user_id)
232+
game_id = Column(Integer, ForeignKey('game.id', ondelete='CASCADE'))
233+
game = relationship('Game', backref=backref('mods', passive_deletes=True))
233234
name = Column(String(100), index=True)
234235
description = Column(Unicode(100000))
235236
short_description = Column(Unicode(1000))
@@ -258,7 +259,7 @@ class Mod(Base): # type: ignore
258259
download_count = Column(Integer, nullable=False, default=0)
259260
ckan = Column(Boolean)
260261
# List of Following objects
261-
followings = relationship('Following', back_populates='mod')
262+
followings = relationship('Following', back_populates='mod', passive_deletes=True)
262263
# List of users that follow this mods
263264
followers = association_proxy('followings', 'user')
264265

@@ -287,10 +288,10 @@ class ModList(Base): # type: ignore
287288
__tablename__ = 'modlist'
288289
id = Column(Integer, primary_key=True)
289290
created = Column(DateTime, default=datetime.now, index=True)
290-
user_id = Column(Integer, ForeignKey('user.id'))
291-
user = relationship('User', backref=backref('packs', order_by=created))
292-
game_id = Column(Integer, ForeignKey('game.id'))
293-
game = relationship('Game', backref='modlists')
291+
user_id = Column(Integer, ForeignKey('user.id', ondelete='CASCADE'))
292+
user = relationship('User', backref=backref('packs', passive_deletes=True, order_by=created))
293+
game_id = Column(Integer, ForeignKey('game.id', ondelete='CASCADE'))
294+
game = relationship('Game', backref=backref('modlists', passive_deletes=True))
294295
# Don't access background directly, use background_url() instead.
295296
background = Column(String(512))
296297
# Don't access thumbnail directly, use background_thumb() instead.
@@ -336,10 +337,10 @@ def __repr__(self) -> str:
336337
class SharedAuthor(Base): # type: ignore
337338
__tablename__ = 'sharedauthor'
338339
id = Column(Integer, primary_key=True)
339-
mod_id = Column(Integer, ForeignKey('mod.id'))
340-
mod = relationship('Mod', backref='shared_authors')
341-
user_id = Column(Integer, ForeignKey('user.id'))
342-
user = relationship('User', backref='shared_authors')
340+
mod_id = Column(Integer, ForeignKey('mod.id', ondelete='CASCADE'))
341+
mod = relationship('Mod', backref=backref('shared_authors', passive_deletes=True))
342+
user_id = Column(Integer, ForeignKey('user.id', ondelete='CASCADE'))
343+
user = relationship('User', backref=backref('shared_authors', passive_deletes=True))
343344
accepted = Column(Boolean, default=False)
344345

345346
def __repr__(self) -> str:
@@ -368,9 +369,10 @@ def __repr__(self) -> str:
368369
class FollowEvent(Base): # type: ignore
369370
__tablename__ = 'followevent'
370371
id = Column(Integer, primary_key=True)
371-
mod_id = Column(Integer, ForeignKey('mod.id'))
372-
mod = relationship('Mod',
373-
backref=backref('follow_events', order_by="desc(FollowEvent.created)"))
372+
mod_id = Column(Integer, ForeignKey('mod.id', ondelete='CASCADE'))
373+
mod = relationship('Mod', backref=backref('follow_events',
374+
passive_deletes=True,
375+
order_by="desc(FollowEvent.created)"))
374376
events = Column(Integer)
375377
delta = Column(Integer, default=0)
376378
created = Column(DateTime, default=datetime.now, index=True)
@@ -382,9 +384,10 @@ def __repr__(self) -> str:
382384
class ReferralEvent(Base): # type: ignore
383385
__tablename__ = 'referralevent'
384386
id = Column(Integer, primary_key=True)
385-
mod_id = Column(Integer, ForeignKey('mod.id'))
386-
mod = relationship('Mod',
387-
backref=backref('referrals', order_by="desc(ReferralEvent.created)"))
387+
mod_id = Column(Integer, ForeignKey('mod.id', ondelete='CASCADE'))
388+
mod = relationship('Mod', backref=backref('referrals',
389+
passive_deletes=True,
390+
order_by="desc(ReferralEvent.created)"))
388391
host = Column(String)
389392
events = Column(Integer, default=0)
390393
created = Column(DateTime, default=datetime.now, index=True)
@@ -396,13 +399,16 @@ def __repr__(self) -> str:
396399
class ModVersion(Base): # type: ignore
397400
__tablename__ = 'modversion'
398401
id = Column(Integer, primary_key=True)
399-
mod_id = Column(Integer, ForeignKey('mod.id'))
400-
mod = relationship('Mod',
401-
backref=backref('versions', order_by="desc(ModVersion.sort_index)"),
402+
mod_id = Column(Integer, ForeignKey('mod.id', ondelete='CASCADE'))
403+
mod = relationship('Mod', backref=backref('versions',
404+
passive_deletes=True,
405+
order_by="desc(ModVersion.sort_index)"),
402406
foreign_keys=mod_id)
403407
friendly_version = Column(String(64))
404-
gameversion_id = Column(Integer, ForeignKey('gameversion.id'))
405-
gameversion = relationship('GameVersion', backref=backref('mod_versions', order_by=id))
408+
gameversion_id = Column(Integer, ForeignKey('gameversion.id', ondelete='CASCADE'))
409+
gameversion = relationship('GameVersion', backref=backref('mod_versions',
410+
passive_deletes=True,
411+
order_by=id))
406412
created = Column(DateTime, default=datetime.now)
407413
download_path = Column(String(512))
408414
changelog = Column(Unicode(10000))
@@ -436,8 +442,8 @@ def __repr__(self) -> str:
436442
class Media(Base): # type: ignore
437443
__tablename__ = 'media'
438444
id = Column(Integer, primary_key=True)
439-
mod_id = Column(Integer, ForeignKey('mod.id'))
440-
mod = relationship('Mod', backref=backref('media', order_by=id))
445+
mod_id = Column(Integer, ForeignKey('mod.id', ondelete='CASCADE'))
446+
mod = relationship('Mod', backref=backref('media', passive_deletes=True, order_by=id))
441447
hash = Column(String(12))
442448
type = Column(String(32))
443449
data = Column(String(512))
@@ -450,8 +456,8 @@ class GameVersion(Base): # type: ignore
450456
__tablename__ = 'gameversion'
451457
id = Column(Integer, primary_key=True)
452458
friendly_version = Column(String(128))
453-
game_id = Column(Integer, ForeignKey('game.id'))
454-
game = relationship('Game', backref='versions')
459+
game_id = Column(Integer, ForeignKey('game.id', ondelete='CASCADE'))
460+
game = relationship('Game', backref=backref('versions', passive_deletes=True))
455461

456462
def __repr__(self) -> str:
457463
return '<Game Version %r>' % self.friendly_version

0 commit comments

Comments
 (0)