Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cascade delete constraints, allow users to delete their accounts #316

Merged
merged 3 commits into from
Mar 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion KerbalStuff/blueprints/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
import zipfile
from datetime import datetime
from functools import wraps
from shutil import rmtree
from typing import Dict, Any, Callable, Optional, Tuple, Iterable, List, Union

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


@api.route("/api/user/<username>/delete", methods=['POST'])
@with_session
@user_required
@json_output
def delete(username: str) -> Tuple[Dict[str, Any], int]:
deletable = False
if current_user:
if current_user.admin:
deletable = True
if current_user.username == username:
deletable = True
if not deletable:
return {'error': True, 'reason': 'Unauthorized'}, 401

form_username = request.form.get('username')
if form_username != username:
return {'error': True, 'reason': 'Wrong username'}, 403

user = User.query.filter(User.username == username).one_or_none()
if not user:
return {'error': True, 'reason': 'User does not exist'}, 404

storage = _cfg('storage')
if storage:
full_path = os.path.join(storage, user.base_path())
rmtree(full_path, ignore_errors=True)

db.delete(user)
if user == current_user:
logout_user()

return {"error": False}, 400


@api.route('/api/mod/<int:mod_id>/update-bg', methods=['POST'])
@with_session
@json_output
Expand Down
51 changes: 25 additions & 26 deletions KerbalStuff/blueprints/mods.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@
import os
import random
import re
import sys
from datetime import datetime, timedelta
from shutil import rmtree
from typing import Any, Dict, Tuple, Optional, Union, List
from typing import Any, Dict, Tuple, Optional, Union

import werkzeug.wrappers
import user_agents

from flask import Blueprint, render_template, send_file, make_response, url_for, abort, session, \
from flask import Blueprint, render_template, make_response, url_for, abort, session, \
redirect, request
from flask_login import current_user
from urllib.parse import urlparse
Expand All @@ -24,9 +23,8 @@
from ..database import db
from ..email import send_autoupdate_notification, send_mod_locked
from ..objects import Mod, ModVersion, DownloadEvent, FollowEvent, ReferralEvent, \
Featured, Media, GameVersion, Game, Following
Featured, GameVersion, Game, Following
from ..search import get_mod_score
from ..thumbnail import thumb_path_from_background_path
from ..purge import purge_download

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

if game_id:
game = Game.query.filter(Game.active == True, Game.id == game_id).one()
game = Game.query.filter(Game.active == True, Game.id == game_id).one_or_none()
# Make sure it's fully set in the session cookie.
set_game_info(game)
return game
if game:
set_game_info(game)
return game

return None

Expand Down Expand Up @@ -393,7 +392,7 @@ def export_referrals(mod_id: int, mod_name: str) -> werkzeug.wrappers.Response:
@loginrequired
@with_session
def delete(mod_id: int) -> werkzeug.wrappers.Response:
mod, game = _get_mod_game_info(mod_id)
mod, _ = _get_mod_game_info(mod_id)
editable = False
if current_user:
if current_user.admin:
Expand All @@ -402,21 +401,15 @@ def delete(mod_id: int) -> werkzeug.wrappers.Response:
editable = True
if not editable:
abort(403)
for featured in Featured.query.filter(Featured.mod_id == mod.id).all():
db.delete(featured)
for media in Media.query.filter(Media.mod_id == mod.id).all():
db.delete(media)
for referral in ReferralEvent.query.filter(ReferralEvent.mod_id == mod.id).all():
db.delete(referral)
for version in ModVersion.query.filter(ModVersion.mod_id == mod.id).all():
db.delete(version)
db.delete(mod)
db.commit()
notify_ckan(mod, 'delete', True)

storage = _cfg('storage')
if storage:
full_path = os.path.join(storage, mod.base_path())
rmtree(full_path)
rmtree(full_path, ignore_errors=True)
db.delete(mod)
db.commit()
notify_ckan(mod, 'delete', True)

return redirect("/profile/" + current_user.username)


Expand Down Expand Up @@ -632,18 +625,24 @@ def download(mod_id: int, mod_name: Optional[str], version: Optional[str]) -> Op
def delete_version(mod_id: int, version_id: str) -> werkzeug.wrappers.Response:
mod, game = _get_mod_game_info(mod_id)
check_mod_editable(mod)
version = [v for v in mod.versions if v.id == int(version_id)]
version = ModVersion.query.get(version_id)
if len(mod.versions) == 1:
abort(400)
if len(version) == 0:
if not version:
abort(404)
if version[0].id == mod.default_version_id:
if version.id == mod.default_version_id:
abort(400)
if version.mod != mod:
abort(400)

purge_download(version[0].download_path)

db.delete(version[0])
mod.versions = [v for v in mod.versions if v.id != int(version_id)]
storage = _cfg('storage')
if storage:
full_path = os.path.join(storage, version.download_path)
os.remove(full_path)

db.delete(version)
db.commit()
return redirect(url_for("mods.mod", _anchor='changelog', mod_id=mod.id, mod_name=mod.name))

Expand Down
80 changes: 43 additions & 37 deletions KerbalStuff/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@
class Following(Base): # type: ignore
__tablename__ = 'mod_followers'
__table_args__ = (PrimaryKeyConstraint('user_id', 'mod_id', name='pk_mod_followers'), )
mod_id = Column(Integer, ForeignKey('mod.id'), index=True)
mod = relationship('Mod', back_populates='followings')
user_id = Column(Integer, ForeignKey('user.id'), index=True)
user = relationship('User', back_populates='followings')
mod_id = Column(Integer, ForeignKey('mod.id', ondelete='CASCADE'), index=True)
mod = relationship('Mod', back_populates='followings', passive_deletes=True)
user_id = Column(Integer, ForeignKey('user.id', ondelete='CASCADE'), index=True)
user = relationship('User', back_populates='followings', passive_deletes=True)
send_update = Column(Boolean(), default=True, nullable=False)
send_autoupdate = Column(Boolean(), default=True, nullable=False)

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

def __repr__(self) -> str:
Expand Down Expand Up @@ -85,7 +85,7 @@ class User(Base): # type: ignore
bgOffsetX = Column(Integer, default=0)
bgOffsetY = Column(Integer, default=0)
# List of Following objects
followings = relationship('Following', back_populates='user')
followings = relationship('Following', back_populates='user', passive_deletes=True)
# List of mods the user follows
following = association_proxy('followings', 'mod')
dark_theme = Column(Boolean, default=False)
Expand Down Expand Up @@ -178,8 +178,8 @@ class Game(Base): # type: ignore
rating = Column(Float())
releasedate = Column(DateTime)
short = Column(Unicode(1024))
publisher_id = Column(Integer, ForeignKey('publisher.id'))
publisher = relationship('Publisher', backref='games')
publisher_id = Column(Integer, ForeignKey('publisher.id', ondelete='CASCADE'))
publisher = relationship('Publisher', backref=backref('games', passive_deletes=True))
description = Column(Unicode(100000))
short_description = Column(Unicode(1000))
created = Column(DateTime, default=datetime.now, index=True)
Expand Down Expand Up @@ -226,10 +226,11 @@ class Mod(Base): # type: ignore
id = Column(Integer, primary_key=True)
created = Column(DateTime, default=datetime.now, index=True)
updated = Column(DateTime, default=datetime.now, index=True)
user_id = Column(Integer, ForeignKey('user.id'))
user = relationship('User', backref=backref('mods', order_by=created), foreign_keys=user_id)
game_id = Column(Integer, ForeignKey('game.id'))
game = relationship('Game', backref='mods')
user_id = Column(Integer, ForeignKey('user.id', ondelete='CASCADE'))
user = relationship('User', backref=backref('mods', passive_deletes=True, order_by=created),
foreign_keys=user_id)
game_id = Column(Integer, ForeignKey('game.id', ondelete='CASCADE'))
game = relationship('Game', backref=backref('mods', passive_deletes=True))
name = Column(String(100), index=True)
description = Column(Unicode(100000))
short_description = Column(Unicode(1000))
Expand Down Expand Up @@ -258,7 +259,7 @@ class Mod(Base): # type: ignore
download_count = Column(Integer, nullable=False, default=0)
ckan = Column(Boolean)
# List of Following objects
followings = relationship('Following', back_populates='mod')
followings = relationship('Following', back_populates='mod', passive_deletes=True)
# List of users that follow this mods
followers = association_proxy('followings', 'user')

Expand Down Expand Up @@ -287,10 +288,10 @@ class ModList(Base): # type: ignore
__tablename__ = 'modlist'
id = Column(Integer, primary_key=True)
created = Column(DateTime, default=datetime.now, index=True)
user_id = Column(Integer, ForeignKey('user.id'))
user = relationship('User', backref=backref('packs', order_by=created))
game_id = Column(Integer, ForeignKey('game.id'))
game = relationship('Game', backref='modlists')
user_id = Column(Integer, ForeignKey('user.id', ondelete='CASCADE'))
user = relationship('User', backref=backref('packs', passive_deletes=True, order_by=created))
game_id = Column(Integer, ForeignKey('game.id', ondelete='CASCADE'))
game = relationship('Game', backref=backref('modlists', passive_deletes=True))
# Don't access background directly, use background_url() instead.
background = Column(String(512))
# Don't access thumbnail directly, use background_thumb() instead.
Expand Down Expand Up @@ -336,10 +337,10 @@ def __repr__(self) -> str:
class SharedAuthor(Base): # type: ignore
__tablename__ = 'sharedauthor'
id = Column(Integer, primary_key=True)
mod_id = Column(Integer, ForeignKey('mod.id'))
mod = relationship('Mod', backref='shared_authors')
user_id = Column(Integer, ForeignKey('user.id'))
user = relationship('User', backref='shared_authors')
mod_id = Column(Integer, ForeignKey('mod.id', ondelete='CASCADE'))
mod = relationship('Mod', backref=backref('shared_authors', passive_deletes=True))
user_id = Column(Integer, ForeignKey('user.id', ondelete='CASCADE'))
user = relationship('User', backref=backref('shared_authors', passive_deletes=True))
accepted = Column(Boolean, default=False)

def __repr__(self) -> str:
Expand Down Expand Up @@ -368,9 +369,10 @@ def __repr__(self) -> str:
class FollowEvent(Base): # type: ignore
__tablename__ = 'followevent'
id = Column(Integer, primary_key=True)
mod_id = Column(Integer, ForeignKey('mod.id'))
mod = relationship('Mod',
backref=backref('follow_events', order_by="desc(FollowEvent.created)"))
mod_id = Column(Integer, ForeignKey('mod.id', ondelete='CASCADE'))
mod = relationship('Mod', backref=backref('follow_events',
passive_deletes=True,
order_by="desc(FollowEvent.created)"))
events = Column(Integer)
delta = Column(Integer, default=0)
created = Column(DateTime, default=datetime.now, index=True)
Expand All @@ -382,9 +384,10 @@ def __repr__(self) -> str:
class ReferralEvent(Base): # type: ignore
__tablename__ = 'referralevent'
id = Column(Integer, primary_key=True)
mod_id = Column(Integer, ForeignKey('mod.id'))
mod = relationship('Mod',
backref=backref('referrals', order_by="desc(ReferralEvent.created)"))
mod_id = Column(Integer, ForeignKey('mod.id', ondelete='CASCADE'))
mod = relationship('Mod', backref=backref('referrals',
passive_deletes=True,
order_by="desc(ReferralEvent.created)"))
host = Column(String)
events = Column(Integer, default=0)
created = Column(DateTime, default=datetime.now, index=True)
Expand All @@ -396,13 +399,16 @@ def __repr__(self) -> str:
class ModVersion(Base): # type: ignore
__tablename__ = 'modversion'
id = Column(Integer, primary_key=True)
mod_id = Column(Integer, ForeignKey('mod.id'))
mod = relationship('Mod',
backref=backref('versions', order_by="desc(ModVersion.sort_index)"),
mod_id = Column(Integer, ForeignKey('mod.id', ondelete='CASCADE'))
mod = relationship('Mod', backref=backref('versions',
passive_deletes=True,
order_by="desc(ModVersion.sort_index)"),
foreign_keys=mod_id)
friendly_version = Column(String(64))
gameversion_id = Column(Integer, ForeignKey('gameversion.id'))
gameversion = relationship('GameVersion', backref=backref('mod_versions', order_by=id))
gameversion_id = Column(Integer, ForeignKey('gameversion.id', ondelete='CASCADE'))
gameversion = relationship('GameVersion', backref=backref('mod_versions',
passive_deletes=True,
order_by=id))
created = Column(DateTime, default=datetime.now)
download_path = Column(String(512))
changelog = Column(Unicode(10000))
Expand Down Expand Up @@ -436,8 +442,8 @@ def __repr__(self) -> str:
class Media(Base): # type: ignore
__tablename__ = 'media'
id = Column(Integer, primary_key=True)
mod_id = Column(Integer, ForeignKey('mod.id'))
mod = relationship('Mod', backref=backref('media', order_by=id))
mod_id = Column(Integer, ForeignKey('mod.id', ondelete='CASCADE'))
mod = relationship('Mod', backref=backref('media', passive_deletes=True, order_by=id))
hash = Column(String(12))
type = Column(String(32))
data = Column(String(512))
Expand All @@ -450,8 +456,8 @@ class GameVersion(Base): # type: ignore
__tablename__ = 'gameversion'
id = Column(Integer, primary_key=True)
friendly_version = Column(String(128))
game_id = Column(Integer, ForeignKey('game.id'))
game = relationship('Game', backref='versions')
game_id = Column(Integer, ForeignKey('game.id', ondelete='CASCADE'))
game = relationship('Game', backref=backref('versions', passive_deletes=True))

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