diff --git a/TEKDB/TEKDB/settings.py b/TEKDB/TEKDB/settings.py
index 1706339b..91f8ac9f 100644
--- a/TEKDB/TEKDB/settings.py
+++ b/TEKDB/TEKDB/settings.py
@@ -262,7 +262,7 @@
TINYMCE_FILEBROWSER = False
# Add Version to the admin site header
-VERSION = "2.10.0"
+VERSION = "2.11.0"
ADMIN_SITE_HEADER = os.environ.get(
"ADMIN_SITE_HEADER", default="ITK DB Admin v{}".format(VERSION)
)
diff --git a/TEKDB/TEKDB/urls.py b/TEKDB/TEKDB/urls.py
index eb694a67..ecf23991 100644
--- a/TEKDB/TEKDB/urls.py
+++ b/TEKDB/TEKDB/urls.py
@@ -35,7 +35,11 @@
urlpatterns = [
- # url(r'^login/', include('login.urls')),
+ path(
+ "change_password/",
+ login_views.TEKDBPasswordChangeView.as_view(),
+ name="change_password",
+ ),
path("admin/filebrowser/", tekdb_filebrowser.urls),
path("login/", login_views.login, name="login"),
path("login_async/", login_views.login_async, name="login_async"),
diff --git a/TEKDB/explore/static/explore/css/forms.css b/TEKDB/explore/static/explore/css/forms.css
index f0ec63bc..0e03731a 100644
--- a/TEKDB/explore/static/explore/css/forms.css
+++ b/TEKDB/explore/static/explore/css/forms.css
@@ -2,10 +2,6 @@
text-align: center;
}
-.registration-form .helptext {
- visibility: hidden;
-}
-
input[type="text"],
.form-control,
input[type="password"] {
diff --git a/TEKDB/explore/static/explore/js/modals.js b/TEKDB/explore/static/explore/js/modals.js
index 6e46d2b2..9b8bc581 100644
--- a/TEKDB/explore/static/explore/js/modals.js
+++ b/TEKDB/explore/static/explore/js/modals.js
@@ -1,21 +1,93 @@
-$("#loginModal").on("shown.bs.modal", function () {
- $("#loginInput").focus();
- var loginForm = document.querySelector("#loginModal form");
- const LOGIN_ERROR_MESSAGE = "Invalid username or password";
- loginForm.addEventListener("submit", function (event) {
- event.preventDefault();
- var signIn = account.signIn(event, this, function (success) {
- if (success) {
- $("#loginModal").modal("hide");
- const exploreLink = document.querySelector(".nav-explore");
- exploreLink.classList.remove("disabled");
- exploreLink.href = "/explore";
- exploreLink.click();
- } else {
- if (!loginForm.innerHTML.includes(LOGIN_ERROR_MESSAGE)) {
- loginForm.append(LOGIN_ERROR_MESSAGE);
+const LOGIN_MODAL_ID = "loginModal";
+const LOGIN_INPUT_ID = "loginInput";
+const loginForm = document.querySelector(`#${LOGIN_MODAL_ID} form`);
+const LOGIN_ERROR_MESSAGE = "Invalid username or password";
+
+$(`#${LOGIN_MODAL_ID}`).on("shown.bs.modal", function () {
+ $(`#${LOGIN_INPUT_ID}`).focus();
+});
+
+loginForm.addEventListener("submit", function (event) {
+ event.preventDefault();
+ account.signIn(event, this, function (success) {
+ if (success) {
+ $(`#${LOGIN_MODAL_ID}`).modal("hide");
+ const exploreLink = document.querySelector(".nav-explore");
+ exploreLink.classList.remove("disabled");
+ exploreLink.href = "/explore";
+ exploreLink.click();
+ } else {
+ if (!loginForm.innerHTML.includes(LOGIN_ERROR_MESSAGE)) {
+ loginForm.append(LOGIN_ERROR_MESSAGE);
+ }
+ }
+ });
+});
+
+const CHANGE_PASSWORD_MODAL_ID = "changePasswordModal";
+const CHANGE_PASSWORD_SUCCESS_ID = "changePasswordSuccessMessage";
+const CHANGE_PASSWORD_SUCCESS_MESSAGE = "Password successfully changed!";
+const changePasswordForm = document.querySelector(`#${CHANGE_PASSWORD_MODAL_ID} form`);
+
+changePasswordForm.addEventListener("submit", function (event) {
+ event.preventDefault();
+ account.changePassword(event, this, function (response) {
+ if (response.success) {
+ // clear any previous error messages
+ const errorLists = document.querySelectorAll(`#${CHANGE_PASSWORD_MODAL_ID} ul`);
+ if (errorLists.length) {
+ errorLists.forEach((list) => list.remove());
+ }
+ // clear any previous success message
+ const previousSuccessMessage = document.querySelector(`#${CHANGE_PASSWORD_SUCCESS_ID}`);
+ if (!previousSuccessMessage) {
+ // display success message
+ const successMessage = document.createElement("p");
+ successMessage.setAttribute("id", `${CHANGE_PASSWORD_SUCCESS_ID}`);
+ successMessage.textContent = CHANGE_PASSWORD_SUCCESS_MESSAGE;
+ successMessage.setAttribute("style", "color: green;");
+ successMessage.setAttribute("class", "m-0");
+
+ // insert success message after submit button
+ const submitButton = changePasswordForm.querySelector('#changePasswordSubmit');
+ submitButton.after(successMessage);
+ }
+
+ // reset form
+ changePasswordForm.reset();
+ } else {
+ // display error messages
+ for (const elementId in response.data.data) {
+ const errorList = document.createElement("ul");
+ const erroredInput = document.querySelector(`#${elementId}`);
+ erroredInput.after(errorList);
+ erroredInput.setAttribute("style", "border-color: red;");
+ if (response.data.data[elementId].length > 0) {
+ for (const error in response.data.data[elementId]) {
+ const listItem = document.createElement("li");
+ listItem.textContent = response.data.data[elementId][error];
+ errorList.appendChild(listItem);
+ }
+
}
}
- });
+ }
});
});
+
+$(`#${CHANGE_PASSWORD_MODAL_ID}`).on("hidden.bs.modal", function () {
+ // clear error messages
+ const errorLists = document.querySelectorAll(`#${CHANGE_PASSWORD_MODAL_ID} ul`);
+ errorLists.forEach((list) => list.remove());
+ // reset input border color
+ const passwordInputs = document.querySelectorAll(`#${CHANGE_PASSWORD_MODAL_ID} input[type='password']`);
+ passwordInputs.forEach((input) => input.setAttribute("style", ""));
+ // clear form
+ document.querySelector(`#${CHANGE_PASSWORD_MODAL_ID} form`).reset();
+ // clear success message
+ const successMessage = document.querySelector(`#${CHANGE_PASSWORD_SUCCESS_ID}`);
+
+ if (successMessage) {
+ successMessage.remove();
+ }
+});
\ No newline at end of file
diff --git a/TEKDB/explore/templates/modals.html b/TEKDB/explore/templates/modals.html
index a408544e..fc0ffd58 100644
--- a/TEKDB/explore/templates/modals.html
+++ b/TEKDB/explore/templates/modals.html
@@ -15,35 +15,32 @@
Please Log In
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
- >
+ >
{% include 'registration/login.html' %}
-{% comment %}
-
-
{% include 'results_map.html' %}
+
{% include 'change_password.html' %}
-{% endcomment %}
diff --git a/TEKDB/explore/templates/navbar.html b/TEKDB/explore/templates/navbar.html
index 4e58c8c5..292486b2 100644
--- a/TEKDB/explore/templates/navbar.html
+++ b/TEKDB/explore/templates/navbar.html
@@ -37,19 +37,20 @@
{% if user.is_authenticated %}
{% else %}
diff --git a/TEKDB/login/static/login/js/account.js b/TEKDB/login/static/login/js/account.js
index c18253fb..c5d1802c 100644
--- a/TEKDB/login/static/login/js/account.js
+++ b/TEKDB/login/static/login/js/account.js
@@ -51,5 +51,28 @@ var account = {
callback(false);
}
});
+ },
+ changePassword: function(event, form, callback) {
+ var formData = $(form).serialize();
+ var url = '/change_password/';
+ $.ajax({
+ url: url,
+ type: 'POST',
+ data: formData,
+ dataType: 'json',
+ success: function(response) {
+ if (response.success) {
+ console.log('%csuccessfully changed password', 'color:green;');
+ callback({success: true, data: response.data});
+ } else {
+ console.log('%cerror changing password: %o', 'color: red;', response.data);
+ callback({success: false, data: response.data});
+ }
+ },
+ error: function(response) {
+ console.log('%cerror with change password request submission: %o', 'color: red', response.data);
+ callback({success: false, data: response.responseJSON});
+ }
+ });
}
};
\ No newline at end of file
diff --git a/TEKDB/login/templates/change_password.html b/TEKDB/login/templates/change_password.html
new file mode 100644
index 00000000..e2bab785
--- /dev/null
+++ b/TEKDB/login/templates/change_password.html
@@ -0,0 +1,14 @@
+
\ No newline at end of file
diff --git a/TEKDB/login/templates/create.html b/TEKDB/login/templates/create.html
deleted file mode 100644
index 78bc3a27..00000000
--- a/TEKDB/login/templates/create.html
+++ /dev/null
@@ -1,21 +0,0 @@
-
diff --git a/TEKDB/login/templates/forgot.html b/TEKDB/login/templates/forgot.html
deleted file mode 100644
index 9f19422e..00000000
--- a/TEKDB/login/templates/forgot.html
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
-
-
-
-
-
-
-
-
Forgot Password?
-
You can reset your password here.
-
-
-
-
-
-
-
-
diff --git a/TEKDB/login/templates/registration/login.html b/TEKDB/login/templates/registration/login.html
index 81f9d852..12a42281 100644
--- a/TEKDB/login/templates/registration/login.html
+++ b/TEKDB/login/templates/registration/login.html
@@ -12,10 +12,3 @@
-
diff --git a/TEKDB/login/templates/registration/registration_form.html b/TEKDB/login/templates/registration/registration_form.html
deleted file mode 100644
index 78dc5d6a..00000000
--- a/TEKDB/login/templates/registration/registration_form.html
+++ /dev/null
@@ -1,11 +0,0 @@
-{% extends "base.html" %}
-
-{% block content %}
-
-{% endblock %}
diff --git a/TEKDB/login/tests/test_views.py b/TEKDB/login/tests/test_views.py
index e69de29b..2ec703ca 100644
--- a/TEKDB/login/tests/test_views.py
+++ b/TEKDB/login/tests/test_views.py
@@ -0,0 +1,102 @@
+from django.test import TestCase, Client, RequestFactory
+from django.contrib.auth import get_user_model
+from django.urls import reverse
+from unittest.mock import MagicMock, patch
+import json
+from login.views import (
+ TEKDBPasswordChangeView,
+ index,
+ login_logic,
+)
+
+User = get_user_model()
+
+
+class LoginViewTests(TestCase):
+ def setUp(self):
+ self.client = Client()
+ self.factory = RequestFactory()
+ self.user = User.objects.create_user(
+ username="testuser", password="testpass123"
+ )
+
+ def test_index_view(self):
+ request = self.factory.get("/")
+ response = index(request)
+ self.assertEqual(response.status_code, 200)
+ self.assertIn("Login", response.content.decode("utf-8"))
+
+ def test_login_success(self):
+ response = self.client.post(
+ reverse("login"), {"username": "testuser", "password": "testpass123"}
+ )
+ self.assertTrue(response.wsgi_request.user.is_authenticated)
+ self.assertIn("explore", response.content.decode("utf-8").lower())
+ self.assertEqual(response.status_code, 200)
+
+ def test_login_invalid_credentials(self):
+ response = self.client.post(
+ reverse("login"), {"username": "testuser", "password": "wrongpassword"}
+ )
+ self.assertFalse(response.wsgi_request.user.is_authenticated)
+ self.assertEqual(response.context["errorcode"], 403)
+ self.assertIn("incorrect", response.context["error"].lower())
+
+ def test_login_logic_success(self):
+ request = MagicMock()
+ request.POST = {"username": "testuser", "password": "testpass123"}
+ result = login_logic(request)
+ self.assertTrue(result["success"])
+ self.assertEqual(result["username"], "testuser")
+
+ def test_login_logic_failure(self):
+ request = MagicMock()
+ request.POST = {"username": "testuser", "password": "wrongpass"}
+ result = login_logic(request)
+ self.assertFalse(result["success"])
+
+ def test_login_async(self):
+ response = self.client.post(
+ reverse("login_async"), {"username": "testuser", "password": "testpass123"}
+ )
+ self.assertEqual(response.status_code, 200)
+ data = json.loads(response.content)
+ self.assertTrue(data["success"])
+
+ def test_login_async_failure(self):
+ response = self.client.post(
+ reverse("login_async"), {"username": "testuser", "password": "wrongpass"}
+ )
+ data = json.loads(response.content)
+ self.assertFalse(data["success"])
+
+ def test_password_change_form_invalid_returns_json_errors(self):
+ view = TEKDBPasswordChangeView()
+ form = MagicMock()
+ form.errors = {"old_password": ["This field is required."]}
+
+ response = view.form_invalid(form)
+
+ self.assertEqual(response.status_code, 400)
+ payload = json.loads(response.content)
+ self.assertEqual(payload["data"], form.errors)
+ self.assertFalse(payload["success"])
+
+ @patch("login.views.update_session_auth_hash")
+ def test_password_change_form_valid_updates_session_and_returns_success(
+ self, mock_update_session_auth_hash
+ ):
+ view = TEKDBPasswordChangeView()
+ view.request = self.factory.post("/change_password/")
+ form = MagicMock()
+ form.save.return_value = self.user
+ form.is_valid.return_value = True
+
+ response = view.form_valid(form)
+
+ self.assertEqual(response.status_code, 200)
+ payload = json.loads(response.content)
+ self.assertTrue(payload["success"])
+ self.assertEqual(view.object, self.user)
+ form.save.assert_called_once_with()
+ mock_update_session_auth_hash.assert_called_once_with(view.request, self.user)
diff --git a/TEKDB/login/urls.py b/TEKDB/login/urls.py
index 34538d68..5119061b 100644
--- a/TEKDB/login/urls.py
+++ b/TEKDB/login/urls.py
@@ -3,8 +3,5 @@
from . import views
urlpatterns = [
- path("create", views.create, name="create"),
- path("forgot", views.forgot, name="forgot"),
path("", views.index, name="index"),
]
-# url(r'^logout$', views.logout, name='logout'),
diff --git a/TEKDB/login/views.py b/TEKDB/login/views.py
index 3f92ad70..f2bae458 100644
--- a/TEKDB/login/views.py
+++ b/TEKDB/login/views.py
@@ -1,8 +1,11 @@
-# Create your views here.
from django.http.response import JsonResponse
from django.shortcuts import render
-from django.contrib.auth import authenticate
-from django.contrib.auth import login as auth_login
+from django.contrib.auth import (
+ authenticate,
+ login as auth_login,
+ update_session_auth_hash,
+)
+from django.contrib.auth.views import PasswordChangeView
def index(request):
@@ -10,15 +13,6 @@ def index(request):
"pageTitle": "Login",
}
return render(request, "index.html", context)
- # return HttpResponse("Server error: Already Logged In")
-
-
-def forgot(request):
- context = {
- "pageTitle": "Forgot Login",
- }
- return render(request, "forgot.html", context)
- # return HttpResponse("Forgot Password")
def login(request):
@@ -66,3 +60,15 @@ def login_async(request):
"success": login_user["success"],
}
return JsonResponse(context)
+
+
+class TEKDBPasswordChangeView(PasswordChangeView):
+ def form_invalid(self, form):
+ return JsonResponse({"data": form.errors, "success": False}, status=400)
+
+ def form_valid(self, form):
+ self.object = form.save()
+ # prevent user’s auth session to be invalidated
+ # and user have to log in again after password change
+ update_session_auth_hash(self.request, self.object)
+ return JsonResponse({"data": None, "success": True}, status=200)