Skip to content

Commit e59c3e5

Browse files
committed
Fixed image validation with other minor fixes
1 parent aef49f5 commit e59c3e5

File tree

15 files changed

+303
-53
lines changed

15 files changed

+303
-53
lines changed

csm_web/csm_web/settings.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
"""
1212

1313
import os
14-
from datetime import datetime
1514

1615
import sentry_sdk
1716
from factory.django import DjangoModelFactory
@@ -185,18 +184,17 @@
185184
STATIC_URL = "/static/"
186185

187186

188-
# xTODO: make sure this actually works
189187
class ProfileImageStorage(S3Boto3Storage):
190188
bucket_name = "csm-web-profile-pictures"
191-
file_overwrite = False
189+
file_overwrite = True # should be true so that we replace one profile for user
192190

193191
def get_accessed_time(self, name):
194192
# Implement logic to get the last accessed time
195-
return datetime.now()
193+
raise NotImplementedError("This backend does not support this method.")
196194

197195
def get_created_time(self, name):
198196
# Implement logic to get the creation time
199-
return datetime.now()
197+
raise NotImplementedError("This backend does not support this method.")
200198

201199
def path(self, name):
202200
# S3 does not support file paths
@@ -254,6 +252,8 @@ def path(self, name):
254252
"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
255253
"DEFAULT_PARSER_CLASSES": [
256254
"djangorestframework_camel_case.parser.CamelCaseJSONParser",
255+
"rest_framework.parsers.FormParser",
256+
"rest_framework.parsers.MultiPartParser",
257257
],
258258
}
259259

csm_web/frontend/src/components/ImageUploader.tsx

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,63 @@
1-
import React, { useState } from "react";
1+
import React, { useState, useEffect } from "react";
22
import { fetchWithMethod, HTTP_METHODS } from "../utils/api";
33

4-
const MAX_FILE_SIZE_BYTES = 3 * 150 * 150;
4+
// file size limits
5+
const MAX_SIZE_MB = 2;
6+
const MAX_FILE_SIZE_BYTES = MAX_SIZE_MB * 1024 * 1024;
57

68
const ImageUploader = () => {
79
const [file, setFile] = useState<File | null>(null);
810
const [status, setStatus] = useState<string>("");
911

12+
// useEffect(() => {
13+
// if (file) {
14+
// }
15+
// }, [file]);
16+
1017
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
1118
if (e.target.files && e.target.files[0]) {
1219
const selectedFile = e.target.files[0];
1320

14-
if (selectedFile.size > MAX_FILE_SIZE_BYTES) {
15-
setStatus(`File size exceeds max limit of ${MAX_FILE_SIZE_BYTES}`);
21+
if (selectedFile) {
22+
if (selectedFile.size > MAX_FILE_SIZE_BYTES) {
23+
setStatus(`File size exceeds max limit of ${MAX_SIZE_MB}MB.`);
24+
} else {
25+
setFile(selectedFile);
26+
setStatus("");
27+
}
1628
}
17-
setFile(selectedFile);
18-
setStatus("");
1929
}
2030
};
2131

2232
const handleUpload = async () => {
23-
if (!file) {
24-
setStatus("Please select a file to upload");
25-
return;
26-
}
2733
try {
34+
if (!file) {
35+
setStatus("Please select a file to upload");
36+
return;
37+
}
2838
const formData = new FormData();
39+
2940
formData.append("file", file);
3041

3142
const response = await fetchWithMethod(`user/upload_image/`, HTTP_METHODS.POST, formData, true);
3243

44+
console.log(response);
45+
3346
if (!response.ok) {
34-
throw new Error("Failed to upload file");
47+
const errorData = await response.json();
48+
console.error("Error:", errorData.error || "Unknown error");
49+
throw new Error(errorData.error || "Failed to upload file");
3550
}
36-
37-
const data = await response.json();
3851
setStatus(`File uploaded successfully`);
3952
} catch (error) {
4053
setStatus(`Upload failed: ${(error as Error).message}`);
4154
}
4255
};
56+
4357
return (
4458
<div>
45-
<input type="file" accept="image/*" onChange={handleFileChange} />
59+
<h1>Image Upload Tester</h1>
60+
<input type="file" onChange={handleFileChange} />
4661
<button onClick={handleUpload}>Upload</button>
4762
{status && <p>{status}</p>}
4863
</div>

csm_web/frontend/src/components/UserProfile.tsx

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ import LoadingSpinner from "./LoadingSpinner";
88
import "../css/base/form.scss";
99
import "../css/base/table.scss";
1010

11+
interface UserInfo {
12+
firstName: string;
13+
lastName: string;
14+
bio: string;
15+
pronouns: string;
16+
pronunciation: string;
17+
profileImage: string;
18+
}
19+
1120
const UserProfile: React.FC = () => {
1221
const { id } = useParams();
1322
let userId = Number(id);
@@ -16,12 +25,13 @@ const UserProfile: React.FC = () => {
1625
const updateMutation = useUserInfoUpdateMutation(userId);
1726
const [isEditing, setIsEditing] = useState(false);
1827

19-
const [formData, setFormData] = useState({
28+
const [formData, setFormData] = useState<UserInfo>({
2029
firstName: "",
2130
lastName: "",
2231
bio: "",
2332
pronouns: "",
24-
pronunciation: ""
33+
pronunciation: "",
34+
profileImage: ""
2535
});
2636

2737
const [showSaveSpinner, setShowSaveSpinner] = useState(false);
@@ -30,12 +40,14 @@ const UserProfile: React.FC = () => {
3040
// Populate form data with fetched user data
3141
useEffect(() => {
3242
if (requestedData) {
43+
console.log(requestedData);
3344
setFormData({
3445
firstName: requestedData.firstName || "",
3546
lastName: requestedData.lastName || "",
3647
bio: requestedData.bio || "",
3748
pronouns: requestedData.pronouns || "",
38-
pronunciation: requestedData.pronunciation || ""
49+
pronunciation: requestedData.pronunciation || "",
50+
profileImage: requestedData.profileImage || ""
3951
});
4052
}
4153
}, [requestedData]);
@@ -59,6 +71,7 @@ const UserProfile: React.FC = () => {
5971
// Handle input changes
6072
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
6173
const { name, value } = e.target;
74+
console.log("Changes");
6275
setFormData(prev => ({
6376
...prev,
6477
[name]: value
@@ -117,9 +130,18 @@ const UserProfile: React.FC = () => {
117130
return (
118131
<div id="user-profile-form">
119132
<h2 className="form-title">User Profile</h2>
120-
121-
<ImageUploader />
122133
<div className="csm-form">
134+
<div className="form-item">
135+
<label htmlFor="profile" className="form-label">
136+
Profile Image
137+
</label>
138+
{isEditing ? (
139+
<ImageUploader />
140+
) : (
141+
// <p>{formData.profileImage}</p>
142+
<img src={formData.profileImage} />
143+
)}
144+
</div>
123145
<div className="form-item">
124146
<label htmlFor="firstName" className="form-label">
125147
First Name:

csm_web/frontend/src/utils/api.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,6 @@ export function fetchWithMethod(
3535
const normalizedEndpoint = endpointWithQueryParams(normalizeEndpoint(endpoint), queryParams);
3636

3737
if (isFormData) {
38-
console.log("test");
39-
// print("test")
4038
return fetch(normalizedEndpoint, {
4139
method: method,
4240
credentials: "same-origin",

csm_web/frontend/src/utils/types.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export interface UserInfo {
4040
pronouns: string;
4141
pronunciation: string;
4242
isEditable: boolean;
43+
profileImage?: string;
4344
}
4445

4546
/**

csm_web/scheduler/admin.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,13 +204,17 @@ class UserAdmin(BasePermissionModelAdmin):
204204
"first_name",
205205
"last_name",
206206
"priority_enrollment",
207+
"bio",
208+
"profile_image",
207209
)
208210

209211
list_display = (
210212
"id",
211213
"name",
212214
"email",
213215
"priority_enrollment",
216+
"bio",
217+
"profile_image",
214218
)
215219
list_display_links = ("name",)
216220
list_filter = ("is_active",)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 4.2.7 on 2024-11-21 00:21
2+
3+
import scheduler.models
4+
import scheduler.storage
5+
from django.db import migrations, models
6+
7+
8+
class Migration(migrations.Migration):
9+
dependencies = [
10+
("scheduler", "0034_user_profile_image_alter_user_bio_and_more"),
11+
]
12+
13+
operations = [
14+
migrations.AlterField(
15+
model_name="user",
16+
name="profile_image",
17+
field=models.ImageField(
18+
blank=True,
19+
storage=scheduler.storage.ProfileImageStorage(),
20+
upload_to=scheduler.models.image_path,
21+
),
22+
),
23+
]

csm_web/scheduler/models.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from django.utils import functional, timezone
1212
from rest_framework.serializers import ValidationError
1313

14-
from csm_web.settings import ProfileImageStorage
14+
from .storage import ProfileImageStorage
1515

1616
logger = logging.getLogger(__name__)
1717

@@ -50,12 +50,22 @@ def week_bounds(date):
5050
return week_start, week_end
5151

5252

53+
def image_path(instance, filename):
54+
"""Compute the full path for a profile image."""
55+
# file will be uploaded to images/<user_id>/<file_name>
56+
extension = filename.rsplit(".", 1)[-1]
57+
return f"images/{instance.id}.{extension}"
58+
59+
5360
class User(AbstractUser):
5461
priority_enrollment = models.DateTimeField(null=True, blank=True)
5562

5663
pronouns = models.CharField(max_length=20, default="", blank=True)
5764
pronunciation = models.CharField(max_length=50, default="", blank=True)
58-
profile_image = models.ImageField(storage=ProfileImageStorage(), blank=True)
65+
# uploaded_at = models.DateTimeField(auto_now_add=True)
66+
profile_image = models.ImageField(
67+
storage=ProfileImageStorage(), upload_to=image_path, blank=True
68+
)
5969
bio = models.CharField(max_length=500, default="", blank=True)
6070

6171
def can_enroll_in_course(self, course, bypass_enrollment_time=False):

csm_web/scheduler/serializers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ class Meta:
176176
"bio",
177177
"pronunciation",
178178
"pronouns",
179+
"profile_image",
179180
)
180181

181182

csm_web/scheduler/storage.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from storages.backends.s3boto3 import S3Boto3Storage
2+
3+
4+
class ProfileImageStorage(S3Boto3Storage):
5+
bucket_name = "csm-web-profile-pictures"
6+
file_overwrite = True # should be true so that we replace one profile for user
7+
8+
def get_accessed_time(self, name):
9+
# Implement logic to get the last accessed time
10+
raise NotImplementedError("This backend does not support this method.")
11+
12+
def get_created_time(self, name):
13+
# Implement logic to get the creation time
14+
raise NotImplementedError("This backend does not support this method.")
15+
16+
def path(self, name):
17+
# S3 does not support file paths
18+
raise NotImplementedError("This backend does not support absolute paths.")

0 commit comments

Comments
 (0)