diff --git a/config/urls.py b/config/urls.py index 2acf36d1..0ff14ff9 100755 --- a/config/urls.py +++ b/config/urls.py @@ -8,7 +8,12 @@ from rest_framework import routers from machina import urls as machina_urls -from users.views import UserViewSet, CurrentUserView, ProfileViewSet +from users.views import ( + UserViewSet, + CurrentUserView, + ProfileViewSet, + ProfilePhotoUploadView, +) from ak.views import ( HomepageView, ForbiddenView, @@ -42,6 +47,7 @@ urlpatterns = ( path("", HomepageView.as_view(), name="home"), path("admin/", admin.site.urls), path("accounts/", include("allauth.urls")), + path("users/me/photo/", ProfilePhotoUploadView.as_view(), name="profile-photo"), path("users/me/", CurrentUserView.as_view(), name="current-user"), path("users//", ProfileViewSet.as_view(), name="profile-user"), path("api/v1/", include(router.urls)), diff --git a/requirements.in b/requirements.in index 6582868a..eb40feac 100755 --- a/requirements.in +++ b/requirements.in @@ -15,6 +15,7 @@ gunicorn psycopg2-binary whitenoise django-click +Pillow==9.4.0 # Logging django-tracer diff --git a/requirements.txt b/requirements.txt index 2fa16a2d..9c128195 100755 --- a/requirements.txt +++ b/requirements.txt @@ -185,8 +185,10 @@ pexpect==4.8.0 # via ipython pickleshare==0.7.5 # via ipython -pillow==8.4.0 - # via django-machina +pillow==9.4.0 + # via + # -r ./requirements.in + # django-machina pip-tools==6.6.2 # via -r ./requirements.in pluggy==0.13.1 diff --git a/templates/users/photo_upload.html b/templates/users/photo_upload.html new file mode 100644 index 00000000..8a1eeb96 --- /dev/null +++ b/templates/users/photo_upload.html @@ -0,0 +1,49 @@ +{% extends "base.html" %} + +{% load static %} + +{% block subnav %} +
+
+ {% if user.image.url %} + user + {% else %} + + {% endif %} +
+
+ {{ user.get_full_name }} + Joined {{ user.date_joined }} +
+
+
+ Account + Settings + Password + Help + Log Out +
+{% endblock %} + +{% block content %} +
+
+
+ +
+
+
+ +
+ {% csrf_token %} +
+ Update Profile Photo + {{ form.as_p }} +
+
+ +
+
+
+
+{% endblock content %} \ No newline at end of file diff --git a/users/forms.py b/users/forms.py new file mode 100644 index 00000000..879138ac --- /dev/null +++ b/users/forms.py @@ -0,0 +1,11 @@ +from django.contrib.auth import get_user_model +from django import forms + + +User = get_user_model() + + +class UserProfilePhotoForm(forms.ModelForm): + class Meta: + model = User + fields = ["image"] diff --git a/users/tests/test_urls.py b/users/tests/test_urls.py index ff3c6c7a..0da5b2bb 100644 --- a/users/tests/test_urls.py +++ b/users/tests/test_urls.py @@ -1,3 +1,9 @@ +import tempfile + +from django.core.files.uploadedfile import SimpleUploadedFile +from django.test import override_settings + + def test_login_url(tp, db): """ GET /accounts/login/ @@ -36,3 +42,33 @@ def test_password_reset_url(tp, db): """ res = tp.get("account_reset_password") tp.response_200(res) + + +def test_profile_photo_auth(tp, db): + """ + POST /users/me/photo/ + + Canary test that the photo upload page is protected. + """ + res = tp.post("profile-photo") + tp.response_302(res) + assert "/accounts/login" in res.url + + +@override_settings(MEDIA_ROOT=tempfile.gettempdir()) +def test_profile_photo_update_success(tp, user, client): + """ + POST /users/me/photo + + Confirm that user can update their profile photo + """ + old_image = user.image + client.force_login(user) + image = SimpleUploadedFile( + "/image/fpo/user.png", b"file_content", content_type="image/png" + ) + res = tp.post("profile-photo", data={"image": image}) + tp.response_302(res) + assert f"/users/{user.pk}" in res.url + user.refresh_from_db() + assert user.image != old_image diff --git a/users/views.py b/users/views.py index e74b3499..e3376c5d 100644 --- a/users/views.py +++ b/users/views.py @@ -1,13 +1,18 @@ +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.urls import reverse_lazy +from django.utils.decorators import method_decorator from django.views.generic import DetailView +from django.views.generic.edit import FormView -from rest_framework import viewsets from rest_framework import generics +from rest_framework import viewsets from rest_framework.permissions import IsAuthenticated -from .serializers import UserSerializer, FullUserSerializer, CurrentUserSerializer - -from .permissions import CustomUserPermissions +from .forms import UserProfilePhotoForm from .models import User +from .permissions import CustomUserPermissions +from .serializers import UserSerializer, FullUserSerializer, CurrentUserSerializer class UserViewSet(viewsets.ModelViewSet): @@ -58,3 +63,23 @@ class ProfileViewSet(DetailView): context["authored"] = user.authors.all() context["maintained"] = user.maintainers.all() return context + + +@method_decorator(login_required, name="dispatch") +class ProfilePhotoUploadView(FormView): + """Allows a user to change their profile photo""" + + template_name = "users/photo_upload.html" + form_class = UserProfilePhotoForm + + def get_success_url(self, **kwargs): + return reverse_lazy("profile-user", args=[self.request.user.pk]) + + def post(self, request, *args, **kwargs): + form = self.form_class(request.POST, request.FILES, instance=self.request.user) + if form.is_valid(): + form.save() + messages.success(request, "Your profile photo has been updated") + return super().form_valid(form) + else: + return super().form_invalid()