diff --git a/backend/backend/profiles/migrations/0006_alter_appuser_profile_picture.py b/backend/backend/profiles/migrations/0006_alter_appuser_profile_picture.py new file mode 100644 index 0000000..a922d45 --- /dev/null +++ b/backend/backend/profiles/migrations/0006_alter_appuser_profile_picture.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.3 on 2025-02-26 12:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0005_appuser_first_name_appuser_profile_picture'), + ] + + operations = [ + migrations.AlterField( + model_name='appuser', + name='profile_picture', + field=models.ImageField(blank=True, null=True, upload_to=''), + ), + ] diff --git a/backend/backend/profiles/models.py b/backend/backend/profiles/models.py index b9e7e54..eaf97fc 100644 --- a/backend/backend/profiles/models.py +++ b/backend/backend/profiles/models.py @@ -31,7 +31,7 @@ def create_superuser(self, email, password=None, **extra_fields): class AppUser(AbstractBaseUser, PermissionsMixin): age = models.PositiveIntegerField(null=True, blank=True, validators=[MinValueValidator(18)]) email = models.EmailField(unique=True) - profile_picture = models.ImageField(upload_to='profile_pictures/', null=True, blank=True) + profile_picture = models.ImageField(upload_to='', null=True, blank=True) first_name = models.CharField(max_length=30, blank=True) is_staff = models.BooleanField(default=False) is_superuser = models.BooleanField(default=False) diff --git a/backend/backend/profiles/serializers.py b/backend/backend/profiles/serializers.py index e961ba8..cc7292d 100644 --- a/backend/backend/profiles/serializers.py +++ b/backend/backend/profiles/serializers.py @@ -1,8 +1,17 @@ from .models import AppUser from rest_framework import serializers +from django.conf import settings class UserSerializer(serializers.ModelSerializer): + # profile_picture = serializers.SerializerMethodField() + + # def get_profile_picture(self, obj): + # request = self.context.get("request") + # if obj.profile_picture: + # return request.build_absolute_uri(obj.profile_picture.url) + # return None + class Meta: model = AppUser fields = ['id', 'email', 'password', 'age', 'is_staff', 'profile_picture', 'first_name'] diff --git a/backend/backend/profiles/views.py b/backend/backend/profiles/views.py index 40112e1..400f963 100644 --- a/backend/backend/profiles/views.py +++ b/backend/backend/profiles/views.py @@ -3,6 +3,7 @@ from rest_framework.views import APIView from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.decorators import action +from rest_framework.parsers import MultiPartParser, FormParser from .models import AppUser from .serializers import UserSerializer from rest_framework_simplejwt.views import TokenObtainPairView @@ -56,6 +57,7 @@ class CustomTokenObtainPairView(TokenObtainPairView): class UserViewSet(viewsets.ModelViewSet): queryset = AppUser.objects.all() serializer_class = UserSerializer + parser_classes = [MultiPartParser, FormParser] # Retrieves the user by email from the URL and returns the user object to the frontend @action(detail=False, methods=['get'], url_path='view/(?P[^/.]+)') diff --git a/backend/backend/settings.py b/backend/backend/settings.py index 7dc50e3..4ef05ca 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -69,6 +69,7 @@ 'backend.escaperooms', 'rest_framework', 'corsheaders', + 'storages', ] MIDDLEWARE = [ @@ -184,6 +185,8 @@ STATIC_URL = '/static/' STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') + + # Default primary key field type # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field @@ -210,4 +213,18 @@ +if not DEBUG: + DEFAULT_FILE_STORAGE = 'storages.backends.azure_storage.AzureStorage' + + AZURE_ACCOUNT_NAME = os.getenv('AZURE_ACCOUNT_NAME') + AZURE_ACCOUNT_KEY = os.getenv('AZURE_ACCOUNT_KEY') + AZURE_CONTAINER = os.getenv('AZURE_CONTAINER') + AZURE_SSL = True + AZURE_CUSTOM_DOMAIN = f"https://{AZURE_ACCOUNT_NAME}.blob.core.windows.net/{AZURE_CONTAINER}" + MEDIA_URL = f"{AZURE_CUSTOM_DOMAIN}/" + +else: + MEDIA_URL = '/profile_pictures/' + MEDIA_ROOT = os.path.join(BASE_DIR, 'profile_pictures') + diff --git a/backend/backend/urls.py b/backend/backend/urls.py index 55c019c..f2e1501 100644 --- a/backend/backend/urls.py +++ b/backend/backend/urls.py @@ -1,6 +1,8 @@ from django.contrib import admin from django.urls import path, include from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView +from django.conf.urls.static import static +from django.conf import settings urlpatterns = [ path('admin/', admin.site.urls), @@ -11,3 +13,6 @@ path('api/token/', TokenObtainPairView.as_view(), name='get_token'), path('api/token/refresh/', TokenRefreshView.as_view(), name='refresh_token'), ] + +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/backend/profile_pictures/2711e3b5d23552e99e87a2e0fa031cef18fd7659ac83d8b95cf94e5cdcada496_1.jpg b/backend/profile_pictures/2711e3b5d23552e99e87a2e0fa031cef18fd7659ac83d8b95cf94e5cdcada496_1.jpg new file mode 100644 index 0000000..85af00b Binary files /dev/null and b/backend/profile_pictures/2711e3b5d23552e99e87a2e0fa031cef18fd7659ac83d8b95cf94e5cdcada496_1.jpg differ diff --git a/backend/profile_pictures/740eed4ddedc5bf49525ef92816170d2.jpg b/backend/profile_pictures/740eed4ddedc5bf49525ef92816170d2.jpg new file mode 100644 index 0000000..5d65bc7 Binary files /dev/null and b/backend/profile_pictures/740eed4ddedc5bf49525ef92816170d2.jpg differ diff --git a/backend/profile_pictures/_780x877-egmh2fc0jn.jpg b/backend/profile_pictures/_780x877-egmh2fc0jn.jpg new file mode 100644 index 0000000..2c98721 Binary files /dev/null and b/backend/profile_pictures/_780x877-egmh2fc0jn.jpg differ diff --git a/backend/requirements.txt b/backend/requirements.txt index d2c259f..4e1181e 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,8 +1,10 @@ asgiref==3.8.1 +azure-storage-blob==12.24.1 Django==5.1.1 django-cors-headers==4.4.0 djangorestframework==3.15.2 djangorestframework-simplejwt==5.3.1 +django-storages==1.14.5 psycopg2-binary==2.9.9 PyJWT==2.9.0 python-dotenv==1.0.1 diff --git a/frontend/public/styles/theme.css b/frontend/public/styles/theme.css index 84ee278..48588b9 100644 --- a/frontend/public/styles/theme.css +++ b/frontend/public/styles/theme.css @@ -482,6 +482,18 @@ small.error { color: red; } +.profile-pic-big { + max-width: 16rem; +} + +.profile-picture-container { + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; + margin-bottom: 2rem; +} + /* Footer */ footer { @@ -656,6 +668,7 @@ footer li a{ width: 80%; } + footer .col-md-6 { width: 100%; text-align: center; diff --git a/frontend/src/pages/UserProfieView.jsx b/frontend/src/pages/UserProfieView.jsx index 907d931..66b3155 100644 --- a/frontend/src/pages/UserProfieView.jsx +++ b/frontend/src/pages/UserProfieView.jsx @@ -37,6 +37,15 @@ export default function UserProfileView() { <>

Email address: {user.email}

{user.age &&

Age: {user.age}

} + {user.profile_picture ? ( +
+

Profile picture:

+ Profile +
+ + ) : ( +

No profile picture uploaded

+ )} ) : (

No user data...

diff --git a/frontend/src/pages/UserProfileEdit.jsx b/frontend/src/pages/UserProfileEdit.jsx index 7eb2054..3df5492 100644 --- a/frontend/src/pages/UserProfileEdit.jsx +++ b/frontend/src/pages/UserProfileEdit.jsx @@ -45,12 +45,23 @@ export default function UserProfileEdit() { return; } + const formData = new FormData(); + formData.append("email", user.email); + formData.append("age", user.age); + + if (user.profile_picture instanceof File) { + formData.append("profile_picture", user.profile_picture); + } + // We pass the user object to the backend // The backend returns the updated user object and we update the state and update the local storage with the new email // So that the UserProfileView component can load and display the updated user data (because we use the email to fetch the user data) // Note that the url has a trailing slash - this is because the backend expects it!!! + // Wew use FormData instead the object itself to send the file to the backend try { - const res = await api.put(`/api/user/edit/${user.id}/`, user ); + const res = await api.put(`/api/user/edit/${user.id}/`, formData, { + headers: { "Content-Type": "multipart/form-data" }, + }); setUser(res.data); localStorage.setItem('email', user.email); navigate('/user-profile', { state: { user: res.data } }); @@ -91,6 +102,16 @@ export default function UserProfileEdit() { onInvalid={(e) => validate(e, 'You must be 18 or older to register')} required /> + { + changeInput(e); + setUser(prevUser => ({ ...prevUser, profile_picture: e.target.files[0] })); + }} + onInvalid={(e) => validate(e, 'Please select a profile picture')} + /> {loading && }