Skip to content

Commit aae93ac

Browse files
authored
Merge pull request #95 from GeoinformationSystems/feature/account_deletion_confirmation
Account deletion must be confirmed
2 parents cfb0f1b + 47f641b commit aae93ac

File tree

12 files changed

+457
-89
lines changed

12 files changed

+457
-89
lines changed

optimap/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@
5050
"sesame.backends.ModelBackend",
5151
]
5252

53+
AUTH_USER_MODEL = "publications.CustomUser"
54+
5355
INSTALLED_APPS = [
5456
'django.contrib.admin',
5557
'django.contrib.auth',

publications/admin.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,6 @@
1010
from django_q.models import Schedule
1111
from datetime import datetime, timedelta
1212

13-
14-
# Unregister the default User admin
15-
admin.site.unregister(User)
16-
1713
@admin.action(description="Mark selected publications as published")
1814
def make_public(modeladmin, request, queryset):
1915
queryset.update(status="p")

publications/models.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
from django.contrib.postgres.fields import ArrayField
33
from django_currentuser.db.models import CurrentUserField
44
from django.utils.timezone import now
5-
from django.contrib.auth import get_user_model
6-
7-
User = get_user_model()
5+
from django.contrib.auth.models import AbstractUser, Group, Permission
6+
import uuid
7+
from django.utils.timezone import now
8+
import logging
9+
logger = logging.getLogger(__name__)
810

911
STATUS_CHOICES = (
1012
("d", "Draft"),
@@ -14,6 +16,27 @@
1416
("h", "Harvested"),
1517
)
1618

19+
class CustomUser(AbstractUser):
20+
deleted = models.BooleanField(default=False)
21+
deleted_at = models.DateTimeField(null=True, blank=True)
22+
groups = models.ManyToManyField(Group, related_name="publications_users", blank=True)
23+
user_permissions = models.ManyToManyField(Permission, related_name="publications_users_permissions", blank=True)
24+
25+
def soft_delete(self):
26+
"""Marks the user as deleted instead of removing from the database."""
27+
self.deleted = True
28+
self.deleted_at = now()
29+
self.save()
30+
logger.info(f"User {self.username} (ID: {self.id}) was soft deleted at {self.deleted_at}")
31+
32+
33+
def restore(self):
34+
"""Restores a previously deleted user."""
35+
self.deleted = False
36+
self.deleted_at = None
37+
self.save()
38+
logger.info(f"User {self.username} (ID: {self.id}) was restored.")
39+
1740
class Publication(models.Model):
1841
# required fields
1942
title = models.TextField()
@@ -88,6 +111,9 @@ class Meta:
88111
ordering = ['user_name']
89112
verbose_name = "subscription"
90113

114+
from django.contrib.auth import get_user_model
115+
User = get_user_model()
116+
91117
class EmailLog(models.Model):
92118
TRIGGER_CHOICES = [
93119
("admin", "Admin Panel"),
@@ -126,7 +152,7 @@ def log_email(cls, recipient, subject, content, sent_by=None, trigger_source="ma
126152
from import_export import fields, resources
127153
from import_export.widgets import ForeignKeyWidget
128154
from django.conf import settings
129-
from django.contrib.auth.models import User
155+
130156
class PublicationResource(resources.ModelResource):
131157
#created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='username')
132158
#updated_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='username')

publications/serializers.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from rest_framework_gis import serializers
44
from .models import Publication
5+
from django.contrib.auth import get_user_model
6+
User = get_user_model()
57

68
from publications.models import Publication,Subscription
79

@@ -23,4 +25,14 @@ class Meta:
2325
fields = ("search_term","timeperiod_startdate","timeperiod_enddate","user_name")
2426
geo_field = "search_area"
2527
auto_bbox = True
26-
28+
29+
class UserSerializer(serializers.ModelSerializer):
30+
class Meta:
31+
model = User
32+
fields = ["id", "username", "email"]
33+
34+
def to_representation(self, instance):
35+
"""Ensure deleted users are excluded from serialization."""
36+
if instance.deleted:
37+
return None
38+
return super().to_representation(instance)

publications/signals.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from django.db.models.signals import pre_save
55
from django.dispatch import receiver
66
from django.contrib.auth import get_user_model
7+
User = get_user_model()
78
from django.conf import settings
89
from django.db.models.signals import post_save
910
User = get_user_model()

publications/templates/base.html

Lines changed: 54 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,57 @@
11
{% load static %}
22
<!DOCTYPE html>
33
<html lang="en">
4-
5-
<head>
6-
<meta charset="utf-8">
7-
<meta name="viewport" content="width=device-width, initial-scale=1">
8-
9-
<title>{% block title %}{% endblock %}OPTIMAP</title>
10-
<link rel="icon" type="image/png" href="{% static 'favicon.png' %}">
11-
12-
<link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}"/>
13-
<link rel="stylesheet" href="{% static 'fontawesome/css/fontawesome.min.css' %}"/>
14-
<link rel="stylesheet" href="{% static 'fontawesome/css/solid.min.css' %}"/>
15-
16-
<script src="{% static 'js/jquery-3.4.1.slim.min.js' %}"></script>
17-
<script src="{% static 'js/bootstrap.min.js' %}"></script>
18-
19-
{% block head %}{% endblock %}
20-
21-
<link rel="stylesheet" type="text/css" href="{% static 'css/main.css' %}"/>
22-
</head>
23-
24-
<body>
25-
26-
<!--Navbar-->
27-
<nav class="navbar navbar-expand-sm navbar-dark nav-fill w-100">
28-
<div class="container-fluid">
29-
<div class="nav navbar-nav">
30-
<a class="navbar-brand" href="/">
31-
<img src="{% static 'optimap_logo_w.svg' %}" width="100" alt="OPTIMAP logo">
32-
</a>
33-
34-
<span class="text-white tagline">Discover Research. Optimized. On a map.</span>
35-
</div>
36-
37-
{% block navbar %}{% endblock %}
38-
</div>
39-
</nav>
40-
41-
<main class="container-fluid">
42-
{% block alert %}{% endblock %}
43-
44-
{% block content %}{% endblock %}
45-
</main>
46-
47-
{% include 'footer.html' %}
48-
49-
{% block scripts %}{% endblock %}
50-
51-
</body>
52-
53-
</html>
4+
<head>
5+
<meta charset="utf-8" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1" />
7+
8+
<title>{% block title %}{% endblock %}OPTIMAP</title>
9+
<link rel="icon" type="image/png" href="{% static 'favicon.png' %}" />
10+
11+
<link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}" />
12+
<link
13+
rel="stylesheet"
14+
href="{% static 'fontawesome/css/fontawesome.min.css' %}"
15+
/>
16+
<link
17+
rel="stylesheet"
18+
href="{% static 'fontawesome/css/solid.min.css' %}"
19+
/>
20+
21+
<script src="{% static 'js/jquery-3.4.1.slim.min.js' %}"></script>
22+
<script src="{% static 'js/bootstrap.min.js' %}"></script>
23+
24+
{% block head %}{% endblock %}
25+
26+
<link rel="stylesheet" type="text/css" href="{% static 'css/main.css' %}" />
27+
</head>
28+
29+
<body>
30+
<!--Navbar-->
31+
<nav class="navbar navbar-expand-sm navbar-dark nav-fill w-100">
32+
<div class="container-fluid">
33+
<div class="nav navbar-nav">
34+
<a class="navbar-brand" href="/">
35+
<img
36+
src="{% static 'optimap_logo_w.svg' %}"
37+
width="100"
38+
alt="OPTIMAP logo"
39+
/>
40+
</a>
41+
42+
<span class="text-white tagline"
43+
>Discover Research. Optimized. On a map.</span
44+
>
45+
</div>
46+
47+
{% block navbar %}{% endblock %}
48+
</div>
49+
</nav>
50+
51+
<main class="container-fluid">
52+
{% block alert %}{% endblock %} {% block content %}{% endblock %}
53+
</main>
54+
55+
{% include 'footer.html' %} {% block scripts %}{% endblock %}
56+
</body>
57+
</html>

publications/templates/menu_snippet.html

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,39 @@
11
<li class="nav-item dropdown">
2-
<a class="nav-link dropdown" href="#" id="navbarDarkDropdown1" role="button" data-toggle="dropdown"
3-
aria-expanded="false">
4-
<i class="fa-solid fa-circle-user fa-3x"></i>
5-
</a>
6-
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDarkDropdown1">
7-
<span class="dropdown-item-text">New around here? Please login to create a new account.</span>
8-
<div class="dropdown-divider"></div>
9-
<li class="px-3 py-2">
10-
<form class="form" method="POST" action="{% url 'optimap:loginres' %}">
11-
{% csrf_token %}
12-
<div class="form-group">
13-
<input id="email" placeholder="Email" class="form-control form-control-sm" type="email" required=""
14-
name="email">
15-
</div>
16-
<div class="form-group">
17-
<button type="submit" class="btn btn-primary btn-block">Login</button>
18-
</div>
19-
</form>
20-
</li>
21-
</ul>
2+
<a
3+
class="nav-link dropdown-toggle"
4+
href="#"
5+
id="navbarDarkDropdown1"
6+
role="button"
7+
data-toggle="dropdown"
8+
aria-expanded="false"
9+
>
10+
<i class="fa-solid fa-circle-user fa-3x"></i>
11+
</a>
12+
<ul
13+
class="dropdown-menu dropdown-menu-right"
14+
aria-labelledby="navbarDarkDropdown1"
15+
>
16+
<span class="dropdown-item-text"
17+
>New around here? Please login to create a new account.</span
18+
>
19+
<div class="dropdown-divider"></div>
20+
<li class="px-3 py-2">
21+
<form class="form" method="POST" action="{% url 'optimap:loginres' %}">
22+
{% csrf_token %}
23+
<div class="form-group">
24+
<input
25+
id="email"
26+
placeholder="Email"
27+
class="form-control form-control-sm"
28+
type="email"
29+
required=""
30+
name="email"
31+
/>
32+
</div>
33+
<div class="form-group">
34+
<button type="submit" class="btn btn-primary btn-block">Login</button>
35+
</div>
36+
</form>
37+
</li>
38+
</ul>
2239
</li>
23-

publications/templates/user_settings.html

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,9 @@ <h2 class="mb-0">
161161
data-parent="#user_settings"
162162
>
163163
<div class="card-body">
164-
<p>Deleting account is permenant. It cannot be reversed.</p>
164+
<p class="text-wrap text-break text-center mb-3">
165+
Deleting account is permanent. It cannot be reversed.
166+
</p>
165167

166168
<button
167169
type="button"
@@ -191,7 +193,10 @@ <h5 class="modal-title">Delete Account</h5>
191193
<p>Do you really want to delete this account ?</p>
192194
</div>
193195
<div class="modal-footer">
194-
<form action="{% url 'optimap:delete' %}" method="post">
196+
<form
197+
action="{% url 'optimap:request_delete' %}"
198+
method="post"
199+
>
195200
{% csrf_token %}
196201
<button
197202
type="submit"
@@ -214,14 +219,62 @@ <h5 class="modal-title">Delete Account</h5>
214219
</div>
215220
</div>
216221
</div>
222+
<!-- Final Confirmation Modal (Only Shows After Clicking Email Link) -->
223+
<div id="finalDeleteModal" class="modal fade" tabindex="-1">
224+
<div class="modal-dialog">
225+
<div class="modal-content">
226+
<div class="modal-header">
227+
<h5 class="modal-title">Final Confirmation</h5>
228+
<button type="button" class="close" data-dismiss="modal">
229+
<span>&times;</span>
230+
</button>
231+
</div>
232+
<div class="modal-body">
233+
<div class="container">
234+
<p class="text-wrap text-center mb-3">
235+
Confirming this will permenantly delete your account.
236+
</p>
237+
</div>
238+
<div class="modal-footer">
239+
<form
240+
method="POST"
241+
action="{% url 'optimap:finalize_delete' %}"
242+
>
243+
{% csrf_token %}
244+
<button type="submit" class="btn btn-danger">
245+
Permanently Delete Account
246+
</button>
247+
</form>
248+
<button
249+
type="button"
250+
class="btn btn-secondary"
251+
data-dismiss="modal"
252+
>
253+
Cancel
254+
</button>
255+
</div>
256+
</div>
257+
</div>
258+
</div>
259+
</div>
217260
</div>
218261
</div>
219262
</div>
220263
</div>
264+
{% if delete_token %}
265+
<script>
266+
document.addEventListener("DOMContentLoaded", function () {
267+
let modalShown = sessionStorage.getItem("modal_shown");
268+
if (!modalShown) {
269+
$("#finalDeleteModal").modal("show");
270+
sessionStorage.setItem("modal_shown", "true");
271+
}
272+
});
273+
</script>
274+
{% endif %}
221275
<script>
222276
document.addEventListener("DOMContentLoaded", function () {
223277
let toggle = document.getElementById("notify_new_manuscripts");
224-
225278
if ("{{ profile.notify_new_manuscripts }}" === "True") {
226279
toggle.checked = true;
227280
} else {

publications/urls.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
path("usersettings/", views.user_settings, name="usersettings"),
3030
path("subscriptions/", views.user_subscriptions, name="subscriptions"),
3131
path("addsubscriptions/", views.add_subscriptions, name="addsubscriptions"),
32-
path("delete/", views.delete_account, name="delete"),
32+
path("request-delete/", views.request_delete, name="request_delete"),
33+
path("confirm-delete/<str:token>/", views.confirm_account_deletion, name="confirm_delete"),
34+
path("finalize-delete/", views.finalize_account_deletion, name="finalize_delete"),
3335
path("changeuser/", views.change_useremail, name="changeuser"),
3436
]

0 commit comments

Comments
 (0)