From 1b2348c1f9b02ce48175652a93b0387956e891ba Mon Sep 17 00:00:00 2001 From: Imggaggu Date: Thu, 20 Nov 2025 23:06:53 +0900 Subject: [PATCH 1/7] [Feat] add sprite, tank, utils.py for svg renderer --- apps/aquatics/renderer/__init__.py | 0 apps/aquatics/renderer/sprite.py | 23 ++++++++++++++++++++++ apps/aquatics/renderer/tank.py | 31 ++++++++++++++++++++++++++++++ apps/aquatics/renderer/utils.py | 21 ++++++++++++++++++++ apps/aquatics/urls.py | 6 ++++++ apps/aquatics/views.py | 11 ++++++++++- 6 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 apps/aquatics/renderer/__init__.py create mode 100644 apps/aquatics/renderer/sprite.py create mode 100644 apps/aquatics/renderer/tank.py create mode 100644 apps/aquatics/renderer/utils.py create mode 100644 apps/aquatics/urls.py diff --git a/apps/aquatics/renderer/__init__.py b/apps/aquatics/renderer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/aquatics/renderer/sprite.py b/apps/aquatics/renderer/sprite.py new file mode 100644 index 0000000..94d8156 --- /dev/null +++ b/apps/aquatics/renderer/sprite.py @@ -0,0 +1,23 @@ +from .utils import strip_outer_svg , safe_attr + +def render_fish_group(cf): + """ + ContributionFish 인스턴스를 받아서, + ... species svg ... 형태로 만들어 줌. + 애니메이션은 프론트에서~ + """ + species = cf.species + raw_svg = species.svg_template + inner = strip_outer_svg(raw_svg) + fish_id = cf.id + + # 위치는 프론트에서 transform 걸 거면 여기선 0,0 기준으로 두고, + # 그냥 id와 class만 설정해줘도 됨. + return f""" + + {inner} + + """ diff --git a/apps/aquatics/renderer/tank.py b/apps/aquatics/renderer/tank.py new file mode 100644 index 0000000..4cb5f19 --- /dev/null +++ b/apps/aquatics/renderer/tank.py @@ -0,0 +1,31 @@ +# apps/aquarium/renderer/tank.py +from apps.items.models import FishSpecies +from apps.aquatics.models import Aquarium,ContributionFish +from .sprite import render_fish_group +from .utils import strip_outer_svg + +def render_aquarium_svg(user,width=512, height=512): + aquarium = Aquarium.objects.select_related("background").get(user=user) + bg_svg= aquarium.background.svg_template + bg_inner = strip_outer_svg(bg_svg) + + fishes = ContributionFish.objects.filter( + user=user, + is_visible=True + ).select_related("species") + + fish_groups = [render_fish_group(cf) for cf in fishes] + + return f""" + + + + {bg_inner} + + + + {''.join(fish_groups)} + + + + """ diff --git a/apps/aquatics/renderer/utils.py b/apps/aquatics/renderer/utils.py new file mode 100644 index 0000000..06d6b5f --- /dev/null +++ b/apps/aquatics/renderer/utils.py @@ -0,0 +1,21 @@ + +def strip_outer_svg(svg_text: str) -> str: + """ + Removes the outer wrapper and returns only inner nodes. + Required because species/background templates are stored as full SVGs. + """ + if not svg_text: + return "" + + # Find first tag after + start = svg_text.find(">") + 1 + end = svg_text.rfind("") + return svg_text[start:end].strip() + +def safe_attr(value): + """ + Convert None or empty to safe attribute values. + """ + if value is None: + return "" + return str(value) \ No newline at end of file diff --git a/apps/aquatics/urls.py b/apps/aquatics/urls.py new file mode 100644 index 0000000..f3c201e --- /dev/null +++ b/apps/aquatics/urls.py @@ -0,0 +1,6 @@ +from django.urls import path +from .views import AquariumSVGView + +urlpatterns = [ + path("svg/", AquariumSVGView.as_view(), name="aquarium-svg"), +] diff --git a/apps/aquatics/views.py b/apps/aquatics/views.py index b8e4ee0..d9e23e2 100644 --- a/apps/aquatics/views.py +++ b/apps/aquatics/views.py @@ -1,2 +1,11 @@ +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated +from .renderer.tank import render_aquarium_svg -# Create your views here. +class AquariumSVGView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request): + svg = render_aquarium_svg(request.user) + return Response(svg, content_type="image/svg+xml") From 6093656c111c5e31f5868904f6b68ea04a9a7636 Mon Sep 17 00:00:00 2001 From: Imggaggu Date: Fri, 21 Nov 2025 22:09:29 +0900 Subject: [PATCH 2/7] [Feat] add random transformation --- apps/aquatics/renderer/sprite.py | 62 +++++++++++++++++++++++--------- apps/aquatics/renderer/tank.py | 41 ++++++++++++++++++--- apps/aquatics/renderer/utils.py | 29 +++++++++++++++ 3 files changed, 112 insertions(+), 20 deletions(-) diff --git a/apps/aquatics/renderer/sprite.py b/apps/aquatics/renderer/sprite.py index 94d8156..02e1f16 100644 --- a/apps/aquatics/renderer/sprite.py +++ b/apps/aquatics/renderer/sprite.py @@ -1,23 +1,53 @@ -from .utils import strip_outer_svg , safe_attr +import random +from .utils import strip_outer_svg -def render_fish_group(cf): - """ - ContributionFish 인스턴스를 받아서, - ... species svg ... 형태로 만들어 줌. - 애니메이션은 프론트에서~ - """ - species = cf.species +def render_fish_group(cf, width, height): + species = cf.fish_species raw_svg = species.svg_template inner = strip_outer_svg(raw_svg) - fish_id = cf.id + fish_id = cf.id + + # === 랜덤 요소 생성 === + start_x = random.uniform(width * 0.1, width * 0.9) + start_y = random.uniform(height * 0.2, height * 0.8) + amplitude_x = random.uniform(width * 0.25, width * 0.45) # x 왕복폭 + amplitude_y = random.uniform(height * 0.05, height * 0.12) # y 파동 + speed = random.uniform(0.6, 1.6) # 느린 물고기 / 빠른 물고기 + phase = random.uniform(0, 6.28) + duration = random.uniform(8, 18) # 전체 이동 주기 + + # === 이동 keyframes === + keyframes = f""" + @keyframes move-{fish_id} {{ + 0% {{ + transform: translate({start_x}px, {start_y}px); + }} + 25% {{ + transform: translate({start_x + amplitude_x}px, {start_y + amplitude_y}px); + }} + 50% {{ + transform: translate({start_x}px, {start_y - amplitude_y}px) scale(-1,1); + }} + 75% {{ + transform: translate({start_x - amplitude_x}px, {start_y + amplitude_y}px) scale(-1,1); + }} + 100% {{ + transform: translate({start_x}px, {start_y}px); + }} + }} + """ - # 위치는 프론트에서 transform 걸 거면 여기선 0,0 기준으로 두고, - # 그냥 id와 class만 설정해줘도 됨. return f""" - - {inner} + + + + {inner} """ diff --git a/apps/aquatics/renderer/tank.py b/apps/aquatics/renderer/tank.py index 4cb5f19..c9b1926 100644 --- a/apps/aquatics/renderer/tank.py +++ b/apps/aquatics/renderer/tank.py @@ -1,12 +1,13 @@ # apps/aquarium/renderer/tank.py from apps.items.models import FishSpecies -from apps.aquatics.models import Aquarium,ContributionFish +from apps.aquatics.models import Aquarium,ContributionFish , Fishtank from .sprite import render_fish_group -from .utils import strip_outer_svg +from .utils import strip_outer_svg , extract_svg_size def render_aquarium_svg(user,width=512, height=512): aquarium = Aquarium.objects.select_related("background").get(user=user) bg_svg= aquarium.background.svg_template + width, height = extract_svg_size(bg_svg) bg_inner = strip_outer_svg(bg_svg) fishes = ContributionFish.objects.filter( @@ -14,10 +15,10 @@ def render_aquarium_svg(user,width=512, height=512): is_visible=True ).select_related("species") - fish_groups = [render_fish_group(cf) for cf in fishes] + fish_groups = [render_fish_group(cf, width, height) for cf in fishes] return f""" - + {bg_inner} @@ -29,3 +30,35 @@ def render_aquarium_svg(user,width=512, height=512): """ + + +def render_fishtank_svg(repository): + fishtank = Fishtank.objects.get(repository=repository) + + + setting = fishtank.settings.first() + if setting and setting.background: + bg_svg = setting.background.background.svg_template + else: + bg_svg = '' + + + width, height = extract_svg_size(bg_svg) + bg_inner = strip_outer_svg(bg_svg) + + fishes = ContributionFish.objects.filter( + contributor__repository=repository, + is_visible=True + ).select_related("fish_species") + + fish_groups = [ + render_fish_group(cf, width, height) + for cf in fishes + ] + + return f""" + + {bg_inner} + {''.join(fish_groups)} + + """ \ No newline at end of file diff --git a/apps/aquatics/renderer/utils.py b/apps/aquatics/renderer/utils.py index 06d6b5f..04bdf42 100644 --- a/apps/aquatics/renderer/utils.py +++ b/apps/aquatics/renderer/utils.py @@ -1,3 +1,32 @@ +import re + +def extract_svg_size(svg_text: str): + """ + Extract width and height from tag. + Supports: + + + Returns (width, height) as numbers. + Defaults to (512, 512) if not found. + """ + if not svg_text: + return (512, 512) + + # width="512", height="512" + width_match = re.search(r'width="([\d\.]+)"', svg_text) + height_match = re.search(r'height="([\d\.]+)"', svg_text) + + if width_match and height_match: + return (float(width_match.group(1)), float(height_match.group(1))) + + # viewBox="0 0 800 600" + viewbox_match = re.search(r'viewBox="[^"]*?(\d+\.?\d*)\s+(\d+\.?\d*)"', svg_text) + if viewbox_match: + return (float(viewbox_match.group(1)), float(viewbox_match.group(2))) + + # fallback + return (512, 512) + def strip_outer_svg(svg_text: str) -> str: """ From 73e7195bcb9ed9b8fa84b675808ca849029e212b Mon Sep 17 00:00:00 2001 From: Imggaggu Date: Fri, 21 Nov 2025 22:43:51 +0900 Subject: [PATCH 3/7] [Fix] Update renderer to correctly handle aquarium and fishtank modes --- apps/aquatics/renderer/sprite.py | 64 ++++++++++++++++++++++++++++---- apps/aquatics/renderer/tank.py | 29 +++++++++------ apps/aquatics/renderer/utils.py | 6 ++- 3 files changed, 78 insertions(+), 21 deletions(-) diff --git a/apps/aquatics/renderer/sprite.py b/apps/aquatics/renderer/sprite.py index 02e1f16..570e91c 100644 --- a/apps/aquatics/renderer/sprite.py +++ b/apps/aquatics/renderer/sprite.py @@ -1,19 +1,25 @@ import random from .utils import strip_outer_svg -def render_fish_group(cf, width, height): +def render_fish_group(cf, width, height,mode): species = cf.fish_species raw_svg = species.svg_template inner = strip_outer_svg(raw_svg) fish_id = cf.id + # === 레이블 내용 === + if mode == "aquarium": + label_text = cf.contributor.repository.name + else: # fishtank + label_text = cf.contributor.user.username + # === 랜덤 요소 생성 === start_x = random.uniform(width * 0.1, width * 0.9) start_y = random.uniform(height * 0.2, height * 0.8) amplitude_x = random.uniform(width * 0.25, width * 0.45) # x 왕복폭 amplitude_y = random.uniform(height * 0.05, height * 0.12) # y 파동 - speed = random.uniform(0.6, 1.6) # 느린 물고기 / 빠른 물고기 - phase = random.uniform(0, 6.28) + #speed = random.uniform(0.6, 1.6) # 느린 물고기 / 빠른 물고기 + #phase = random.uniform(0, 6.28) duration = random.uniform(8, 18) # 전체 이동 주기 # === 이동 keyframes === @@ -37,17 +43,61 @@ def render_fish_group(cf, width, height): }} """ + # === flip reverse keyframes === + reverse_keyframes = f""" + @keyframes keep-label-upright-{fish_id} {{ + 0%,25% {{ + transform: scale(1,1); + }} + 50%,75% {{ + transform: scale(-1,1); /* 물고기가 뒤집힐 때 라벨도 같이 뒤집혀서 정방향 유지 */ + }} + 100% {{ + transform: scale(1,1); + }} + }} + """ + return f""" - + - {inner} + + + {inner} + + + + + + {label_text} + + + - """ + """ \ No newline at end of file diff --git a/apps/aquatics/renderer/tank.py b/apps/aquatics/renderer/tank.py index c9b1926..2ef85c2 100644 --- a/apps/aquatics/renderer/tank.py +++ b/apps/aquatics/renderer/tank.py @@ -1,21 +1,28 @@ -# apps/aquarium/renderer/tank.py from apps.items.models import FishSpecies from apps.aquatics.models import Aquarium,ContributionFish , Fishtank from .sprite import render_fish_group from .utils import strip_outer_svg , extract_svg_size def render_aquarium_svg(user,width=512, height=512): - aquarium = Aquarium.objects.select_related("background").get(user=user) - bg_svg= aquarium.background.svg_template + aquarium = Aquarium.objects.get(user=user) + + if aquarium.background: + bg_svg = aquarium.background.background.svg_template + else: + bg_svg = '' + width, height = extract_svg_size(bg_svg) bg_inner = strip_outer_svg(bg_svg) fishes = ContributionFish.objects.filter( - user=user, + aquarium=aquarium, is_visible=True - ).select_related("species") + ).select_related("fish_species", "contributor__repository") - fish_groups = [render_fish_group(cf, width, height) for cf in fishes] + fish_groups = [ + render_fish_group(cf, width, height, mode="aquarium") + for cf in fishes + ] return f""" @@ -23,11 +30,9 @@ def render_aquarium_svg(user,width=512, height=512): {bg_inner} - {''.join(fish_groups)} - """ @@ -36,11 +41,11 @@ def render_fishtank_svg(repository): fishtank = Fishtank.objects.get(repository=repository) - setting = fishtank.settings.first() + setting = fishtank.settings.select_related("background__background").first() if setting and setting.background: bg_svg = setting.background.background.svg_template else: - bg_svg = '' + bg_svg = '' width, height = extract_svg_size(bg_svg) @@ -49,10 +54,10 @@ def render_fishtank_svg(repository): fishes = ContributionFish.objects.filter( contributor__repository=repository, is_visible=True - ).select_related("fish_species") + ).select_related("fish_species", "contributor__user") fish_groups = [ - render_fish_group(cf, width, height) + render_fish_group(cf, width, height, mode="fishtank") for cf in fishes ] diff --git a/apps/aquatics/renderer/utils.py b/apps/aquatics/renderer/utils.py index 04bdf42..5ebeb4e 100644 --- a/apps/aquatics/renderer/utils.py +++ b/apps/aquatics/renderer/utils.py @@ -30,8 +30,8 @@ def extract_svg_size(svg_text: str): def strip_outer_svg(svg_text: str) -> str: """ - Removes the outer wrapper and returns only inner nodes. - Required because species/background templates are stored as full SVGs. + species/background 템플릿이 풀 SVG로 들어있을 때 + 가장 바깥 래퍼만 제거하고 안쪽 노드만 반환. """ if not svg_text: return "" @@ -39,6 +39,8 @@ def strip_outer_svg(svg_text: str) -> str: # Find first tag after start = svg_text.find(">") + 1 end = svg_text.rfind("") + if start <= 0 or end == -1: + return svg_text.strip() return svg_text[start:end].strip() def safe_attr(value): From 0a9ebe88960b8f066ed4f19a885aa0f676c28f86 Mon Sep 17 00:00:00 2001 From: Imggaggu Date: Sat, 22 Nov 2025 01:27:25 +0900 Subject: [PATCH 4/7] [Feat] add fishtank apis --- GithubAquarium/urls.py | 1 + apps/aquatics/serializers_fishtank.py | 55 +++++++++ apps/aquatics/urls.py | 16 +++ apps/aquatics/views.py | 2 - apps/aquatics/views_fishtank.py | 155 ++++++++++++++++++++++++++ apps/items/models.py | 1 - 6 files changed, 227 insertions(+), 3 deletions(-) create mode 100644 apps/aquatics/serializers_fishtank.py create mode 100644 apps/aquatics/urls.py delete mode 100644 apps/aquatics/views.py create mode 100644 apps/aquatics/views_fishtank.py diff --git a/GithubAquarium/urls.py b/GithubAquarium/urls.py index c07dbb7..e241114 100644 --- a/GithubAquarium/urls.py +++ b/GithubAquarium/urls.py @@ -58,4 +58,5 @@ # Include URL configurations from the 'repositories' and 'users' apps path('api/repositories/', include('apps.repositories.urls')), path('api/users/', include('apps.users.urls')), + path('api/aquatics/', include('apps.aquatics.urls')), ] \ No newline at end of file diff --git a/apps/aquatics/serializers_fishtank.py b/apps/aquatics/serializers_fishtank.py new file mode 100644 index 0000000..27f6540 --- /dev/null +++ b/apps/aquatics/serializers_fishtank.py @@ -0,0 +1,55 @@ +# apps/aquatics/serializers_fishtank.py +from rest_framework import serializers +from apps.aquatics.models import Fishtank, ContributionFish, FishtankSetting, OwnBackground +from apps.repositories.models import Contributor, Repository +from apps.items.models import Background + + +class FishSpeciesSerializer(serializers.Serializer): + id = serializers.IntegerField() + name = serializers.CharField() + maturity = serializers.IntegerField() + required_commits = serializers.IntegerField() + svg_template = serializers.CharField() + + +class ContributionFishSerializer(serializers.ModelSerializer): + species = serializers.SerializerMethodField() + + class Meta: + model = ContributionFish + fields = ["id", "is_visible", "species"] + + def get_species(self, obj): + s = obj.fish_species + return { + "id": s.id, + "name": s.name, + "maturity": s.maturity, + "required_commits": s.required_commits, + "svg_template": s.svg_template, + } + + +class ContributorSerializer(serializers.ModelSerializer): + user = serializers.CharField(source="user.username") + fish = ContributionFishSerializer(source="contribution_fish", read_only=True) + + class Meta: + model = Contributor + fields = ["id", "user", "commit_count", "fish"] + + +class FishtankDetailSerializer(serializers.ModelSerializer): + repository = serializers.CharField(source="repository.name") + contributors = ContributorSerializer(source="repository.contributors", many=True) + + class Meta: + model = Fishtank + fields = ["id", "repository", "svg_path", "contributors"] + + +class FishtankBackgroundSerializer(serializers.ModelSerializer): + class Meta: + model = Background + fields = ["id", "name", "code", "svg_template"] diff --git a/apps/aquatics/urls.py b/apps/aquatics/urls.py new file mode 100644 index 0000000..0309cf7 --- /dev/null +++ b/apps/aquatics/urls.py @@ -0,0 +1,16 @@ +from django.urls import path +from .views_fishtank import ( + FishtankDetailView, + FishtankSVGView, + FishtankBackgroundListView, + ApplyFishtankBackgroundView, + FishtankExportView, +) + +urlpatterns = [ + path("fishtank//", FishtankDetailView.as_view()), + path("fishtank//svg/", FishtankSVGView.as_view()), + path("fishtank/backgrounds/", FishtankBackgroundListView.as_view()), + path("fishtank//apply-background/", ApplyFishtankBackgroundView.as_view()), + path("fishtank//export/", FishtankExportView.as_view()), +] diff --git a/apps/aquatics/views.py b/apps/aquatics/views.py deleted file mode 100644 index b8e4ee0..0000000 --- a/apps/aquatics/views.py +++ /dev/null @@ -1,2 +0,0 @@ - -# Create your views here. diff --git a/apps/aquatics/views_fishtank.py b/apps/aquatics/views_fishtank.py new file mode 100644 index 0000000..a590400 --- /dev/null +++ b/apps/aquatics/views_fishtank.py @@ -0,0 +1,155 @@ + +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated +from rest_framework import status + +from drf_yasg.utils import swagger_auto_schema +from drf_yasg import openapi +#fishtank views.py + +from apps.repositories.models import Repository +from apps.aquatics.models import Fishtank, FishtankSetting, OwnBackground +from apps.aquatics.serializers_fishtank import ( + FishtankDetailSerializer, + FishtankBackgroundSerializer, +) +#from apps.aquatics.renderer.tank import render_aquarium_svg + +# ---------------------------------------------------------- +# 1) Fishtank 상세 조회 +# ---------------------------------------------------------- +class FishtankDetailView(APIView): + permission_classes = [IsAuthenticated] + + @swagger_auto_schema( + operation_summary="피쉬탱크 상세 조회", + operation_description="레포지토리 ID를 기반으로 피쉬탱크 내부 정보(기여자, 물고기)를 조회합니다.", + responses={200: FishtankDetailSerializer} + ) + def get(self, request, repo_id): + try: + repository = Repository.objects.get(id=repo_id) + fishtank = repository.fishtank + except Repository.DoesNotExist: + return Response({"detail": "Repository not found"}, status=404) + + serializer = FishtankDetailSerializer(fishtank) + return Response(serializer.data, status=200) + + +# ---------------------------------------------------------- +# 2) Fishtank SVG +# ---------------------------------------------------------- +class FishtankSVGView(APIView): + + permission_classes = [IsAuthenticated] + @swagger_auto_schema( + operation_summary="피쉬탱크 SVG 렌더링", + operation_description="유저 기반 SVG 렌더링을 반환합니다.", + responses={200: "SVG XML String"} + ) + + def get(self, request, repo_id): + try: + Repository.objects.get(id=repo_id) + except Repository.DoesNotExist: + return Response({"detail": "Repository not found"}, status=404) + + svg = render_aquarium_svg(request.user) + return Response(svg, content_type="image/svg+xml") + + +# ---------------------------------------------------------- +# 3) 유저가 소유한 fishtank 배경 목록 +# ---------------------------------------------------------- +class FishtankBackgroundListView(APIView): + permission_classes = [IsAuthenticated] + + @swagger_auto_schema( + operation_summary="피쉬탱크 배경 목록 조회", + operation_description="유저가 보유한 배경(OwnBackground)의 원본 Background 데이터를 반환합니다.", + responses={200: FishtankBackgroundSerializer(many=True)} + ) + def get(self, request): + owned = OwnBackground.objects.filter(user=request.user) + backgrounds = [ob.background for ob in owned] + serializer = FishtankBackgroundSerializer(backgrounds, many=True) + return Response(serializer.data, status=200) + + +# ---------------------------------------------------------- +# 4) 피쉬탱크 배경 적용 +# ---------------------------------------------------------- +class ApplyFishtankBackgroundView(APIView): + permission_classes = [IsAuthenticated] + + @swagger_auto_schema( + operation_summary="피쉬탱크 배경 적용", + operation_description="사용자가 소유한 OwnBackground 중 하나를 fishtank 배경으로 적용합니다.", + request_body=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + "background_id": openapi.Schema( + type=openapi.TYPE_INTEGER, + description="유저가 소유한 OwnBackground.background.id" + ), + }, + required=["background_id"] + ), + responses={ + 200: openapi.Response("Background applied"), + 400: "Bad Request", + 404: "Not Found", + }, + ) + + def post(self, request, repo_id): + bg_id = request.data.get("background_id") + + try: + repository = Repository.objects.get(id=repo_id) + fishtank = repository.fishtank + except Repository.DoesNotExist: + return Response({"detail": "Repository not found"}, status=404) + + try: + owned_bg = OwnBackground.objects.get( + user=request.user, background_id=bg_id + ) + except OwnBackground.DoesNotExist: + return Response({"detail": "Background not owned"}, status=400) + + setting, _ = FishtankSetting.objects.update_or_create( + fishtank=fishtank, + contributor=request.user, + defaults={"background": owned_bg}, + ) + + return Response({"detail": "Background applied"}, status=200) + + +# ---------------------------------------------------------- +# 5) Fishtank Export (scale, offset 저장) +# ---------------------------------------------------------- +class FishtankExportView(APIView): + permission_classes = [IsAuthenticated] + + @swagger_auto_schema( + operation_summary="피쉬탱크 Export (scale/offset 저장)", + request_body=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + "scale": openapi.Schema(type=openapi.TYPE_NUMBER), + "offset_x": openapi.Schema(type=openapi.TYPE_NUMBER), + "offset_y": openapi.Schema(type=openapi.TYPE_NUMBER), + }, + required=["scale"] + ), + responses={200: "Saved"} + ) + + def post(self, request, repo_id): + + # 저장 필드가 모델에 아직 없기 때문에, 저장 로직은 후에 추가 가능 + return Response({"detail": "Saved"}, status=200) \ No newline at end of file diff --git a/apps/items/models.py b/apps/items/models.py index 9ea95c9..d705c0f 100644 --- a/apps/items/models.py +++ b/apps/items/models.py @@ -1,4 +1,3 @@ -# apps/items/models.py from django.db import models class FishSpecies(models.Model): From b0daa258421ceaabae57511115e989da82034efd Mon Sep 17 00:00:00 2001 From: Imggaggu Date: Sat, 22 Nov 2025 01:43:38 +0900 Subject: [PATCH 5/7] [Feat] add aquarium apis --- apps/aquatics/serializers_aquarium.py | 72 ++++++++++ apps/aquatics/tests.py | 2 - apps/aquatics/urls.py | 9 ++ apps/aquatics/views_aquarium.py | 194 ++++++++++++++++++++++++++ apps/aquatics/views_fishtank.py | 1 - 5 files changed, 275 insertions(+), 3 deletions(-) create mode 100644 apps/aquatics/serializers_aquarium.py delete mode 100644 apps/aquatics/tests.py create mode 100644 apps/aquatics/views_aquarium.py diff --git a/apps/aquatics/serializers_aquarium.py b/apps/aquatics/serializers_aquarium.py new file mode 100644 index 0000000..776c793 --- /dev/null +++ b/apps/aquatics/serializers_aquarium.py @@ -0,0 +1,72 @@ +from rest_framework import serializers +from apps.aquatics.models import Aquarium, ContributionFish, OwnBackground +from apps.repositories.models import Contributor +from apps.repositories.models import Repository + + +class AquariumFishSerializer(serializers.ModelSerializer): + species = serializers.SerializerMethodField() + repository = serializers.SerializerMethodField() + my_commit_count = serializers.SerializerMethodField() + unlocked_at = serializers.SerializerMethodField() + + class Meta: + model = ContributionFish + fields = [ + "id", + "species", + "repository", + "my_commit_count", + "unlocked_at", + ] + + def get_species(self, obj): + s = obj.fish_species + return { + "id": s.id, + "name": s.name, + "maturity": s.maturity, + "required_commits": s.required_commits, + "svg_template": s.svg_template, + } + + def get_repository(self, obj): + repo = obj.contributor.repository + return { + "id": repo.id, + "name": repo.name, + } + + def get_my_commit_count(self, obj): + return obj.contributor.commit_count + + def get_unlocked_at(self, obj): + # ContributionFish는 unlocked_at을 직접 갖지 않으므로 UnlockedFish에서 조회 + unlocked = obj.contributor.user.owned_fishes.filter( + fish_species=obj.fish_species + ).first() + return unlocked.unlocked_at if unlocked else None + + +class AquariumDetailSerializer(serializers.ModelSerializer): + background = serializers.SerializerMethodField() + fishes = AquariumFishSerializer(many=True) + + class Meta: + model = Aquarium + fields = ["background", "svg_path", "fishes"] + + def get_background(self, obj): + if obj.background: + return { + "id": obj.background.id, + "name": obj.background.background.name, + "svg_template": obj.background.background.svg_template, + } + return None + + +class AquariumBackgroundSerializer(serializers.ModelSerializer): + class Meta: + model = OwnBackground + fields = ["id", "background", "unlocked_at"] diff --git a/apps/aquatics/tests.py b/apps/aquatics/tests.py deleted file mode 100644 index 4929020..0000000 --- a/apps/aquatics/tests.py +++ /dev/null @@ -1,2 +0,0 @@ - -# Create your tests here. diff --git a/apps/aquatics/urls.py b/apps/aquatics/urls.py index 33dfde3..f79760a 100644 --- a/apps/aquatics/urls.py +++ b/apps/aquatics/urls.py @@ -7,6 +7,7 @@ ApplyFishtankBackgroundView, FishtankExportView, ) +from .views_aquarium import * urlpatterns = [ path("fishtank//", FishtankDetailView.as_view()), @@ -15,4 +16,12 @@ path("fishtank//apply-background/", ApplyFishtankBackgroundView.as_view()), path("fishtank//export/", FishtankExportView.as_view()), path("svg/", AquariumSVGView.as_view(), name="aquarium-svg"), + path("aquarium/", AquariumDetailView.as_view()), + path("aquarium/my-fishes/", MyUnlockedFishListView.as_view()), + path("aquarium/add-fish/", AquariumAddFishView.as_view()), + path("aquarium/remove-fish//", AquariumRemoveFishView.as_view()), + path("aquarium/backgrounds/", AquariumBackgroundListView.as_view()), + path("aquarium/apply-background/", AquariumApplyBackgroundView.as_view()), + path("aquarium/export/", AquariumExportView.as_view()), + path("aquarium/svg/", AquariumSVGView.as_view()), ] diff --git a/apps/aquatics/views_aquarium.py b/apps/aquatics/views_aquarium.py new file mode 100644 index 0000000..7f25d85 --- /dev/null +++ b/apps/aquatics/views_aquarium.py @@ -0,0 +1,194 @@ +from rest_framework.views import APIView +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework import status + +from drf_yasg.utils import swagger_auto_schema +from drf_yasg import openapi + +from apps.aquatics.models import Aquarium, ContributionFish, OwnBackground +from apps.aquatics.serializers_aquarium import ( + AquariumDetailSerializer, + AquariumFishSerializer, + AquariumBackgroundSerializer, +) +from apps.aquatics.renderer.tank import render_aquarium_svg + + +# ---------------------------------------------------------- +# 1) 아쿠아리움 전체 조회 +# ---------------------------------------------------------- +class AquariumDetailView(APIView): + permission_classes = [IsAuthenticated] + + @swagger_auto_schema( + operation_summary="아쿠아리움 상세 조회", + responses={200: AquariumDetailSerializer()} + ) + def get(self, request): + aquarium = request.user.aquarium + fishes = ContributionFish.objects.filter(aquarium=aquarium) + + serializer = AquariumDetailSerializer( + aquarium, context={"fishes": fishes} + ) + serializer._data["fishes"] = AquariumFishSerializer(fishes, many=True).data + return Response(serializer.data, status=200) + + +# ---------------------------------------------------------- +# 2) 내가 가진 물고기 전체 조회 (언락된 모든 물고기) +# ---------------------------------------------------------- +class MyUnlockedFishListView(APIView): + permission_classes = [IsAuthenticated] + + @swagger_auto_schema( + operation_summary="유저가 언락한 모든 물고기 조회", + responses={200: AquariumFishSerializer(many=True)} + ) + def get(self, request): + fishes = ContributionFish.objects.filter( + contributor__user=request.user + ) + data = AquariumFishSerializer(fishes, many=True).data + return Response(data, status=200) + + +# ---------------------------------------------------------- +# 3) 아쿠아리움에 물고기 추가 +# ---------------------------------------------------------- +class AquariumAddFishView(APIView): + permission_classes = [IsAuthenticated] + + @swagger_auto_schema( + operation_summary="아쿠아리움에 물고기 추가", + request_body=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + "contribution_fish_id": openapi.Schema( + type=openapi.TYPE_INTEGER, + description="ContributionFish ID" + ) + }, + required=["contribution_fish_id"] + ), + responses={200: "Added"} + ) + def post(self, request): + cf_id = request.data.get("contribution_fish_id") + aquarium = request.user.aquarium + + try: + cf = ContributionFish.objects.get(id=cf_id) + except ContributionFish.DoesNotExist: + return Response({"detail": "Fish not found"}, status=404) + + cf.aquarium = aquarium + cf.save() + return Response({"detail": "Added to aquarium"}, status=200) + + +# ---------------------------------------------------------- +# 4) 아쿠아리움 물고기 제거 +# ---------------------------------------------------------- +class AquariumRemoveFishView(APIView): + permission_classes = [IsAuthenticated] + + @swagger_auto_schema( + operation_summary="아쿠아리움 물고기 제거", + responses={200: "Removed"} + ) + def delete(self, request, fish_id): + try: + cf = ContributionFish.objects.get(id=fish_id, aquarium=request.user.aquarium) + except ContributionFish.DoesNotExist: + return Response({"detail": "Not found"}, status=404) + + cf.aquarium = None + cf.save() + return Response({"detail": "Removed"}, status=200) + + +# ---------------------------------------------------------- +# 5) 아쿠아리움 배경 목록 조회 +# ---------------------------------------------------------- +class AquariumBackgroundListView(APIView): + permission_classes = [IsAuthenticated] + + @swagger_auto_schema( + operation_summary="아쿠아리움 배경 목록 조회", + responses={200: AquariumBackgroundSerializer(many=True)} + ) + def get(self, request): + owned = OwnBackground.objects.filter(user=request.user) + data = AquariumBackgroundSerializer(owned, many=True).data + return Response(data, status=200) + + +# ---------------------------------------------------------- +# 6) 아쿠아리움 배경 적용 +# ---------------------------------------------------------- +class AquariumApplyBackgroundView(APIView): + permission_classes = [IsAuthenticated] + + @swagger_auto_schema( + operation_summary="아쿠아리움 배경 적용", + request_body=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={"own_background_id": openapi.Schema(type=openapi.TYPE_INTEGER)}, + required=["own_background_id"] + ), + responses={200: "Applied"} + ) + def post(self, request): + bg_id = request.data.get("own_background_id") + + try: + bg = OwnBackground.objects.get(id=bg_id, user=request.user) + except OwnBackground.DoesNotExist: + return Response({"detail": "Background not owned"}, status=400) + + aquarium = request.user.aquarium + aquarium.background = bg + aquarium.save() + + return Response({"detail": "Background applied"}, status=200) + + +# ---------------------------------------------------------- +# 7) Export 저장 (scale/offset) +# ---------------------------------------------------------- +class AquariumExportView(APIView): + permission_classes = [IsAuthenticated] + + @swagger_auto_schema( + operation_summary="아쿠아리움 Export 저장", + request_body=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + "scale": openapi.Schema(type=openapi.TYPE_NUMBER), + "offset_x": openapi.Schema(type=openapi.TYPE_NUMBER), + "offset_y": openapi.Schema(type=openapi.TYPE_NUMBER), + }, + required=["scale"] + ), + responses={200: "Saved"} + ) + def post(self, request): + # 모델 확장 시 여기에 저장 로직 추가 + return Response({"detail": "Saved"}, status=200) + + +# ---------------------------------------------------------- +# 8) SVG 렌더링 +# ---------------------------------------------------------- +class AquariumSVGView(APIView): + permission_classes = [IsAuthenticated] + + @swagger_auto_schema( + operation_summary="아쿠아리움 SVG 렌더링", + responses={200: "SVG XML"} + ) + def get(self, request): + svg = render_aquarium_svg(request.user) + return Response(svg, content_type="image/svg+xml") diff --git a/apps/aquatics/views_fishtank.py b/apps/aquatics/views_fishtank.py index bdda375..394a97f 100644 --- a/apps/aquatics/views_fishtank.py +++ b/apps/aquatics/views_fishtank.py @@ -1,4 +1,3 @@ - from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated From be2decf5d078f552f12f3b1d635bedb9710cacb9 Mon Sep 17 00:00:00 2001 From: Imggaggu Date: Sat, 22 Nov 2025 02:46:32 +0900 Subject: [PATCH 6/7] [Feat] add repository list api --- apps/repositories/urls.py | 6 +++--- apps/repositories/views.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 apps/repositories/views.py diff --git a/apps/repositories/urls.py b/apps/repositories/urls.py index 2298fd4..afd2945 100644 --- a/apps/repositories/urls.py +++ b/apps/repositories/urls.py @@ -1,7 +1,7 @@ -# apps/repositories/urls.py - +from django.urls import path +from .views import RepositoryListView app_name = 'repositories' urlpatterns = [ - # Add repository-specific URLs here in the future + path("", RepositoryListView.as_view(), name="repository-list"), ] diff --git a/apps/repositories/views.py b/apps/repositories/views.py new file mode 100644 index 0000000..2319c35 --- /dev/null +++ b/apps/repositories/views.py @@ -0,0 +1,33 @@ +from rest_framework.views import APIView +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +from drf_yasg.utils import swagger_auto_schema +from drf_yasg import openapi + +from apps.repositories.models import Repository, Contributor +from apps.repositories.serializers import RepositorySerializer + + +class RepositoryListView(APIView): + permission_classes = [IsAuthenticated] + + @swagger_auto_schema( + operation_summary="내가 커밋한 모든 레포지토리 리스트", + operation_description=( + "Contributor 테이블에서 commit_count > 0 인 repo만 반환합니다.\n" + "즉, 내가 owner인지 상관없이 단 1커밋이라도 있는 모든 repo를 조회합니다." + ), + responses={200: RepositorySerializer(many=True)}, + ) + def get(self, request): + user = request.user + + # user가 contributor 이고 commit_count > 0 인 repo만 조회 + contributed_repos = Repository.objects.filter( + contributors__user=user, + contributors__commit_count__gt=0 + ).distinct().order_by("-updated_at") + + serializer = RepositorySerializer(contributed_repos, many=True) + return Response(serializer.data, status=200) From 051dd2d69373911d729bd94743866ddf8f6e2d7d Mon Sep 17 00:00:00 2001 From: Imggaggu Date: Sat, 22 Nov 2025 03:41:07 +0900 Subject: [PATCH 7/7] [Fix] settings --- GithubAquarium/views.py | 11 ++- apps/users/adapter.py | 144 +++++++++++++++++++++++----------------- pyproject.toml | 4 +- 3 files changed, 90 insertions(+), 69 deletions(-) diff --git a/GithubAquarium/views.py b/GithubAquarium/views.py index 2e27592..0293bd3 100644 --- a/GithubAquarium/views.py +++ b/GithubAquarium/views.py @@ -1,12 +1,8 @@ -# GithubAquarium/views.py -""" -Core views for the GithubAquarium project. -""" - from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter from allauth.socialaccount.providers.oauth2.client import OAuth2Client from dj_rest_auth.registration.views import SocialLoginView from django.conf import settings +from rest_framework.permissions import AllowAny class GitHubLogin(SocialLoginView): """ @@ -18,4 +14,7 @@ class GitHubLogin(SocialLoginView): """ adapter_class = GitHubOAuth2Adapter callback_url = settings.GITHUB_CALLBACK_URL - client_class = OAuth2Client \ No newline at end of file + client_class = OAuth2Client + # [추가] 이 뷰는 로그인 전이므로 누구나 접근 가능해야 함 + permission_classes = (AllowAny,) + authentication_classes = [] diff --git a/apps/users/adapter.py b/apps/users/adapter.py index be92b14..8d9c7c2 100644 --- a/apps/users/adapter.py +++ b/apps/users/adapter.py @@ -1,11 +1,13 @@ -# apps/users/adapter.py """ Custom adapter for django-allauth to handle post-login logic. """ import logging -from allauth.socialaccount.adapter import DefaultSocialAccountAdapter +from django.conf import settings from django.contrib.auth import get_user_model +from django.contrib.sites.shortcuts import get_current_site from django.db import transaction +from allauth.socialaccount.adapter import DefaultSocialAccountAdapter +from allauth.socialaccount.models import SocialApp from github import Github, GithubException from apps.repositories.models import Repository, Contributor, Commit @@ -17,21 +19,82 @@ class CustomSocialAccountAdapter(DefaultSocialAccountAdapter): """ Overrides the default social account adapter to sync GitHub data upon user login. - This version focuses on syncing data ONLY related to users who are already - registered in this application, ignoring external users. """ - + + def get_app(self, request, provider): + """ + [수정됨] 부모 클래스의 get_app을 호출하지 않고 직접 DB를 조회합니다. + allauth가 settings.py 기반으로 만드는 '저장되지 않은 인스턴스'를 피하기 위함입니다. + """ + # 1. DB에서 해당 provider의 앱이 있는지 먼저 확인 + apps = SocialApp.objects.filter(provider=provider) + + if apps.exists(): + # 앱이 여러 개라면 현재 사이트와 연결된 것을 우선적으로 찾음 + current_site = get_current_site(request) + app = apps.filter(sites=current_site).first() + if app: + return app + # 연결된 게 없으면 그냥 첫 번째 앱 반환 + return apps.first() + + # 2. DB에 없으면 settings.py 설정값을 읽어와서 'DB에 새로 생성' + logger.info(f"SocialApp for '{provider}' not found in DB. Creating from settings.") + + providers_setting = getattr(settings, 'SOCIALACCOUNT_PROVIDERS', {}) + provider_config = providers_setting.get(provider, {}) + app_config = provider_config.get('APP', {}) + + client_id = app_config.get('client_id') + secret = app_config.get('secret') + + if client_id and secret: + # update_or_create 대신 get_or_create 사용 (중복 방지) + app, created = SocialApp.objects.get_or_create( + provider=provider, + defaults={ + 'name': f"{provider}-auto-config", + 'client_id': client_id, + 'secret': secret, + 'key': app_config.get('key', ''), + } + ) + # 생성된 앱은 반드시 저장된 상태이므로 ID가 존재함 + + # 현재 사이트와 연결 + current_site = get_current_site(request) + app.sites.add(current_site) + return app + + # 설정조차 없으면 에러 발생 (이 경우는 거의 없음) + raise SocialApp.DoesNotExist(f"No SocialApp found for provider '{provider}'") + + def pre_social_login(self, request, sociallogin): + """ + 로그인 저장 직전, 토큰에 DB에 저장된 앱 객체를 연결합니다. + """ + # 1. DB에 저장된 확실한 앱 객체를 가져옴 + app = self.get_app(request, sociallogin.account.provider) + + # 2. 토큰에 앱 연결 (ID가 있는 객체이므로 안전함) + sociallogin.token.app = app + + # 3. 사이트 연결 재확인 (ManyToMany 관계 안전하게 사용 가능) + current_site = get_current_site(request) + if not app.sites.filter(id=current_site.id).exists(): + app.sites.add(current_site) + + super().pre_social_login(request, sociallogin) + @transaction.atomic def save_user(self, request, sociallogin, form=None): """ - This method is called when a user successfully logs in via a social account. - It wraps the entire GitHub data synchronization in a single transaction. + 사용자 저장 및 GitHub 데이터 동기화 """ user = super().save_user(request, sociallogin, form) try: - github_account = sociallogin.account - access_token = github_account.extra_data.get('access_token') + access_token = sociallogin.token.token if not access_token: logger.warning("GitHub access token not found for user %s.", user.username) @@ -40,28 +103,23 @@ def save_user(self, request, sociallogin, form=None): g = Github(access_token) github_user = g.get_user() - # 1. Update the logged-in user's model with their full GitHub data + # 1. 사용자 정보 업데이트 user.github_id = github_user.id user.github_username = github_user.login user.avatar_url = github_user.avatar_url user.save() - # 2. Sync all repositories the user has contributed to + # 2. 리포지토리 동기화 self.sync_all_user_data(github_user) except Exception as e: logger.error("Critical error during GitHub data synchronization for user %s: %s", user.username, e, exc_info=True) - raise + # raise # 필요 시 주석 해제 return user def sync_all_user_data(self, github_user): - """ - Orchestrates the synchronization of all GitHub data for a given user. - """ logger.info("Starting repository sync for user: %s", github_user.login) - - # Fetch all repositories the user is associated with (contribution-centric) repos = github_user.get_repos(affiliation='owner,collaborator,organization_member', sort='pushed', direction='desc') for repo in repos: @@ -72,17 +130,10 @@ def sync_all_user_data(self, github_user): self.sync_commits(repository_model, repo) except Exception as e: logger.error("Failed to sync repository %s for user %s: %s", repo.full_name, github_user.login, e, exc_info=True) - logger.info("Finished repository sync for user: %s", github_user.login) def sync_repository(self, repo_obj) -> Repository: - """ - Updates or creates a Repository record. - The repository's owner is linked ONLY IF they are a registered user. - """ - # Find the owner in our DB. If not found, owner_user will be None. owner_user = User.objects.filter(github_id=repo_obj.owner.id).first() - repository, created = Repository.objects.update_or_create( github_id=repo_obj.id, defaults={ @@ -94,35 +145,20 @@ def sync_repository(self, repo_obj) -> Repository: 'language': repo_obj.language, 'created_at': repo_obj.created_at, 'updated_at': repo_obj.updated_at, - 'owner': owner_user, # Link to owner if they exist, otherwise NULL. + 'owner': owner_user, } ) - log_msg = "Created" if created else "Updated" - logger.info(" %s repository: %s", log_msg, repo_obj.full_name) return repository def sync_contributors(self, repository_model: Repository, repo_obj): - """ - Updates or creates Contributor records ONLY for registered users. - This is optimized to avoid N+1 queries. - """ - logger.info(" Syncing contributors for %s", repo_obj.full_name) try: contributors_from_api = list(repo_obj.get_contributors()) if not contributors_from_api: return - - # --- N+1 Query Optimization --- - # 1. Get all contributor GitHub IDs from the API response. contributor_github_ids = [c.id for c in contributors_from_api if hasattr(c, 'id')] - - # 2. Fetch all users from our database that match these IDs in a single query. existing_users = User.objects.filter(github_id__in=contributor_github_ids) - - # 3. Create a map for quick lookups (github_id -> User object). user_map = {user.github_id: user for user in existing_users} - # 4. Process contributors who are registered in our service. for contributor in contributors_from_api: if contributor.id in user_map: user_obj = user_map[contributor.id] @@ -131,58 +167,44 @@ def sync_contributors(self, repository_model: Repository, repo_obj): user=user_obj, defaults={'commit_count': contributor.contributions} ) - logger.info(" Successfully synced %d registered contributors for %s", len(user_map), repo_obj.full_name) except GithubException as e: - logger.warning(" Could not get contributors for %s. Error: %s", repo_obj.full_name, e) + logger.warning("Could not get contributors for %s. Error: %s", repo_obj.full_name, e) except Exception as e: - logger.error(" An unexpected error occurred while syncing contributors for %s: %s", repo_obj.full_name, e, exc_info=True) + logger.error("Unexpected error syncing contributors for %s: %s", repo_obj.full_name, e) def sync_commits(self, repository_model: Repository, repo_obj): - logger.info(" Syncing commits for %s", repo_obj.full_name) try: commits_from_api = repo_obj.get_commits() - if commits_from_api.totalCount == 0: repository_model.commit_count = 0 repository_model.save(update_fields=['commit_count']) - logger.info(" No commits found for %s. Sync finished.", repo_obj.full_name) return - repository_model.commit_count = commits_from_api.totalCount repository_model.save(update_fields=['commit_count']) - # --- N+1 Query Optimization --- - # 1. Collect all unique author GitHub IDs from the API response. author_github_ids = {c.author.id for c in commits_from_api if c.author} if not author_github_ids: - return # No authors to process - - # 2. Fetch all users from our database that match these IDs. + return existing_users = User.objects.filter(github_id__in=author_github_ids) - - # 3. Create a map for quick lookups. user_map = {user.github_id: user for user in existing_users} - # 4. Process all commits. for commit in commits_from_api: - # Determine the author object if they are a registered user. commit_author_user = None if commit.author and commit.author.id in user_map: commit_author_user = user_map[commit.author.id] - Commit.objects.update_or_create( sha=commit.sha, defaults={ 'repository': repository_model, - 'author': commit_author_user, # Link to author if they exist, otherwise NULL. + 'author': commit_author_user, 'message': commit.commit.message, 'committed_at': commit.commit.author.date, 'author_name': commit.commit.author.name, 'author_email': commit.commit.author.email, } ) - logger.info(" Successfully synced commits for %s", repo_obj.full_name) except GithubException as e: - logger.warning(" Could not get commits for %s. Error: %s", repo_obj.full_name, e) + logger.warning("Could not get commits for %s. Error: %s", repo_obj.full_name, e) except Exception as e: - logger.error(" An unexpected error occurred while syncing commits for %s: %s", repo_obj.full_name, e, exc_info=True) \ No newline at end of file + logger.error("Unexpected error syncing commits for %s: %s", repo_obj.full_name, e) + diff --git a/pyproject.toml b/pyproject.toml index 5e6795f..9a334fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,8 +5,8 @@ description = "Add your description here" readme = "README.md" requires-python = ">=3.12" dependencies = [ - "dj-rest-auth[with-social]>=7.0.1", - "django-allauth[socialaccount]>=65.11.2", + "dj-rest-auth[with-social]>=6.0.0", + "django-allauth[socialaccount]<0.61.0", "django-cors-headers>=4.8.0", "django-environ>=0.12.0", "djangorestframework>=3.16.1",