Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add user profile pictures #2253

Merged
merged 26 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
927f288
new profile pic feature
UdaySagar-Git Jun 7, 2024
e038343
Merge branch 'develop' into profile-pic
UdaySagar-Git Jun 7, 2024
c30735b
bug fix
UdaySagar-Git Jun 7, 2024
348351c
Merge branch 'profile-pic' of https://github.com/UdaySagar-Git/care i…
UdaySagar-Git Jun 7, 2024
fc4c573
removes new bucket
UdaySagar-Git Jul 9, 2024
4fb9910
fix bug
UdaySagar-Git Jul 9, 2024
1f408c6
checks for write permissions
UdaySagar-Git Jul 9, 2024
8f5020c
Merge branch 'develop' into profile-pic
UdaySagar-Git Aug 15, 2024
0057a6a
lint fix
UdaySagar-Git Aug 15, 2024
39bee6c
adds permission checks
UdaySagar-Git Aug 18, 2024
e2d7aa5
Merge branch 'develop' into profile-pic
UdaySagar-Git Sep 17, 2024
54bcf29
Merge branch 'develop' into profile-pic
UdaySagar-Git Sep 20, 2024
aaa48e2
Merge branch 'develop' into profile-pic
sainak Sep 21, 2024
9e83dc0
refactor cover image upload logic
sainak Sep 21, 2024
d6f4baf
update perms and cleanup
sainak Sep 21, 2024
8ef37b8
update migrations
sainak Sep 21, 2024
a95a80a
Merge remote-tracking branch 'origin/develop' into profile-pic
sainak Sep 21, 2024
2191cdb
add tests
sainak Sep 21, 2024
69de284
cleanup parsers
sainak Sep 21, 2024
ec87341
lint fix
sainak Sep 21, 2024
db5926c
optimize unique key generation
sainak Sep 21, 2024
237258e
delete images from s3 with delete operation
sainak Sep 21, 2024
8a8b533
fix tests
sainak Sep 21, 2024
777bffc
fix
sainak Sep 22, 2024
4caf143
Merge remote-tracking branch 'origin/develop' into profile-pic
sainak Sep 22, 2024
8dd0f6a
Merge branch 'develop' into profile-pic
vigneshhari Sep 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ BUCKET_ENDPOINT=http://localhost:4566
BUCKET_EXTERNAL_ENDPOINT=http://localhost:4566
FILE_UPLOAD_BUCKET=patient-bucket
FACILITY_S3_BUCKET=facility-bucket
USER_S3_BUCKET=user-bucket
8 changes: 8 additions & 0 deletions aws/backend.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@
"name": "FACILITY_S3_STATIC_PREFIX",
"value": "https://egov-s3-facility-10bedicu.s3.ap-south-1.amazonaws.com/egov-s3-facility-10bedicu"
},
{
"name": "USER_S3_BUCKET",
"value": "egov-s3-user-10bedicu"
},
{
"name": "USER_S3_BUCKET_ENDPOINT",
"value": "https://egov-s3-user-10bedicu.s3.amazonaws.com"
},
{
"name": "FILE_UPLOAD_BUCKET",
"value": "egov-s3-patient-data-10bedicu"
Expand Down
24 changes: 24 additions & 0 deletions aws/celery.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@
"name": "FILE_UPLOAD_BUCKET_ENDPOINT",
"value": "https://egov-s3-patient-data-10bedicu.s3.amazonaws.com"
},
{
"name": "USER_S3_BUCKET",
"value": "egov-s3-user-10bedicu"
},
{
"name": "USER_S3_BUCKET_ENDPOINT",
"value": "https://egov-s3-user-10bedicu.s3.amazonaws.com"
},
{
"name": "MAINTENANCE_MODE",
"value": "0"
Expand Down Expand Up @@ -355,6 +363,14 @@
"name": "FILE_UPLOAD_BUCKET_ENDPOINT",
"value": "https://egov-s3-patient-data-10bedicu.s3.amazonaws.com"
},
{
"name": "USER_S3_BUCKET",
"value": "egov-s3-user-10bedicu"
},
{
"name": "USER_S3_BUCKET_ENDPOINT",
"value": "https://egov-s3-user-10bedicu.s3.amazonaws.com"
},
{
"name": "MAINTENANCE_MODE",
"value": "0"
Expand Down Expand Up @@ -493,6 +509,14 @@
"valueFrom": "/care/backend/FACILITY_S3_SECRET",
"name": "FACILITY_S3_SECRET"
},
{
"valueFrom": "/care/backend/USER_S3_KEY",
"name": "USER_S3_KEY"
},
{
"valueFrom": "/care/backend/USER_S3_SECRET",
"name": "USER_S3_SECRET"
},
{
"valueFrom": "/care/backend/VAPID_PUBLIC_KEY",
"name": "VAPID_PUBLIC_KEY"
Expand Down
30 changes: 30 additions & 0 deletions care/users/api/serializers/user.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import boto3
from django.contrib.auth.hashers import make_password
from django.db import transaction
from django.utils.timezone import now
Expand All @@ -15,6 +16,7 @@
from care.utils.queryset.facility import get_home_facility_queryset
from care.utils.serializer.external_id_field import ExternalIdSerializerField
from config.serializers import ChoiceField
from care.utils.csp.config import BucketType, get_client_config


class SignUpSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -279,6 +281,7 @@ class UserSerializer(SignUpSerializer):
source="home_facility",
read_only=True,
)
read_profile_picture_url = serializers.URLField(read_only=True)

home_facility = ExternalIdSerializerField(queryset=Facility.objects.all())

Expand Down Expand Up @@ -316,6 +319,7 @@ class Meta:
"pf_endpoint",
"pf_p256dh",
"pf_auth",
"read_profile_picture_url",
)
read_only_fields = (
"is_superuser",
Expand Down Expand Up @@ -409,6 +413,7 @@ class UserListSerializer(serializers.ModelSerializer):
read_only=True,
)
home_facility = ExternalIdSerializerField(queryset=Facility.objects.all())
read_profile_picture_url = serializers.URLField(read_only=True)

class Meta:
model = User
Expand All @@ -431,4 +436,29 @@ class Meta:
"home_facility_object",
"home_facility",
"video_connect_link",
"read_profile_picture_url",
)

class UserImageUploadSerializer(serializers.ModelSerializer):
profile_picture_url = serializers.ImageField(required=True, write_only=True)
read_profile_picture_url = serializers.CharField(read_only=True)

class Meta:
model = User
fields = ("profile_picture_url", "read_profile_picture_url")

def save(self, **kwargs):
user = self.instance
image = self.validated_data["profile_picture_url"]
image_extension = image.name.split(".")[-1]
config, bucket_name = get_client_config(BucketType.USER)
s3 = boto3.client("s3", **config)
image_location = f"{user.external_id}/profile.{image_extension}"
s3.put_object(
Bucket=bucket_name,
Key=image_location,
Body=image.file,
)
user.profile_picture_url = image_location
user.save()
return user
26 changes: 26 additions & 0 deletions care/users/api/viewsets/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@
from rest_framework.response import Response
from rest_framework.serializers import ValidationError
from rest_framework.viewsets import GenericViewSet
from rest_framework.parsers import MultiPartParser

from care.facility.api.serializers.facility import FacilityBasicInfoSerializer
from care.facility.models.facility import Facility, FacilityUser
from care.users.api.serializers.user import (
UserCreateSerializer,
UserListSerializer,
UserSerializer,
UserImageUploadSerializer,
)
from care.users.models import User
from care.utils.cache.cache_allowed_facilities import get_accessible_facilities
Expand Down Expand Up @@ -151,6 +153,11 @@ def get_queryset(self):
)
return self.queryset.filter(query)

def get_parsers(self):
if self.request.method == "POST" and self.request.path.endswith("profile_picture"):
return [MultiPartParser()]
return super().get_parsers()

def get_object(self):
try:
return super().get_object()
Expand All @@ -164,6 +171,8 @@ def get_serializer_class(self):
return UserCreateSerializer
# elif self.action == "create":
# return SignUpSerializer
elif self.action == "profile_picture":
return UserImageUploadSerializer
else:
return UserSerializer

Expand Down Expand Up @@ -366,3 +375,20 @@ def check_availability(self, request, username):
if User.check_username_exists(username):
return Response(status=status.HTTP_409_CONFLICT)
return Response(status=status.HTTP_200_OK)

@extend_schema(tags=["users"])
@action(detail=True, methods=["POST"], permission_classes=[IsAuthenticated])
def profile_picture(self, request, username):
user = self.get_object()
UdaySagar-Git marked this conversation as resolved.
Show resolved Hide resolved
serializer = UserImageUploadSerializer(user, data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(status=status.HTTP_200_OK)

@extend_schema(tags=["users"])
@profile_picture.mapping.delete
def profile_picture_delete(self, *args, **kwargs):
user = self.get_object()
user.profile_picture_url = None
user.save()
return Response(status=status.HTTP_204_NO_CONTENT)
18 changes: 18 additions & 0 deletions care/users/migrations/0017_user_profile_picture_url.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.10 on 2024-06-07 06:16

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("users", "0016_upgrade_user_skills"),
]

operations = [
migrations.AddField(
model_name="user",
name="profile_picture_url",
field=models.CharField(blank=True, default=None, max_length=500, null=True),
),
]
8 changes: 8 additions & 0 deletions care/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,9 @@ class User(AbstractUser):

gender = models.IntegerField(choices=GENDER_CHOICES, blank=False)
date_of_birth = models.DateField(null=True, blank=True)
profile_picture_url = models.CharField(
blank=True, null=True, default=None, max_length=500
)
skills = models.ManyToManyField("Skill", through=UserSkill)
home_facility = models.ForeignKey(
"facility.Facility", on_delete=models.PROTECT, null=True, blank=True
Expand Down Expand Up @@ -311,6 +314,11 @@ class User(AbstractUser):

CSV_MAKE_PRETTY = {"user_type": (lambda x: User.REVERSE_TYPE_MAP[x])}

def read_profile_picture_url(self):
if self.profile_picture_url:
return f"{settings.USER_S3_BUCKET_EXTERNAL_ENDPOINT}/{settings.USER_S3_BUCKET}/{self.profile_picture_url}"
return None

@staticmethod
def has_read_permission(request):
return True
Expand Down
1 change: 1 addition & 0 deletions care/users/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def get_detail_representation(self, obj=None) -> dict:
"doctor_qualification": obj.doctor_qualification,
"weekly_working_hours": obj.weekly_working_hours,
"video_connect_link": obj.video_connect_link,
"profile_picture_url": obj.profile_picture_url,
**self.get_local_body_district_state_representation(obj),
}

Expand Down
12 changes: 12 additions & 0 deletions care/utils/csp/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class CSProvider(enum.Enum):
class BucketType(enum.Enum):
PATIENT = "PATIENT"
FACILITY = "FACILITY"
USER = "USER"


def get_facility_bucket_config(external) -> tuple[ClientConfig, BucketName]:
Expand All @@ -47,10 +48,21 @@ def get_patient_bucket_config(external) -> tuple[ClientConfig, BucketName]:
else settings.FILE_UPLOAD_BUCKET_ENDPOINT,
}, settings.FILE_UPLOAD_BUCKET

def get_user_bucket_config(external) -> tuple[ClientConfig, BucketName]:
return {
"region_name": settings.USER_S3_REGION,
"aws_access_key_id": settings.USER_S3_KEY,
"aws_secret_access_key": settings.USER_S3_SECRET,
"endpoint_url": settings.USER_S3_BUCKET_EXTERNAL_ENDPOINT
if external
else settings.USER_S3_BUCKET_ENDPOINT,
}, settings.USER_S3_BUCKET

def get_client_config(bucket_type: BucketType, external=False):
if bucket_type == BucketType.FACILITY:
return get_facility_bucket_config(external=external)
elif bucket_type == BucketType.PATIENT:
return get_patient_bucket_config(external=external)
elif bucket_type == BucketType.USER:
return get_user_bucket_config(external=external)
raise ValueError("Invalid Bucket Type")
13 changes: 13 additions & 0 deletions config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,19 @@
),
)

USER_S3_BUCKET = env("USER_S3_BUCKET", default="")
USER_S3_REGION = env("USER_S3_REGION_CODE", default=BUCKET_REGION)
USER_S3_KEY = env("USER_S3_KEY", default=BUCKET_KEY)
USER_S3_SECRET = env("USER_S3_SECRET", default=BUCKET_SECRET)
USER_S3_BUCKET_ENDPOINT = env("USER_S3_BUCKET_ENDPOINT", default=BUCKET_ENDPOINT)
USER_S3_BUCKET_EXTERNAL_ENDPOINT = env(
"USER_S3_BUCKET_EXTERNAL_ENDPOINT",
default= BUCKET_EXTERNAL_ENDPOINT
if BUCKET_ENDPOINT
else USER_S3_BUCKET_ENDPOINT,
)


UdaySagar-Git marked this conversation as resolved.
Show resolved Hide resolved
# for setting the shifting mode
PEACETIME_MODE = env.bool("PEACETIME_MODE", default=True)

Expand Down
Loading