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

SS-643 Users can edit their account details #235

Merged
merged 30 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
d321a3f
Updated info about using Serve for teaching (#194)
akochari Apr 19, 2024
2b5cadc
Revert "Updated info about using Serve for teaching (#194)"
akochari Apr 19, 2024
2d769be
Merge branch 'staging' into main
sandstromviktor May 23, 2024
ab7d54f
Merge branch 'staging'
churnikov Aug 14, 2024
46f589c
Merge branch 'staging'
churnikov Sep 19, 2024
c958069
SS-643-Make-an-option-to-edit-account-details
anondo1969 Oct 7, 2024
7c8099b
SS-643-Make-an-option-to-edit-account-details
anondo1969 Oct 7, 2024
4e3de76
SS-643 extending ProfileForm
anondo1969 Oct 8, 2024
d38f4ce
SS-643 Updated edit HTML form
anondo1969 Oct 10, 2024
d86a443
SS-643 Updated edit HTML form
anondo1969 Oct 10, 2024
32b49eb
SS-643 pre-commit check fixed
anondo1969 Oct 10, 2024
bba6f8b
SS-643 pre-commit with black check
anondo1969 Oct 10, 2024
0048d66
SS-643 pre-commit with black check 2
anondo1969 Oct 10, 2024
cf33450
Merge branch 'develop' into SS-643-Make-an-option-to-edit-account-det…
anondo1969 Oct 14, 2024
cded668
fix failing e2e test
akochari Oct 14, 2024
8194013
change the edit profile icon
akochari Oct 14, 2024
a05bf15
uncomment the line I accidentally commented out
akochari Oct 14, 2024
ee23c4e
Update common/views.py
anondo1969 Oct 15, 2024
b3481fd
Update common/forms.py
anondo1969 Oct 15, 2024
6bb12aa
Update common/forms.py
anondo1969 Oct 15, 2024
41b2862
fix super class init.
anondo1969 Oct 15, 2024
491b6f8
changes in view
anondo1969 Oct 15, 2024
da00974
ensure login required in form post method
anondo1969 Oct 15, 2024
f78edf6
Merge branch 'develop' into SS-643-Make-an-option-to-edit-account-det…
anondo1969 Oct 15, 2024
7f13f1e
ensuring curl injection does not work
anondo1969 Oct 17, 2024
a96fdfd
fixing the profile-edit bug in superuser mode
anondo1969 Oct 21, 2024
5a7fa52
Merge branch 'develop' into SS-643-Make-an-option-to-edit-account-det…
anondo1969 Oct 21, 2024
0d50532
Merge branch 'develop' into SS-643-Make-an-option-to-edit-account-det…
anondo1969 Oct 21, 2024
2d11a65
differentiate admin user and common user with or without Staff/Superu…
anondo1969 Oct 22, 2024
ce5e4a8
differentiate admin user and common user with or without Staff/Superu…
anondo1969 Oct 22, 2024
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
62 changes: 62 additions & 0 deletions common/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,3 +321,65 @@ class Meta:
fields = [
"token",
]


# SS-643 We've created a new form because UserForm above
# is a UserCreationForm,
# which means 'exclude' in Meta or change in
# initialization won't work
class UserEditForm(BootstrapErrorFormMixin, forms.ModelForm):
first_name = forms.CharField(
min_length=1,
max_length=30,
label="First name",
widget=forms.TextInput(attrs={"class": "form-control"}),
)
last_name = forms.CharField(
min_length=1,
max_length=30,
label="Last name",
widget=forms.TextInput(attrs={"class": "form-control"}),
)
email = forms.EmailField(
max_length=254,
label="Email address",
widget=forms.TextInput(attrs={"class": "form-control"}),
help_text=mark_safe("Email address can not be changed. Please email [email protected] with any questions."),
disabled=True,
)

required_css_class = "required"

class Meta:
model = User
fields = [
"username",
"first_name",
"last_name",
"email",
"password1",
"password2",
]
exclude = [
"username",
"password1",
"password2",
]

def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.data})"


class ProfileEditForm(ProfileForm):
class Meta(ProfileForm.Meta):
exclude = [
"note",
"why_account_needed",
]

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["affiliation"].disabled = True
self.fields[
"affiliation"
].help_text = "Affiliation can not be changed. Please email [email protected] with any questions."
2 changes: 2 additions & 0 deletions common/urls.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django.contrib.auth import views as auth_views
from django.contrib.auth.decorators import login_required
from django.urls import include, path

from . import views
Expand All @@ -9,4 +10,5 @@
path("success/", views.RegistrationCompleteView.as_view(), name="success"),
path("signup/", views.SignUpView.as_view(), name="signup"),
path("verify/", views.VerifyView.as_view(), name="verify"),
path("edit-profile/", login_required(views.EditProfileView.as_view()), name="edit-profile"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you define login_required here and in common/views.py?

Just curious

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to make sure only logged-in user can access the view/form. I removed in the last commit.

]
136 changes: 133 additions & 3 deletions common/views.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ObjectDoesNotExist
from django.core.mail import send_mail
from django.db import transaction
from django.http.response import HttpResponseRedirect
from django.shortcuts import redirect, render
from django.shortcuts import HttpResponse, redirect, render
from django.urls import reverse_lazy
from django.utils.decorators import method_decorator
from django.views.generic import CreateView, TemplateView

from .forms import ProfileForm, SignUpForm, TokenVerificationForm, UserForm
from .models import EmailVerificationTable
from studio.utils import get_logger

from .forms import (
ProfileEditForm,
ProfileForm,
SignUpForm,
TokenVerificationForm,
UserEditForm,
UserForm,
)
from .models import EmailVerificationTable, UserProfile

logger = get_logger(__name__)


# Create your views here.
Expand Down Expand Up @@ -129,3 +143,119 @@ def post(self, request, *args, **kwargs):
messages.error(request, "Invalid token!")
return redirect("portal:home")
return render(request, self.template_name, {"form": form})


@method_decorator(login_required, name="post")
class EditProfileView(TemplateView):
template_name = "user/profile_edit_form.html"

profile_edit_form_class = ProfileEditForm
user_edit_form_class = UserEditForm

def get_user_profile_info(self, request):
# Get the user profile from database
try:
# Note that not all users have a user profile object
# such as the admin superuser
user_profile = UserProfile.objects.get(user_id=request.user.id)
except ObjectDoesNotExist as e:
logger.error(str(e), exc_info=True)
user_profile = UserProfile()
except Exception as e:
logger.error(str(e), exc_info=True)
user_profile = UserProfile()
Comment on lines +164 to +166
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw, just want to tell you that it doesn't solve it for the admin user.
image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some dummy values for the super user to fix it.


return user_profile

def get(self, request, *args, **kwargs):
user_profile_data = self.get_user_profile_info(request)

profile_edit_form = self.profile_edit_form_class(
initial={"affiliation": user_profile_data.affiliation, "department": user_profile_data.department}
)

user_edit_form = self.user_edit_form_class(
initial={
"email": user_profile_data.user.email,
"first_name": user_profile_data.user.first_name,
"last_name": user_profile_data.user.last_name,
}
)

return render(request, self.template_name, {"form": user_edit_form, "profile_form": profile_edit_form})

def post(self, request, *args, **kwargs):
user_profile_data = self.get_user_profile_info(request)

user_form_details = self.user_edit_form_class(
request.POST,
instance=request.user,
initial={
"email": user_profile_data.user.email,
},
)

profile_form_details = self.profile_edit_form_class(
request.POST,
instance=user_profile_data,
initial={
"affiliation": user_profile_data.affiliation,
},
)
Comment on lines +199 to +213
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make sure, that user cannot pass any other information in the form data.

For instance, that user via direct curl request cannot change their email or password.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used decorator to ensure login-required in 'common/urls.py' and in 'EditProfileView' class in 'common/views.py'.


if user_form_details.is_valid() and profile_form_details.is_valid():
try:
with transaction.atomic():
user_form_retrived_data = user_form_details.save(commit=False)

# Only saving the new values, overwriting other existing values
if (
user_form_retrived_data.first_name != user_profile_data.user.first_name
or user_form_retrived_data.last_name != user_profile_data.user.last_name
):
user_form_retrived_data.username = user_profile_data.user.username
user_form_retrived_data.email = user_profile_data.user.email
user_form_retrived_data.password1 = user_profile_data.user.password
user_form_retrived_data.password2 = user_profile_data.user.password
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't fully get it, why are we doing this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to save only the new information. I removed it, as I checked the the previous way is okay to prevent the curl injection.

user_form_retrived_data.save()

else:
logger.info("Not saving user form info as nothing has changed", exc_info=True)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
logger.info("Not saving user form info as nothing has changed", exc_info=True)
logger.info("Not saving user form info as nothing has changed")

Correct me if I'm wrong, but I don't think that exc_info needed here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. I removed it.


profile_form_retrived_data = profile_form_details.save(commit=False)

# Only saving the new values, overwriting other existing values
profile_form_retrived_data.affiliation = user_profile_data.affiliation
profile_form_retrived_data.deleted_on = user_profile_data.deleted_on
profile_form_retrived_data.why_account_needed = user_profile_data.why_account_needed
profile_form_retrived_data.save()

# profile_form_details.save_m2m()
# user_form_details.save()
# profile_form_details.save()
anondo1969 marked this conversation as resolved.
Show resolved Hide resolved

logger.info(
"Updated First Name: " + str(self.get_user_profile_info(request).user.first_name), exc_info=True
)
logger.info(
"Updated Last Name: " + str(self.get_user_profile_info(request).user.last_name), exc_info=True
)
logger.info(
"Updated Department: " + str(self.get_user_profile_info(request).department), exc_info=True
)

except Exception as e:
return HttpResponse("Error updating records: " + str(e))

return render(request, "user/profile.html", {"user_profile": self.get_user_profile_info(request)})

else:
if not user_form_details.is_valid():
logger.error("Edit user error: " + str(user_form_details.errors), exc_info=True)

if not profile_form_details.is_valid():
logger.error("Edit profile error: " + str(profile_form_details.errors), exc_info=True)

return render(
request, self.template_name, {"form": user_form_details, "profile_form": profile_form_details}
)
2 changes: 1 addition & 1 deletion cypress/e2e/ui-tests/test-brute-force-login-attempts.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe("Test brute force login attempts are blocked", () => {

// Sign out before logging in again
cy.logf("Sign out before logging in again", Cypress.currentTest)
cy.get('button.btn-profile').click()
cy.get('button.btn-profile').contains("Profile").click()
cy.get('li.btn-group').find('button').contains("Sign out").click()
cy.get("title").should("have.text", "Logout | SciLifeLab Serve (beta)")

Expand Down
1 change: 1 addition & 0 deletions templates/common/navbar.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{% url 'user-profile' %}"><i class="bi bi-person me-1"></i>My profile</a></li>
<li><a class="dropdown-item" href="{% url 'common:edit-profile' %}"><i class="bi bi-pencil-square me-1"></i>Edit profile</a></li>
<li><a class="dropdown-item" href="{% url 'password_change' %}"><i class="bi bi-key me-1"></i>Change password</a></li>
<form action="{% url 'logout' %}" method="post">
{% csrf_token %}
Expand Down
14 changes: 11 additions & 3 deletions templates/user/profile.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,16 @@
<div class="card-body">
<div class="row">
<div class="col">
<h3 class="card-title mb-0">My user profile</h3>

<div class="row">
<div class="col">
<h3 class="card-title mb-0">My user profile</h3>
</div>
<div class="col">
<button type="button" type="submit" aria-expanded="false" class="btn btn-profile">
<a href="{% url 'common:edit-profile' %}"> <i class="bi bi-pencil-square"></i> Edit </a>
</button>
</div>
</div>
<div class="row pt-4">
<h6 class="fw-bold">Account</h6>
</div>
Expand Down Expand Up @@ -45,7 +53,7 @@ <h6 class="fw-bold">Contact</h6>
<div class="row pt-5">
<h5>Delete Account</h5>
<p>
To delete your account, <a href="{% url 'delete_account' %}">visit this page</a>
To delete your account, <a href="{% url 'delete_account' %}">visit this page</a>.
</p>
</div>

Expand Down
Loading
Loading