diff --git a/config/urls.py b/config/urls.py index 634741cc..e4ebb856 100755 --- a/config/urls.py +++ b/config/urls.py @@ -52,6 +52,7 @@ from support.views import ContactView, SupportView from users.views import ( CurrentUserAPIView, CurrentUserProfileView, + NewCurrentUserProfileView, ProfilePhotoGitHubUpdateView, ProfilePhotoUploadView, ProfilePreferencesView, @@ -85,6 +86,11 @@ urlpatterns = ( ProfilePreferencesView.as_view(), name="profile-preferences", ), + path( + "users/me/new/", + NewCurrentUserProfileView.as_view(), + name="profile-account-new", + ), path("users/me/", CurrentUserProfileView.as_view(), name="profile-account"), # Temp route to prove antora header path("testheader/", antora_header_view, name="antora-header"), diff --git a/templates/users/profile_base_new.html b/templates/users/profile_base_new.html new file mode 100644 index 00000000..847198a5 --- /dev/null +++ b/templates/users/profile_base_new.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} + +{% load static %} + +{% block subnav %} +
+
+ {% if user.image %} + user + {% else %} + + {% endif %} +
+
+ {{ user.get_full_name }} + Joined {{ user.date_joined }} +
+
+
+
+{% endblock %} diff --git a/templates/users/profile_new.html b/templates/users/profile_new.html new file mode 100644 index 00000000..17a93133 --- /dev/null +++ b/templates/users/profile_new.html @@ -0,0 +1,61 @@ +{% extends "users/profile_base_new.html" %} + +{% load static i18n %} + +{% block content %} +
+ +
+
+ +
+ +

{% trans "Set Password" %}

+ +
+ {% csrf_token %} +
{{ change_password_form.as_p }}
+
+ +
+
+ +

{% trans "Update Profile Photo" %}

+ + {% if user.github_username %} +
+ {% csrf_token %} + +
+ {% endif %} + +
+ {% csrf_token %} +
+ {{ profile_photo_form.as_p }} +
+
+ +
+
+ +
+

{% trans "Update Preferences" %}

+ +
+ {% csrf_token %} +

Notify me via email when:

+ {{ profile_preferences_form.errors }} +
+ {{ profile_preferences_form.as_div }} +
+
+ +
+
+
+ +
+ + +{% endblock %} diff --git a/users/tests/test_views.py b/users/tests/test_views.py index 8f7c674b..cb62f718 100644 --- a/users/tests/test_views.py +++ b/users/tests/test_views.py @@ -1,6 +1,12 @@ import pytest +import tempfile -from ..forms import PreferencesForm +from django.core.files.uploadedfile import SimpleUploadedFile + +from allauth.account.forms import ChangePasswordForm +from PIL import Image + +from ..forms import PreferencesForm, UserProfilePhotoForm from ..models import Preferences @@ -57,3 +63,122 @@ def test_preferences_post_clears_options( assert_messages( response, [("success", "Your preferences were successfully updated.")] ) + + +@pytest.mark.django_db +def test_new_current_user_profile_not_authenticated(tp, user): + tp.assertLoginRequired("profile-account-new") + + +@pytest.mark.django_db +def test_new_current_user_profile_view_get(user, tp): + with tp.login(user): + response = tp.assertGoodView(tp.reverse("profile-account-new"), verbose=True) + assert isinstance(response.context["change_password_form"], ChangePasswordForm) + assert isinstance(response.context["profile_photo_form"], UserProfilePhotoForm) + assert isinstance(response.context["profile_preferences_form"], PreferencesForm) + + +@pytest.mark.django_db +def test_new_current_user_profile_view_post_valid_password(user, tp): + with tp.login(user): + response = tp.post( + tp.reverse("profile-account-new"), + data={ + "email": user.email, + "oldpassword": "password", + "password1": "new_password", + "password2": "new_password", + }, + follow=True, + ) + assert response.status_code == 200 + user.refresh_from_db() + user.check_password("new_password") + + +@pytest.mark.django_db +def test_new_current_user_profile_view_post_invalid_password(user, tp): + old_password = "password" + with tp.login(user): + response = tp.post( + tp.reverse("profile-account-new"), + data={ + "email": user.email, + "oldpassword": "not the right password", + "password1": "new_password", + "password2": "new_password", + }, + follow=True, + ) + assert response.status_code == 200 + user.refresh_from_db() + user.check_password(old_password) + + +@pytest.mark.django_db +def test_new_current_user_profile_view_post_valid_photo(user, tp): + """Test that a user can upload a new profile picture.""" + # Create a temporary image file for testing + with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as temp_image: + image = Image.new("RGB", (200, 200)) + image.save(temp_image, "jpeg") + temp_image.seek(0) + + # Wrap temp_image in Django's File object + uploaded_file = SimpleUploadedFile( + name="test_image.jpg", content=temp_image.read(), content_type="image/jpeg" + ) + + with tp.login(user): + response = tp.post( + tp.reverse("profile-account-new"), + data={ + "image": uploaded_file, + }, + follow=True, + ) + assert response.status_code == 200 + user.refresh_from_db() + assert user.image + # confirm that password was not changed, as these are on the same screen + assert user.check_password("password") + + +@pytest.mark.django_db +@pytest.mark.parametrize("user_type", ["user", "moderator_user"]) +@pytest.mark.parametrize("form_field", PreferencesForm.Meta.fields) +def test_new_current_user_profile_view_post_valid_preferences( + user_type, form_field, tp, request, assert_messages +): + user = request.getfixturevalue(user_type) + original_preferences = { + field: getattr(user.preferences, field) for field in PreferencesForm.Meta.fields + } + new_preferences = { + field: [] for field in PreferencesForm.Meta.fields + } # Clear all options + + with tp.login(user): + response = tp.post( + tp.reverse("profile-account-new"), + data={**new_preferences, "update_preferences": "Update Preeferences"}, + follow=True, + ) + + assert response.status_code == 200 + user.refresh_from_db() + for field in PreferencesForm.Meta.fields: + if ( + field == "allow_notification_others_news_needs_moderation" + and user_type != "moderator_user" + ): + assert ( + getattr(user.preferences, field) == original_preferences[field] + ) # Field should be unchanged for non-moderators + else: + assert getattr(user.preferences, field) == [] # Field should be cleared + + assert_messages( + response, [("success", "Your preferences were successfully updated.")] + ) diff --git a/users/views.py b/users/views.py index c29a9c76..23dd5f0b 100644 --- a/users/views.py +++ b/users/views.py @@ -5,6 +5,9 @@ from django.http import HttpResponseRedirect from django.urls import reverse_lazy from django.views.generic import DetailView, UpdateView from django.views.generic.edit import FormView +from django.views.generic.base import TemplateView + +from allauth.account.forms import ChangePasswordForm from rest_framework import generics from rest_framework import viewsets @@ -123,3 +126,79 @@ class ProfilePreferencesView(LoginRequiredMixin, SuccessMessageMixin, UpdateView def get_object(self): return self.request.user.preferences + + +class NewCurrentUserProfileView(LoginRequiredMixin, SuccessMessageMixin, TemplateView): + template_name = "users/profile_new.html" + success_message = "Your profile was successfully updated." + success_url = reverse_lazy("profile-account-new") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["change_password_form"] = ChangePasswordForm(user=self.request.user) + context["profile_photo_form"] = UserProfilePhotoForm(instance=self.request.user) + context["profile_preferences_form"] = PreferencesForm( + instance=self.request.user.preferences + ) + return context + + def post(self, request, *args, **kwargs): + """ + Process each form submission individually if present + """ + if "change_password" in request.POST: + change_password_form = ChangePasswordForm( + data=request.POST, user=self.request.user + ) + self.change_password(change_password_form, request) + + if "update_photo" in request.POST: + profile_photo_form = UserProfilePhotoForm( + self.request.POST, self.request.FILES, instance=self.request.user + ) + self.update_photo(profile_photo_form, request) + + if "update_github_photo" in request.POST: + self.update_github_photo(request) + + if "update_preferences" in request.POST: + profile_preferences_form = PreferencesForm( + self.request.POST, instance=request.user.preferences + ) + self.update_preferences(profile_preferences_form, request) + + return HttpResponseRedirect(self.success_url) + + def change_password(self, form, request): + """Change the password of the user.""" + if form.is_valid(): + self.object = request.user + self.object.set_password(form.cleaned_data["password1"]) + self.object.save() + messages.success(request, "Your password was successfully updated.") + else: + for error in form.errors.values(): + messages.error(request, f"{error}") + + def update_photo(self, form, request): + """Update the profile photo of the user.""" + if form.is_valid(): + form.save() + messages.success(request, "Your profile photo was successfully updated.") + else: + for error in form.errors.values(): + messages.error(request, f"{error}") + + def update_github_photo(self, request): + """Update the GitHub photo of the user.""" + tasks.update_user_github_photo(str(request.user.pk)) + messages.success(request, "Your GitHub photo has been retrieved.") + + def update_preferences(self, form, request): + """Update the preferences of the user.""" + if form.is_valid(): + form.save() + messages.success(request, "Your preferences were successfully updated.") + else: + for error in form.errors.values(): + messages.error(request, f"{error}")