diff --git a/config/api_urls.py b/config/api_urls.py new file mode 100644 index 0000000..0cbfb39 --- /dev/null +++ b/config/api_urls.py @@ -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"))] diff --git a/config/settings.py b/config/settings.py index 7b08d08..583e479 100644 --- a/config/settings.py +++ b/config/settings.py @@ -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", + ), +} diff --git a/config/urls.py b/config/urls.py index ea6928c..c4391eb 100644 --- a/config/urls.py +++ b/config/urls.py @@ -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")), ] diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 18d1cb9..7825a44 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index 37950ed..01ae778 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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" } } diff --git a/frontend/src/lib/LevelTag.svelte b/frontend/src/lib/LevelTag.svelte new file mode 100644 index 0000000..a10a3c6 --- /dev/null +++ b/frontend/src/lib/LevelTag.svelte @@ -0,0 +1,25 @@ + + +{level.levelString} diff --git a/frontend/src/lib/NavLink.svelte b/frontend/src/lib/NavLink.svelte new file mode 100644 index 0000000..ae242bc --- /dev/null +++ b/frontend/src/lib/NavLink.svelte @@ -0,0 +1,9 @@ + + + + {title} + diff --git a/frontend/src/lib/WordForm.svelte b/frontend/src/lib/WordForm.svelte new file mode 100644 index 0000000..45e4116 --- /dev/null +++ b/frontend/src/lib/WordForm.svelte @@ -0,0 +1,14 @@ + + +
+ +
+
{word}
+
{definition}
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts new file mode 100644 index 0000000..88eacd5 --- /dev/null +++ b/frontend/src/lib/api.ts @@ -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; \ No newline at end of file diff --git a/frontend/src/lib/case.ts b/frontend/src/lib/case.ts new file mode 100644 index 0000000..5745aa9 --- /dev/null +++ b/frontend/src/lib/case.ts @@ -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()}`); +} \ No newline at end of file diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts new file mode 100644 index 0000000..60311e5 --- /dev/null +++ b/frontend/src/lib/types.ts @@ -0,0 +1,13 @@ + +export interface Level { + id: number; + levelNumber: number; + levelString: string; +} + +export interface Word { + id: number; + word: string; + definition: string; + level: Level; +} \ No newline at end of file diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 0e19760..37bc047 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -1,6 +1,9 @@ @@ -13,7 +16,9 @@ diff --git a/frontend/src/routes/kaeliou/+page.svelte b/frontend/src/routes/kaeliou/+page.svelte new file mode 100644 index 0000000..08caf9f --- /dev/null +++ b/frontend/src/routes/kaeliou/+page.svelte @@ -0,0 +1,7 @@ + + +
+ {$page.url.pathname} +
diff --git a/frontend/src/routes/ma_chaeliou/+page.svelte b/frontend/src/routes/ma_chaeliou/+page.svelte new file mode 100644 index 0000000..08caf9f --- /dev/null +++ b/frontend/src/routes/ma_chaeliou/+page.svelte @@ -0,0 +1,7 @@ + + +
+ {$page.url.pathname} +
diff --git a/frontend/src/routes/ma_geriou/+page.svelte b/frontend/src/routes/ma_geriou/+page.svelte new file mode 100644 index 0000000..bd050d9 --- /dev/null +++ b/frontend/src/routes/ma_geriou/+page.svelte @@ -0,0 +1,81 @@ + + +
+ {#await wordResponse} +

+ {:then} +
+
Live
+
Ger
+
Termenadur
+ + {#if !isFormNewShown} +
+ +
+ {:else} +
+
+ +
+
+ +
+
+ +
+
+ {/if} + + {#each words as word} + + {/each} +
+ {/await} +
diff --git a/grids/admin.py b/grids/admin.py index 8c38f3f..845c3de 100644 --- a/grids/admin.py +++ b/grids/admin.py @@ -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) diff --git a/grids/migrations/0001_initial.py b/grids/migrations/0001_initial.py new file mode 100644 index 0000000..0776a1a --- /dev/null +++ b/grids/migrations/0001_initial.py @@ -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"), + ), + ] diff --git a/grids/migrations/0002_rename_level_numer_level_level_number.py b/grids/migrations/0002_rename_level_numer_level_level_number.py new file mode 100644 index 0000000..32098fb --- /dev/null +++ b/grids/migrations/0002_rename_level_numer_level_level_number.py @@ -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", + ), + ] diff --git a/grids/migrations/0003_rename_string_number_level_level_string.py b/grids/migrations/0003_rename_string_number_level_level_string.py new file mode 100644 index 0000000..e1c13b6 --- /dev/null +++ b/grids/migrations/0003_rename_string_number_level_level_string.py @@ -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", + ), + ] diff --git a/grids/models.py b/grids/models.py index 71a8362..4cc5232 100644 --- a/grids/models.py +++ b/grids/models.py @@ -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")], + ) diff --git a/grids/serializers.py b/grids/serializers.py new file mode 100644 index 0000000..e039bf0 --- /dev/null +++ b/grids/serializers.py @@ -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"] diff --git a/grids/views.py b/grids/views.py index 91ea44a..1ba2f0f 100644 --- a/grids/views.py +++ b/grids/views.py @@ -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 diff --git a/poetry.lock b/poetry.lock index 8edb67a..ee80b25 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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" diff --git a/pyproject.toml b/pyproject.toml index bb8364b..1809310 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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]