Skip to content

Commit

Permalink
Require users delete classes they created before deleting their user …
Browse files Browse the repository at this point in the history
…data.
  • Loading branch information
liffiton committed Dec 5, 2024
1 parent 2479c1a commit 23b08f7
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 15 deletions.
31 changes: 29 additions & 2 deletions src/gened/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,21 @@ def main() -> str:
ORDER BY classes.id DESC
""", [user_id, cur_class_id]).fetchall()

return render_template("profile_view.html", user=user, other_classes=other_classes, archived_classes=archived_classes)
# Get any classes created by this user
created_classes = db.execute("""
SELECT classes.name
FROM classes_user
JOIN classes ON classes_user.class_id = classes.id
WHERE creator_user_id = ?
""", [user_id]).fetchall()

return render_template(
"profile_view.html",
user=user,
other_classes=other_classes,
archived_classes=archived_classes,
created_classes=created_classes
)


@bp.route("/delete_data", methods=['POST'])
Expand All @@ -71,10 +85,23 @@ def delete_data() -> Response:
flash("Data deletion requires confirmation. Please type DELETE to confirm.", "warning")
return safe_redirect(request.referrer, default_endpoint="profile.main")

db = get_db()
auth = get_auth()
user_id = auth.user_id
assert user_id is not None # due to @login_required
db = get_db()

# Check if user has any classes they created
created_classes = db.execute("""
SELECT classes.name
FROM classes_user
JOIN classes ON classes_user.class_id = classes.id
WHERE creator_user_id = ?
""", [user_id]).fetchall()

if created_classes:
class_names = ", ".join(row['name'] for row in created_classes)
flash(f"You must delete all classes you created before deleting your data. Please delete these classes first: {class_names}", "danger")
return safe_redirect(request.referrer, default_endpoint="profile.main")

# Call application-specific data deletion handler(s)
delete_user_data(user_id)
Expand Down
41 changes: 32 additions & 9 deletions src/gened/templates/profile_view.html
Original file line number Diff line number Diff line change
Expand Up @@ -130,22 +130,45 @@ <h2 class="has-text-danger">Delete Your Data</h2>
</ul>
</p>
{% if auth.user.auth_provider == 'lti' %}
<p>This user was created using a link from an LMS. If you follow that link after deleting your data here, you will create a new user on CodeHelp separate from this one.</p>
<p>This user was created using a link from an LMS. If you connect to CodeHelp from your LMS after deleting your data here, you will create a new user on CodeHelp separate from this one.</p>
{% endif %}
<p>Type "DELETE" in the text box to confirm you want to delete your data.</p>
<p class="has-text-danger"><strong class="has-text-danger">Warning:</strong> Deleting your data cannot be undone.</p>
</div>
<form method="POST" action="{{ url_for('profile.delete_data') }}">
<div class="field has-addons">
<div class="control">
<input class="input is-danger" type="text" name="confirm_delete" placeholder="Type DELETE to confirm">
{% if created_classes %}
<div class="field has-addons">
<div class="control">
<input disabled class="input is-danger" type="text" name="confirm_delete" placeholder="Type DELETE to confirm">
</div>
<div class="control">
<button disabled type="submit" class="button is-danger">
Delete Class Data
</button>
</div>
</div>
<div class="control">
<button type="submit" class="button is-danger">
Delete Class Data
</button>
<div class="notification is-danger">
<p><strong>Classes cannot be orphaned.</strong></p>
<p>Before you can delete your data, you must first delete all classes you have created:</p>
<ul>
{% for class in created_classes %}
<li>{{ class.name }}</li>
{% endfor %}
</ul>
<p>This must be done manually, because it may require deleting other user's data.</p>
</div>
</div>
{% else %}
<div class="field has-addons">
<div class="control">
<input class="input is-danger" type="text" name="confirm_delete" placeholder="Type DELETE to confirm">
</div>
<div class="control">
<button type="submit" class="button is-danger">
Delete Class Data
</button>
</div>
</div>
{% endif %}
<div class="control">
<button class="button" type="submit" formnovalidate formmethod="dialog">Cancel</button>
</div>
Expand Down
4 changes: 3 additions & 1 deletion tests/test_data.sql
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ VALUES
-- testuser, testadmin, and testinstructor are in class 2 -- but not testuser2
(4, 11, 2, 'instructor'), -- testuser created the class
(5, 12, 2, 'student'), -- testadmin is a student
(6, 13, 2, 'student'); -- testinstructor is a student
(6, 13, 2, 'student'), -- testinstructor is a student
(7, 11, 3, 'instructor'), -- testuser created the class
(8, 11, 4, 'instructor'); -- testuser created the class

INSERT INTO context_strings (id, ctx_str)
VALUES (1, 'context 1'), (2, 'context 2');
Expand Down
20 changes: 17 additions & 3 deletions tests/test_privacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def verify_row_count(table: str, where_clause: str, params: list[str | int], exp

def test_delete_user_data_requires_confirmation(app, client, auth):
"""Test that user data deletion requires proper confirmation"""
auth.login()
login_instructor_in_class(client, auth)
with app.app_context():
user_id = get_db().execute("SELECT id FROM users WHERE auth_name='testuser'").fetchone()['id']

Expand All @@ -47,7 +47,7 @@ def test_delete_user_data_requires_confirmation(app, client, auth):

def test_delete_user_data_full_process(app, client, auth):
"""Test complete user data deletion process"""
auth.login()
login_instructor_in_class(client, auth)
with app.app_context():
user_id = get_db().execute("SELECT id FROM users WHERE auth_name='testuser'").fetchone()['id']

Expand All @@ -64,7 +64,21 @@ def test_delete_user_data_full_process(app, client, auth):
# Perform deletion with proper confirmation
response = client.post('/profile/delete_data', data={'confirm_delete': 'DELETE'})
assert response.status_code == 302
assert response.location == "/auth/login"
assert response.location == "/profile/" # redirect to profile because we have a user-created class
response = client.get('/profile/')
assert b"You must delete all classes you created before deleting your data." in response.data

# Delete the classes so user deletion can proceed
client.post('/instructor/class/delete', data={'class_id': 2, 'confirm_delete': 'DELETE'})
client.get('/classes/switch/3')
client.post('/instructor/class/delete', data={'class_id': 3, 'confirm_delete': 'DELETE'})
client.get('/classes/switch/4')
client.post('/instructor/class/delete', data={'class_id': 4, 'confirm_delete': 'DELETE'})

# Perform deletion with proper confirmation
response = client.post('/profile/delete_data', data={'confirm_delete': 'DELETE'})
assert response.status_code == 302
assert response.location == "/auth/login" # redirect to login because user was successfully deleted

# Verify final state of all affected tables
with app.app_context():
Expand Down

0 comments on commit 23b08f7

Please sign in to comment.