diff --git a/client/next.config.mjs b/client/next.config.mjs
index 8b6bf03..c8a0f71 100644
--- a/client/next.config.mjs
+++ b/client/next.config.mjs
@@ -1,12 +1,6 @@
-// import os from "node:os";
-// import isInsideContainer from "is-inside-container";
-
-// const isWindowsDevContainer = () =>
-// os.release().toLowerCase().includes("microsoft") && isInsideContainer();
-
/** @type {import('next').NextConfig} */
-const config = {
+const nextConfig = {
reactStrictMode: true,
turbopack: {
root: import.meta.dirname,
@@ -14,14 +8,15 @@ const config = {
outputFileTracingRoot: import.meta.dirname,
images: {
domains: ["localhost"],
+ remotePatterns: [
+ {
+ protocol: 'http',
+ hostname: 'localhost',
+ port: '8000',
+ pathname: '/media/**',
+ },
+ ],
},
- // Turns on file change polling for the Windows Dev Container
- // Doesn't work currently for turbopack, so file changes will not automatically update the client.
- // watchOptions: isWindowsDevContainer()
- // ? {
- // pollIntervalMs: 1000
- // }
- // : undefined,
};
-export default config;
+export default nextConfig;
diff --git a/client/public/placeholder1293x405.svg b/client/public/placeholder1293x405.svg
new file mode 100644
index 0000000..34f928c
--- /dev/null
+++ b/client/public/placeholder1293x405.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/src/components/ui/go-back-button.tsx b/client/src/components/ui/go-back-button.tsx
new file mode 100644
index 0000000..5f1ebce
--- /dev/null
+++ b/client/src/components/ui/go-back-button.tsx
@@ -0,0 +1,38 @@
+import Link from "next/link";
+
+interface GoBackButtonProps {
+ url: string;
+ label: string;
+}
+const GoBackButton = ({ url, label }: GoBackButtonProps) => {
+ return (
+
+
+
+ );
+};
+
+export default GoBackButton;
diff --git a/client/src/components/ui/image-card.tsx b/client/src/components/ui/image-card.tsx
new file mode 100644
index 0000000..f3ca51b
--- /dev/null
+++ b/client/src/components/ui/image-card.tsx
@@ -0,0 +1,30 @@
+import Image from "next/image";
+import React from "react";
+
+interface ImageCard {
+ imageSrc?: string;
+ imageAlt?: string;
+ children?: React.ReactNode;
+}
+
+const ImageCard = ({ imageSrc, imageAlt = "Image", children }: ImageCard) => {
+ return (
+
+
+ {imageSrc ? (
+
+ ) : (
+ children || No Image
+ )}
+
+
+ );
+};
+
+export default ImageCard;
diff --git a/client/src/components/ui/image-placeholder.tsx b/client/src/components/ui/image-placeholder.tsx
new file mode 100644
index 0000000..b7e25e5
--- /dev/null
+++ b/client/src/components/ui/image-placeholder.tsx
@@ -0,0 +1,26 @@
+import React from "react";
+
+const ImagePlaceholder = () => {
+ return (
+
+ );
+};
+export default ImagePlaceholder;
diff --git a/client/src/components/ui/modal/error-modal.tsx b/client/src/components/ui/modal/error-modal.tsx
new file mode 100644
index 0000000..ba1c240
--- /dev/null
+++ b/client/src/components/ui/modal/error-modal.tsx
@@ -0,0 +1,45 @@
+import React, { useState } from "react";
+
+interface ErrorModalProps {
+ message: string | null;
+ onClose: () => void;
+}
+
+const ErrorModal = ({ message, onClose = () => {} }: ErrorModalProps) => {
+ const [isVisible, setIsVisible] = useState(true);
+ if (!isVisible || !message) {
+ return null;
+ }
+
+ function onModalClose() {
+ setIsVisible(false);
+ onClose();
+ }
+
+ return (
+ // Backdrop overlay
+
+ {/* Modal content container */}
+
e.stopPropagation()} // Prevent closing when clicking inside the modal
+ >
+
Error
+
{message}
+
+
+
+
+
+ );
+};
+
+export default ErrorModal;
diff --git a/client/src/hooks/use-artwork-data.ts b/client/src/hooks/use-artwork-data.ts
new file mode 100644
index 0000000..da08c9e
--- /dev/null
+++ b/client/src/hooks/use-artwork-data.ts
@@ -0,0 +1,49 @@
+import { Art } from "@/types/art";
+
+export const generateMockArtworks = (count: number): Art[] => {
+ const artworks: Art[] = [];
+ for (let i = 1; i <= count; i++) {
+ artworks.push({
+ id: i,
+ name: `Artwork ${i}`,
+ description: "Mock artwork description",
+ //source_game: "Mock Game",
+ media: "",
+ active: true,
+ contributors: [],
+ //created_at: new Date().toISOString(),
+ });
+ }
+ return artworks;
+};
+
+export const generateMockArtwork = (id: string): Art => {
+ return {
+ id: Number(id),
+ name: "Mock Artwork Title",
+ description:
+ "Lorem ipsum dolor sit amet. Non numquam dicta nam autem dicta 33 error molestias et repellat consequatur eum iste expedita est dolorem libero et quas provident!",
+ //source_game: "Mock Game",
+ media: "",
+ active: true,
+ //created_at: new Date().toISOString(),
+ contributors: [
+ {
+ id: 1,
+ art_id: Number(id),
+ member_name: "Contributor 1",
+ role: "user1",
+ discord_url: "https://discord.com",
+ instagram_url: "",
+ },
+ {
+ id: 2,
+ art_id: Number(id),
+ member_name: "Contributor 2",
+ role: "user2",
+ discord_url: "",
+ instagram_url: "https://instagram.com",
+ },
+ ],
+ };
+};
diff --git a/client/src/pages/artwork/[id].tsx b/client/src/pages/artwork/[id].tsx
new file mode 100644
index 0000000..3b2261e
--- /dev/null
+++ b/client/src/pages/artwork/[id].tsx
@@ -0,0 +1,201 @@
+import { Instagram, MessageSquare } from "lucide-react";
+import { GetServerSideProps } from "next";
+import Image from "next/image";
+import { useRouter } from "next/navigation";
+
+import GoBackButton from "@/components/ui/go-back-button";
+import ImagePlaceholder from "@/components/ui/image-placeholder";
+import ErrorModal from "@/components/ui/modal/error-modal";
+import api from "@/lib/api";
+import { Art } from "@/types/art";
+
+interface ArtworkPageProps {
+ artwork?: Art;
+ error?: string;
+}
+
+function displayContributors(artwork: Art) {
+ return (
+
+
+
+
+ {artwork.contributors?.map((contributor) => (
+
+
+ {contributor.member_name}
+
+
+ {contributor.discord_url && (
+
+
+
+ )}
+ {contributor.instagram_url && (
+
+
+
+ )}
+
+
+ ))}
+
+
+
+ );
+}
+
+export default function ArtworkPage({ artwork, error }: ArtworkPageProps) {
+ const router = useRouter();
+ if (error) {
+ return router.back()} />;
+ }
+ return (
+
+
+
+
+ {artwork!.media ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {artwork!.name}
+
+
+
+
+ {artwork!.description}
+
+
+
+ {displayContributors(artwork!)}
+
+
+
+
+
+ {artwork!.name}
+
+
+
+
+ {artwork!.description}
+
+
+
+ {displayContributors(artwork!)}
+
+
+
+
+ TODO add footer
+
+
+ );
+}
+
+export const getServerSideProps: GetServerSideProps = async (
+ context,
+) => {
+ const { id } = context.params as { id: string };
+ try {
+ const artResponse = await api.get(`arts/${id}`);
+ const artwork = artResponse.data;
+ return { props: { artwork } };
+ } catch (err: unknown) {
+ return {
+ props: { error: (err as Error).message || "Failed to load artwork." },
+ };
+ }
+};
diff --git a/client/src/pages/artwork/index.tsx b/client/src/pages/artwork/index.tsx
new file mode 100644
index 0000000..4b765b7
--- /dev/null
+++ b/client/src/pages/artwork/index.tsx
@@ -0,0 +1,132 @@
+import { GetServerSideProps } from "next";
+import Link from "next/link";
+import { useRouter } from "next/navigation";
+
+import { Button } from "@/components/ui/button";
+import ImageCard from "@/components/ui/image-card";
+import ErrorModal from "@/components/ui/modal/error-modal";
+import { generateMockArtworks } from "@/hooks/use-artwork-data";
+import api from "@/lib/api";
+import { Art } from "@/types/art";
+import { PageResult } from "@/types/page-response";
+
+interface ArtworksPageProps {
+ artworks?: PageResult;
+ error?: string;
+}
+
+const PLACEHOLDER_ICON = (
+
+);
+
+function renderArtworkCard(artwork: Art) {
+ return (
+
+
+ {!artwork.media && PLACEHOLDER_ICON}
+
+
+ );
+}
+
+export default function ArtworksPage({ artworks, error }: ArtworksPageProps) {
+ const router = useRouter();
+ if (error) {
+ return router.back()} />;
+ }
+ return (
+
+
+
+ FEATURED
+
+ SOME GAME
+
+
+ {PLACEHOLDER_ICON}
+
+
+
+
+
+
+
+
+
+
+ {artworks!.results.map((artwork) => renderArtworkCard(artwork))}
+
+
+
+ TODO add footer
+
+
+ );
+}
+
+export const getServerSideProps: GetServerSideProps<
+ ArtworksPageProps
+> = async () => {
+ try {
+ const res = await api.get>("arts");
+ return { props: { artworks: res.data } };
+ //} catch (err: unknown) {
+ } catch {
+ // return {
+ // props: { error: (err as Error).message || "Failed to load artworks." },
+ // };
+
+ // Fallback to mock data on error
+ const mockArtworks = generateMockArtworks(12);
+ return {
+ props: {
+ artworks: {
+ results: mockArtworks,
+ count: mockArtworks.length,
+ next: "",
+ previous: "",
+ },
+ },
+ };
+ }
+};
diff --git a/client/src/types/art-contributor.ts b/client/src/types/art-contributor.ts
new file mode 100644
index 0000000..ed38150
--- /dev/null
+++ b/client/src/types/art-contributor.ts
@@ -0,0 +1,9 @@
+import { BaseDto } from "./base-dto";
+
+export interface ArtContributor extends BaseDto {
+ art_id: number;
+ member_name: string;
+ role: string;
+ instagram_url?: string; // TODO [HanMinh] to refine where to get these info
+ discord_url?: string;
+}
diff --git a/client/src/types/art.ts b/client/src/types/art.ts
new file mode 100644
index 0000000..f00c297
--- /dev/null
+++ b/client/src/types/art.ts
@@ -0,0 +1,10 @@
+import { ArtContributor } from "./art-contributor";
+import { BaseDto } from "./base-dto";
+
+export interface Art extends BaseDto {
+ name: string;
+ description: string;
+ media: string;
+ active: boolean;
+ contributors: ArtContributor[];
+}
diff --git a/client/src/types/base-dto.ts b/client/src/types/base-dto.ts
new file mode 100644
index 0000000..9e3b687
--- /dev/null
+++ b/client/src/types/base-dto.ts
@@ -0,0 +1,3 @@
+export interface BaseDto {
+ id: number;
+}
diff --git a/client/src/types/page-response.ts b/client/src/types/page-response.ts
new file mode 100644
index 0000000..e5fa692
--- /dev/null
+++ b/client/src/types/page-response.ts
@@ -0,0 +1,6 @@
+export interface PageResult {
+ count: number;
+ next: string;
+ previous: string;
+ results: T[];
+}
diff --git a/client/tailwind.config.ts b/client/tailwind.config.ts
index 52306b8..9c4bd79 100644
--- a/client/tailwind.config.ts
+++ b/client/tailwind.config.ts
@@ -21,7 +21,8 @@ const config = {
extend: {
fontFamily: {
sans: ["var(--font-sans)", ...fontFamily.sans],
- jersey10: ["var(--font-jersey10)", ...fontFamily.sans],
+ jersey10: ["Jersey 10", ...fontFamily.sans],
+ dmSans: ["DM Sans", ...fontFamily.sans],
firaCode: ["var(--font-firaCode)", ...fontFamily.sans],
},
@@ -70,6 +71,12 @@ const config = {
neutral_4: "var(--neutral-4)",
light_1: "var(--light-1)",
light_2: "var(--light-2)",
+ light_3: "var(--light-3)",
+ light_alt: "var(--light-alt)",
+ light_alt_2: "var(--light-alt-2)",
+ logo_blue_2: "var(--logo-blue-2)",
+ logo_blue_1: "var(--logo-blue-1)",
+ error: "var(--error)",
},
borderRadius: {
lg: "var(--radius)",
diff --git a/server/game_dev/admin.py b/server/game_dev/admin.py
index 46d358c..c2d8a5e 100644
--- a/server/game_dev/admin.py
+++ b/server/game_dev/admin.py
@@ -1,5 +1,5 @@
from django.contrib import admin
-from .models import Member, Event
+from .models import Art, ArtContributor, Member, Event
class MemberAdmin(admin.ModelAdmin):
@@ -12,3 +12,5 @@ class EventAdmin(admin.ModelAdmin):
admin.site.register(Member, MemberAdmin)
admin.site.register(Event, EventAdmin)
+admin.site.register(Art)
+admin.site.register(ArtContributor)
diff --git a/server/game_dev/migrations/0005_art_artcontributor.py b/server/game_dev/migrations/0005_art_artcontributor.py
new file mode 100644
index 0000000..f3d0c90
--- /dev/null
+++ b/server/game_dev/migrations/0005_art_artcontributor.py
@@ -0,0 +1,68 @@
+# Generated by Django 5.1.14 on 2025-11-28 17:32
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("game_dev", "0004_alter_event_date"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="Art",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("name", models.CharField(max_length=200)),
+ ("description", models.CharField(max_length=200)),
+ ("path_to_media", models.CharField(max_length=500)),
+ ("active", models.BooleanField()),
+ ],
+ ),
+ migrations.CreateModel(
+ name="ArtContributor",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("role", models.CharField(max_length=100)),
+ (
+ "art",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="contributors",
+ to="game_dev.art",
+ ),
+ ),
+ (
+ "member",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="art_contributions",
+ to="game_dev.member",
+ ),
+ ),
+ ],
+ options={
+ "verbose_name": "Art Contributor",
+ "verbose_name_plural": "Art Contributors",
+ "unique_together": {("art", "member")},
+ },
+ ),
+ ]
diff --git a/server/game_dev/migrations/0006_rename_path_to_media_to_media.py b/server/game_dev/migrations/0006_rename_path_to_media_to_media.py
new file mode 100644
index 0000000..9571355
--- /dev/null
+++ b/server/game_dev/migrations/0006_rename_path_to_media_to_media.py
@@ -0,0 +1,23 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("game_dev", "0005_art_artcontributor"),
+ ]
+
+ operations = [
+ # First, rename the field
+ migrations.RenameField(
+ model_name="art",
+ old_name="path_to_media",
+ new_name="media",
+ ),
+ # Then, alter the field to ImageField
+ migrations.AlterField(
+ model_name="art",
+ name="media",
+ field=models.ImageField(upload_to='art_images/'),
+ ),
+ ]
diff --git a/server/game_dev/migrations/0007_alter_artcontributor_unique_together_and_more.py b/server/game_dev/migrations/0007_alter_artcontributor_unique_together_and_more.py
new file mode 100644
index 0000000..3c917f6
--- /dev/null
+++ b/server/game_dev/migrations/0007_alter_artcontributor_unique_together_and_more.py
@@ -0,0 +1,33 @@
+# Generated by Django 5.1.15 on 2026-01-16 15:09
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("game_dev", "0006_rename_path_to_media_to_media"),
+ ]
+
+ operations = [
+ migrations.AlterUniqueTogether(
+ name="artcontributor",
+ unique_together=set(),
+ ),
+ migrations.AlterField(
+ model_name="art",
+ name="active",
+ field=models.BooleanField(default=True),
+ ),
+ migrations.AlterField(
+ model_name="art",
+ name="media",
+ field=models.ImageField(upload_to="art/"),
+ ),
+ migrations.AddConstraint(
+ model_name="artcontributor",
+ constraint=models.UniqueConstraint(
+ fields=("art", "member"), name="unique_art_member"
+ ),
+ ),
+ ]
diff --git a/server/game_dev/models.py b/server/game_dev/models.py
index 6398070..58c2e1f 100644
--- a/server/game_dev/models.py
+++ b/server/game_dev/models.py
@@ -22,3 +22,30 @@ class Event(models.Model):
def __str__(self):
return self.name
+
+
+class Art(models.Model):
+ name = models.CharField(null=False, max_length=200)
+ description = models.CharField(max_length=200,)
+ # source_game = models.ForeignKey(Games, on_delete=models.CASCADE, related_name='art_pieces') #Need implement Games model
+ media = models.ImageField(upload_to='art/', null=False)
+ active = models.BooleanField(default=True)
+
+ def __str__(self):
+ return str(self.name)
+
+
+class ArtContributor(models.Model):
+ art = models.ForeignKey('Art', on_delete=models.CASCADE, related_name='contributors')
+ member = models.ForeignKey('Member', on_delete=models.CASCADE, related_name='art_contributions')
+ role = models.CharField(max_length=100)
+
+ class Meta:
+ constraints = [
+ models.UniqueConstraint(fields=['art', 'member'], name='unique_art_member')
+ ]
+ verbose_name = 'Art Contributor'
+ verbose_name_plural = 'Art Contributors'
+
+ def __str__(self):
+ return f"{self.member.name} - {self.art.name} ({self.role})"
diff --git a/server/game_dev/serializers.py b/server/game_dev/serializers.py
index b50638d..d4715fa 100644
--- a/server/game_dev/serializers.py
+++ b/server/game_dev/serializers.py
@@ -1,5 +1,5 @@
from rest_framework import serializers
-from .models import Event, Member
+from .models import Event, Art, ArtContributor, Member
class EventSerializer(serializers.ModelSerializer):
@@ -16,6 +16,23 @@ class Meta:
]
+class ArtContributorSerializer(serializers.ModelSerializer):
+ member_name = serializers.CharField(source='member.name', read_only=True)
+ art_id = serializers.IntegerField(source='art.id', read_only=True)
+
+ class Meta:
+ model = ArtContributor
+ fields = ['id', 'art_id', 'member', 'member_name', 'role']
+
+
+class ArtSerializer(serializers.ModelSerializer):
+ contributors = ArtContributorSerializer(many=True, read_only=True)
+
+ class Meta:
+ model = Art
+ fields = ['id', 'name', 'description', 'media', 'active', 'contributors']
+
+
class MemberSerializer(serializers.ModelSerializer):
class Meta:
model = Member
diff --git a/server/game_dev/urls.py b/server/game_dev/urls.py
index e387e58..9fb36b6 100644
--- a/server/game_dev/urls.py
+++ b/server/game_dev/urls.py
@@ -1,6 +1,9 @@
from django.urls import path
+from rest_framework.routers import DefaultRouter
from .views import EventDetailAPIView
+router = DefaultRouter()
+
urlpatterns = [
path("events//", EventDetailAPIView.as_view()),
-]
+] + router.urls
diff --git a/server/game_dev/views.py b/server/game_dev/views.py
index 71a747c..d38423c 100644
--- a/server/game_dev/views.py
+++ b/server/game_dev/views.py
@@ -2,8 +2,8 @@
# Create your views here.
from rest_framework import generics
-from .models import Event
-from .serializers import EventSerializer
+from .models import Event, Art
+from .serializers import EventSerializer, ArtSerializer
class EventDetailAPIView(generics.RetrieveAPIView):
@@ -15,3 +15,14 @@ class EventDetailAPIView(generics.RetrieveAPIView):
def get_queryset(self):
return Event.objects.filter(id=self.kwargs["id"])
+
+
+class ArtDetailAPIView(generics.RetrieveAPIView):
+ """
+ GET /api/artworks//
+ """
+ serializer_class = ArtSerializer
+ lookup_url_kwarg = "id"
+
+ def get_queryset(self):
+ return Art.objects.filter(id=self.kwargs["id"])
diff --git a/server/poetry.lock b/server/poetry.lock
index 9e7859f..8e6deac 100644
--- a/server/poetry.lock
+++ b/server/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
[[package]]
name = "asgiref"
@@ -6,7 +6,6 @@ version = "3.8.1"
description = "ASGI specs, helper code, and adapters"
optional = false
python-versions = ">=3.8"
-groups = ["main"]
files = [
{file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"},
{file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"},
@@ -21,7 +20,6 @@ version = "2.15.8"
description = "An abstract syntax tree for Python with inference support."
optional = false
python-versions = ">=3.7.2"
-groups = ["dev"]
files = [
{file = "astroid-2.15.8-py3-none-any.whl", hash = "sha256:1aa149fc5c6589e3d0ece885b4491acd80af4f087baafa3fb5203b113e68cd3c"},
{file = "astroid-2.15.8.tar.gz", hash = "sha256:6c107453dffee9055899705de3c9ead36e74119cee151e5a9aaf7f0b0e020a6a"},
@@ -37,8 +35,6 @@ version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
-groups = ["dev"]
-markers = "sys_platform == \"win32\""
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
@@ -50,7 +46,6 @@ version = "5.1.15"
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
optional = false
python-versions = ">=3.10"
-groups = ["main"]
files = [
{file = "django-5.1.15-py3-none-any.whl", hash = "sha256:117871e58d6eda37f09870b7d73a3d66567b03aecd515b386b1751177c413432"},
{file = "django-5.1.15.tar.gz", hash = "sha256:46a356b5ff867bece73fc6365e081f21c569973403ee7e9b9a0316f27d0eb947"},
@@ -71,7 +66,6 @@ version = "4.4.0"
description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)."
optional = false
python-versions = ">=3.8"
-groups = ["main"]
files = [
{file = "django_cors_headers-4.4.0-py3-none-any.whl", hash = "sha256:5c6e3b7fe870876a1efdfeb4f433782c3524078fa0dc9e0195f6706ce7a242f6"},
{file = "django_cors_headers-4.4.0.tar.gz", hash = "sha256:92cf4633e22af67a230a1456cb1b7a02bb213d6536d2dcb2a4a24092ea9cebc2"},
@@ -87,7 +81,6 @@ version = "3.2.3"
description = "Extensions for Django"
optional = false
python-versions = ">=3.6"
-groups = ["main"]
files = [
{file = "django-extensions-3.2.3.tar.gz", hash = "sha256:44d27919d04e23b3f40231c4ab7af4e61ce832ef46d610cc650d53e68328410a"},
{file = "django_extensions-3.2.3-py3-none-any.whl", hash = "sha256:9600b7562f79a92cbf1fde6403c04fee314608fefbb595502e34383ae8203401"},
@@ -102,7 +95,6 @@ version = "3.15.2"
description = "Web APIs for Django, made easy."
optional = false
python-versions = ">=3.8"
-groups = ["main"]
files = [
{file = "djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20"},
{file = "djangorestframework-3.15.2.tar.gz", hash = "sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad"},
@@ -117,7 +109,6 @@ version = "6.1.0"
description = "the modular source code checker: pep8 pyflakes and co"
optional = false
python-versions = ">=3.8.1"
-groups = ["dev"]
files = [
{file = "flake8-6.1.0-py2.py3-none-any.whl", hash = "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5"},
{file = "flake8-6.1.0.tar.gz", hash = "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23"},
@@ -134,7 +125,6 @@ version = "1.4"
description = "Plugin to catch bad style specific to Django Projects."
optional = false
python-versions = ">=3.7.2,<4.0.0"
-groups = ["dev"]
files = [
{file = "flake8_django-1.4.tar.gz", hash = "sha256:4debba883084191568e3187416d1d6bdd4abd826da988f197a3c36572e9f30de"},
]
@@ -149,7 +139,6 @@ version = "1.5.1"
description = "Let your Python tests travel through time"
optional = false
python-versions = ">=3.7"
-groups = ["main"]
files = [
{file = "freezegun-1.5.1-py3-none-any.whl", hash = "sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1"},
{file = "freezegun-1.5.1.tar.gz", hash = "sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9"},
@@ -164,7 +153,6 @@ version = "23.0.0"
description = "WSGI HTTP Server for UNIX"
optional = false
python-versions = ">=3.7"
-groups = ["main"]
files = [
{file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"},
{file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"},
@@ -186,7 +174,6 @@ version = "2.0.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.7"
-groups = ["dev"]
files = [
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
@@ -198,7 +185,6 @@ version = "1.10.0"
description = "A fast and thorough lazy object proxy."
optional = false
python-versions = ">=3.8"
-groups = ["dev"]
files = [
{file = "lazy-object-proxy-1.10.0.tar.gz", hash = "sha256:78247b6d45f43a52ef35c25b5581459e85117225408a4128a3daf8bf9648ac69"},
{file = "lazy_object_proxy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:855e068b0358ab916454464a884779c7ffa312b8925c6f7401e952dcf3b89977"},
@@ -245,7 +231,6 @@ version = "0.7.0"
description = "McCabe checker, plugin for flake8"
optional = false
python-versions = ">=3.6"
-groups = ["dev"]
files = [
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
@@ -257,7 +242,6 @@ version = "24.1"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
-groups = ["main", "dev"]
files = [
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
@@ -269,7 +253,6 @@ version = "11.3.0"
description = "Python Imaging Library (Fork)"
optional = false
python-versions = ">=3.9"
-groups = ["main"]
files = [
{file = "pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860"},
{file = "pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad"},
@@ -385,7 +368,7 @@ fpx = ["olefile"]
mic = ["olefile"]
test-arrow = ["pyarrow"]
tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"]
-typing = ["typing-extensions ; python_version < \"3.10\""]
+typing = ["typing-extensions"]
xmp = ["defusedxml"]
[[package]]
@@ -394,7 +377,6 @@ version = "1.5.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.8"
-groups = ["dev"]
files = [
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
@@ -410,7 +392,6 @@ version = "2.11.1"
description = "Python style guide checker"
optional = false
python-versions = ">=3.8"
-groups = ["dev"]
files = [
{file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"},
{file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"},
@@ -422,7 +403,6 @@ version = "3.1.0"
description = "passive checker of Python programs"
optional = false
python-versions = ">=3.8"
-groups = ["dev"]
files = [
{file = "pyflakes-3.1.0-py2.py3-none-any.whl", hash = "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774"},
{file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"},
@@ -434,7 +414,6 @@ version = "8.3.2"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.8"
-groups = ["dev"]
files = [
{file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"},
{file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"},
@@ -455,7 +434,6 @@ version = "2.9.0.post0"
description = "Extensions to the standard Python datetime module"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
-groups = ["main"]
files = [
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
@@ -470,7 +448,6 @@ version = "1.0.1"
description = "Read key-value pairs from a .env file and set them as environment variables"
optional = false
python-versions = ">=3.8"
-groups = ["main"]
files = [
{file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"},
{file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"},
@@ -485,7 +462,6 @@ version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
-groups = ["main"]
files = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
@@ -497,7 +473,6 @@ version = "0.5.1"
description = "A non-validating SQL parser."
optional = false
python-versions = ">=3.8"
-groups = ["main"]
files = [
{file = "sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4"},
{file = "sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e"},
@@ -513,8 +488,6 @@ version = "2024.1"
description = "Provider of IANA time zone data"
optional = false
python-versions = ">=2"
-groups = ["main"]
-markers = "sys_platform == \"win32\""
files = [
{file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"},
{file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"},
@@ -526,7 +499,6 @@ version = "1.16.0"
description = "Module for decorators, wrappers and monkey patching."
optional = false
python-versions = ">=3.6"
-groups = ["dev"]
files = [
{file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"},
{file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"},
@@ -601,6 +573,6 @@ files = [
]
[metadata]
-lock-version = "2.1"
+lock-version = "2.0"
python-versions = "^3.12"
content-hash = "f804c2f3998772b91e34ad214e5fcafe900bec97675f73046d3bcc79aba0f7db"