Commit cdd2fb7b authored by Anton Akhmerov's avatar Anton Akhmerov

Merge branch '308-sqlalchemy-cascades' into 'master'

Use SQLAlchemy cascades for deleting objects

Closes #308

See merge request !160
parents a0be891d b1fadb3d
import os
import pytest
from flask import Flask
from zesje.api import api_bp
from zesje.database import db
# Adapted from https://stackoverflow.com/a/46062148/1062698
@pytest.fixture
def datadir():
return os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data')
@pytest.fixture(scope="module")
def app():
app = Flask(__name__, static_folder=None)
app.config.update(
SQLALCHEMY_DATABASE_URI='sqlite:///:memory:',
SQLALCHEMY_TRACK_MODIFICATIONS=False # Suppress future deprecation warning
)
db.init_app(app)
with app.app_context():
db.create_all()
app.register_blueprint(api_bp, url_prefix='/api')
return app
@pytest.fixture
def empty_app(app):
with app.app_context():
db.drop_all()
db.create_all()
return app
import pytest
from flask import Flask
from zesje.database import db, Exam, _generate_exam_token
from zesje.database import db, _generate_exam_token, Exam, Problem, ProblemWidget, Solution
from zesje.database import Submission, Scan, Page, ExamWidget, FeedbackOption
@pytest.mark.parametrize('duplicate_count', [
......@@ -32,3 +33,141 @@ def test_exam_generate_token_length_uppercase(duplicate_count, monkeypatch):
id = _generate_exam_token()
assert len(id) == 12
assert id.isupper()
def test_cascades_exam(empty_app, exam, problem, submission, scan, exam_widget):
"""Tests the cascades defined for an exam
Tests the cascades for the following relations:
- Exam -> Submission
- Exam -> Problem
- Exam -> Scan
- Exam -> ExamWidget
"""
empty_app.app_context().push()
exam.problems = [problem]
exam.scans = [scan]
exam.submissions = [submission]
exam.widgets = [exam_widget]
db.session.add(exam)
db.session.commit()
assert problem in db.session
assert submission in db.session
assert scan in db.session
assert exam_widget in db.session
db.session.delete(exam)
db.session.commit()
assert problem not in db.session
assert submission not in db.session
assert scan not in db.session
assert exam_widget not in db.session
def test_cascades_problem(empty_app, exam, problem, submission, solution, problem_widget, feedback_option):
"""Tests the cascades defined for a problem
Tests the cascades for the following relations:
- Problem -> Solution
- Problem -> ProblemWidget
- Problem -> FeedbackOption
"""
empty_app.app_context().push()
exam.problems = [problem]
exam.submissions = [submission]
solution.submission = submission
problem.widget = problem_widget
problem.solutions = [solution]
problem.feedback_options = [feedback_option]
db.session.add_all([exam, problem, submission])
db.session.commit()
assert solution in db.session
assert problem_widget in db.session
assert feedback_option in db.session
db.session.delete(problem)
db.session.commit()
assert solution not in db.session
assert problem_widget not in db.session
assert feedback_option not in db.session
def test_cascades_submission(empty_app, exam, problem, submission, solution, page):
"""Tests the cascades defined for a submission
Tests the cascades for the following relations:
- Submission -> Solution
- Submission -> Page
"""
empty_app.app_context().push()
exam.problems = [problem]
exam.submissions = [submission]
solution.problem = problem
solution.submission = submission
page.submission = submission
db.session.add_all([exam, problem, submission])
db.session.commit()
assert solution in db.session
assert page in db.session
db.session.delete(submission)
db.session.commit()
assert solution not in db.session
assert page not in db.session
@pytest.fixture
def exam():
return Exam(name='')
@pytest.fixture
def problem():
return Problem(name='')
@pytest.fixture
def problem_widget():
return ProblemWidget(name='', page=0, x=0, y=0, width=0, height=0)
@pytest.fixture
def exam_widget():
return ExamWidget(name='', x=0, y=0)
@pytest.fixture
def submission():
return Submission(copy_number=0)
@pytest.fixture
def solution():
return Solution()
@pytest.fixture
def scan():
return Scan(name='', status='')
@pytest.fixture
def page():
return Page(path='', number=0)
@pytest.fixture
def feedback_option():
return FeedbackOption(text='')
......@@ -42,19 +42,7 @@ class Exams(Resource):
elif Submission.query.filter(Submission.exam_id == exam.id).count():
return dict(status=500, message='Exam is not finalized but already has submissions.'), 500
else:
# Delete any scans that were wrongly uploaded to this exam
for scan in exam.scans:
db.session.delete(scan)
for widget in exam.widgets:
db.session.delete(widget)
for problem in exam.problems:
for fb_option in problem.feedback_options:
db.session.delete(fb_option)
db.session.delete(problem.widget)
db.session.delete(problem)
# All corresponding solutions, scans and problems are automatically deleted
db.session.delete(exam)
db.session.commit()
......
......@@ -105,10 +105,7 @@ class Problems(Resource):
if any([sol.graded_by is not None for sol in problem.solutions]):
return dict(status=403, message=f'Problem has already been graded'), 403
else:
# Delete all solutions associated with this problem
for sol in problem.solutions:
db.session.delete(sol)
db.session.delete(problem.widget)
# The widget and all associated solutions are automatically deleted
db.session.delete(problem)
db.session.commit()
return dict(status=200, message="ok"), 200
......@@ -62,10 +62,11 @@ class Exam(db.Model):
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(Text, nullable=False)
token = Column(String(token_length), unique=True, default=_generate_exam_token)
submissions = db.relationship('Submission', backref='exam', lazy=True)
problems = db.relationship('Problem', backref='exam', order_by='Problem.id', lazy=True)
scans = db.relationship('Scan', backref='exam', lazy=True)
widgets = db.relationship('ExamWidget', backref='exam', order_by='ExamWidget.id', lazy=True)
submissions = db.relationship('Submission', backref='exam', cascade='all', lazy=True)
problems = db.relationship('Problem', backref='exam', cascade='all', order_by='Problem.id', lazy=True)
scans = db.relationship('Scan', backref='exam', cascade='all', lazy=True)
widgets = db.relationship('ExamWidget', backref='exam', cascade='all',
order_by='ExamWidget.id', lazy=True)
finalized = Column(Boolean, default=False, server_default='f')
......@@ -75,8 +76,9 @@ class Submission(db.Model):
id = Column(Integer, primary_key=True, autoincrement=True)
copy_number = Column(Integer, nullable=False)
exam_id = Column(Integer, ForeignKey('exam.id'), nullable=False)
solutions = db.relationship('Solution', backref='submission', order_by='Solution.problem_id', lazy=True)
pages = db.relationship('Page', backref='submission', lazy=True)
solutions = db.relationship('Solution', backref='submission', cascade='all',
order_by='Solution.problem_id', lazy=True)
pages = db.relationship('Page', backref='submission', cascade='all', lazy=True)
student_id = Column(Integer, ForeignKey('student.id'), nullable=True)
signature_validated = Column(Boolean, default=False, server_default='f', nullable=False)
......@@ -96,9 +98,10 @@ class Problem(db.Model):
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(Text, nullable=False)
exam_id = Column(Integer, ForeignKey('exam.id'), nullable=False)
feedback_options = db.relationship('FeedbackOption', backref='problem', order_by='FeedbackOption.id', lazy=True)
solutions = db.relationship('Solution', backref='problem', lazy=True)
widget = db.relationship('ProblemWidget', backref='problem', uselist=False, lazy=True)
feedback_options = db.relationship('FeedbackOption', backref='problem', cascade='all',
order_by='FeedbackOption.id', lazy=True)
solutions = db.relationship('Solution', backref='problem', cascade='all', lazy=True)
widget = db.relationship('ProblemWidget', backref='problem', cascade='all', uselist=False, lazy=True)
class FeedbackOption(db.Model):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment