This commit is contained in:
Ewen 2023-09-12 17:07:47 +02:00
parent 34f1de698f
commit c742b244b4
24 changed files with 626 additions and 8 deletions

15
config/api_urls.py Normal file
View file

@ -0,0 +1,15 @@
from django.conf.urls import include
from django.urls import re_path
from rest_framework import routers
from grids import views as grids_views
router = routers.DefaultRouter()
router.register(r"levels", grids_views.LevelViewSet, basename="level")
router.register(r"grids", grids_views.GridViewSet, basename="grid")
router.register(r"words", grids_views.WordViewSet, basename="word")
router.register(r"placement", grids_views.PlacementViewSet, basename="placement")
v1_pattern = router.urls
urlpatterns = [re_path(r"^v1/", include((v1_pattern, "v1"), namespace="v1"))]

View file

@ -50,6 +50,7 @@ INSTALLED_APPS = [
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"corsheaders.middleware.CorsMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
@ -132,3 +133,19 @@ STATIC_URL = "static/"
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# Origins
if DEBUG:
CORS_ALLOW_ALL_ORIGINS = True
# Rest framework
REST_FRAMEWORK = {
# "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
"DEFAULT_PARSER_CLASSES": (
"rest_framework.parsers.JSONParser",
"rest_framework.parsers.FormParser",
),
}

View file

@ -15,9 +15,10 @@ Including another URLconf
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from django.urls import path, include, re_path
urlpatterns = [
path("admin/", admin.site.urls),
path("users/", include("users.urls", namespace="users")),
re_path(r"^api/", include(("config.api_urls", "api"), namespace="api")),
]

View file

@ -8,8 +8,10 @@
"name": "frontend",
"version": "0.0.0",
"dependencies": {
"@fortawesome/fontawesome-free": "^6.4.2",
"@sveltejs/adapter-auto": "^2.1.0",
"@sveltejs/kit": "^1.24.1"
"@sveltejs/kit": "^1.24.1",
"axios": "^1.5.0"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^2.4.2",
@ -378,6 +380,15 @@
"node": ">=12"
}
},
"node_modules/@fortawesome/fontawesome-free": {
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.2.tgz",
"integrity": "sha512-m5cPn3e2+FDCOgi1mz0RexTUvvQibBebOUlUlW0+YrMjDTPkiJ6VTKukA1GRsvRw+12KyJndNjj0O4AgTxm2Pg==",
"hasInstallScript": true,
"engines": {
"node": ">=6"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
@ -606,6 +617,11 @@
"dequal": "^2.0.3"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/autoprefixer": {
"version": "10.4.15",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.15.tgz",
@ -643,6 +659,16 @@
"postcss": "^8.1.0"
}
},
"node_modules/axios": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz",
"integrity": "sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axobject-query": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
@ -817,6 +843,17 @@
"periscopic": "^3.1.0"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@ -888,6 +925,14 @@
"node": ">=0.10.0"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@ -1029,6 +1074,38 @@
"node": ">=8"
}
},
"node_modules/follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fraction.js": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.6.tgz",
@ -1316,6 +1393,25 @@
"node": ">=10.0.0"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/min-indent": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
@ -1660,6 +1756,11 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",

View file

@ -22,7 +22,9 @@
"vite": "^4.4.5"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.4.2",
"@sveltejs/adapter-auto": "^2.1.0",
"@sveltejs/kit": "^1.24.1"
"@sveltejs/kit": "^1.24.1",
"axios": "^1.5.0"
}
}

View file

@ -0,0 +1,25 @@
<script lang="ts">
import type { Level } from "./types";
export let level: Level;
let classes: string;
$: switch (level.levelNumber) {
case 1: {
classes = "bg-lime-500";
break;
}
case 2: {
classes = "bg-blue-500";
break;
}
case 3: {
classes = "bg-red-500";
break;
}
}
</script>
<span class="text-sm font-bold text-white px-2 rounded-md {classes}"
>{level.levelString}</span
>

View file

@ -0,0 +1,9 @@
<script lang="ts">
import { page } from "$app/stores";
export let title: string;
export let href: string;
</script>
<a {href} class:font-bold={$page.url.pathname == href}>
{title}
</a>

View file

@ -0,0 +1,14 @@
<script lang="ts">
import type { Level } from "./types";
import LevelTag from "./LevelTag.svelte";
export let word: string;
export let definition: string;
export let level: Level;
</script>
<div class="">
<LevelTag {level} />
</div>
<div>{word}</div>
<div>{definition}</div>

8
frontend/src/lib/api.ts Normal file
View file

@ -0,0 +1,8 @@
import axios from "axios";
import { PUBLIC_API_ENDPOINT } from "$env/static/public";
const axiosAPI = axios.create({
baseURL: PUBLIC_API_ENDPOINT
});
export default axiosAPI;

59
frontend/src/lib/case.ts Normal file
View file

@ -0,0 +1,59 @@
// camelCase to snake_case
export function camelToSnakeCase(o: any): any {
if (o === null || typeof o != "object") {
return o;
}
if (Array.isArray(o)) {
return o.map(camelToSnakeCase);
}
const build: { [key: string]: string } = {};
for (const key in o) {
const newKey = toSnake(key);
let value = o[key];
if (typeof value === "object") {
value = camelToSnakeCase(value);
}
build[newKey] = value;
}
return build;
}
// snake_case to camelCase
export function snakeToCamelCase(o: any): any {
if (o === null || typeof o != "object") {
return o;
}
if (Array.isArray(o)) {
return o.map(snakeToCamelCase);
}
const build: { [key: string]: string } = {};
for (const key in o) {
const newKey = toCamel(key);
let value = o[key];
if (typeof value === "object") {
value = snakeToCamelCase(value);
}
build[newKey] = value;
}
return build;
}
function toCamel(s: string): string {
return s.replace(/([-_][a-z])/gi, ($1) => {
return $1.toUpperCase().replace("-", "").replace("_", "");
});
}
function toSnake(s: string): string {
return s.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
}

13
frontend/src/lib/types.ts Normal file
View file

@ -0,0 +1,13 @@
export interface Level {
id: number;
levelNumber: number;
levelString: string;
}
export interface Word {
id: number;
word: string;
definition: string;
level: Level;
}

View file

@ -1,6 +1,9 @@
<script>
import "../app.css";
import UserNavbar from "$lib/UserNavbar.svelte";
import NavLink from "../lib/NavLink.svelte";
import { page } from "$app/stores";
import "@fortawesome/fontawesome-free/css/all.min.css";
</script>
<svelte:head>
@ -13,7 +16,9 @@
<nav class="p-2 md:p-4 border-b flex flex-row justify-between">
<div><a href="/" class="font-bold">Gerioù-bir</a></div>
<div class="flex flex-row gap-2">
<a href="/">Kaelioù publik</a>
<NavLink href="/kaeliou" title="Kaelioù publik" />
<NavLink href="/ma_geriou" title="Ma gerioù" />
<NavLink href="/ma_chaeliou" title="Ma c'haelioù" />
<UserNavbar />
</div>
</nav>

View file

@ -0,0 +1,7 @@
<script>
import { page } from "$app/stores";
</script>
<main>
{$page.url.pathname}
</main>

View file

@ -0,0 +1,7 @@
<script>
import { page } from "$app/stores";
</script>
<main>
{$page.url.pathname}
</main>

View file

@ -0,0 +1,81 @@
<script lang="ts">
import { page } from "$app/stores";
import axios from "$lib/api.ts";
import WordForm from "$lib/WordForm.svelte";
import { type Word } from "../../lib/types";
import { snakeToCamelCase } from "$lib/case";
let words: Word[];
const wordResponse = axios.get("words/").then((data) => {
words = snakeToCamelCase(data.data);
});
let isFormNewShown = false;
function toggleFormNew() {
isFormNewShown = !isFormNewShown;
}
function newWord(e) {
const formData = new FormData(e.target);
const data = {};
for (let field of formData) {
const [key, value] = field;
data[key] = value;
}
console.log(data);
}
</script>
<main>
{#await wordResponse}
<p></p>
{:then}
<div class="grid grid-cols-3 gap-y-2 gap-x-1">
<div class="font-bold">Live</div>
<div class="font-bold">Ger</div>
<div class="font-bold">Termenadur</div>
{#if !isFormNewShown}
<div />
<button
type="button"
class="bg-violet-600 hover:bg-violet-700 rounded-md text-white p-1 w-8 text-center"
on:click={toggleFormNew}
>
<span class="fa-regular fa-plus" />
</button>
<div />
{:else}
<form on:submit|preventDefault={newWord}>
<div>
<input
name="level"
placeholder="Live"
class="p-1 rounded-md border w-1/2"
/>
</div>
<div>
<input
name="word"
placeholder="Ger"
class="p-1 rounded-md border w-full"
/>
</div>
<div>
<input
name="definition"
placeholder="Termenadur"
class="p-1 rounded-md border w-full"
/>
</div>
</form>
{/if}
{#each words as word}
<WordForm {...word} />
{/each}
</div>
{/await}
</main>

View file

@ -1,3 +1,7 @@
from django.contrib import admin
from .models import Word, Grid, Placement, Level
# Register your models here.
admin.site.register(Level)
admin.site.register(Word)
admin.site.register(Grid)
admin.site.register(Placement)

View file

@ -0,0 +1,114 @@
# Generated by Django 4.2.5 on 2023-09-12 12:51
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="Grid",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
],
),
migrations.CreateModel(
name="Level",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("level_numer", models.IntegerField()),
("string_number", models.CharField(max_length=50)),
],
),
migrations.CreateModel(
name="Word",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("word", models.CharField(max_length=50)),
("definition", models.CharField(max_length=255)),
(
"level",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="words",
to="grids.level",
),
),
],
),
migrations.CreateModel(
name="Placement",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("coordinates_first_letter", models.CharField(max_length=10)),
(
"direction",
models.SmallIntegerField(
choices=[(1, "Right"), (2, "Bottom")], default=1
),
),
(
"grid",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="grids.grid"
),
),
(
"word",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="grids.word"
),
),
],
),
migrations.AddField(
model_name="grid",
name="level",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="grids",
to="grids.level",
),
),
migrations.AddField(
model_name="grid",
name="words",
field=models.ManyToManyField(through="grids.Placement", to="grids.word"),
),
]

View file

@ -0,0 +1,17 @@
# Generated by Django 4.2.5 on 2023-09-12 12:56
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("grids", "0001_initial"),
]
operations = [
migrations.RenameField(
model_name="level",
old_name="level_numer",
new_name="level_number",
),
]

View file

@ -0,0 +1,17 @@
# Generated by Django 4.2.5 on 2023-09-12 12:56
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("grids", "0002_rename_level_numer_level_level_number"),
]
operations = [
migrations.RenameField(
model_name="level",
old_name="string_number",
new_name="level_string",
),
]

View file

@ -1,3 +1,32 @@
from django.db import models
# Create your models here.
class Level(models.Model):
level_number = models.IntegerField()
level_string = models.CharField(max_length=50)
class Word(models.Model):
word = models.CharField(max_length=50)
definition = models.CharField(max_length=255)
level = models.ForeignKey(Level, on_delete=models.CASCADE, related_name="words")
class Grid(models.Model):
level = models.ForeignKey(Level, on_delete=models.CASCADE, related_name="grids")
words = models.ManyToManyField(Word, through="Placement")
class Direction:
RIGHT = 1
BOTTOM = 2
class Placement(models.Model):
word = models.ForeignKey(Word, on_delete=models.CASCADE)
grid = models.ForeignKey(Grid, on_delete=models.CASCADE)
coordinates_first_letter = models.CharField(max_length=10)
direction = models.SmallIntegerField(
default=Direction.RIGHT,
choices=[(Direction.RIGHT, "Right"), (Direction.BOTTOM, "Bottom")],
)

28
grids/serializers.py Normal file
View file

@ -0,0 +1,28 @@
from rest_framework import serializers
from .models import Word, Grid, Placement, Level
class LevelSerializer(serializers.ModelSerializer):
class Meta:
model = Level
fields = ["id", "level_number", "level_string"]
class WordSerializer(serializers.ModelSerializer):
level = LevelSerializer(many=False, read_only=True)
class Meta:
model = Word
fields = ["id", "word", "definition", "level"]
class GridSerializer(serializers.ModelSerializer):
class Meta:
model = Grid
fields = ["id", "words"]
class PlacementSerializer(serializers.ModelSerializer):
class Meta:
model = Placement
fields = ["id", "word", "grid", "coordinates_first_letter", "direction"]

View file

@ -1,3 +1,33 @@
from django.shortcuts import render
from rest_framework import pagination, status, viewsets
from rest_framework.response import Response
# Create your views here.
from .models import Word, Grid, Placement, Level
from .serializers import (
WordSerializer,
GridSerializer,
PlacementSerializer,
LevelSerializer,
)
class LevelViewSet(viewsets.ModelViewSet):
queryset = Level.objects.all()
serializer_class = LevelSerializer
ordering_fields = ["level_number"]
class WordViewSet(viewsets.ModelViewSet):
queryset = Word.objects.all()
serializer_class = WordSerializer
class GridViewSet(viewsets.ModelViewSet):
queryset = Grid.objects.all()
serializer_class = GridSerializer
class PlacementViewSet(viewsets.ModelViewSet):
queryset = Placement.objects.all()
serializer_class = PlacementSerializer

16
poetry.lock generated
View file

@ -34,6 +34,20 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""}
argon2 = ["argon2-cffi (>=19.1.0)"]
bcrypt = ["bcrypt"]
[[package]]
name = "django-cors-headers"
version = "4.2.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"
files = [
{file = "django_cors_headers-4.2.0-py3-none-any.whl", hash = "sha256:9ada212b0e2efd4a5e339360ffc869cb21ac5605e810afe69f7308e577ea5bde"},
{file = "django_cors_headers-4.2.0.tar.gz", hash = "sha256:f9749c6410fe738278bc2b6ef17f05195bc7b251693c035752d8257026af024f"},
]
[package.dependencies]
Django = ">=3.2"
[[package]]
name = "django-environ"
version = "0.11.2"
@ -106,4 +120,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "d592e1d1d4d9e64b28486f934462658ca397646908507648190a2711704eb769"
content-hash = "892a80c189815eb735e7fc36932e4bb9f534ba248bfa6e6af37ca882fec04f14"

View file

@ -11,6 +11,7 @@ python = "^3.11"
django = "^4.2.5"
django-environ = "^0.11.2"
djangorestframework = "^3.14.0"
django-cors-headers = "^4.2.0"
[build-system]