Repository: ISPP-2425-G4/borroo
Pull request #435: Foto de perfil
Author: login: frafermot
Created: 2025-04-07T10:59:14Z | Merged: 2025-04-07T12:01:35Z

Ahora al editar el perfil puedes añadir una foto de perfil que se mostrará tanto en esa vista como en la de detalles de un objeto en caso de que el propietario tenga una foto.


Diff:
diff --git a/backend/seeders.py b/backend/seeders.py
index cdcbf3ca..7339858d 100644
--- a/backend/seeders.py
+++ b/backend/seeders.py
@@ -82,7 +82,7 @@ def create_users():
         verified_account=True,
         is_verified=True,
         pricing_plan='premium',
-        is_admin=True
+        is_admin=True,
     )
 
 
diff --git a/backend/usuarios/models.py b/backend/usuarios/models.py
index ba3c1ba2..a3230e59 100644
--- a/backend/usuarios/models.py
+++ b/backend/usuarios/models.py
@@ -106,6 +106,7 @@ class User(AbstractUser):
     reset_token_expiration = models.DateTimeField(blank=True, null=True)
     verify_token = models.CharField(max_length=255, blank=True, null=True)
     is_admin = models.BooleanField(default=False)
+    image = models.URLField(blank=True, null=True)
     stripe_customer_id = models.CharField(max_length=255,
                                           null=True, blank=True)
     stripe_subscription_id = models.CharField(max_length=255,
diff --git a/backend/usuarios/serializers.py b/backend/usuarios/serializers.py
index c1a2c9db..091063df 100644
--- a/backend/usuarios/serializers.py
+++ b/backend/usuarios/serializers.py
@@ -1,3 +1,4 @@
+from utils.utils import upload_image_to_imgbb
 from rest_framework import serializers
 from objetos.serializers import ItemSerializer
 from .models import Review, User, Report
@@ -6,6 +7,7 @@
 
 class UserSerializer(serializers.ModelSerializer):
     items = ItemSerializer(many=True, read_only=True)
+    user_image = serializers.ImageField(write_only=True, required=False)
 
     class Meta:
         model = User
@@ -14,11 +16,18 @@ class Meta:
             'phone_number', 'country', 'city', 'address', 'postal_code', 'cif',
             'dni',
             'is_verified', 'pricing_plan', 'owner_rating', 'renter_rating',
-            'items', 'is_admin'
+            'items', 'is_admin', 'image', 'user_image',
         ]
         read_only_fields = ['id', 'owner_rating', 'renter_rating', 'items',
                             'is_admin']
 
+    def update(self, instance, validated_data):
+        """Sobrescribir el método update para manejar la imagen."""
+        user_image = validated_data.pop('user_image', None)
+        if user_image:
+            instance.image = upload_image_to_imgbb(user_image)
+        return super().update(instance, validated_data)
+
     def validate_username(self, value):
         if " " in value:
             raise serializers.ValidationError(
diff --git a/backend/usuarios/urls.py b/backend/usuarios/urls.py
index 8253f1e2..b9cfee37 100644
--- a/backend/usuarios/urls.py
+++ b/backend/usuarios/urls.py
@@ -62,6 +62,8 @@
     path('adminCustome/items/', ListItemsView.as_view(), name='list-items'),
     path('adminCustome/rent/list/', RentListView.as_view(), name='rent-list'),
     path('update/', UpdateUserPerfilView.as_view(), name='update-user'),
+    path('update-image/', UpdateUserPerfilView.as_view(),
+         name='update-user-image'),
 
 
 ]
diff --git a/frontend/src/Profile.jsx b/frontend/src/Profile.jsx
index f965ad24..2b4d2b86 100644
--- a/frontend/src/Profile.jsx
+++ b/frontend/src/Profile.jsx
@@ -41,6 +41,7 @@ import IconButton from "@mui/material/IconButton";
 import MenuItem from "@mui/material/MenuItem";
 import ReportIcon from "@mui/icons-material/Report";
 import CloseIcon from "@mui/icons-material/Close";
+import EditIcon from "@mui/icons-material/Edit";
 
 const Profile = () => {
   const { username } = useParams();
@@ -78,7 +79,10 @@ const Profile = () => {
     postal_code: "",
     pricing_plan: "free",
     dni: "",
+    image: null,
   });
+  const [image, setImage] = useState(null);
+  const [imagePreview, setImagePreview] = useState(null);
 
 
   useEffect(() => {
@@ -278,7 +282,7 @@ const Profile = () => {
         dni: user.dni || "",
       });
     }
-  }, [user]);
+  }, [user, image]);
 
   const validateDni = (dni) => {
     const dniPattern = /^\d{8}[A-Z]$/;
@@ -379,6 +383,8 @@ const Profile = () => {
     setFormData((prev) => ({ ...prev, [name]: value }));
   };
 
+
+
   const handleUpdateUser = async () => {
     const token = localStorage.getItem("access_token");
 
@@ -397,16 +403,21 @@ const Profile = () => {
     }
 
     try {
+      if (image) {
+        formData.user_image = image;
+      }
       const response = await axios.put(
         `${import.meta.env.VITE_API_BASE_URL}/usuarios/update/`,
         formData,
         {
           headers: {
             Authorization: `Bearer ${token}`,
+            "Content-Type": "multipart/form-data",
           },
         }
       );
 
+
       alert("Perfil actualizado correctamente.");
       setUser(response.data.user);
       setEditMode(false);
@@ -430,16 +441,21 @@ const Profile = () => {
     }
 
     try {
+      if (image) {
+        formData.user_image = image;
+      }
       const response = await axios.put(
         `${import.meta.env.VITE_API_BASE_URL}/usuarios/adminCustome/users/update/${user.id}/`,
         formData,
         {
           headers: {
             Authorization: `Bearer ${token}`,
+            "Content-Type": "multipart/form-data",
           },
         }
       );
 
+
       alert("Usuario actualizado correctamente.");
       setUser(response.data);
       setEditMode(false);
@@ -459,10 +475,61 @@ const Profile = () => {
         <Paper elevation={3} sx={{ p: 4, borderRadius: 3, textAlign: "center" }}>
           {/* 📌 AVATAR Y DATOS PRINCIPALES */}
           <Box display="flex" flexDirection="column" alignItems="center">
-            <Avatar sx={{ width: 100, height: 100, mb: 2 }}>
-              <PersonIcon sx={{ fontSize: 60 }} />
+            <Box
+              sx={{
+                position: 'relative',
+                display: 'inline-block',
+                width: 100,
+                height: 100,
+                mb: 2,
+                cursor: editMode ? 'pointer' : 'default',
+                '&:hover .edit-icon': {
+                  opacity: 1,
+                },
+              }}
+              onClick={() => {
+                if (editMode) {
+                  document.getElementById("imageInput").click();
+                }
+              }}
+            >
+            <Avatar sx={{ width: 100, height: 100}}
+              src = {imagePreview || (user.image ? user.image : "")}
+            >
+              {!image && !user.image && <PersonIcon sx={{ fontSize: 60 }} />}
             </Avatar>
-
+            <input
+              type="file"
+              id="imageInput"
+              accept="image/*"
+              onChange={(e) => {
+                const file = e.target.files[0];
+                if (file) {
+                  setImage(e.target.files[0])
+                  setImagePreview(URL.createObjectURL(file));
+                }
+              }}
+              style={{ display: "none" }}
+            />
+            {editMode && (
+              <EditIcon
+                className="edit-icon"
+                sx={{
+                  position: 'absolute',
+                  bottom: 4,
+                  right: 4,
+                  backgroundColor: 'white',
+                  borderRadius: '50%',
+                  padding: '2px',
+                  fontSize: 20,
+                  color: 'black',
+                  opacity: 0,
+                  transition: 'opacity 0.2s',
+                  boxShadow: 1,
+                }}
+              />
+            )}
+          </Box>
             <Typography variant="h4" fontWeight="bold">
               {user.name} {user.surname}
             </Typography>
diff --git a/frontend/src/ShowItem.jsx b/frontend/src/ShowItem.jsx
index b86f6959..c71d2c2b 100644
--- a/frontend/src/ShowItem.jsx
+++ b/frontend/src/ShowItem.jsx
@@ -23,7 +23,8 @@ import {
   FormControl,
   MenuItem,
   Select,
-  InputLabel
+  InputLabel,
+  Avatar
 } from '@mui/material';
 
 import {  
@@ -84,6 +85,7 @@ const ShowItemScreen = () => {
   const [showReportModal, setShowReportModal] = useState(false);
   const [reportCategory, setReportCategory] = useState("");
   const [reportDescription, setReportDescription] = useState("");
+  const [userImage, setUserImage] = useState("");
   const [dialogOpen, setDialogOpen] = useState(false);
 
   const currentUser = JSON.parse(localStorage.getItem("user"));
@@ -202,6 +204,9 @@ const ShowItemScreen = () => {
       );
       const userData = response.data;
       setUserName(userData.username);
+      if (userData.image) {
+        setUserImage(userData.image);
+      }
     } catch (error) {
       console.error("Error fetching user:", error);
       setUserName("Usuario desconocido");
@@ -688,7 +693,10 @@ const ShowItemScreen = () => {
               <Card elevation={2} sx={{ mb: 3 }}>
                 <CardContent sx={{ display: "flex", justifyContent: "space-between", gap: 2 }}>
                   <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
-                  <PersonIcon fontSize="large" color="primary" />
+                  <Avatar sx={{ width: 100, height: 100}}
+                    src = {userImage ? userImage : ""}
+                  >
+                  </Avatar>
                   <Box>
                   <Typography variant="caption" color="textSecondary">
                     Publicado por:


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.




state: COMMENTED
author_association: NONE
submitted_at: 2025-04-07T11:00:34Z
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.




state: COMMENTED
author_association: NONE
submitted_at: 2025-04-07T11:13:14Z
user: login: Sangros02
body: Se añade bien la imagen y se visualiza correctamente
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T12:01:28Z

Review comments:
diff_hunk: @@ -459,10 +475,61 @@ const Profile = () => {
         <Paper elevation={3} sx={{ p: 4, borderRadius: 3, textAlign: "center" }}>
           {/* 📌 AVATAR Y DATOS PRINCIPALES */}
           <Box display="flex" flexDirection="column" alignItems="center">
-            <Avatar sx={{ width: 100, height: 100, mb: 2 }}>
-              <PersonIcon sx={{ fontSize: 60 }} />
+            <Box
+              sx={{
+                position: 'relative',
+                display: 'inline-block',
+                width: 100,
+                height: 100,
+                mb: 2,
+                cursor: editMode ? 'pointer' : 'default',
+                '&:hover .edit-icon': {
+                  opacity: 1,
+                },
+              }}
+              onClick={() => {
+                if (editMode) {
+                  document.getElementById("imageInput").click();
+                }
+              }}
+            >
+            <Avatar sx={{ width: 100, height: 100}}
+              src = {imagePreview || (user.image ? user.image : "")}
+            >
+              {!image && !user.image && <PersonIcon sx={{ fontSize: 60 }} />}
             </Avatar>
-
+            <input
+              type="file"
+              id="imageInput"
+              accept="image/*"
+              onChange={(e) => {
+                const file = e.target.files[0];
+                if (file) {
+                  setImage(e.target.files[0])
+                  setImagePreview(URL.createObjectURL(file));
+                }
+              }}
+              style={{ display: "none" }}
+            />
+            {editMode && (
+              <EditIcon
+                className="edit-icon"
+                sx={{
+                  position: 'absolute',
+                  bottom: 4,
+                  right: 4,
+                  backgroundColor: 'white',
+                  borderRadius: '50%',
+                  padding: '2px',
+                  fontSize: 20,
+                  color: '#black',
path: frontend/src/Profile.jsx
user: login: Copilot
body: The color value '#black' is not a valid hex code. Please replace it with a valid CSS color value such as '#000' or 'black'.
```suggestion
                  color: 'black',
```
created_at: 2025-04-07T11:00:33Z
author_association: NONE
diff_hunk: @@ -82,7 +82,8 @@ def create_users():
         verified_account=True,
         is_verified=True,
         pricing_plan='premium',
-        is_admin=True
+        is_admin=True,
+        verified_account=True,
path: backend/seeders.py
user: login: Copilot
body: The 'verified_account' argument is duplicated in the user creation call. Remove the duplicate to avoid potential conflicts.
```suggestion

```
created_at: 2025-04-07T11:00:33Z
author_association: NONE

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-04-07T11:06:43Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=ISPP-2425-G4_borroo&pullRequest=435) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [2 New issues](https://sonarcloud.io/project/issues?id=ISPP-2425-G4_borroo&pullRequest=435&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=ISPP-2425-G4_borroo&pullRequest=435&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=ISPP-2425-G4_borroo&pullRequest=435&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=ISPP-2425-G4_borroo&pullRequest=435&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=ISPP-2425-G4_borroo&pullRequest=435&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=ISPP-2425-G4_borroo&pullRequest=435)


author_association: NONE