Compare commits
96 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e7bbb6209e | |||
| a6a03e3f04 | |||
| ddc5797549 | |||
| 65f5e4bb2e | |||
| 976a116799 | |||
| c8736577e5 | |||
| b945cf726c | |||
| 83338df181 | |||
| 60d314abaf | |||
| 0810ca8d50 | |||
| 68b4bbb979 | |||
| 427850bd64 | |||
| 6201e433ae | |||
| 450bfdb394 | |||
| aed2413b88 | |||
| 01788286e2 | |||
| 169b349277 | |||
| 9c0de2a0be | |||
| 3b4098cb1a | |||
| 0d99f383fb | |||
| c6fd80408b | |||
| d2d4deb350 | |||
| ee280e0b8e | |||
| 1c2c2f38a4 | |||
| b5cca50960 | |||
| 0a505fabcf | |||
| 4b8053c6e2 | |||
| 5df3c1c47e | |||
| adad4f88da | |||
| 957c7a702e | |||
| b5344a9ed1 | |||
| 16ad701eed | |||
| 2ba7465675 | |||
| c703dfc6e3 | |||
| b62cc89f71 | |||
| e741a46f69 | |||
| 4f87ff83ce | |||
| 4871f56320 | |||
| ec404c9956 | |||
| fc9db8200f | |||
| 69caa825de | |||
| 123be2e9c0 | |||
| 928536b414 | |||
| 8ed8599f17 | |||
| 435de2ec92 | |||
| 529a199262 | |||
| 510e2f8130 | |||
| da4f286180 | |||
| d9136e45f6 | |||
| b610947403 | |||
| 6e520a46f0 | |||
| b4a0c2d7a5 | |||
| ea9f596abe | |||
| 4bb7d61f80 | |||
| cc8a166f5c | |||
| a70bf8ae96 | |||
| 902ee03e3f | |||
| f1f49ab014 | |||
| 7a16225c3b | |||
| 8e13af5b8c | |||
| 767c069404 | |||
| dd5281e2a1 | |||
| aefd56d1ff | |||
| 293a0b3478 | |||
| 076466b895 | |||
| 370738ff14 | |||
| 1151b0e45e | |||
| 7c5c571716 | |||
| 1b636efdb7 | |||
| 374c242890 | |||
| 87b1c24783 | |||
| 456162da44 | |||
| 8cfb60f9a1 | |||
| 4c70046a32 | |||
| 546f8ebbdf | |||
| bd86379d07 | |||
| edcaed1b1a | |||
| 20ed0b2f40 | |||
| 3197288eee | |||
| 84d64f04d2 | |||
| 1ecf64a228 | |||
| 41d513e402 | |||
| aa4b47e032 | |||
| 3537642df9 | |||
| 06a48a03ac | |||
| d60e606663 | |||
| 13dad3961c | |||
| 4cf1941f44 | |||
| ace568a800 | |||
| c77adb0e04 | |||
| 7ff2bf912d | |||
| 72c1a7d265 | |||
| 2efacf6d67 | |||
| d6549132ea | |||
| bbf422cc5d | |||
| 4c96de27db |
+5
-13
@@ -1,16 +1,8 @@
|
|||||||
.idea/
|
|
||||||
.theia/
|
|
||||||
.vscode/
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
.angular/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
bonus/
|
node_modules/
|
||||||
icons/
|
.editorconfig
|
||||||
icons-shadowless/
|
|
||||||
springboot/.factorypath
|
|
||||||
java-ee/.settings
|
|
||||||
java-ee/.project
|
|
||||||
kontor-schema/env
|
|
||||||
kontor-schema/kontor_schema.egg-info
|
|
||||||
kontor-gui/.pdm-python
|
|
||||||
kontor-gui/dist
|
|
||||||
fastapi/.coverage
|
|
||||||
db-password.txt
|
db-password.txt
|
||||||
|
couchdb-password.txt
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
kontor_api := kontor-api
|
kontor_api := kontor-api
|
||||||
kontor_spring := kontor-spring
|
kontor_spring := kontor-spring
|
||||||
|
kontor_angular := kontor-angular
|
||||||
|
TARGET=docker
|
||||||
|
|
||||||
.PHONY: all $(kontor_spring) $(kontor_api)
|
.PHONY: all $(kontor_spring) $(kontor_api) $(kontor_angular)
|
||||||
all: $(kontor_spring) $(kontor_api)
|
|
||||||
|
|
||||||
$(kontor_spring) $(kontor_api):
|
all: $(kontor_spring) $(kontor_api) $(kontor_angular)
|
||||||
|
|
||||||
|
$(kontor_spring) $(kontor_api) $(kontor_angular):
|
||||||
$(MAKE) --directory=$@ $(TARGET)
|
$(MAKE) --directory=$@ $(TARGET)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
services:
|
||||||
|
couchdb:
|
||||||
|
image: couchdb
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- COUCHDB_USER=admin
|
||||||
|
- COUCHDB_PASSWORD=admin
|
||||||
|
ports:
|
||||||
|
- 5984:5984
|
||||||
|
networks:
|
||||||
|
- database
|
||||||
|
- frontend
|
||||||
|
volumes:
|
||||||
|
- couchdb-data:/opt/couchdb/data
|
||||||
|
secrets:
|
||||||
|
- couchdb-password
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
couchdb-data:
|
||||||
|
secrets:
|
||||||
|
couchdb-password:
|
||||||
|
file: couchdb-password.txt
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- POSTGRES_DB=kontor
|
||||||
|
- POSTGRES_USER=kontor
|
||||||
|
#- POSTGRES_PASSWORD_FILE=/run/secrets/db-password
|
||||||
|
- POSTGRES_PASSWORD=kontor
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U kontor"]
|
||||||
|
interval: 1s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
networks:
|
||||||
|
- database
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data:rw
|
||||||
|
secrets:
|
||||||
|
- db-password
|
||||||
|
adminer:
|
||||||
|
image: adminer
|
||||||
|
ports:
|
||||||
|
- 8090:8080
|
||||||
|
networks:
|
||||||
|
- database
|
||||||
|
- frontend
|
||||||
|
volumes:
|
||||||
|
postgres-data:
|
||||||
|
secrets:
|
||||||
|
db-password:
|
||||||
|
file: db-password.txt
|
||||||
+70
-16
@@ -1,44 +1,98 @@
|
|||||||
|
include:
|
||||||
|
- ./compose-postgres.yaml
|
||||||
|
|
||||||
services:
|
services:
|
||||||
mariadb:
|
activemq:
|
||||||
image: mariadb
|
image: apache/activemq-artemis:latest-alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
|
||||||
MYSQL_ROOT_PASSWORD: kontor
|
|
||||||
MYSQL_USER: kontor
|
|
||||||
MYSQL_PASSWORD: kontor
|
|
||||||
MYSQL_DATABASE: kontor
|
|
||||||
ports:
|
ports:
|
||||||
- 3316:3306
|
- 61616:61616
|
||||||
|
- 8161:8161
|
||||||
|
- 5672:5672
|
||||||
networks:
|
networks:
|
||||||
- database
|
- integration
|
||||||
|
- frontend
|
||||||
volumes:
|
volumes:
|
||||||
- mariadb-storage:/var/lib/mysql:rw
|
- activemq-data:/var/lib/artemis-instance
|
||||||
kontor:
|
kontor:
|
||||||
image: kontor
|
build:
|
||||||
|
context: ./kontor-spring
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
tags:
|
||||||
|
- kontor:0.2.0-SNAPSHOT
|
||||||
|
image: kontor:0.2.0-SNAPSHOT
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- database
|
- database
|
||||||
|
- integration
|
||||||
- frontend
|
- frontend
|
||||||
ports:
|
ports:
|
||||||
- 8000:8000
|
- 8000:8000
|
||||||
|
volumes:
|
||||||
|
- images-data:/data/images
|
||||||
depends_on:
|
depends_on:
|
||||||
- mariadb
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
kontor-api:
|
kontor-api:
|
||||||
image: kontor-api
|
build:
|
||||||
|
context: ./kontor-api
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
tags:
|
||||||
|
- kontor-api:0.2.0-SNAPSHOT
|
||||||
|
image: kontor-api:0.2.0-SNAPSHOT
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- database
|
- database
|
||||||
|
- integration
|
||||||
- frontend
|
- frontend
|
||||||
ports:
|
ports:
|
||||||
- 8800:8800
|
- 8800:8800
|
||||||
|
volumes:
|
||||||
|
- images-data:/data/images
|
||||||
depends_on:
|
depends_on:
|
||||||
- mariadb
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
kontor-angular:
|
||||||
|
build:
|
||||||
|
context: ./kontor-angular
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
tags:
|
||||||
|
- kontor-angular:0.2.0-SNAPSHOT
|
||||||
|
image: kontor-angular:0.2.0-SNAPSHOT
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- database
|
||||||
|
- integration
|
||||||
|
- frontend
|
||||||
|
ports:
|
||||||
|
- 8200:80
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
kontor-vue:
|
||||||
|
build:
|
||||||
|
context: ./kontor-vue
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
tags:
|
||||||
|
- kontor-vue:0.2.0-SNAPSHOT
|
||||||
|
image: kontor-vue:0.2.0-SNAPSHOT
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- database
|
||||||
|
- integration
|
||||||
|
- frontend
|
||||||
|
ports:
|
||||||
|
- 8300:80
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
database:
|
database:
|
||||||
|
integration:
|
||||||
|
name: integration
|
||||||
frontend:
|
frontend:
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mariadb-storage:
|
activemq-data:
|
||||||
|
images-data:
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||||
|
|
||||||
|
# Compiled output
|
||||||
|
/dist
|
||||||
|
/tmp
|
||||||
|
/out-tsc
|
||||||
|
/bazel-out
|
||||||
|
|
||||||
|
# Node
|
||||||
|
/node_modules
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
.idea/
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.history/*
|
||||||
|
|
||||||
|
# Miscellaneous
|
||||||
|
/.angular/cache
|
||||||
|
.sass-cache/
|
||||||
|
/connect.lock
|
||||||
|
/coverage
|
||||||
|
/libpeerconnection.log
|
||||||
|
testem.log
|
||||||
|
/typings
|
||||||
|
|
||||||
|
# System files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
### STAGE 1: Build ###
|
||||||
|
FROM node:22.15-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm install
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
### STAGE 2: Run ###
|
||||||
|
FROM nginx:1.17.1-alpine
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY --from=build /app/dist/kontor-angular/browser /usr/share/nginx/html
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
.PHONY: docker dev
|
||||||
|
|
||||||
|
docker:
|
||||||
|
docker build -t kontor-angular:0.2.0-SNAPSHOT .
|
||||||
|
|
||||||
|
dev:
|
||||||
|
ng serve
|
||||||
|
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# KontorAngular
|
||||||
|
|
||||||
|
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.1.1.
|
||||||
|
|
||||||
|
## Development server
|
||||||
|
|
||||||
|
To start a local development server, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng serve
|
||||||
|
```
|
||||||
|
|
||||||
|
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
|
||||||
|
|
||||||
|
## Code scaffolding
|
||||||
|
|
||||||
|
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng generate component component-name
|
||||||
|
```
|
||||||
|
|
||||||
|
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng generate --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
To build the project run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng build
|
||||||
|
```
|
||||||
|
|
||||||
|
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
|
||||||
|
|
||||||
|
## Running unit tests
|
||||||
|
|
||||||
|
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running end-to-end tests
|
||||||
|
|
||||||
|
For end-to-end (e2e) testing, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
|
"version": 1,
|
||||||
|
"newProjectRoot": "projects",
|
||||||
|
"projects": {
|
||||||
|
"kontor-angular": {
|
||||||
|
"projectType": "application",
|
||||||
|
"schematics": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"style": "scss"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"prefix": "app",
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular/build:application",
|
||||||
|
"options": {
|
||||||
|
"browser": "src/main.ts",
|
||||||
|
"tsConfig": "tsconfig.app.json",
|
||||||
|
"inlineStyleLanguage": "scss",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "public"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.scss"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "500kB",
|
||||||
|
"maximumError": "1MB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "4kB",
|
||||||
|
"maximumError": "8kB"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputHashing": "all"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"optimization": false,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"sourceMap": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "production"
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"builder": "@angular/build:dev-server",
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"buildTarget": "kontor-angular:build:production"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"buildTarget": "kontor-angular:build:development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "development"
|
||||||
|
},
|
||||||
|
"extract-i18n": {
|
||||||
|
"builder": "@angular/build:extract-i18n"
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"builder": "@angular/build:karma",
|
||||||
|
"options": {
|
||||||
|
"tsConfig": "tsconfig.spec.json",
|
||||||
|
"inlineStyleLanguage": "scss",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "public"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.scss"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cli": {
|
||||||
|
"analytics": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
server {
|
||||||
|
# Root-Verzeichnis für den Server setzen (wir kopieren unsere Anwendung hierher)
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
|
||||||
|
# Definieren der Standard-Indexdatei (Angular erstellt die Datei index.html für uns und sie befindet sich im oben genannten Verzeichnis)
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Cache-Header für Medien-ASsets
|
||||||
|
location ~* \.(?:cur|jpe?g|gif|htc|ico|png|xml|otf|ttf|eot|woff|woff2|svg)$ {
|
||||||
|
access_log off;
|
||||||
|
add_header Pragma "must-revalidate, public";
|
||||||
|
add_header Cache-Control "must-revalidate, public";
|
||||||
|
expires max;
|
||||||
|
|
||||||
|
tcp_nodelay off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cache-Header für HTML, CSS und JS-Dateien
|
||||||
|
location ~* \.(?:css|js|html)$ {
|
||||||
|
access_log off;
|
||||||
|
add_header Pragma "must-revalidate, public";
|
||||||
|
add_header Cache-Control "must-revalidate, public";
|
||||||
|
expires 2d;
|
||||||
|
|
||||||
|
tcp_nodelay off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Konfiguration für den /-Pfad
|
||||||
|
location / {
|
||||||
|
# Zunächst versuchen wir die angeforderte URI auzuliefern
|
||||||
|
# Klappt das nicht, versuchen wir es mit einem abschließenden Slash
|
||||||
|
# Klappt auch das nicht, liefern wir die index.html aus.
|
||||||
|
# Das ist nötig, damit Angular-Routen korrekt augeflöst und ausgeliefert werden
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Generated
+9522
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "kontor-angular",
|
||||||
|
"version": "0.2.0",
|
||||||
|
"scripts": {
|
||||||
|
"ng": "ng",
|
||||||
|
"start": "ng serve",
|
||||||
|
"build": "ng build",
|
||||||
|
"watch": "ng build --watch --configuration development",
|
||||||
|
"test": "ng test"
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.html",
|
||||||
|
"options": {
|
||||||
|
"parser": "angular"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@angular/common": "^20.1.0",
|
||||||
|
"@angular/compiler": "^20.1.0",
|
||||||
|
"@angular/core": "^20.1.0",
|
||||||
|
"@angular/forms": "^20.1.0",
|
||||||
|
"@angular/platform-browser": "^20.1.0",
|
||||||
|
"@angular/router": "^20.1.0",
|
||||||
|
"rxjs": "~7.8.0",
|
||||||
|
"tslib": "^2.3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular/build": "^20.1.1",
|
||||||
|
"@angular/cli": "^20.1.1",
|
||||||
|
"@angular/compiler-cli": "^20.1.0",
|
||||||
|
"@types/jasmine": "~5.1.0",
|
||||||
|
"jasmine-core": "~5.8.0",
|
||||||
|
"karma": "~6.4.0",
|
||||||
|
"karma-chrome-launcher": "~3.2.0",
|
||||||
|
"karma-coverage": "~2.2.0",
|
||||||
|
"karma-jasmine": "~5.1.0",
|
||||||
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
|
"typescript": "~5.8.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,12 @@
|
|||||||
|
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZonelessChangeDetection } from '@angular/core';
|
||||||
|
import { provideRouter } from '@angular/router';
|
||||||
|
|
||||||
|
import { routes } from './app.routes';
|
||||||
|
|
||||||
|
export const appConfig: ApplicationConfig = {
|
||||||
|
providers: [
|
||||||
|
provideBrowserGlobalErrorListeners(),
|
||||||
|
provideZonelessChangeDetection(),
|
||||||
|
provideRouter(routes)
|
||||||
|
]
|
||||||
|
};
|
||||||
@@ -0,0 +1,342 @@
|
|||||||
|
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
||||||
|
<!-- * * * * * * * * * * * The content below * * * * * * * * * * * -->
|
||||||
|
<!-- * * * * * * * * * * is only a placeholder * * * * * * * * * * -->
|
||||||
|
<!-- * * * * * * * * * * and can be replaced. * * * * * * * * * * -->
|
||||||
|
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
||||||
|
<!-- * * * * * * * * * Delete the template below * * * * * * * * * -->
|
||||||
|
<!-- * * * * * * * to get started with your project! * * * * * * * -->
|
||||||
|
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:host {
|
||||||
|
--bright-blue: oklch(51.01% 0.274 263.83);
|
||||||
|
--electric-violet: oklch(53.18% 0.28 296.97);
|
||||||
|
--french-violet: oklch(47.66% 0.246 305.88);
|
||||||
|
--vivid-pink: oklch(69.02% 0.277 332.77);
|
||||||
|
--hot-red: oklch(61.42% 0.238 15.34);
|
||||||
|
--orange-red: oklch(63.32% 0.24 31.68);
|
||||||
|
|
||||||
|
--gray-900: oklch(19.37% 0.006 300.98);
|
||||||
|
--gray-700: oklch(36.98% 0.014 302.71);
|
||||||
|
--gray-400: oklch(70.9% 0.015 304.04);
|
||||||
|
|
||||||
|
--red-to-pink-to-purple-vertical-gradient: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
var(--orange-red) 0%,
|
||||||
|
var(--vivid-pink) 50%,
|
||||||
|
var(--electric-violet) 100%
|
||||||
|
);
|
||||||
|
|
||||||
|
--red-to-pink-to-purple-horizontal-gradient: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
var(--orange-red) 0%,
|
||||||
|
var(--vivid-pink) 50%,
|
||||||
|
var(--electric-violet) 100%
|
||||||
|
);
|
||||||
|
|
||||||
|
--pill-accent: var(--bright-blue);
|
||||||
|
|
||||||
|
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||||
|
Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
|
||||||
|
"Segoe UI Symbol";
|
||||||
|
box-sizing: border-box;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3.125rem;
|
||||||
|
color: var(--gray-900);
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 100%;
|
||||||
|
letter-spacing: -0.125rem;
|
||||||
|
margin: 0;
|
||||||
|
font-family: "Inter Tight", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||||
|
Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
|
||||||
|
"Segoe UI Symbol";
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--gray-700);
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem;
|
||||||
|
box-sizing: inherit;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.angular-logo {
|
||||||
|
max-width: 9.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 700px;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content h1 {
|
||||||
|
margin-top: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content p {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
width: 1px;
|
||||||
|
background: var(--red-to-pink-to-purple-vertical-gradient);
|
||||||
|
margin-inline: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
--pill-accent: var(--bright-blue);
|
||||||
|
background: color-mix(in srgb, var(--pill-accent) 5%, transparent);
|
||||||
|
color: var(--pill-accent);
|
||||||
|
padding-inline: 0.75rem;
|
||||||
|
padding-block: 0.375rem;
|
||||||
|
border-radius: 2.75rem;
|
||||||
|
border: 0;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
font-family: var(--inter-font);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.4rem;
|
||||||
|
letter-spacing: -0.00875rem;
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill:hover {
|
||||||
|
background: color-mix(in srgb, var(--pill-accent) 15%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill-group .pill:nth-child(6n + 1) {
|
||||||
|
--pill-accent: var(--bright-blue);
|
||||||
|
}
|
||||||
|
.pill-group .pill:nth-child(6n + 2) {
|
||||||
|
--pill-accent: var(--electric-violet);
|
||||||
|
}
|
||||||
|
.pill-group .pill:nth-child(6n + 3) {
|
||||||
|
--pill-accent: var(--french-violet);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill-group .pill:nth-child(6n + 4),
|
||||||
|
.pill-group .pill:nth-child(6n + 5),
|
||||||
|
.pill-group .pill:nth-child(6n + 6) {
|
||||||
|
--pill-accent: var(--hot-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill-group svg {
|
||||||
|
margin-inline-start: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.73rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links path {
|
||||||
|
transition: fill 0.3s ease;
|
||||||
|
fill: var(--gray-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links a:hover svg path {
|
||||||
|
fill: var(--gray-900);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 650px) {
|
||||||
|
.content {
|
||||||
|
flex-direction: column;
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
height: 1px;
|
||||||
|
width: 100%;
|
||||||
|
background: var(--red-to-pink-to-purple-horizontal-gradient);
|
||||||
|
margin-block: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
<div class="content">
|
||||||
|
<div class="left-side">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 982 239"
|
||||||
|
fill="none"
|
||||||
|
class="angular-logo"
|
||||||
|
>
|
||||||
|
<g clip-path="url(#a)">
|
||||||
|
<path
|
||||||
|
fill="url(#b)"
|
||||||
|
d="M388.676 191.625h30.849L363.31 31.828h-35.758l-56.215 159.797h30.848l13.174-39.356h60.061l13.256 39.356Zm-65.461-62.675 21.602-64.311h1.227l21.602 64.311h-44.431Zm126.831-7.527v70.202h-28.23V71.839h27.002v20.374h1.392c2.782-6.71 7.2-12.028 13.255-15.956 6.056-3.927 13.584-5.89 22.503-5.89 8.264 0 15.465 1.8 21.684 5.318 6.137 3.518 10.964 8.673 14.319 15.382 3.437 6.71 5.074 14.81 4.992 24.383v76.175h-28.23v-71.92c0-8.019-2.046-14.237-6.219-18.819-4.173-4.5-9.819-6.791-17.102-6.791-4.91 0-9.328 1.063-13.174 3.272-3.846 2.128-6.792 5.237-9.001 9.328-2.046 4.009-3.191 8.918-3.191 14.728ZM589.233 239c-10.147 0-18.82-1.391-26.103-4.091-7.282-2.7-13.092-6.382-17.511-10.964-4.418-4.582-7.528-9.655-9.164-15.219l25.448-6.136c1.145 2.372 2.782 4.663 4.991 6.954 2.209 2.291 5.155 4.255 8.837 5.81 3.683 1.554 8.428 2.291 14.074 2.291 8.019 0 14.647-1.964 19.884-5.81 5.237-3.845 7.856-10.227 7.856-19.064v-22.665h-1.391c-1.473 2.946-3.601 5.892-6.383 9.001-2.782 3.109-6.464 5.645-10.965 7.691-4.582 2.046-10.228 3.109-17.101 3.109-9.165 0-17.511-2.209-25.039-6.545-7.446-4.337-13.42-10.883-17.757-19.474-4.418-8.673-6.628-19.473-6.628-32.565 0-13.091 2.21-24.301 6.628-33.383 4.419-9.082 10.311-15.955 17.839-20.7 7.528-4.746 15.874-7.037 25.039-7.037 7.037 0 12.846 1.145 17.347 3.518 4.582 2.373 8.182 5.236 10.883 8.51 2.7 3.272 4.746 6.382 6.137 9.327h1.554v-19.8h27.821v121.749c0 10.228-2.454 18.737-7.364 25.447-4.91 6.709-11.538 11.7-20.048 15.055-8.509 3.355-18.165 4.991-28.884 4.991Zm.245-71.266c5.974 0 11.047-1.473 15.302-4.337 4.173-2.945 7.446-7.118 9.573-12.519 2.21-5.482 3.274-12.027 3.274-19.637 0-7.609-1.064-14.155-3.274-19.8-2.127-5.646-5.318-10.064-9.491-13.255-4.174-3.11-9.329-4.746-15.384-4.746s-11.537 1.636-15.792 4.91c-4.173 3.272-7.365 7.772-9.492 13.418-2.128 5.727-3.191 12.191-3.191 19.392 0 7.2 1.063 13.745 3.273 19.228 2.127 5.482 5.318 9.736 9.573 12.764 4.174 3.027 9.41 4.582 15.629 4.582Zm141.56-26.51V71.839h28.23v119.786h-27.412v-21.273h-1.227c-2.7 6.709-7.119 12.191-13.338 16.446-6.137 4.255-13.747 6.382-22.748 6.382-7.855 0-14.81-1.718-20.783-5.237-5.974-3.518-10.72-8.591-14.075-15.382-3.355-6.709-5.073-14.891-5.073-24.464V71.839h28.312v71.921c0 7.609 2.046 13.664 6.219 18.083 4.173 4.5 9.655 6.709 16.365 6.709 4.173 0 8.183-.982 12.111-3.028 3.927-2.045 7.118-5.072 9.655-9.082 2.537-4.091 3.764-9.164 3.764-15.218Zm65.707-109.395v159.796h-28.23V31.828h28.23Zm44.841 162.169c-7.61 0-14.402-1.391-20.457-4.091-6.055-2.7-10.883-6.791-14.32-12.109-3.518-5.319-5.237-11.946-5.237-19.801 0-6.791 1.228-12.355 3.765-16.773 2.536-4.419 5.891-7.937 10.228-10.637 4.337-2.618 9.164-4.664 14.647-6.055 5.4-1.391 11.046-2.373 16.856-3.027 7.037-.737 12.683-1.391 17.102-1.964 4.337-.573 7.528-1.555 9.574-2.782 1.963-1.309 3.027-3.273 3.027-5.973v-.491c0-5.891-1.718-10.391-5.237-13.664-3.518-3.191-8.51-4.828-15.056-4.828-6.955 0-12.356 1.473-16.447 4.5-4.009 3.028-6.71 6.546-8.183 10.719l-26.348-3.764c2.046-7.282 5.483-13.336 10.31-18.328 4.746-4.909 10.638-8.59 17.511-11.045 6.955-2.455 14.565-3.682 22.912-3.682 5.809 0 11.537.654 17.265 2.045s10.965 3.6 15.711 6.71c4.746 3.109 8.51 7.282 11.455 12.6 2.864 5.318 4.337 11.946 4.337 19.883v80.184h-27.166v-16.446h-.9c-1.719 3.355-4.092 6.464-7.201 9.328-3.109 2.864-6.955 5.237-11.619 6.955-4.828 1.718-10.229 2.536-16.529 2.536Zm7.364-20.701c5.646 0 10.556-1.145 14.729-3.354 4.173-2.291 7.364-5.237 9.655-9.001 2.292-3.763 3.355-7.854 3.355-12.273v-14.155c-.9.737-2.373 1.391-4.5 2.046-2.128.654-4.419 1.145-7.037 1.636-2.619.491-5.155.9-7.692 1.227-2.537.328-4.746.655-6.628.901-4.173.572-8.019 1.472-11.292 2.781-3.355 1.31-5.973 3.11-7.855 5.401-1.964 2.291-2.864 5.318-2.864 8.918 0 5.237 1.882 9.164 5.728 11.782 3.682 2.782 8.51 4.091 14.401 4.091Zm64.643 18.328V71.839h27.412v19.965h1.227c2.21-6.955 5.974-12.274 11.292-16.038 5.319-3.763 11.456-5.645 18.329-5.645 1.555 0 3.355.082 5.237.163 1.964.164 3.601.328 4.91.573v25.938c-1.227-.41-3.109-.819-5.646-1.146a58.814 58.814 0 0 0-7.446-.49c-5.155 0-9.738 1.145-13.829 3.354-4.091 2.209-7.282 5.236-9.655 9.164-2.373 3.927-3.519 8.427-3.519 13.5v70.448h-28.312ZM222.077 39.192l-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="url(#c)"
|
||||||
|
d="M388.676 191.625h30.849L363.31 31.828h-35.758l-56.215 159.797h30.848l13.174-39.356h60.061l13.256 39.356Zm-65.461-62.675 21.602-64.311h1.227l21.602 64.311h-44.431Zm126.831-7.527v70.202h-28.23V71.839h27.002v20.374h1.392c2.782-6.71 7.2-12.028 13.255-15.956 6.056-3.927 13.584-5.89 22.503-5.89 8.264 0 15.465 1.8 21.684 5.318 6.137 3.518 10.964 8.673 14.319 15.382 3.437 6.71 5.074 14.81 4.992 24.383v76.175h-28.23v-71.92c0-8.019-2.046-14.237-6.219-18.819-4.173-4.5-9.819-6.791-17.102-6.791-4.91 0-9.328 1.063-13.174 3.272-3.846 2.128-6.792 5.237-9.001 9.328-2.046 4.009-3.191 8.918-3.191 14.728ZM589.233 239c-10.147 0-18.82-1.391-26.103-4.091-7.282-2.7-13.092-6.382-17.511-10.964-4.418-4.582-7.528-9.655-9.164-15.219l25.448-6.136c1.145 2.372 2.782 4.663 4.991 6.954 2.209 2.291 5.155 4.255 8.837 5.81 3.683 1.554 8.428 2.291 14.074 2.291 8.019 0 14.647-1.964 19.884-5.81 5.237-3.845 7.856-10.227 7.856-19.064v-22.665h-1.391c-1.473 2.946-3.601 5.892-6.383 9.001-2.782 3.109-6.464 5.645-10.965 7.691-4.582 2.046-10.228 3.109-17.101 3.109-9.165 0-17.511-2.209-25.039-6.545-7.446-4.337-13.42-10.883-17.757-19.474-4.418-8.673-6.628-19.473-6.628-32.565 0-13.091 2.21-24.301 6.628-33.383 4.419-9.082 10.311-15.955 17.839-20.7 7.528-4.746 15.874-7.037 25.039-7.037 7.037 0 12.846 1.145 17.347 3.518 4.582 2.373 8.182 5.236 10.883 8.51 2.7 3.272 4.746 6.382 6.137 9.327h1.554v-19.8h27.821v121.749c0 10.228-2.454 18.737-7.364 25.447-4.91 6.709-11.538 11.7-20.048 15.055-8.509 3.355-18.165 4.991-28.884 4.991Zm.245-71.266c5.974 0 11.047-1.473 15.302-4.337 4.173-2.945 7.446-7.118 9.573-12.519 2.21-5.482 3.274-12.027 3.274-19.637 0-7.609-1.064-14.155-3.274-19.8-2.127-5.646-5.318-10.064-9.491-13.255-4.174-3.11-9.329-4.746-15.384-4.746s-11.537 1.636-15.792 4.91c-4.173 3.272-7.365 7.772-9.492 13.418-2.128 5.727-3.191 12.191-3.191 19.392 0 7.2 1.063 13.745 3.273 19.228 2.127 5.482 5.318 9.736 9.573 12.764 4.174 3.027 9.41 4.582 15.629 4.582Zm141.56-26.51V71.839h28.23v119.786h-27.412v-21.273h-1.227c-2.7 6.709-7.119 12.191-13.338 16.446-6.137 4.255-13.747 6.382-22.748 6.382-7.855 0-14.81-1.718-20.783-5.237-5.974-3.518-10.72-8.591-14.075-15.382-3.355-6.709-5.073-14.891-5.073-24.464V71.839h28.312v71.921c0 7.609 2.046 13.664 6.219 18.083 4.173 4.5 9.655 6.709 16.365 6.709 4.173 0 8.183-.982 12.111-3.028 3.927-2.045 7.118-5.072 9.655-9.082 2.537-4.091 3.764-9.164 3.764-15.218Zm65.707-109.395v159.796h-28.23V31.828h28.23Zm44.841 162.169c-7.61 0-14.402-1.391-20.457-4.091-6.055-2.7-10.883-6.791-14.32-12.109-3.518-5.319-5.237-11.946-5.237-19.801 0-6.791 1.228-12.355 3.765-16.773 2.536-4.419 5.891-7.937 10.228-10.637 4.337-2.618 9.164-4.664 14.647-6.055 5.4-1.391 11.046-2.373 16.856-3.027 7.037-.737 12.683-1.391 17.102-1.964 4.337-.573 7.528-1.555 9.574-2.782 1.963-1.309 3.027-3.273 3.027-5.973v-.491c0-5.891-1.718-10.391-5.237-13.664-3.518-3.191-8.51-4.828-15.056-4.828-6.955 0-12.356 1.473-16.447 4.5-4.009 3.028-6.71 6.546-8.183 10.719l-26.348-3.764c2.046-7.282 5.483-13.336 10.31-18.328 4.746-4.909 10.638-8.59 17.511-11.045 6.955-2.455 14.565-3.682 22.912-3.682 5.809 0 11.537.654 17.265 2.045s10.965 3.6 15.711 6.71c4.746 3.109 8.51 7.282 11.455 12.6 2.864 5.318 4.337 11.946 4.337 19.883v80.184h-27.166v-16.446h-.9c-1.719 3.355-4.092 6.464-7.201 9.328-3.109 2.864-6.955 5.237-11.619 6.955-4.828 1.718-10.229 2.536-16.529 2.536Zm7.364-20.701c5.646 0 10.556-1.145 14.729-3.354 4.173-2.291 7.364-5.237 9.655-9.001 2.292-3.763 3.355-7.854 3.355-12.273v-14.155c-.9.737-2.373 1.391-4.5 2.046-2.128.654-4.419 1.145-7.037 1.636-2.619.491-5.155.9-7.692 1.227-2.537.328-4.746.655-6.628.901-4.173.572-8.019 1.472-11.292 2.781-3.355 1.31-5.973 3.11-7.855 5.401-1.964 2.291-2.864 5.318-2.864 8.918 0 5.237 1.882 9.164 5.728 11.782 3.682 2.782 8.51 4.091 14.401 4.091Zm64.643 18.328V71.839h27.412v19.965h1.227c2.21-6.955 5.974-12.274 11.292-16.038 5.319-3.763 11.456-5.645 18.329-5.645 1.555 0 3.355.082 5.237.163 1.964.164 3.601.328 4.91.573v25.938c-1.227-.41-3.109-.819-5.646-1.146a58.814 58.814 0 0 0-7.446-.49c-5.155 0-9.738 1.145-13.829 3.354-4.091 2.209-7.282 5.236-9.655 9.164-2.373 3.927-3.519 8.427-3.519 13.5v70.448h-28.312ZM222.077 39.192l-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<radialGradient
|
||||||
|
id="c"
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
r="1"
|
||||||
|
gradientTransform="rotate(118.122 171.182 60.81) scale(205.794)"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop stop-color="#FF41F8" />
|
||||||
|
<stop offset=".707" stop-color="#FF41F8" stop-opacity=".5" />
|
||||||
|
<stop offset="1" stop-color="#FF41F8" stop-opacity="0" />
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="b"
|
||||||
|
x1="0"
|
||||||
|
x2="982"
|
||||||
|
y1="192"
|
||||||
|
y2="192"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop stop-color="#F0060B" />
|
||||||
|
<stop offset="0" stop-color="#F0070C" />
|
||||||
|
<stop offset=".526" stop-color="#CC26D5" />
|
||||||
|
<stop offset="1" stop-color="#7702FF" />
|
||||||
|
</linearGradient>
|
||||||
|
<clipPath id="a"><path fill="#fff" d="M0 0h982v239H0z" /></clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
<h1>Hello, {{ title() }}</h1>
|
||||||
|
<p>Congratulations! Your app is running. 🎉</p>
|
||||||
|
</div>
|
||||||
|
<div class="divider" role="separator" aria-label="Divider"></div>
|
||||||
|
<div class="right-side">
|
||||||
|
<div class="pill-group">
|
||||||
|
@for (item of [
|
||||||
|
{ title: 'Explore the Docs', link: 'https://angular.dev' },
|
||||||
|
{ title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' },
|
||||||
|
{ title: 'Prompt and best practices for AI', link: 'https://angular.dev/ai/develop-with-ai'},
|
||||||
|
{ title: 'CLI Docs', link: 'https://angular.dev/tools/cli' },
|
||||||
|
{ title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' },
|
||||||
|
{ title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' },
|
||||||
|
]; track item.title) {
|
||||||
|
<a
|
||||||
|
class="pill"
|
||||||
|
[href]="item.link"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
>
|
||||||
|
<span>{{ item.title }}</span>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 -960 960 960"
|
||||||
|
width="14"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="social-links">
|
||||||
|
<a
|
||||||
|
href="https://github.com/angular/angular"
|
||||||
|
aria-label="Github"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="25"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 25 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
alt="Github"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12.3047 0C5.50634 0 0 5.50942 0 12.3047C0 17.7423 3.52529 22.3535 8.41332 23.9787C9.02856 24.0946 9.25414 23.7142 9.25414 23.3871C9.25414 23.0949 9.24389 22.3207 9.23876 21.2953C5.81601 22.0377 5.09414 19.6444 5.09414 19.6444C4.53427 18.2243 3.72524 17.8449 3.72524 17.8449C2.61064 17.082 3.81137 17.0973 3.81137 17.0973C5.04697 17.1835 5.69604 18.3647 5.69604 18.3647C6.79321 20.2463 8.57636 19.7029 9.27978 19.3881C9.39052 18.5924 9.70736 18.0499 10.0591 17.7423C7.32641 17.4347 4.45429 16.3765 4.45429 11.6618C4.45429 10.3185 4.9311 9.22133 5.72065 8.36C5.58222 8.04931 5.16694 6.79833 5.82831 5.10337C5.82831 5.10337 6.85883 4.77319 9.2121 6.36459C10.1965 6.09082 11.2424 5.95546 12.2883 5.94931C13.3342 5.95546 14.3801 6.09082 15.3644 6.36459C17.7023 4.77319 18.7328 5.10337 18.7328 5.10337C19.3942 6.79833 18.9789 8.04931 18.8559 8.36C19.6403 9.22133 20.1171 10.3185 20.1171 11.6618C20.1171 16.3888 17.2409 17.4296 14.5031 17.7321C14.9338 18.1012 15.3337 18.8559 15.3337 20.0084C15.3337 21.6552 15.3183 22.978 15.3183 23.3779C15.3183 23.7009 15.5336 24.0854 16.1642 23.9623C21.0871 22.3484 24.6094 17.7341 24.6094 12.3047C24.6094 5.50942 19.0999 0 12.3047 0Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://twitter.com/angular"
|
||||||
|
aria-label="Twitter"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
alt="Twitter"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://www.youtube.com/channel/UCbn1OgGei-DV7aSRo_HaAiw"
|
||||||
|
aria-label="Youtube"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="29"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 29 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
alt="Youtube"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M27.4896 1.52422C27.9301 1.96749 28.2463 2.51866 28.4068 3.12258C29.0004 5.35161 29.0004 10 29.0004 10C29.0004 10 29.0004 14.6484 28.4068 16.8774C28.2463 17.4813 27.9301 18.0325 27.4896 18.4758C27.0492 18.9191 26.5 19.2389 25.8972 19.4032C23.6778 20 14.8068 20 14.8068 20C14.8068 20 5.93586 20 3.71651 19.4032C3.11363 19.2389 2.56449 18.9191 2.12405 18.4758C1.68361 18.0325 1.36732 17.4813 1.20683 16.8774C0.613281 14.6484 0.613281 10 0.613281 10C0.613281 10 0.613281 5.35161 1.20683 3.12258C1.36732 2.51866 1.68361 1.96749 2.12405 1.52422C2.56449 1.08095 3.11363 0.76113 3.71651 0.596774C5.93586 0 14.8068 0 14.8068 0C14.8068 0 23.6778 0 25.8972 0.596774C26.5 0.76113 27.0492 1.08095 27.4896 1.52422ZM19.3229 10L11.9036 5.77905V14.221L19.3229 10Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
||||||
|
<!-- * * * * * * * * * * * The content above * * * * * * * * * * * * -->
|
||||||
|
<!-- * * * * * * * * * * is only a placeholder * * * * * * * * * * * -->
|
||||||
|
<!-- * * * * * * * * * * and can be replaced. * * * * * * * * * * * -->
|
||||||
|
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
||||||
|
<!-- * * * * * * * * * * End of Placeholder * * * * * * * * * * * * -->
|
||||||
|
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
||||||
|
|
||||||
|
|
||||||
|
<router-outlet />
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { Routes } from '@angular/router';
|
||||||
|
|
||||||
|
export const routes: Routes = [];
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { provideZonelessChangeDetection } from '@angular/core';
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { App } from './app';
|
||||||
|
|
||||||
|
describe('App', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [App],
|
||||||
|
providers: [provideZonelessChangeDetection()]
|
||||||
|
}).compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create the app', () => {
|
||||||
|
const fixture = TestBed.createComponent(App);
|
||||||
|
const app = fixture.componentInstance;
|
||||||
|
expect(app).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render title', () => {
|
||||||
|
const fixture = TestBed.createComponent(App);
|
||||||
|
fixture.detectChanges();
|
||||||
|
const compiled = fixture.nativeElement as HTMLElement;
|
||||||
|
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, kontor-angular');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Component, signal } from '@angular/core';
|
||||||
|
import { RouterOutlet } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
imports: [RouterOutlet],
|
||||||
|
templateUrl: './app.html',
|
||||||
|
styleUrl: './app.scss'
|
||||||
|
})
|
||||||
|
export class App {
|
||||||
|
protected readonly title = signal('kontor-angular');
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>KontorAngular</title>
|
||||||
|
<base href="/">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<app-root></app-root>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { bootstrapApplication } from '@angular/platform-browser';
|
||||||
|
import { appConfig } from './app/app.config';
|
||||||
|
import { App } from './app/app';
|
||||||
|
|
||||||
|
bootstrapApplication(App, appConfig)
|
||||||
|
.catch((err) => console.error(err));
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* You can add global styles to this file, and also import other style files */
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||||
|
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./out-tsc/app",
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"src/**/*.spec.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||||
|
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||||
|
{
|
||||||
|
"compileOnSave": false,
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"importHelpers": true,
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "preserve"
|
||||||
|
},
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
"strictInjectionParameters": true,
|
||||||
|
"strictInputAccessModifiers": true,
|
||||||
|
"typeCheckHostBindings": true,
|
||||||
|
"strictTemplates": true
|
||||||
|
},
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.spec.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||||
|
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./out-tsc/spec",
|
||||||
|
"types": [
|
||||||
|
"jasmine"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -1 +1,4 @@
|
|||||||
.env
|
.env
|
||||||
|
.coverage
|
||||||
|
app.log
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
## ------------------------------- Builder Stage ------------------------------ ##
|
## ------------------------------- Builder Stage ------------------------------ ##
|
||||||
FROM python:3.13-bookworm AS builder
|
FROM python:3.13-bookworm AS builder
|
||||||
|
|
||||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
RUN apt-get update && apt-get install --no-install-recommends -y build-essential && \
|
||||||
build-essential libmariadb-dev && \
|
|
||||||
apt-get clean && rm -rf /var/lib/apt/lists/*
|
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Download the latest installer, install it and then remove it
|
# Download the latest installer, install it and then remove it
|
||||||
@@ -42,7 +41,6 @@ WORKDIR /app
|
|||||||
|
|
||||||
COPY /src src
|
COPY /src src
|
||||||
COPY --from=builder /app/.venv .venv
|
COPY --from=builder /app/.venv .venv
|
||||||
COPY --from=builder /usr/lib/x86_64-linux-gnu/libmariadb.so.3 /usr/lib/x86_64-linux-gnu
|
|
||||||
|
|
||||||
# Set up environment variables for production
|
# Set up environment variables for production
|
||||||
ENV PATH="/app/.venv/bin:$PATH"
|
ENV PATH="/app/.venv/bin:$PATH"
|
||||||
|
|||||||
+3
-3
@@ -4,11 +4,11 @@ clean:
|
|||||||
find . -name '*.py[co]' -delete
|
find . -name '*.py[co]' -delete
|
||||||
|
|
||||||
test:
|
test:
|
||||||
DB_HOST=localhost uv run pytest -v --cov --cov-report=term --cov-report=html:coverage-report
|
DB_SERVER=localhost uv run pytest -v --cov --cov-report=term --cov-report=html:coverage-report
|
||||||
|
|
||||||
docker: clean
|
docker: clean
|
||||||
docker build --target=production -t kontor-api -t kontor-api:0.1.0 -t kontor-api .
|
docker build --target=production -t kontor-api:0.2.0-SNAPSHOT .
|
||||||
|
|
||||||
dev:
|
dev:
|
||||||
MARIADB_SERVER=localhost uv run fastapi dev src/main.py --port 8008
|
DB_SERVER=127.0.0.1 uv run fastapi dev src/main.py --port 8008
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ dependencies = [
|
|||||||
"beautifulsoup4>=4.13.4",
|
"beautifulsoup4>=4.13.4",
|
||||||
"fastapi[standard]>=0.115.12",
|
"fastapi[standard]>=0.115.12",
|
||||||
"httpx==0.24.1",
|
"httpx==0.24.1",
|
||||||
"mariadb>=1.1.12",
|
|
||||||
"sqlalchemy>=2.0.40",
|
"sqlalchemy>=2.0.40",
|
||||||
"platformdirs>=4.3.7",
|
"platformdirs>=4.3.7",
|
||||||
"pathlib>=1.0.1",
|
"pathlib>=1.0.1",
|
||||||
@@ -22,4 +21,11 @@ dependencies = [
|
|||||||
"python-jose>=3.4.0",
|
"python-jose>=3.4.0",
|
||||||
"python-multipart>=0.0.20",
|
"python-multipart>=0.0.20",
|
||||||
"natsort>=8.4.0",
|
"natsort>=8.4.0",
|
||||||
|
"psycopg2-binary>=2.9.10",
|
||||||
|
"pytest-cov>=6.1.1",
|
||||||
|
"databases[sqlite]>=0.9.0",
|
||||||
|
"pydantic[email]>=2.11.3",
|
||||||
|
"jinja2>=3.1.6",
|
||||||
|
"asyncpg>=0.30.0",
|
||||||
|
"bcrypt>=4.3.0",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from src.apis.version1 import comic, media, tysc
|
from src.apis.version1 import comic, mediaactor, mediafile, tysc, admin
|
||||||
|
|
||||||
api_router = APIRouter(prefix="/api")
|
api_router = APIRouter(prefix="/api")
|
||||||
api_router.include_router(comic.router, prefix="/comics", tags=["comics"])
|
api_router.include_router(comic.router, prefix="/comics", tags=["comics"])
|
||||||
api_router.include_router(media.router, prefix="/media", tags=["media"])
|
api_router.include_router(mediafile.router, prefix="/media", tags=["media"])
|
||||||
|
api_router.include_router(mediaactor.router, prefix="/media", tags=["media"])
|
||||||
api_router.include_router(tysc.router, prefix="/tysc", tags=["tysc"])
|
api_router.include_router(tysc.router, prefix="/tysc", tags=["tysc"])
|
||||||
|
api_router.include_router(admin.router, prefix="/login", tags=["login"])
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
from typing import Dict
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from fastapi import Request
|
||||||
|
from fastapi import status
|
||||||
|
from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel
|
||||||
|
from fastapi.security import OAuth2
|
||||||
|
from fastapi.security.utils import get_authorization_scheme_param
|
||||||
|
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
@@ -6,3 +15,34 @@ from sqlalchemy.orm import Session
|
|||||||
from src.db.session import get_db
|
from src.db.session import get_db
|
||||||
|
|
||||||
SessionDep = Annotated[Session, Depends(get_db)]
|
SessionDep = Annotated[Session, Depends(get_db)]
|
||||||
|
|
||||||
|
|
||||||
|
class OAuth2PasswordBearerWithCookie(OAuth2):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
tokenUrl: str,
|
||||||
|
scheme_name: Optional[str] = None,
|
||||||
|
scopes: Optional[Dict[str, str]] = None,
|
||||||
|
auto_error: bool = True,
|
||||||
|
):
|
||||||
|
if not scopes:
|
||||||
|
scopes = {}
|
||||||
|
flows = OAuthFlowsModel(password={"tokenUrl": tokenUrl, "scopes": scopes})
|
||||||
|
super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error)
|
||||||
|
|
||||||
|
async def __call__(self, request: Request) -> Optional[str]:
|
||||||
|
authorization: str = request.cookies.get(
|
||||||
|
"access_token"
|
||||||
|
) # changed to accept access token from httpOnly Cookie
|
||||||
|
|
||||||
|
scheme, param = get_authorization_scheme_param(authorization)
|
||||||
|
if not authorization or scheme.lower() != "bearer":
|
||||||
|
if self.auto_error:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Not authenticated",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
return param
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import bcrypt
|
||||||
|
from fastapi import APIRouter, HTTPException, status, Response, Depends
|
||||||
|
from fastapi.security import OAuth2PasswordRequestForm
|
||||||
|
from jose import jwt, JWTError
|
||||||
|
from src.apis.utils import SessionDep, OAuth2PasswordBearerWithCookie
|
||||||
|
from src.core.config import settings
|
||||||
|
from src.core.security import create_access_token
|
||||||
|
from src.db.models.admin import Profile
|
||||||
|
from src.db.repository.admin import get_profile
|
||||||
|
from src.schema.admin import Token
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
def authenticate_user(username: str, password: str, db: SessionDep) -> Profile | None:
|
||||||
|
user = get_profile(username=username, db=db)
|
||||||
|
print(user)
|
||||||
|
if not user:
|
||||||
|
return None
|
||||||
|
if bcrypt.checkpw(password.encode(), user.password.encode()):
|
||||||
|
print("User successful authenticated")
|
||||||
|
else:
|
||||||
|
logging.info("Authentication failed!")
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/token", response_model=Token)
|
||||||
|
def login_for_access_token(response: Response, db: SessionDep, form_data: OAuth2PasswordRequestForm = Depends()):
|
||||||
|
user = authenticate_user(form_data.username, form_data.password, db)
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Incorrect username or password",
|
||||||
|
)
|
||||||
|
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||||
|
access_token = create_access_token(
|
||||||
|
data={"sub": user.email}, expires_delta=access_token_expires
|
||||||
|
)
|
||||||
|
response.set_cookie(
|
||||||
|
key="access_token", value=f"Bearer {access_token}", httponly=True
|
||||||
|
)
|
||||||
|
return {"access_token": access_token, "token_type": "bearer"}
|
||||||
|
|
||||||
|
|
||||||
|
oauth2_scheme = OAuth2PasswordBearerWithCookie(tokenUrl="/api/login/token")
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_user_from_token(db: SessionDep, token: str = Depends(oauth2_scheme)):
|
||||||
|
credentials_exception = HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Could not validate credentials",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(
|
||||||
|
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
|
||||||
|
)
|
||||||
|
username: str = payload.get("sub")
|
||||||
|
print("username/email extracted is ", username)
|
||||||
|
if username is None:
|
||||||
|
raise credentials_exception
|
||||||
|
except JWTError:
|
||||||
|
raise credentials_exception
|
||||||
|
user = get_profile(username=username, db=db)
|
||||||
|
if user is None:
|
||||||
|
raise credentials_exception
|
||||||
|
return user
|
||||||
@@ -1,31 +1,28 @@
|
|||||||
from uuid import UUID
|
from typing import List, AnyStr
|
||||||
from typing import List
|
|
||||||
from fastapi import APIRouter, HTTPException, status
|
from fastapi import APIRouter, HTTPException, status
|
||||||
from sqlalchemy import select
|
|
||||||
|
|
||||||
from src.apis.utils import SessionDep
|
from src.apis.utils import SessionDep
|
||||||
|
from src.db.repository.comics.artist import get_artist_details
|
||||||
|
from src.db.repository.comics.comic import list_comics, get_issue_details
|
||||||
from src.schema.comics.comic import ComicResponse, ComicDetailsResponse, get_comic_details, get_short_info
|
from src.schema.comics.comic import ComicResponse, ComicDetailsResponse, get_comic_details, get_short_info
|
||||||
from src.schema.comics.artist import ArtistCreation, ArtistDetailResponse, ArtistResponse, get_artist_details
|
from src.schema.comics.artist import ArtistCreation, ArtistDetailResponse, ArtistResponse
|
||||||
from src.db.models.comic import Comic, Artist
|
from src.db.models.comic import Comic, Artist, Issue
|
||||||
|
from src.schema.comics.issue import IssueDetailsResponse
|
||||||
|
|
||||||
router = APIRouter(
|
router = APIRouter()
|
||||||
prefix="/comic",
|
|
||||||
tags=["comics"],
|
|
||||||
responses={404: {"description": "Not found"}},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/comics")
|
@router.get("/comics")
|
||||||
def get_all_comics(db: SessionDep) -> List[ComicResponse]:
|
def get_all_comics(db: SessionDep) -> List[ComicResponse]:
|
||||||
results: List[ComicResponse] = []
|
results: List[ComicResponse] = []
|
||||||
comics = db.scalars(select(Comic)).all()
|
comics = list_comics(db)
|
||||||
for comic in comics:
|
for comic in comics:
|
||||||
response = get_short_info(comic)
|
response = get_short_info(comic)
|
||||||
results.append(response)
|
results.append(response)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@router.get("/comics/{comic_id}", response_model=ComicDetailsResponse)
|
@router.get("/comics/{comic_id}", response_model=ComicDetailsResponse)
|
||||||
def get_comic(comic_id: UUID, db: SessionDep) -> ComicDetailsResponse:
|
def get_comic(comic_id: AnyStr, db: SessionDep) -> ComicDetailsResponse:
|
||||||
comic = db.get(Comic, comic_id)
|
comic = db.get(Comic, comic_id)
|
||||||
if comic is None:
|
if comic is None:
|
||||||
raise HTTPException(status_code=404, detail="Comic could not be found")
|
raise HTTPException(status_code=404, detail="Comic could not be found")
|
||||||
@@ -41,7 +38,7 @@ def get_all_artists(db: SessionDep) -> List[ArtistResponse]:
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
@router.get("/artists/{artist_id}", response_model=ArtistDetailResponse)
|
@router.get("/artists/{artist_id}", response_model=ArtistDetailResponse)
|
||||||
def get_artist(artist_id: UUID, db: SessionDep) -> ArtistDetailResponse:
|
def get_artist(artist_id: AnyStr, db: SessionDep) -> ArtistDetailResponse:
|
||||||
artist = db.get(Artist, artist_id)
|
artist = db.get(Artist, artist_id)
|
||||||
if artist is None:
|
if artist is None:
|
||||||
raise HTTPException(status_code=404, detail="Artist could not be found")
|
raise HTTPException(status_code=404, detail="Artist could not be found")
|
||||||
@@ -60,3 +57,11 @@ def add_artist(db: SessionDep, artist_creation: ArtistCreation) -> ArtistRespons
|
|||||||
response = ArtistResponse(id=artist.id, name=artist.name)
|
response = ArtistResponse(id=artist.id, name=artist.name)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@router.get("/issues", response_model=List[IssueDetailsResponse])
|
||||||
|
def get_issues(db: SessionDep) -> List[IssueDetailsResponse]:
|
||||||
|
results: List[IssueDetailsResponse] = []
|
||||||
|
issues = db.query(Issue).all()
|
||||||
|
for issue in issues:
|
||||||
|
results.append(get_issue_details(issue))
|
||||||
|
return results
|
||||||
|
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
from typing import List
|
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from fastapi import APIRouter, status, HTTPException
|
|
||||||
from sqlalchemy import select, Sequence
|
|
||||||
|
|
||||||
from src.apis.utils import SessionDep
|
|
||||||
from src.schema.media.file import MediaFileResponse, Link, get_file_details, set_file
|
|
||||||
from src.db.models.media import MediaFile
|
|
||||||
|
|
||||||
router = APIRouter(
|
|
||||||
prefix="/media",
|
|
||||||
tags=["media"]
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.get("/update-titles")
|
|
||||||
def update_titles(db: SessionDep) -> list[MediaFileResponse]:
|
|
||||||
results: list[MediaFileResponse] = []
|
|
||||||
files = db.query(MediaFile).filter(MediaFile.review == 1).all()
|
|
||||||
for mediafile in files:
|
|
||||||
mediafile.update_title()
|
|
||||||
db.add(mediafile)
|
|
||||||
response = get_file_details(mediafile)
|
|
||||||
results.append(response)
|
|
||||||
db.commit()
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/files", response_model=List[MediaFileResponse])
|
|
||||||
def get_all_files(db: SessionDep, review: bool = False, download: bool = False) -> List[MediaFileResponse]:
|
|
||||||
results: list[MediaFileResponse] = []
|
|
||||||
files: Sequence[MediaFile]
|
|
||||||
if review:
|
|
||||||
files = db.query(MediaFile).filter(MediaFile.review == 1).all()
|
|
||||||
elif download:
|
|
||||||
files = db.query(MediaFile).filter(MediaFile.should_download == 1).all()
|
|
||||||
else:
|
|
||||||
files = db.scalars(select(MediaFile)).all()
|
|
||||||
for mediafile in files:
|
|
||||||
response = get_file_details(mediafile)
|
|
||||||
results.append(response)
|
|
||||||
return results
|
|
||||||
|
|
||||||
@router.get("/files/{file_id}", response_model=MediaFileResponse)
|
|
||||||
def get_file(file_id: UUID, db: SessionDep) -> MediaFileResponse:
|
|
||||||
mediafile = db.get(MediaFile, file_id)
|
|
||||||
if not mediafile:
|
|
||||||
raise HTTPException(status_code=404, detail="MediaFile could not be found")
|
|
||||||
response = get_file_details(mediafile)
|
|
||||||
return response
|
|
||||||
|
|
||||||
@router.put("/files/{file_id}", response_model=MediaFileResponse)
|
|
||||||
def update_file(file_id: UUID, db: SessionDep, info: MediaFileResponse) -> MediaFileResponse:
|
|
||||||
mediaFile = db.get(MediaFile, file_id)
|
|
||||||
if not mediaFile:
|
|
||||||
raise HTTPException(status_code=404, detail="MediaFile could not be found")
|
|
||||||
set_file(info, mediaFile)
|
|
||||||
db.add(mediaFile)
|
|
||||||
db.commit()
|
|
||||||
return info
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/files", status_code=status.HTTP_201_CREATED)
|
|
||||||
def add_file(new_link: Link, db: SessionDep) -> MediaFileResponse:
|
|
||||||
print(new_link.url)
|
|
||||||
try:
|
|
||||||
mediaFile: MediaFile = MediaFile()
|
|
||||||
setattr(mediaFile, "url", new_link.url)
|
|
||||||
setattr(mediaFile, "review", 1)
|
|
||||||
setattr(mediaFile, "should_download", 1)
|
|
||||||
db.add(mediaFile)
|
|
||||||
db.commit()
|
|
||||||
except:
|
|
||||||
raise HTTPException(status_code=409, detail="Link duplicate")
|
|
||||||
response = get_file_details(mediaFile)
|
|
||||||
return response
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
from typing import List, AnyStr
|
||||||
|
|
||||||
|
from fastapi import APIRouter, status, HTTPException, Depends
|
||||||
|
from sqlalchemy import select, Sequence
|
||||||
|
from src.core.log_conf import logger
|
||||||
|
from src.apis.utils import SessionDep
|
||||||
|
from src.db.repository.media import create_new_mediafile
|
||||||
|
from src.schema.media.actor import MediaActorResponse
|
||||||
|
from src.db.models.media import MediaActor
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get("/actors", response_model=List[MediaActorResponse])
|
||||||
|
#def get_all_files(db: SessionDep, review: bool = False, download: bool = False, current_user: Profile = Depends(get_current_user_from_token)) -> List[MediaFileResponse]:
|
||||||
|
def get_all_files(db: SessionDep, review: bool = False, download: bool = False) -> List[MediaActorResponse]:
|
||||||
|
results: List[MediaActorResponse] = []
|
||||||
|
actors = db.scalars(select(MediaActor)).all()
|
||||||
|
for mediaactor in actors:
|
||||||
|
response = MediaActorResponse(id=mediaactor.id, name=str(mediaactor.name), url=str(mediaactor.url))
|
||||||
|
results.append(response)
|
||||||
|
return results
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
from typing import List, AnyStr
|
||||||
|
|
||||||
|
from fastapi import APIRouter, status, HTTPException, Depends
|
||||||
|
from sqlalchemy import select, Sequence
|
||||||
|
from src.core.log_conf import logger
|
||||||
|
from src.apis.utils import SessionDep
|
||||||
|
from src.db.repository.media import create_new_mediaactorfile, create_new_mediafile
|
||||||
|
from src.schema.media.actor import MediaActorResponse
|
||||||
|
from src.schema.media.actorfile import MediaActorFileResponse
|
||||||
|
from src.schema.media.file import MediaFileResponse, Link, get_file_details, set_file
|
||||||
|
from src.db.models.media import MediaFile
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get("/update-titles")
|
||||||
|
def update_titles(db: SessionDep) -> list[MediaFileResponse]:
|
||||||
|
results: list[MediaFileResponse] = []
|
||||||
|
files = db.query(MediaFile).filter(MediaFile.review == True).all()
|
||||||
|
for mediafile in files:
|
||||||
|
mediafile.update_title()
|
||||||
|
db.add(mediafile)
|
||||||
|
response = get_file_details(mediafile)
|
||||||
|
results.append(response)
|
||||||
|
db.commit()
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/files", response_model=List[MediaFileResponse])
|
||||||
|
#def get_all_files(db: SessionDep, review: bool = False, download: bool = False, current_user: Profile = Depends(get_current_user_from_token)) -> List[MediaFileResponse]:
|
||||||
|
def get_all_files(db: SessionDep, review: bool = False, download: bool = False) -> List[MediaFileResponse]:
|
||||||
|
results: list[MediaFileResponse] = []
|
||||||
|
files: Sequence[MediaFile]
|
||||||
|
if review:
|
||||||
|
files = db.query(MediaFile).filter(MediaFile.review == True).all()
|
||||||
|
elif download:
|
||||||
|
files = db.query(MediaFile).filter(MediaFile.should_download == True).all()
|
||||||
|
else:
|
||||||
|
files = db.scalars(select(MediaFile)).all()
|
||||||
|
for mediafile in files:
|
||||||
|
response = get_file_details(mediafile)
|
||||||
|
results.append(response)
|
||||||
|
return results
|
||||||
|
|
||||||
|
@router.get("/files/{file_id}", response_model=MediaFileResponse)
|
||||||
|
def get_file(file_id: AnyStr, db: SessionDep) -> MediaFileResponse:
|
||||||
|
mediafile = db.get(MediaFile, file_id)
|
||||||
|
if not mediafile:
|
||||||
|
raise HTTPException(status_code=404, detail="MediaFile could not be found")
|
||||||
|
response = get_file_details(mediafile)
|
||||||
|
return response
|
||||||
|
|
||||||
|
@router.get("/files/{file_id}/actors", response_model=List[MediaActorResponse])
|
||||||
|
def get_file_actors(file_id: AnyStr, db: SessionDep) -> List[MediaActorResponse]:
|
||||||
|
mediafile = db.get(MediaFile, file_id)
|
||||||
|
if not mediafile:
|
||||||
|
raise HTTPException(status_code=404, detail="MediaFile could not be found")
|
||||||
|
actor_files = mediafile.media_actor_files
|
||||||
|
logger.info(f"already known actors: {actor_files}")
|
||||||
|
results: List[MediaActorResponse] = []
|
||||||
|
for actor_file in actor_files:
|
||||||
|
response = MediaActorResponse(id=actor_file.media_actor.id, name=actor_file.media_actor.name, url=actor_file.media_actor.url)
|
||||||
|
results.append(response)
|
||||||
|
return results
|
||||||
|
|
||||||
|
@router.put("/files/{file_id}/actors", response_model=List[MediaActorFileResponse])
|
||||||
|
def update_file_actors(file_id: AnyStr, db: SessionDep, actors: List[MediaActorResponse]) -> List[MediaActorFileResponse]:
|
||||||
|
mediafile = db.get(MediaFile, file_id)
|
||||||
|
if not mediafile:
|
||||||
|
raise HTTPException(status_code=404, detail="MediaFile could not be found")
|
||||||
|
actor_files = mediafile.media_actor_files
|
||||||
|
logger.info(f"already known actors: {actor_files}")
|
||||||
|
for actor in actors:
|
||||||
|
already_associated = False
|
||||||
|
for actor_file in actor_files:
|
||||||
|
if actor.id == actor_file.media_actor_id:
|
||||||
|
logger.info("alreay associated - do nothing")
|
||||||
|
already_associated = True
|
||||||
|
break
|
||||||
|
if not already_associated:
|
||||||
|
create_new_mediaactorfile(db, actor.id, mediafile.id)
|
||||||
|
db.refresh(mediafile)
|
||||||
|
actor_files = mediafile.media_actor_files
|
||||||
|
results: List[MediaActorFileResponse] = []
|
||||||
|
for actor_file in actor_files:
|
||||||
|
response = MediaActorFileResponse(id=actor_file.id, actor_id=actor_file.media_actor_id, file_id=actor_file.media_file_id)
|
||||||
|
results.append(response)
|
||||||
|
return results
|
||||||
|
|
||||||
|
@router.put("/files/{file_id}", response_model=MediaFileResponse)
|
||||||
|
def update_file(file_id: AnyStr, db: SessionDep, info: MediaFileResponse) -> MediaFileResponse:
|
||||||
|
mediaFile = db.get(MediaFile, file_id)
|
||||||
|
if not mediaFile:
|
||||||
|
raise HTTPException(status_code=404, detail="MediaFile could not be found")
|
||||||
|
set_file(info, mediaFile)
|
||||||
|
db.add(mediaFile)
|
||||||
|
db.commit()
|
||||||
|
mediafile = db.get(MediaFile, file_id)
|
||||||
|
if not mediafile:
|
||||||
|
raise HTTPException(status_code=404, detail="MediaFile could not be updated")
|
||||||
|
response = get_file_details(mediafile)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/files", status_code=status.HTTP_201_CREATED)
|
||||||
|
def add_file(new_link: Link, db: SessionDep) -> MediaFileResponse:
|
||||||
|
logger.info(f"add url {new_link.url}")
|
||||||
|
try:
|
||||||
|
mediaFile: MediaFile = create_new_mediafile(new_link.url, db)
|
||||||
|
except:
|
||||||
|
raise HTTPException(status_code=409, detail="Link duplicate")
|
||||||
|
response = get_file_details(mediaFile)
|
||||||
|
return response
|
||||||
@@ -5,11 +5,7 @@ from src.apis.utils import SessionDep
|
|||||||
from src.schema.tysc.sport import SportResponse
|
from src.schema.tysc.sport import SportResponse
|
||||||
from src.db.models.tysc import Sport
|
from src.db.models.tysc import Sport
|
||||||
|
|
||||||
router = APIRouter(
|
router = APIRouter()
|
||||||
prefix="/tysc",
|
|
||||||
tags=["tysc"],
|
|
||||||
responses={404: {"description": "Not found"}},
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.get("/sports")
|
@router.get("/sports")
|
||||||
def get_all_sports(db: SessionDep) -> List[SportResponse]:
|
def get_all_sports(db: SessionDep) -> List[SportResponse]:
|
||||||
|
|||||||
@@ -9,15 +9,17 @@ load_dotenv(dotenv_path=env_path)
|
|||||||
|
|
||||||
class Settings:
|
class Settings:
|
||||||
PROJECT_NAME: str = "Kontor"
|
PROJECT_NAME: str = "Kontor"
|
||||||
PROJECT_VERSION: str = "0.1.0"
|
PROJECT_VERSION: str = "0.2.0"
|
||||||
|
|
||||||
MARIADB_USER: str = os.getenv("MARIADB_USER", "kontor")
|
|
||||||
MARIADB_PASSWORD: str = os.getenv("MARIADB_PASSWORD", "kontor")
|
|
||||||
MARIADB_SERVER: str = os.getenv("MARIADB_SERVER", "mariadb")
|
|
||||||
MARIADB_PORT: str = os.getenv("MARIADB_PORT", 3306)
|
|
||||||
MARIADB_DB: str = os.getenv("MARIADB_DB", "kontor")
|
|
||||||
DATABASE_URL: str = f"mariadb+mariadbconnector://{MARIADB_USER}:{MARIADB_PASSWORD}@{MARIADB_SERVER}:{MARIADB_PORT}/{MARIADB_DB}"
|
|
||||||
|
|
||||||
|
DB_USER: str = os.getenv("DB_USER", "kontor")
|
||||||
|
DB_PASSWORD: str = os.getenv("DB_PASSWORD", "kontor")
|
||||||
|
DB_SERVER: str = os.getenv("DB_SERVER", "postgres")
|
||||||
|
DB_PORT: str = os.getenv("DB_PORT", 5432)
|
||||||
|
DB_DBNAME: str = os.getenv("DB_DBNAME", "kontor")
|
||||||
|
DATABASE_URL: str = f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_SERVER}:{DB_PORT}/{DB_DBNAME}"
|
||||||
|
SECRET_KEY: str = os.getenv("SECRET_KEY", "J6GOtcwC2NJI1l0VkHu20PacPFGTxpirBxWwynoHjsc=")
|
||||||
|
ALGORITHM = "HS256"
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES = 600 # in mins
|
||||||
|
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import logging
|
||||||
|
import logging.config
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
LOGGING_CONFIG: dict[str, Any] = {
|
||||||
|
"version": 1,
|
||||||
|
"disable_existing_loggers": False,
|
||||||
|
"formatters": {
|
||||||
|
"default": {
|
||||||
|
"()": "uvicorn.logging.DefaultFormatter",
|
||||||
|
"fmt": "%(asctime)s - %(name)s - %(levelprefix)s %(message)s"
|
||||||
|
},
|
||||||
|
"access": {
|
||||||
|
"()": "uvicorn.logging.AccessFormatter",
|
||||||
|
"fmt": '%(asctime)s - %(name)s - %(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s', # noqa: E501
|
||||||
|
},
|
||||||
|
"access_file": {
|
||||||
|
"()": "uvicorn.logging.AccessFormatter",
|
||||||
|
"fmt": '%(asctime)s - %(name)s - %(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s', # noqa: E501
|
||||||
|
"use_colors": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"handlers": {
|
||||||
|
"default": {
|
||||||
|
"formatter": "default",
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
"stream": "ext://sys.stdout",
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"formatter": "access",
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
"stream": "ext://sys.stderr",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"loggers": {
|
||||||
|
"root": {"handlers": ["default"], "level": "INFO", "propagate": False},
|
||||||
|
"uvicorn": {"handlers": ["default"], "level": "INFO", "propagate": False},
|
||||||
|
"uvicorn.error": {"level": "INFO"},
|
||||||
|
"uvicorn.access": {"handlers": ["default"], "level": "INFO", "propagate": False},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.config.dictConfig(LOGGING_CONFIG)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from datetime import timedelta
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from src.core.config import settings
|
||||||
|
from jose import jwt
|
||||||
|
|
||||||
|
|
||||||
|
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
||||||
|
to_encode = data.copy()
|
||||||
|
if expires_delta:
|
||||||
|
expire = datetime.utcnow() + expires_delta
|
||||||
|
else:
|
||||||
|
expire = datetime.utcnow() + timedelta(
|
||||||
|
minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
|
||||||
|
)
|
||||||
|
to_encode.update({"exp": expire})
|
||||||
|
encoded_jwt = jwt.encode(
|
||||||
|
to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM
|
||||||
|
)
|
||||||
|
return encoded_jwt
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from sqlalchemy import Column, ForeignKey, Integer, String
|
from sqlalchemy import Column, ForeignKey, Integer, String, Boolean
|
||||||
from sqlalchemy.dialects.mysql import BIT
|
|
||||||
from sqlalchemy.orm import relationship, mapped_column, Mapped
|
from sqlalchemy.orm import relationship, mapped_column, Mapped
|
||||||
|
|
||||||
from src.db.models.base import Base, BaseMixin
|
from src.db.models.base import Base, BaseMixin
|
||||||
@@ -9,12 +8,12 @@ from src.db.models.base import Base, BaseMixin
|
|||||||
|
|
||||||
class Profile(Base, BaseMixin):
|
class Profile(Base, BaseMixin):
|
||||||
__tablename__ = 'profile'
|
__tablename__ = 'profile'
|
||||||
first_name = Column(String(255))
|
first_name = Column(String)
|
||||||
last_name = Column(String(255))
|
last_name = Column(String)
|
||||||
user_name = Column(String(255), nullable=False)
|
user_name = Column(String, nullable=False)
|
||||||
email = Column(String(255))
|
email = Column(String)
|
||||||
password = Column(String(255))
|
password = Column(String)
|
||||||
enabled = Column(BIT(1))
|
enabled = Column(Boolean)
|
||||||
assignments = relationship("Assignment")
|
assignments = relationship("Assignment")
|
||||||
tokens = relationship("Token")
|
tokens = relationship("Token")
|
||||||
|
|
||||||
@@ -28,20 +27,23 @@ class Profile(Base, BaseMixin):
|
|||||||
full_name += self.last_name
|
full_name += self.last_name
|
||||||
return full_name
|
return full_name
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Profile({self.id} {self.user_name}, {self.email})"
|
||||||
|
|
||||||
|
|
||||||
class Token(Base, BaseMixin):
|
class Token(Base, BaseMixin):
|
||||||
__tablename__ = "token"
|
__tablename__ = "token"
|
||||||
token = Column(String(255), nullable=False, unique=True)
|
token = Column(String, nullable=False, unique=True)
|
||||||
name = Column(String(255))
|
name = Column(String)
|
||||||
last_used_date: Mapped[datetime] = mapped_column()
|
last_used_date: Mapped[datetime] = mapped_column()
|
||||||
enabled = Column(BIT(1))
|
enabled = Column(Boolean)
|
||||||
profile_id = Column(String(255), ForeignKey("profile.id"), nullable=False)
|
profile_id = Column(String, ForeignKey("profile.id"), nullable=False)
|
||||||
profile = relationship("Profile", back_populates="tokens")
|
profile = relationship("Profile", back_populates="tokens")
|
||||||
|
|
||||||
|
|
||||||
class Permission(Base, BaseMixin):
|
class Permission(Base, BaseMixin):
|
||||||
__tablename__ = "permission"
|
__tablename__ = "permission"
|
||||||
name = Column(String(255), nullable=False)
|
name = Column(String, nullable=False)
|
||||||
assignments = relationship("Assignment")
|
assignments = relationship("Assignment")
|
||||||
|
|
||||||
|
|
||||||
@@ -53,20 +55,14 @@ class Assignment(Base, BaseMixin):
|
|||||||
permission = relationship("Permission", back_populates="assignments")
|
permission = relationship("Permission", back_populates="assignments")
|
||||||
|
|
||||||
|
|
||||||
class ModuleData(Base, BaseMixin):
|
|
||||||
__tablename__ = "module_data"
|
|
||||||
module_name = Column(String(255), nullable=False)
|
|
||||||
import_data = Column(BIT(1))
|
|
||||||
|
|
||||||
|
|
||||||
class MailAccount(Base, BaseMixin):
|
class MailAccount(Base, BaseMixin):
|
||||||
__tablename__ = "mail_account"
|
__tablename__ = "mail_account"
|
||||||
host = Column(String(255))
|
host = Column(String)
|
||||||
port = Column(Integer)
|
port = Column(Integer)
|
||||||
protocol = Column(String(255))
|
protocol = Column(String)
|
||||||
user_name = Column(String(255))
|
user_name = Column(String)
|
||||||
password = Column(String(255))
|
password = Column(String)
|
||||||
start_tls = Column(BIT(1))
|
start_tls = Column(Boolean)
|
||||||
|
|
||||||
|
|
||||||
class Mail(Base, BaseMixin):
|
class Mail(Base, BaseMixin):
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from sqlalchemy import func, Column, String
|
from sqlalchemy import func, Column, String, Boolean
|
||||||
from sqlalchemy.dialects.mysql import BIT
|
|
||||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||||
|
|
||||||
|
|
||||||
@@ -11,8 +10,8 @@ class Base(DeclarativeBase):
|
|||||||
|
|
||||||
|
|
||||||
class BaseMixin:
|
class BaseMixin:
|
||||||
id = Column(String(255), primary_key=True, default=uuid.uuid4())
|
#id = Column(String, primary_key=True, default=uuid.uuid4)
|
||||||
# id: Mapped[str] = mapped_column(primary_key=True, default=uuid.uuid4())
|
id: Mapped[str] = mapped_column(primary_key=True, default=str(uuid.uuid4()))
|
||||||
# created_date = Column(DateTime)
|
# created_date = Column(DateTime)
|
||||||
created_date: Mapped[datetime] = mapped_column(default=func.now())
|
created_date: Mapped[datetime] = mapped_column(default=func.now())
|
||||||
# last_modified_date = Column(DateTime)
|
# last_modified_date = Column(DateTime)
|
||||||
@@ -22,10 +21,10 @@ class BaseMixin:
|
|||||||
|
|
||||||
|
|
||||||
class BaseVideoMixin:
|
class BaseVideoMixin:
|
||||||
cloud_link = Column(String(255))
|
cloud_link = Column(String, nullable=True)
|
||||||
file_name = Column(String(255))
|
file_name = Column(String, nullable=True)
|
||||||
path = Column(String(255))
|
path = Column(String)
|
||||||
review = Column(BIT(1))
|
review = Column(Boolean)
|
||||||
title = Column(String(255))
|
title = Column(String)
|
||||||
url = Column(String(255), unique=True)
|
url = Column(String, nullable=True)
|
||||||
should_download = Column(BIT(1))
|
should_download = Column(Boolean)
|
||||||
|
|||||||
@@ -6,28 +6,28 @@ from src.db.models.base import Base, BaseMixin
|
|||||||
|
|
||||||
class Article(Base, BaseMixin):
|
class Article(Base, BaseMixin):
|
||||||
__tablename__ = 'article'
|
__tablename__ = 'article'
|
||||||
title = Column(String(length=255), unique=True)
|
title = Column(String, unique=True)
|
||||||
article_authors = relationship("ArticleAuthor")
|
article_authors = relationship("ArticleAuthor")
|
||||||
|
|
||||||
|
|
||||||
class Author(Base, BaseMixin):
|
class Author(Base, BaseMixin):
|
||||||
__tablename__ = 'author'
|
__tablename__ = 'author'
|
||||||
first_name = Column(String(255))
|
first_name = Column(String)
|
||||||
last_name = Column(String(255))
|
last_name = Column(String)
|
||||||
article_authors = relationship("ArticleAuthor")
|
article_authors = relationship("ArticleAuthor")
|
||||||
book_authors = relationship("BookAuthor")
|
book_authors = relationship("BookAuthor")
|
||||||
|
|
||||||
|
|
||||||
class BookshelfPublisher(Base, BaseMixin):
|
class BookshelfPublisher(Base, BaseMixin):
|
||||||
__tablename__ = 'bookshelf_publisher'
|
__tablename__ = 'bookshelf_publisher'
|
||||||
name = Column(String(length=255), unique=True)
|
name = Column(String, unique=True)
|
||||||
books = relationship("Book")
|
books = relationship("Book")
|
||||||
|
|
||||||
|
|
||||||
class Book(Base, BaseMixin):
|
class Book(Base, BaseMixin):
|
||||||
__tablename__ = 'book'
|
__tablename__ = 'book'
|
||||||
isbn = Column(String(255), unique=True)
|
isbn = Column(String, unique=True)
|
||||||
title = Column(String(255))
|
title = Column(String)
|
||||||
year = Column(Integer, nullable=False)
|
year = Column(Integer, nullable=False)
|
||||||
publisher_id = Column(String, ForeignKey('bookshelf_publisher.id'), nullable=False)
|
publisher_id = Column(String, ForeignKey('bookshelf_publisher.id'), nullable=False)
|
||||||
publisher = relationship('BookshelfPublisher', back_populates="books")
|
publisher = relationship('BookshelfPublisher', back_populates="books")
|
||||||
|
|||||||
@@ -1,15 +1,24 @@
|
|||||||
from typing import Dict, List
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import AnyStr, Dict, List, Optional, Any
|
||||||
from natsort import natsorted
|
from natsort import natsorted
|
||||||
from sqlalchemy import Column, ForeignKey, Integer, String
|
from sqlalchemy import Column, ForeignKey, Integer, String, Boolean, func
|
||||||
from sqlalchemy.dialects.mysql import BIT
|
from sqlalchemy.orm import relationship, Mapped, mapped_column
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
|
|
||||||
from src.db.models.base import Base, BaseMixin
|
from src.db.models.base import Base, BaseMixin
|
||||||
|
|
||||||
|
|
||||||
class Publisher(Base, BaseMixin):
|
class Publisher(Base):
|
||||||
__tablename__ = "publisher"
|
__tablename__ = "publisher"
|
||||||
name = Column(String(length=255), unique=True)
|
id: Mapped[str] = mapped_column(primary_key=True, default=uuid.uuid4)
|
||||||
|
created_date: Mapped[datetime] = mapped_column(default=func.now())
|
||||||
|
last_modified_date: Mapped[datetime] = mapped_column(default=func.now())
|
||||||
|
version: Mapped[int] = mapped_column(default=0)
|
||||||
|
name = Column(String, unique=True)
|
||||||
|
weblink = Column(String, nullable=True)
|
||||||
|
parent_publisher_id: Mapped[Optional[str]] = mapped_column(ForeignKey('publisher.id'))
|
||||||
|
parent_publisher: Mapped[Optional['Publisher']] = relationship("Publisher", back_populates="imprints", remote_side=[id])
|
||||||
|
imprints: Mapped[List['Publisher']] = relationship('Publisher', back_populates="parent_publisher")
|
||||||
comics = relationship("Comic")
|
comics = relationship("Comic")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
@@ -21,11 +30,12 @@ class Publisher(Base, BaseMixin):
|
|||||||
|
|
||||||
class Comic(Base, BaseMixin):
|
class Comic(Base, BaseMixin):
|
||||||
__tablename__ = 'comic'
|
__tablename__ = 'comic'
|
||||||
title = Column(String(length=255), unique=True)
|
title = Column(String, unique=True)
|
||||||
publisher_id = Column(String, ForeignKey('publisher.id'), nullable=False)
|
publisher_id = Column(String, ForeignKey('publisher.id'), nullable=False)
|
||||||
publisher = relationship("Publisher", back_populates="comics")
|
publisher = relationship("Publisher", back_populates="comics")
|
||||||
current_order = Column(BIT(1))
|
current_order = Column(Boolean)
|
||||||
completed = Column(BIT(1))
|
completed = Column(Boolean)
|
||||||
|
weblink = Column(String, nullable=True)
|
||||||
issues = relationship("Issue", order_by="Issue.issue_number")
|
issues = relationship("Issue", order_by="Issue.issue_number")
|
||||||
story_arcs = relationship("StoryArc")
|
story_arcs = relationship("StoryArc")
|
||||||
trade_paperbacks = relationship("TradePaperback")
|
trade_paperbacks = relationship("TradePaperback")
|
||||||
@@ -38,10 +48,10 @@ class Comic(Base, BaseMixin):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{self.title}({self.id})'
|
return f'{self.title}({self.id})'
|
||||||
|
|
||||||
def get_artists(self) -> Dict[str, List[str]]:
|
def get_artists(self) -> Dict[Any, List[Any]]:
|
||||||
works: Dict[str, List[str]] = {}
|
works: Dict[Any, List[Any]] = {}
|
||||||
for work in self.comic_works:
|
for work in self.comic_works:
|
||||||
work_type = work.work_type.name
|
work_type = work.work_type
|
||||||
artist = work.artist
|
artist = work.artist
|
||||||
if work_type in works:
|
if work_type in works:
|
||||||
works[work_type].append(artist)
|
works[work_type].append(artist)
|
||||||
@@ -56,15 +66,16 @@ class Comic(Base, BaseMixin):
|
|||||||
|
|
||||||
class Volume(Base, BaseMixin):
|
class Volume(Base, BaseMixin):
|
||||||
__tablename__ = "volume"
|
__tablename__ = "volume"
|
||||||
name = Column(String(length=255), nullable=False)
|
name = Column(String, nullable=False)
|
||||||
comic_id = Column(String, ForeignKey("comic.id"), nullable=False)
|
comic_id = Column(String, ForeignKey("comic.id"), nullable=False)
|
||||||
comic = relationship("Comic", back_populates="volumes")
|
comic = relationship("Comic", back_populates="volumes")
|
||||||
|
story_arcs = relationship("StoryArc")
|
||||||
issues = relationship("Issue")
|
issues = relationship("Issue")
|
||||||
|
|
||||||
|
|
||||||
class TradePaperback(Base, BaseMixin):
|
class TradePaperback(Base, BaseMixin):
|
||||||
__tablename__ = "trade_paperback"
|
__tablename__ = "trade_paperback"
|
||||||
name = Column(String(length=255), nullable=False)
|
name = Column(String, nullable=False)
|
||||||
issue_start = Column(Integer)
|
issue_start = Column(Integer)
|
||||||
issue_end = Column(Integer)
|
issue_end = Column(Integer)
|
||||||
comic_id = Column(String, ForeignKey("comic.id"), nullable=False)
|
comic_id = Column(String, ForeignKey("comic.id"), nullable=False)
|
||||||
@@ -73,31 +84,58 @@ class TradePaperback(Base, BaseMixin):
|
|||||||
|
|
||||||
class StoryArc(Base, BaseMixin):
|
class StoryArc(Base, BaseMixin):
|
||||||
__tablename__ = "story_arc"
|
__tablename__ = "story_arc"
|
||||||
name = Column(String(length=255), nullable=False)
|
name = Column(String, nullable=False)
|
||||||
comic_id = Column(String, ForeignKey("comic.id"), nullable=False)
|
comic_id = Column(String, ForeignKey("comic.id"), nullable=False)
|
||||||
comic = relationship("Comic", back_populates="story_arcs")
|
comic = relationship("Comic", back_populates="story_arcs")
|
||||||
|
volume_id = Column(String, ForeignKey("volume.id"), nullable=True)
|
||||||
|
volume = relationship("Volume", back_populates="story_arcs")
|
||||||
|
issues = relationship("Issue")
|
||||||
|
|
||||||
|
|
||||||
class Issue(Base, BaseMixin):
|
class Issue(Base, BaseMixin):
|
||||||
__tablename__ = "issue"
|
__tablename__ = "issue"
|
||||||
issue_number = Column(String(255))
|
issue_number = Column(String)
|
||||||
in_stock = Column(BIT(1))
|
title = Column(String, nullable=True)
|
||||||
is_read = Column(BIT(1))
|
published_on: Mapped[datetime] = mapped_column(nullable=True)
|
||||||
|
in_stock = Column(Boolean)
|
||||||
|
is_read = Column(Boolean)
|
||||||
comic_id = Column(String, ForeignKey("comic.id"), nullable=False)
|
comic_id = Column(String, ForeignKey("comic.id"), nullable=False)
|
||||||
comic = relationship("Comic", back_populates="issues")
|
comic = relationship("Comic", back_populates="issues")
|
||||||
volume_id = Column(String, ForeignKey("volume.id"), nullable=True)
|
volume_id = Column(String, ForeignKey("volume.id"), nullable=True)
|
||||||
volume = relationship("Volume", back_populates="issues")
|
volume = relationship("Volume", back_populates="issues")
|
||||||
|
story_arc_id = Column(String, ForeignKey("story_arc.id"), nullable=True)
|
||||||
|
story_arc = relationship("StoryArc", back_populates="issues")
|
||||||
|
issue_works = relationship("IssueWork")
|
||||||
|
|
||||||
|
def get_full_title(self) -> AnyStr:
|
||||||
|
full_title: AnyStr = self.issue_number
|
||||||
|
if self.title:
|
||||||
|
full_title += ": " + self.title
|
||||||
|
return full_title
|
||||||
|
|
||||||
|
def get_artists(self) -> Dict[Any, List[Any]]:
|
||||||
|
works: Dict[Any, List[Any]] = {}
|
||||||
|
for work in self.issue_works:
|
||||||
|
work_type = work.work_type
|
||||||
|
artist = work.artist
|
||||||
|
if work_type in works:
|
||||||
|
works[work_type].append(artist)
|
||||||
|
else:
|
||||||
|
works[work_type] = [artist]
|
||||||
|
return works
|
||||||
|
|
||||||
|
|
||||||
class Artist(Base, BaseMixin):
|
class Artist(Base, BaseMixin):
|
||||||
__tablename__ = "artist"
|
__tablename__ = "artist"
|
||||||
name = Column(String(length=255), nullable=False)
|
name = Column(String, nullable=False)
|
||||||
|
weblink = Column(String, nullable=True)
|
||||||
comic_works = relationship("ComicWork")
|
comic_works = relationship("ComicWork")
|
||||||
|
issue_works = relationship("IssueWork")
|
||||||
|
|
||||||
def get_comics(self) -> Dict[str, List[str]]:
|
def get_comics(self) -> Dict[Any, List[Comic]]:
|
||||||
works: Dict[str, List[str]] = {}
|
works: Dict[Any, List[Comic]] = {}
|
||||||
for work in self.comic_works:
|
for work in self.comic_works:
|
||||||
work_type = work.work_type.name
|
work_type = work.work_type
|
||||||
comic = work.comic
|
comic = work.comic
|
||||||
if work_type in works:
|
if work_type in works:
|
||||||
works[work_type].append(comic)
|
works[work_type].append(comic)
|
||||||
@@ -108,8 +146,20 @@ class Artist(Base, BaseMixin):
|
|||||||
|
|
||||||
class WorkType(Base, BaseMixin):
|
class WorkType(Base, BaseMixin):
|
||||||
__tablename__ = "worktype"
|
__tablename__ = "worktype"
|
||||||
name = Column(String(length=255), nullable=False, unique=True)
|
name = Column(String, nullable=False, unique=True)
|
||||||
comic_works = relationship("ComicWork")
|
comic_works = relationship("ComicWork")
|
||||||
|
issue_works = relationship("IssueWork")
|
||||||
|
|
||||||
|
def get_artists(self) -> Dict[str, List[str]]:
|
||||||
|
works: Dict[str, List[str]] = {}
|
||||||
|
for work in self.comic_works:
|
||||||
|
comic = work.comic.title
|
||||||
|
artist = work.artist
|
||||||
|
if comic in works:
|
||||||
|
works[comic].append(artist)
|
||||||
|
else:
|
||||||
|
works[comic] = [artist]
|
||||||
|
return works
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'Worktype({self.id} {self.version} {self.name} {len(self.comic_works)})'
|
return f'Worktype({self.id} {self.version} {self.name} {len(self.comic_works)})'
|
||||||
@@ -126,3 +176,14 @@ class ComicWork(Base, BaseMixin):
|
|||||||
artist = relationship("Artist", back_populates="comic_works")
|
artist = relationship("Artist", back_populates="comic_works")
|
||||||
work_type_id = Column(String, ForeignKey("worktype.id"), nullable=False)
|
work_type_id = Column(String, ForeignKey("worktype.id"), nullable=False)
|
||||||
work_type = relationship("WorkType", back_populates="comic_works")
|
work_type = relationship("WorkType", back_populates="comic_works")
|
||||||
|
|
||||||
|
|
||||||
|
class IssueWork(Base, BaseMixin):
|
||||||
|
__tablename__ = "issue_work"
|
||||||
|
issue_id = Column(String, ForeignKey("issue.id"), nullable=False)
|
||||||
|
issue = relationship("Issue", back_populates="issue_works")
|
||||||
|
artist_id = Column(String, ForeignKey("artist.id"), nullable=False)
|
||||||
|
artist = relationship("Artist", back_populates="issue_works")
|
||||||
|
work_type_id = Column(String, ForeignKey("worktype.id"), nullable=False)
|
||||||
|
work_type = relationship("WorkType", back_populates="issue_works")
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any, List
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
@@ -13,7 +12,7 @@ from sqlalchemy.orm import sessionmaker
|
|||||||
from src.db.models.tysc import Card, CardSet, Rooster, Team, FieldPosition, Player, Vendor, Sport
|
from src.db.models.tysc import Card, CardSet, Rooster, Team, FieldPosition, Player, Vendor, Sport
|
||||||
from src.db.models.comic import Issue, TradePaperback, StoryArc, Volume, ComicWork, Artist, Comic, Publisher, WorkType
|
from src.db.models.comic import Issue, TradePaperback, StoryArc, Volume, ComicWork, Artist, Comic, Publisher, WorkType
|
||||||
from src.db.models.bookshelf import ArticleAuthor, BookAuthor, BookshelfPublisher, Article, Book, Author
|
from src.db.models.bookshelf import ArticleAuthor, BookAuthor, BookshelfPublisher, Article, Book, Author
|
||||||
from src.db.models.admin import Mail, MailAccount, ModuleData, Role, User, Token, AuthorizationMatrix
|
from src.db.models.admin import Mail, MailAccount, ModuleData, Token, Assignment, Permission, Profile
|
||||||
from src.db.models.metadata import MetaDataTable, MetaDataColumn
|
from src.db.models.metadata import MetaDataTable, MetaDataColumn
|
||||||
from src.db.models.media import MediaVideo, MediaArticle, MediaFile, MediaActor, MediaActorFile
|
from src.db.models.media import MediaVideo, MediaArticle, MediaFile, MediaActor, MediaActorFile
|
||||||
|
|
||||||
@@ -79,10 +78,10 @@ class KontorDB:
|
|||||||
self.registry[MediaVideo.__tablename__] = MediaVideo
|
self.registry[MediaVideo.__tablename__] = MediaVideo
|
||||||
self.registry[MetaDataColumn.__tablename__] = MetaDataColumn
|
self.registry[MetaDataColumn.__tablename__] = MetaDataColumn
|
||||||
self.registry[MetaDataTable.__tablename__] = MetaDataTable
|
self.registry[MetaDataTable.__tablename__] = MetaDataTable
|
||||||
self.registry[AuthorizationMatrix.__tablename__] = AuthorizationMatrix
|
self.registry[Assignment.__tablename__] = Assignment
|
||||||
self.registry[Token.__tablename__] = Token
|
self.registry[Token.__tablename__] = Token
|
||||||
self.registry[User.__tablename__] = User
|
self.registry[Profile.__tablename__] = Profile
|
||||||
self.registry[Role.__tablename__] = Role
|
self.registry[Permission.__tablename__] = Permission
|
||||||
self.registry[ModuleData.__tablename__] = ModuleData
|
self.registry[ModuleData.__tablename__] = ModuleData
|
||||||
self.registry[MailAccount.__tablename__] = MailAccount
|
self.registry[MailAccount.__tablename__] = MailAccount
|
||||||
self.registry[Mail.__tablename__] = Mail
|
self.registry[Mail.__tablename__] = Mail
|
||||||
@@ -360,7 +359,7 @@ class KontorDB:
|
|||||||
update_list[link.id] = link.title
|
update_list[link.id] = link.title
|
||||||
return update_list
|
return update_list
|
||||||
|
|
||||||
def get_download_list(self) -> list[uuid.UUID]:
|
def get_download_list(self) -> List[str]:
|
||||||
download_list = []
|
download_list = []
|
||||||
__session__ = sessionmaker(self.engine)
|
__session__ = sessionmaker(self.engine)
|
||||||
_filter = { 'should_download': True}
|
_filter = { 'should_download': True}
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from sqlalchemy import Column, String, ForeignKey
|
from sqlalchemy import Column, String, ForeignKey, Boolean
|
||||||
from sqlalchemy.dialects.mysql import BIT
|
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from src.db.models.base import Base, BaseMixin, BaseVideoMixin
|
from src.db.models.base import Base, BaseMixin, BaseVideoMixin
|
||||||
@@ -30,10 +29,10 @@ class MediaFile(Base, BaseMixin, BaseVideoMixin):
|
|||||||
soup = BeautifulSoup(r.content, "html.parser")
|
soup = BeautifulSoup(r.content, "html.parser")
|
||||||
title = soup.title.string
|
title = soup.title.string
|
||||||
self.title = title
|
self.title = title
|
||||||
self.review = 0
|
self.review = False
|
||||||
except:
|
except:
|
||||||
self.title = None
|
self.title = None
|
||||||
self.review = 1
|
self.review = True
|
||||||
self.last_modified_date = datetime.now()
|
self.last_modified_date = datetime.now()
|
||||||
|
|
||||||
def download_file(self, download_dir: str, dl_tool: str):
|
def download_file(self, download_dir: str, dl_tool: str):
|
||||||
@@ -45,12 +44,12 @@ class MediaFile(Base, BaseMixin, BaseVideoMixin):
|
|||||||
lines_list = output.splitlines()
|
lines_list = output.splitlines()
|
||||||
file_name = self.__parse_output__(lines_list)
|
file_name = self.__parse_output__(lines_list)
|
||||||
if file_name is None:
|
if file_name is None:
|
||||||
self.review = 1
|
self.review = True
|
||||||
self.should_download = 1
|
self.should_download = True
|
||||||
self.file_name = None
|
self.file_name = None
|
||||||
else:
|
else:
|
||||||
download_file = Path(file_name)
|
download_file = Path(file_name)
|
||||||
self.should_download = 0
|
self.should_download = False
|
||||||
self.file_name = download_file.name
|
self.file_name = download_file.name
|
||||||
self.cloud_link = str(download_file.absolute())
|
self.cloud_link = str(download_file.absolute())
|
||||||
self.last_modified_date = datetime.now()
|
self.last_modified_date = datetime.now()
|
||||||
@@ -71,31 +70,46 @@ class MediaFile(Base, BaseMixin, BaseVideoMixin):
|
|||||||
|
|
||||||
class MediaActor(Base, BaseMixin):
|
class MediaActor(Base, BaseMixin):
|
||||||
__tablename__ = 'media_actor'
|
__tablename__ = 'media_actor'
|
||||||
name = Column(String(255))
|
name = Column(String)
|
||||||
|
url = Column(String, unique=True, nullable=True)
|
||||||
media_actor_files = relationship("MediaActorFile")
|
media_actor_files = relationship("MediaActorFile")
|
||||||
|
|
||||||
|
|
||||||
class MediaActorFile(Base, BaseMixin):
|
class MediaActorFile(Base, BaseMixin):
|
||||||
__tablename__ = 'media_actor_file'
|
__tablename__ = 'media_actor_file'
|
||||||
media_actor_id = Column(String(255), ForeignKey("media_actor.id"), nullable=False)
|
media_actor_id = Column(String, ForeignKey("media_actor.id"), nullable=False)
|
||||||
media_actor = relationship("MediaActor", back_populates="media_actor_files")
|
media_actor = relationship("MediaActor", back_populates="media_actor_files")
|
||||||
media_file_id = Column(String(255), ForeignKey("media_file.id"), nullable=True)
|
media_file_id = Column(String, ForeignKey("media_file.id"), nullable=True)
|
||||||
media_file = relationship("MediaFile", back_populates="media_actor_files")
|
media_file = relationship("MediaFile", back_populates="media_actor_files")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'MediaActorFile({self.id} {self.media_actor_id} {self.media_file_id})'
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f'{self.id} {self.media_actor_id} {self.media_file_id}'
|
||||||
|
|
||||||
class MediaArticle(Base, BaseMixin):
|
class MediaArticle(Base, BaseMixin):
|
||||||
__tablename__ = 'media_article'
|
__tablename__ = 'media_article'
|
||||||
review = Column(BIT(1))
|
review = Column(Boolean)
|
||||||
title = Column(String(255))
|
title = Column(String)
|
||||||
url = Column(String(255), unique=True)
|
url = Column(String, unique=True)
|
||||||
|
|
||||||
|
|
||||||
class MediaVideo(Base, BaseMixin):
|
class MediaVideo(Base, BaseMixin):
|
||||||
__tablename__ = 'media_video'
|
__tablename__ = 'media_video'
|
||||||
cloud_link = Column(String(255))
|
cloud_link = Column(String)
|
||||||
file_name = Column(String(255))
|
file_name = Column(String)
|
||||||
path = Column(String(255))
|
path = Column(String)
|
||||||
review = Column(BIT(1))
|
review = Column(Boolean)
|
||||||
title = Column(String(255))
|
title = Column(String)
|
||||||
url = Column(String(255), unique=True)
|
url = Column(String, unique=True)
|
||||||
should_download = Column(BIT(1))
|
should_download = Column(Boolean)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'MediaFile({self.id} {self.title} {self.url})'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self.title is None:
|
||||||
|
return f'{self.url}({self.id})'
|
||||||
|
else:
|
||||||
|
return f'{self.title}({self.id})'
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
from sqlalchemy import Column, String, ForeignKey, Integer
|
|
||||||
from sqlalchemy.dialects.mysql import BIT
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
|
|
||||||
from src.db.models.base import Base, BaseMixin
|
|
||||||
|
|
||||||
|
|
||||||
class MetaDataTable(Base, BaseMixin):
|
|
||||||
__tablename__ = 'meta_data_table'
|
|
||||||
table_name = Column(String(255), unique=True)
|
|
||||||
table_columns = relationship("MetaDataColumn")
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f'MetaDataTable({self.id} {self.table_name})'
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f'{self.table_name}({self.id})'
|
|
||||||
|
|
||||||
|
|
||||||
class MetaDataColumn(Base, BaseMixin):
|
|
||||||
__tablename__ = 'meta_data_column'
|
|
||||||
column_name = Column(String(255), nullable=False)
|
|
||||||
column_sync_name = Column(String(255))
|
|
||||||
column_type = Column(String(255))
|
|
||||||
column_modifier = Column(String(255), nullable=True)
|
|
||||||
column_order = Column(Integer)
|
|
||||||
table_id = Column(String, ForeignKey('meta_data_table.id'))
|
|
||||||
table = relationship("MetaDataTable", back_populates="table_columns")
|
|
||||||
column_label = Column(String(255))
|
|
||||||
filter_label = Column(String(255))
|
|
||||||
is_shown = Column(BIT(1))
|
|
||||||
show_filter = Column(BIT(1))
|
|
||||||
ref_column = Column(String, nullable=True)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
if self.column_name is None:
|
|
||||||
return f'MetaDataColumn({self.id} {self.table.table_name}.__)'
|
|
||||||
else:
|
|
||||||
return f'MetaDataColumn({self.id} {self.table.table_name}.{self.column_name})'
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f'{self.column_name}({self.id})'
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint
|
from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint, Boolean
|
||||||
from sqlalchemy.dialects.mysql import BIT
|
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from src.db.models.base import Base, BaseMixin
|
from src.db.models.base import Base, BaseMixin
|
||||||
@@ -10,15 +9,15 @@ class Sport(Base, BaseMixin):
|
|||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
UniqueConstraint("name"),
|
UniqueConstraint("name"),
|
||||||
)
|
)
|
||||||
name = Column(String(255), nullable=False, index=True, unique=True)
|
name = Column(String, nullable=False, index=True, unique=True)
|
||||||
teams = relationship("Team")
|
teams = relationship("Team")
|
||||||
positions = relationship("FieldPosition")
|
positions = relationship("FieldPosition")
|
||||||
|
|
||||||
|
|
||||||
class Team(Base, BaseMixin):
|
class Team(Base, BaseMixin):
|
||||||
__tablename__ = "team"
|
__tablename__ = "team"
|
||||||
name = Column(String(255), nullable=False, index=True, unique=True)
|
name = Column(String, nullable=False, index=True, unique=True)
|
||||||
short_name = Column(String(255), nullable=False, )
|
short_name = Column(String, nullable=False, )
|
||||||
sport_id = Column(String, ForeignKey("sport.id"), nullable=False)
|
sport_id = Column(String, ForeignKey("sport.id"), nullable=False)
|
||||||
sport = relationship("Sport", back_populates="teams")
|
sport = relationship("Sport", back_populates="teams")
|
||||||
roosters = relationship("Rooster")
|
roosters = relationship("Rooster")
|
||||||
@@ -30,8 +29,8 @@ class FieldPosition(Base, BaseMixin):
|
|||||||
UniqueConstraint("name", "sport_id"),
|
UniqueConstraint("name", "sport_id"),
|
||||||
UniqueConstraint("short_name", "sport_id"),
|
UniqueConstraint("short_name", "sport_id"),
|
||||||
)
|
)
|
||||||
name = Column(String(255), nullable=False, index=True)
|
name = Column(String, nullable=False, index=True)
|
||||||
short_name = Column(String(255), nullable=False)
|
short_name = Column(String, nullable=False)
|
||||||
sport_id = Column(String, ForeignKey("sport.id"), nullable=False, index=True)
|
sport_id = Column(String, ForeignKey("sport.id"), nullable=False, index=True)
|
||||||
sport = relationship("Sport", back_populates="positions")
|
sport = relationship("Sport", back_populates="positions")
|
||||||
roosters = relationship("Rooster")
|
roosters = relationship("Rooster")
|
||||||
@@ -42,8 +41,8 @@ class Player(Base, BaseMixin):
|
|||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
UniqueConstraint("first_name", "last_name"),
|
UniqueConstraint("first_name", "last_name"),
|
||||||
)
|
)
|
||||||
first_name = Column(String(255), nullable=False, index=True)
|
first_name = Column(String, nullable=False, index=True)
|
||||||
last_name = Column(String(255), nullable=False, index=True)
|
last_name = Column(String, nullable=False, index=True)
|
||||||
roosters = relationship("Rooster")
|
roosters = relationship("Rooster")
|
||||||
|
|
||||||
def get_full_name(self) -> str:
|
def get_full_name(self) -> str:
|
||||||
@@ -67,7 +66,7 @@ class Rooster(Base, BaseMixin):
|
|||||||
|
|
||||||
class Vendor(Base, BaseMixin):
|
class Vendor(Base, BaseMixin):
|
||||||
__tablename__ = "vendor"
|
__tablename__ = "vendor"
|
||||||
name = Column(String(255), nullable=False, unique=True, index=True)
|
name = Column(String, nullable=False, unique=True, index=True)
|
||||||
card_sets = relationship("CardSet")
|
card_sets = relationship("CardSet")
|
||||||
cards = relationship("Card")
|
cards = relationship("Card")
|
||||||
|
|
||||||
@@ -77,9 +76,9 @@ class CardSet(Base, BaseMixin):
|
|||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
UniqueConstraint("name", "vendor_id"),
|
UniqueConstraint("name", "vendor_id"),
|
||||||
)
|
)
|
||||||
name = Column(String(255), index=True)
|
name = Column(String, index=True)
|
||||||
parallel_set = Column(BIT(1))
|
parallel_set = Column(Boolean)
|
||||||
insert_set = Column(BIT(1))
|
insert_set = Column(Boolean)
|
||||||
vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False, index=True)
|
vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False, index=True)
|
||||||
vendor = relationship("Vendor", back_populates="card_sets")
|
vendor = relationship("Vendor", back_populates="card_sets")
|
||||||
cards = relationship("Card")
|
cards = relationship("Card")
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from typing import AnyStr
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from src.db.models.admin import Profile
|
||||||
|
|
||||||
|
|
||||||
|
def get_profile(username: AnyStr, db: Session):
|
||||||
|
profile = db.query(Profile).filter(Profile.email == username).first()
|
||||||
|
return profile
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
from src.db.models.comic import Artist
|
||||||
|
from src.schema.comics.artist import ArtistDetailResponse
|
||||||
|
|
||||||
|
|
||||||
|
def get_artist_details(artist: Artist) -> ArtistDetailResponse:
|
||||||
|
works = {}
|
||||||
|
for work in artist.comic_works:
|
||||||
|
work_type = work.work_type.name
|
||||||
|
comic_title = work.comic.title
|
||||||
|
if work_type in works:
|
||||||
|
works[work_type].append(comic_title)
|
||||||
|
else:
|
||||||
|
works[work_type] = [comic_title]
|
||||||
|
response = ArtistDetailResponse(
|
||||||
|
id=artist.id,
|
||||||
|
name=artist.name,
|
||||||
|
works=works
|
||||||
|
)
|
||||||
|
return response
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
from typing import List, Type, AnyStr
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from src.core.log_conf import logger
|
||||||
|
from src.db.models.comic import Comic, Issue
|
||||||
|
from src.schema.comics.comic import ComicSchema
|
||||||
|
from src.schema.comics.issue import IssueDetailsResponse
|
||||||
|
|
||||||
|
|
||||||
|
def list_comics(db: Session) -> List[Type[Comic]]:
|
||||||
|
comics = db.query(Comic).all()
|
||||||
|
return comics
|
||||||
|
|
||||||
|
|
||||||
|
def get_issue_details(issue: Issue) -> IssueDetailsResponse:
|
||||||
|
response = IssueDetailsResponse(
|
||||||
|
id=issue.id,
|
||||||
|
issue_number=issue.issue_number,
|
||||||
|
in_stock=issue.in_stock,
|
||||||
|
is_read=issue.is_read,
|
||||||
|
comic_id=issue.comic_id,
|
||||||
|
volume_id=issue.volume_id
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def update_comic(comic: ComicSchema, comic_id: AnyStr, db: Session) -> type[Comic] | None:
|
||||||
|
logger.info(f"update_comic: {comic} with {comic_id}")
|
||||||
|
comic = db.get(Comic, comic_id)
|
||||||
|
return comic
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import AnyStr
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from src.core.log_conf import logger
|
||||||
|
from src.db.models.comic import WorkType
|
||||||
|
from src.schema.comics.worktype import AddWorkType
|
||||||
|
|
||||||
|
|
||||||
|
def create_new_worktype(work: AddWorkType, db: Session) -> WorkType:
|
||||||
|
worktype = WorkType()
|
||||||
|
worktype.id = str(uuid.uuid4())
|
||||||
|
worktype.created_date = datetime.now()
|
||||||
|
worktype.last_modified_date = datetime.now()
|
||||||
|
worktype.name = work.worktype
|
||||||
|
db.add(worktype)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(worktype)
|
||||||
|
logger.info(f"create_new_worktype: {worktype}")
|
||||||
|
return worktype
|
||||||
|
|
||||||
|
|
||||||
|
def update_worktype(work: AddWorkType, worktype_id: AnyStr, db: Session) -> WorkType:
|
||||||
|
logger.info("update worktype")
|
||||||
|
worktype = db.get(WorkType, worktype_id)
|
||||||
|
worktype.name = work.worktype
|
||||||
|
db.add(worktype)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(worktype)
|
||||||
|
return worktype
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from typing import AnyStr
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
from src.core.log_conf import logger
|
||||||
|
from src.db.models.media import MediaActorFile, MediaFile, MediaVideo
|
||||||
|
from src.webapps.media.forms import AddLinkForm
|
||||||
|
|
||||||
|
|
||||||
|
def create_new_video(video: AddLinkForm, db: Session) -> MediaVideo:
|
||||||
|
print(video.url)
|
||||||
|
media_video = MediaVideo()
|
||||||
|
media_video.id = str(uuid.uuid4())
|
||||||
|
media_video.url = video.url
|
||||||
|
media_video.created_date = datetime.now()
|
||||||
|
media_video.last_modified_date = datetime.now()
|
||||||
|
media_video.review = True
|
||||||
|
media_video.should_download = True
|
||||||
|
db.add(media_video)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(media_video)
|
||||||
|
print(media_video)
|
||||||
|
return media_video
|
||||||
|
|
||||||
|
def create_new_mediafile(link: AnyStr, db: Session) -> MediaFile:
|
||||||
|
logger.info("create MediaFile with url {link}")
|
||||||
|
media_file: MediaFile = MediaFile()
|
||||||
|
media_file.id = str(uuid.uuid4())
|
||||||
|
media_file.url = link
|
||||||
|
media_file.created_date = datetime.now()
|
||||||
|
media_file.last_modified_date = datetime.now()
|
||||||
|
media_file.version = 0
|
||||||
|
media_file.review = True
|
||||||
|
media_file.should_download = True
|
||||||
|
db.add(media_file)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(media_file)
|
||||||
|
logger.info(f"created {media_file}")
|
||||||
|
return media_file
|
||||||
|
|
||||||
|
def create_new_mediaactorfile(db: Session, actor_id: AnyStr, file_id: AnyStr) -> MediaActorFile:
|
||||||
|
logger.info(f"create MediaActorFile with actor {actor_id} and file {file_id}")
|
||||||
|
media_actor_file: MediaActorFile = MediaActorFile()
|
||||||
|
media_actor_file.id = str(uuid.uuid4())
|
||||||
|
media_actor_file.created_date = datetime.now()
|
||||||
|
media_actor_file.last_modified_date = datetime.now()
|
||||||
|
media_actor_file.version = 0
|
||||||
|
media_actor_file.media_actor_id = actor_id
|
||||||
|
media_actor_file.media_file_id = file_id
|
||||||
|
db.add(media_actor_file)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(media_actor_file)
|
||||||
|
return media_actor_file
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
from typing import Generator, Annotated
|
from typing import Generator
|
||||||
|
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import sessionmaker, Session
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
from src.core.config import settings
|
from src.core.config import settings
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import databases
|
||||||
|
from src.db.session import SQLALCHEMY_DATABASE_URL
|
||||||
|
|
||||||
|
|
||||||
|
async def check_db_connected():
|
||||||
|
try:
|
||||||
|
if not str(SQLALCHEMY_DATABASE_URL).__contains__("sqlite"):
|
||||||
|
database = databases.Database(SQLALCHEMY_DATABASE_URL)
|
||||||
|
if not database.is_connected:
|
||||||
|
await database.connect()
|
||||||
|
await database.execute("SELECT 1")
|
||||||
|
print("Database is connected (^_^)")
|
||||||
|
except Exception as e:
|
||||||
|
print(
|
||||||
|
"Looks like db is missing or is there is some problem in connection,see below traceback"
|
||||||
|
)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
async def check_db_disconnected():
|
||||||
|
try:
|
||||||
|
if not str(SQLALCHEMY_DATABASE_URL).__contains__("sqlite"):
|
||||||
|
database = databases.Database(SQLALCHEMY_DATABASE_URL)
|
||||||
|
if database.is_connected:
|
||||||
|
await database.disconnect()
|
||||||
|
print("Database is Disconnected (-_-) zZZ")
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
+17
-5
@@ -1,12 +1,25 @@
|
|||||||
|
import logging
|
||||||
|
import logging.config
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
from src.apis.base import api_router
|
from src.apis.base import api_router
|
||||||
|
from src.core.log_conf import LOGGING_CONFIG, logger
|
||||||
from src.db.session import engine
|
from src.db.session import engine
|
||||||
|
from src.db.utils import check_db_connected, check_db_disconnected
|
||||||
from src.webapps.base import api_router as web_app_router
|
from src.webapps.base import api_router as web_app_router
|
||||||
from src.core.config import settings
|
from src.core.config import settings
|
||||||
from src.db.models.base import Base
|
from src.db.models.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
await check_db_connected()
|
||||||
|
yield
|
||||||
|
await check_db_disconnected()
|
||||||
|
|
||||||
def include_router(app: FastAPI):
|
def include_router(app: FastAPI):
|
||||||
app.include_router(api_router)
|
app.include_router(api_router)
|
||||||
app.include_router(web_app_router)
|
app.include_router(web_app_router)
|
||||||
@@ -17,13 +30,12 @@ def configure_static(app: FastAPI):
|
|||||||
def create_tables():
|
def create_tables():
|
||||||
Base.metadata.create_all(bind=engine)
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
def start_application():
|
def start_application(log):
|
||||||
app = FastAPI(title=settings.PROJECT_NAME, version=settings.PROJECT_VERSION)
|
log.info(f"using database: {settings.DATABASE_URL}")
|
||||||
|
app = FastAPI(title=settings.PROJECT_NAME, version=settings.PROJECT_VERSION, lifespan=lifespan)
|
||||||
include_router(app)
|
include_router(app)
|
||||||
configure_static(app)
|
configure_static(app)
|
||||||
create_tables()
|
create_tables()
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
kontor = start_application(logger)
|
||||||
kontor = start_application()
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Token(BaseModel):
|
||||||
|
access_token: str
|
||||||
|
token_type: str
|
||||||
@@ -1,36 +1,19 @@
|
|||||||
from typing import List, Dict
|
from typing import List, Dict
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from src.db.models.comic import Artist
|
|
||||||
|
|
||||||
|
|
||||||
class ArtistCreation(BaseModel):
|
class ArtistCreation(BaseModel):
|
||||||
|
id: str
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
class ArtistResponse(BaseModel):
|
class ArtistResponse(BaseModel):
|
||||||
id: UUID
|
id: str
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
class ArtistDetailResponse(BaseModel):
|
class ArtistDetailResponse(BaseModel):
|
||||||
id: UUID
|
id: str
|
||||||
name: str
|
name: str
|
||||||
|
weblink: str
|
||||||
works: Dict[str, List[str]]
|
works: Dict[str, List[str]]
|
||||||
|
|
||||||
def get_artist_details(artist: Artist) -> ArtistDetailResponse:
|
|
||||||
works = {}
|
|
||||||
for work in artist.comic_works:
|
|
||||||
work_type = work.work_type.name
|
|
||||||
comic_title = work.comic.title
|
|
||||||
if work_type in works:
|
|
||||||
works[work_type].append(comic_title)
|
|
||||||
else:
|
|
||||||
works[work_type] = [comic_title]
|
|
||||||
response = ArtistDetailResponse(
|
|
||||||
id=artist.id,
|
|
||||||
name=artist.name,
|
|
||||||
works=works
|
|
||||||
)
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,35 @@
|
|||||||
from typing import List, Dict
|
from typing import List, Dict, Optional
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, AnyUrl
|
||||||
|
|
||||||
from src.db.models.comic import Comic
|
from src.db.models.comic import Comic
|
||||||
|
|
||||||
|
|
||||||
class ComicResponse(BaseModel):
|
class ComicResponse(BaseModel):
|
||||||
id: UUID
|
id: str
|
||||||
title: str
|
title: str
|
||||||
completed: bool
|
completed: bool
|
||||||
|
|
||||||
class ComicDetailsResponse(BaseModel):
|
class ComicDetailsResponse(BaseModel):
|
||||||
id: UUID
|
id: str
|
||||||
created: str
|
created: str
|
||||||
title: str
|
title: str
|
||||||
completed : bool
|
completed : bool
|
||||||
current_order : bool
|
current_order : bool
|
||||||
|
weblink: str
|
||||||
publisher: str
|
publisher: str
|
||||||
volumes: List[str]
|
volumes: List[str]
|
||||||
works: Dict[str, List[str]]
|
works: Dict[str, List[str]]
|
||||||
|
|
||||||
|
|
||||||
|
class ComicSchema(BaseModel):
|
||||||
|
id: str
|
||||||
|
title: str
|
||||||
|
weblink: Optional[AnyUrl]
|
||||||
|
completed: Optional[bool]
|
||||||
|
current_order: Optional[bool]
|
||||||
|
|
||||||
|
|
||||||
def get_short_info(comic: Comic) -> ComicResponse:
|
def get_short_info(comic: Comic) -> ComicResponse:
|
||||||
response = ComicResponse(
|
response = ComicResponse(
|
||||||
id=comic.id,
|
id=comic.id,
|
||||||
@@ -46,11 +55,11 @@ def get_comic_details(comic: Comic) -> ComicDetailsResponse | None:
|
|||||||
id=comic.id,
|
id=comic.id,
|
||||||
created=str(comic.created_date),
|
created=str(comic.created_date),
|
||||||
title=comic.title,
|
title=comic.title,
|
||||||
completed=(comic.completed == 1),
|
completed=comic.completed,
|
||||||
current_order=(comic.current_order == 1),
|
current_order=comic.current_order,
|
||||||
|
weblink=comic.weblink,
|
||||||
publisher=comic.publisher.name,
|
publisher=comic.publisher.name,
|
||||||
volumes=volumes,
|
volumes=volumes,
|
||||||
works=works
|
works=works
|
||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class IssueDetailsResponse(BaseModel):
|
||||||
|
id: str
|
||||||
|
issue_number: str
|
||||||
|
in_stock: bool
|
||||||
|
is_read: bool
|
||||||
|
comic_id: str
|
||||||
|
volume_id: str | None
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class AddWorkType(BaseModel):
|
||||||
|
worktype: str
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from src.db.models.media import MediaActor
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class MediaActorResponse(BaseModel):
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
url: str
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from src.db.models.media import MediaFile
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class MediaActorFileResponse(BaseModel):
|
||||||
|
id: str
|
||||||
|
file_id: str
|
||||||
|
actor_id: str
|
||||||
@@ -1,30 +1,29 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from src.db.models.media import MediaFile
|
from src.db.models.media import MediaFile
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class MediaFileResponse(BaseModel):
|
class MediaFileResponse(BaseModel):
|
||||||
id: UUID
|
id: str
|
||||||
title: str | None = None
|
title: str | None = None
|
||||||
file_name: str | None = None
|
file_name: str | None = None
|
||||||
cloud_link: str | None = None
|
cloud_link: str | None = None
|
||||||
url: str
|
url: str | None = None
|
||||||
review: bool = False
|
review: bool = False
|
||||||
should_download: bool = False
|
should_download: bool = False
|
||||||
|
|
||||||
class Link(BaseModel):
|
class Link(BaseModel):
|
||||||
url: str
|
url: str
|
||||||
|
|
||||||
def get_file_details(mediafile: MediaFile) -> MediaFileResponse | None:
|
def get_file_details(mediafile: MediaFile) -> MediaFileResponse:
|
||||||
response = MediaFileResponse(id=mediafile.id,
|
response = MediaFileResponse(id=mediafile.id,
|
||||||
title=mediafile.title,
|
title=mediafile.title,
|
||||||
file_name=mediafile.file_name,
|
file_name=mediafile.file_name,
|
||||||
cloud_link=mediafile.cloud_link,
|
cloud_link=mediafile.cloud_link,
|
||||||
url=str(mediafile.url),
|
url=str(mediafile.url),
|
||||||
review=(mediafile.review == 1),
|
review=mediafile.review,
|
||||||
should_download=(mediafile.should_download == 1))
|
should_download=mediafile.should_download)
|
||||||
#print(f"id: {mediafile.id}: review: {response.review} <- {mediafile.review}")
|
#print(f"id: {mediafile.id}: review: {response.review} <- {mediafile.review}")
|
||||||
#print(f"id: {mediafile.id}: download: {response.should_download} <- {mediafile.should_download}")
|
#print(f"id: {mediafile.id}: download: {response.should_download} <- {mediafile.should_download}")
|
||||||
return response
|
return response
|
||||||
@@ -35,11 +34,5 @@ def set_file(model: MediaFileResponse, mediafile: MediaFile) -> None:
|
|||||||
mediafile.url = model.url
|
mediafile.url = model.url
|
||||||
mediafile.title = model.title
|
mediafile.title = model.title
|
||||||
mediafile.last_modified_date = datetime.now()
|
mediafile.last_modified_date = datetime.now()
|
||||||
if model.review:
|
mediafile.review = model.review
|
||||||
mediafile.review = 1
|
mediafile.should_download = model.should_download
|
||||||
else:
|
|
||||||
mediafile.review = 0
|
|
||||||
if model.should_download:
|
|
||||||
mediafile.should_download = 1
|
|
||||||
else:
|
|
||||||
mediafile.should_download = 0
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class AddLink(BaseModel):
|
||||||
|
url: str
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
from uuid import UUID
|
|
||||||
|
|
||||||
|
from typing import AnyStr
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class SportResponse(BaseModel):
|
class SportResponse(BaseModel):
|
||||||
id: UUID
|
id: AnyStr
|
||||||
name: str
|
name: str
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: Arial;
|
||||||
|
padding: 10px;
|
||||||
|
background: lightgrey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header/Blog Title */
|
||||||
|
.header {
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
background-color: lightblue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style the top navigation bar */
|
||||||
|
.topnav {
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: darkgrey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style the topnav links */
|
||||||
|
.topnav a {
|
||||||
|
float: left;
|
||||||
|
display: block;
|
||||||
|
color: #f2f2f2;
|
||||||
|
text-align: center;
|
||||||
|
padding: 14px 16px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Change color on hover */
|
||||||
|
.topnav a:hover {
|
||||||
|
background-color: #ddd;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subnav {
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subnav a {
|
||||||
|
float: left;
|
||||||
|
display: block;
|
||||||
|
color: #f2f2f2;
|
||||||
|
text-align: center;
|
||||||
|
padding: 14px 16px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-inline {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-inline label {
|
||||||
|
margin: 5px 10px 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-inline input {
|
||||||
|
padding-right: 50px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-inline button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: dodgerblue;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
color: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-inline button:hover {
|
||||||
|
background-color: royalblue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill-nav a {
|
||||||
|
display: inline-block;
|
||||||
|
color: white;
|
||||||
|
background-color: dodgerblue;
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Change the color of links on mouse-over */
|
||||||
|
.pill-nav a:hover {
|
||||||
|
background-color: royalblue;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add a color to the active/current link */
|
||||||
|
.pill-nav a.active {
|
||||||
|
background-color: royalblue;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maincolumn {
|
||||||
|
float:left;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
/* Create two unequal columns that floats next to each other */
|
||||||
|
/* Left column */
|
||||||
|
.leftcolumn {
|
||||||
|
float: left;
|
||||||
|
width: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Right column */
|
||||||
|
.rightcolumn {
|
||||||
|
float: left;
|
||||||
|
width: 25%;
|
||||||
|
background-color: #f1f1f1;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fake image */
|
||||||
|
.fakeimg {
|
||||||
|
background-color: #aaa;
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add a card effect for articles */
|
||||||
|
.card {
|
||||||
|
background-color: white;
|
||||||
|
padding: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Clear floats after the columns */
|
||||||
|
.row::after {
|
||||||
|
content: "";
|
||||||
|
display: table;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.footer {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
background: #ddd;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive layout - when the screen is less than 800px wide, make the two columns stack on top of each other instead of next to each other */
|
||||||
|
@media screen and (max-width: 800px) {
|
||||||
|
.leftcolumn, .rightcolumn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive layout - when the screen is less than 400px wide, make the navigation links stack on top of each other instead of next to each other */
|
||||||
|
@media screen and (max-width: 400px) {
|
||||||
|
.topnav a {
|
||||||
|
float: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{% extends "shared/base.html" %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
<title>Permissions</title>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% with msg=msg %}
|
||||||
|
{% include "components/alerts.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<h1 class="display-5">Permissions..</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead><tr>
|
||||||
|
<th scope="col">Name</th>
|
||||||
|
<th colspan="2">Actions</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for permission in permissions %}
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><a href="/admins/permissions/{{permission.id}}">{{permission.name}}</a></th>
|
||||||
|
<td><a href="/admin/permission/edit/{{permission.id}}" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Edit</a></td>
|
||||||
|
<td><a href="/admin/permission/delete/{{permission.id}}" class="btn btn-outline-danger btn-sm active" role="button" aria-pressed="true">Delete</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div>
|
||||||
|
<a href="/admin/permission/add" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Add Permission</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{% extends "shared/base.html" %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
<title>Profiles</title>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% with msg=msg %}
|
||||||
|
{% include "components/alerts.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<h1 class="display-5">Profiles..</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead><tr>
|
||||||
|
<th scope="col">Username</th>
|
||||||
|
<th scope="col">First Name</th>
|
||||||
|
<th scope="col">Last Name</th>
|
||||||
|
<th colspan="2">Actions</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for profile in profiles %}
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><a href="/admins/profiles/{{profile.id}}">{{profile.user_name}}</a></th>
|
||||||
|
<th scope="row">{{profile.first_name}}</th>
|
||||||
|
<th scope="row">{{profile.last_name}}</th>
|
||||||
|
<th scope="row">{{profile.email}}</th>
|
||||||
|
<td><a href="/admin/profile/edit/{{profile.id}}" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Edit</a></td>
|
||||||
|
<td><a href="/admin/profile/delete/{{profile.id}}" class="btn btn-outline-danger btn-sm active" role="button" aria-pressed="true">Delete</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div>
|
||||||
|
<a href="/admin/profile/add" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Add Profile</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{% extends "shared/base.html" %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
<title>Login</title>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<h5 class="display-5">Login to Kontor</h5>
|
||||||
|
<div class="text-danger font-weight-bold">
|
||||||
|
{% for error in errors %}
|
||||||
|
<li>{{error}}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="text-success font-weight-bold">
|
||||||
|
{% if msg %}
|
||||||
|
<div class="badge bg-success text-wrap font-weight-bold" style="font-size: large;">
|
||||||
|
{{msg}}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row my-5">
|
||||||
|
<form method="POST">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label>Email</label>
|
||||||
|
<input type="text" required placeholder="Your email" name="email" value="{{email}}" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label>Password</label>
|
||||||
|
<input type="password" required placeholder="Choose a secure password" value="{{password}}" name="password" class="form-control">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Submit</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
<div class="card shadow p-3 mb-2 bg-body rounded" style="width: 18rem;">
|
<div class="card shadow p-3 mb-2 bg-body rounded" style="width: 18rem;">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">{{obj.name}}</h5>
|
<h5 class="card-title">{{obj.name}}</h5>
|
||||||
|
<p class="card-text">Link : {{obj.weblink}}</p>
|
||||||
<a href="/comic/artists/{{obj.id}}" class="btn btn-primary">Read more</a>
|
<a href="/comic/artists/{{obj.id}}" class="btn btn-primary">Read more</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -20,12 +20,16 @@
|
|||||||
<th scope="row">Artist Name</th>
|
<th scope="row">Artist Name</th>
|
||||||
<td colspan="2">{{artist.name}}</td>
|
<td colspan="2">{{artist.name}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Link</th>
|
||||||
|
<td colspan="2">{{artist.weblink}}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Works</th>
|
<th scope="row">Works</th>
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
{% for work in artist.get_comics() %}
|
{% for work in artist.get_comics() %}
|
||||||
<p>
|
<p>
|
||||||
{{work}}:
|
<a href="/comic/worktypes/{{work.id}}">{{work.name}}</a>
|
||||||
<ul>
|
<ul>
|
||||||
{% for comic in artist.get_comics()[work] %}
|
{% for comic in artist.get_comics()[work] %}
|
||||||
<li><a href="/comic/comics/{{comic.id}}">{{comic.title}}</a></li>
|
<li><a href="/comic/comics/{{comic.id}}">{{comic.title}}</a></li>
|
||||||
@@ -43,8 +47,20 @@
|
|||||||
<th scope="row">Data Modified</th>
|
<th scope="row">Data Modified</th>
|
||||||
<td colspan="2">{{artist.last_modified_date}}</td>
|
<td colspan="2">{{artist.last_modified_date}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Data Version</th>
|
||||||
|
<td colspan="2">{{artist.version}}</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div>
|
||||||
|
<a href="/comic/artists" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Back to list</a>
|
||||||
|
<a href="/comic/artist/edit/{{artist.id}}" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Edit</a>
|
||||||
|
<a href="/comic/artist/delete/{{artist.id}}" class="btn btn-outline-danger btn-sm active" role="button" aria-pressed="true">Delete</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{% extends "shared/base.html" %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
<title>Edit Artist</title>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="text-danger font-weight-bold">
|
||||||
|
{% for error in errors %}
|
||||||
|
<li>{{error}}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row my-5">
|
||||||
|
<h3 class="text-center display-4">Edit an Artist entry</h3>
|
||||||
|
<form method="POST">
|
||||||
|
<div class="mb-3">
|
||||||
|
<input type="text" required class="form-control" name="artist.name" value="{{artist_name}}" placeholder="Artist name here">
|
||||||
|
<input type="text" required class="form-control" name="artist.link" value="{{artist_link}}" placeholder="Web link for artist here">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button type="submit" class="btn btn-primary">Submit</button>
|
||||||
|
<button type="cancel" class="btn btn-primary">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
{% for artist in artists %}
|
{% for artist in artists %}
|
||||||
<div class="col-lg-4 col-md-3 col-sm-10 mr-auto">
|
<div class="col-lg-4 col-md-3 col-sm-10 mr-auto">
|
||||||
{% with obj=artist %}
|
{% with obj=artist %}
|
||||||
{% include "components/artist_cards.html" %}
|
{% include "comic/artist_cards.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
{% if loop.index %3 %}
|
{% if loop.index %3 %}
|
||||||
|
|||||||
@@ -27,12 +27,17 @@
|
|||||||
{% endwith %}
|
{% endwith %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Link</th>
|
||||||
|
<td colspan="2">{{comic.weblink}}</td>
|
||||||
|
</tr>
|
||||||
|
{% if comic.get_artists()|length > 0 %}
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Works</th>
|
<th scope="row">Works</th>
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
{% for work in comic.get_artists() %}
|
{% for work in comic.get_artists() %}
|
||||||
<p>
|
<p>
|
||||||
{{work}}:
|
<a href="/comic/worktypes/{{work.id}}">{{work.name}}</a>
|
||||||
<ul>
|
<ul>
|
||||||
{% for artist in comic.get_artists()[work] %}
|
{% for artist in comic.get_artists()[work] %}
|
||||||
<li><a href="/comic/artists/{{artist.id}}">{{artist.name}}</a></li>
|
<li><a href="/comic/artists/{{artist.id}}">{{artist.name}}</a></li>
|
||||||
@@ -42,6 +47,29 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% if comic.volumes|length > 0 %}
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Volumes</th>
|
||||||
|
<td colspan="2">
|
||||||
|
<ul>
|
||||||
|
{% for volume in comic.volumes %}
|
||||||
|
<li><a href="/comic/volumes/{{volume.id}}">{{volume.name}}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Issues</th>
|
||||||
|
<td colspan="2">
|
||||||
|
<ul>
|
||||||
|
{% for issue in comic.sorted_issues() %}
|
||||||
|
<li><a href="/comic/issues/{{issue.id}}">{{issue.get_full_title()}}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Data Created</th>
|
<th scope="row">Data Created</th>
|
||||||
<td colspan="2">{{comic.created_date}}</td>
|
<td colspan="2">{{comic.created_date}}</td>
|
||||||
@@ -54,18 +82,15 @@
|
|||||||
<th scope="row">Data Version</th>
|
<th scope="row">Data Version</th>
|
||||||
<td colspan="2">{{comic.version}}</td>
|
<td colspan="2">{{comic.version}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<th scope="row">Issues</th>
|
|
||||||
<td colspan="2">
|
|
||||||
<ul>
|
|
||||||
{% for issue in comic.sorted_issues() %}
|
|
||||||
<li><a href="comic/issues/{{issue.id}}">{{issue.issue_number}}</a></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div>
|
||||||
|
<a href="/comic/comics" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Back to list</a>
|
||||||
|
<a href="/comic/comic/edit/{{comic.id}}" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Edit</a>
|
||||||
|
<a href="/comic/comic/delete/{{comic.id}}" class="btn btn-outline-danger btn-sm active" role="button" aria-pressed="true">Delete</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
{% extends "shared/base.html" %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
<title>Edit Comic</title>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="text-danger font-weight-bold">
|
||||||
|
{% for error in errors %}
|
||||||
|
<li>{{error}}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row my-5">
|
||||||
|
<h3 class="text-center display-4">Edit an Comic entry</h3>
|
||||||
|
<form class="form-horizontal" method="POST">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label col-sm-2" for="title">Title</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="text" class="form-control" id="title" name="title" value="{{comic_title}}" placeholder="Comic title here">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label col-sm-2" for="weblink">Link</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="text" class="form-control" id="weblink" name="weblink" value="{{comic_weblink}}" placeholder="Web link for comic here">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-offset-2 col-sm-10">
|
||||||
|
<div class="checkbox">
|
||||||
|
<label><input type="checkbox" id="completed" name="completed" value="{{comic_completed}}"> Completed</label>
|
||||||
|
</div
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-offset-2 col-sm-10">
|
||||||
|
<div class="checkbox">
|
||||||
|
<label><input type="checkbox" id="current_order" name="current_order" value="{{comic_current_order}}"> Current Order</label>
|
||||||
|
</div
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-offset-2 col-sm-10">
|
||||||
|
<button type="submit" class="btn btn-primary" name="action" value="submit">Submit</button>
|
||||||
|
<button type="cancel" class="btn btn-primary" name="action" value="cancel">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -4,28 +4,58 @@
|
|||||||
<title>Comic List</title>
|
<title>Comic List</title>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<h1>Comics..</Comics></h1>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block subnav %}
|
||||||
|
<div class="subnav">
|
||||||
|
<a href="/comic/artists/">Artists</a>
|
||||||
|
<a href="/comic/publishers/">Publishers</a>
|
||||||
|
<a href="/comic/worktypes">WorkTypes</a>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% with msg=msg %}
|
{% with msg=msg %}
|
||||||
{% include "components/alerts.html" %}
|
{% include "components/alerts.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<form class="form-inline" action="/comic/comics">
|
||||||
<h1 class="display-5">Find Jobs..</h1>
|
<input type="text" placeholder="Search" name="query">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="completed" {% if request.query_params.get("completed")=="on" %}checked{% endif %}>Completed
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="order" {% if request.query_params.get("order")=="on" %}checked{% endif %}>Order
|
||||||
|
</label>
|
||||||
|
<button type="submit">Search</button>
|
||||||
|
<div class="pill-nav">
|
||||||
|
<a href="/comic/comic/add">Add Comic</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="maincolumn">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead><tr>
|
||||||
|
<th scope="col">Title</th>
|
||||||
|
<th scope="col">Publisher</th>
|
||||||
|
<th scope="col">Completed</th>
|
||||||
|
<th colspan="2">Actions</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for comic in comics %}
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><a href="/comic/comics/{{comic.id}}">{{comic.title}}</a></th>
|
||||||
|
<td><a href="/comic/publishers/{{comic.publisher.id}}">{{comic.publisher.name}}</a></td>
|
||||||
|
<td>{% with check=comic.completed %}{% include "components/check.html" %}{% endwith %}</td>
|
||||||
|
<td><a href="/comic/comics/edit/{{comic.id}}" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Edit</a></td>
|
||||||
|
<td><a href="/comic/comics/delete/{{comic.id}}" class="btn btn-outline-danger btn-sm active" role="button" aria-pressed="true">Delete</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
|
||||||
{% for comic in comics %}
|
|
||||||
<div class="col-lg-4 col-md-3 col-sm-10 mr-auto">
|
|
||||||
{% with obj=comic %}
|
|
||||||
{% include "components/comic_cards.html" %}
|
|
||||||
{% endwith %}
|
|
||||||
{% if loop.index %3 %}
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
</div></div><br><div class="row">
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
{% extends "shared/base.html" %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
<title>Issue Detail</title>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<h1 class="display-5">Issue Detail</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<table class="table table-striped table-hover">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Issue Number</th>
|
||||||
|
<td colspan="2">{{issue.issue_number}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Full Title</th>
|
||||||
|
<td colspan="2">{{issue.title}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Published</th>
|
||||||
|
<td colspan="2">{{issue.published_on}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Auf Lager</th>
|
||||||
|
<td colspan="2">
|
||||||
|
{% with check=issue.in_stock %}
|
||||||
|
{% include "components/check.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Gelesen</th>
|
||||||
|
<td colspan="2">
|
||||||
|
{% with check=issue.is_read %}
|
||||||
|
{% include "components/check.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Comic</th>
|
||||||
|
<td colspan="2">
|
||||||
|
<a href="/comic/comics/{{issue.comic_id}}">{{issue.comic.title}}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% if issue.volume %}
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Volume</th>
|
||||||
|
<td colspan="2">
|
||||||
|
<a href="/comic/comics/{{issue.volume_id}}">{{issue.volume.name}}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Works</th>
|
||||||
|
<td colspan="2">
|
||||||
|
{% for work in issue.get_artists() %}
|
||||||
|
<p>
|
||||||
|
<a href="/comic/worktypes/{{work.id}}">{{work.name}}</a>
|
||||||
|
<ul>
|
||||||
|
{% for artist in issue.get_artists()[work] %}
|
||||||
|
<li><a href="/comic/artists/{{artist.id}}">{{artist.name}}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Data Created</th>
|
||||||
|
<td colspan="2">{{issue.created_date}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Data Modified</th>
|
||||||
|
<td colspan="2">{{issue.last_modified_date}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Data Version</th>
|
||||||
|
<td colspan="2">{{issue.version}}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -20,6 +20,24 @@
|
|||||||
<th scope="row">Publisher Name</th>
|
<th scope="row">Publisher Name</th>
|
||||||
<td colspan="2">{{publisher.name}}</td>
|
<td colspan="2">{{publisher.name}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% if publisher.parent_publisher_id %}
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Parent Company</th>
|
||||||
|
<td colspan="2"><a href="/comic/publishers/{{publisher.parent_publisher_id}}">{{publisher.parent_publisher.name}}</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% if publisher.imprints|length > 0 %}
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Imprints</th>
|
||||||
|
<td colspan="2">
|
||||||
|
<ul>
|
||||||
|
{% for imprint in publisher.imprints %}
|
||||||
|
<li><a href="/comic/publishers/{{imprint.id}}">{{imprint.name}}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Comics</th>
|
<th scope="row">Comics</th>
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
@@ -41,5 +59,12 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div>
|
||||||
|
<a href="/comic/publishers" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Back to list</a>
|
||||||
|
<a href="/comic/publisher/edit/{{publisher.id}}" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Edit</a>
|
||||||
|
<a href="/comic/publisher/delete/{{publisher.id}}" class="btn btn-outline-danger btn-sm active" role="button" aria-pressed="true">Delete</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
{% for publisher in publishers %}
|
{% for publisher in publishers %}
|
||||||
<div class="col-lg-4 col-md-3 col-sm-10 mr-auto">
|
<div class="col-lg-4 col-md-3 col-sm-10 mr-auto">
|
||||||
{% with obj=publisher %}
|
{% with obj=publisher %}
|
||||||
{% include "components/publisher_cards.html" %}
|
{% include "comic/publisher_cards.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% if loop.index %3 %}
|
{% if loop.index %3 %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
{% extends "shared/base.html" %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
<title>WorkType Detail</title>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<table class="table table-striped table-hover">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">WorkType Name</th>
|
||||||
|
<td colspan="2">{{worktype.name}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Works</th>
|
||||||
|
<td colspan="2">
|
||||||
|
{% for comic in worktype.get_artists() %}
|
||||||
|
<p>
|
||||||
|
{{comic}}:
|
||||||
|
<ul>
|
||||||
|
{% for artist in worktype.get_artists()[comic] %}
|
||||||
|
<li><a href="/comic/artists/{{artist.id}}">{{artist.name}}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">ID</th>
|
||||||
|
<td colspan="2">{{worktype.id}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Data Created</th>
|
||||||
|
<td colspan="2">{{worktype.created_date}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Data Modified</th>
|
||||||
|
<td colspan="2">{{worktype.last_modified_date}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Data Version</th>
|
||||||
|
<td colspan="2">{{worktype.version}}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div>
|
||||||
|
<a href="/comic/worktypes" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Back to list</a>
|
||||||
|
<a href="/comic/worktype/edit/{{worktype.id}}" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Edit</a>
|
||||||
|
<a href="/comic/worktype/delete/{{worktype.id}}" class="btn btn-outline-danger btn-sm active" role="button" aria-pressed="true">Delete</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{% extends "shared/base.html" %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
<title>Edit WorkType</title>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="text-danger font-weight-bold">
|
||||||
|
{% for error in errors %}
|
||||||
|
<li>{{error}}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row my-5">
|
||||||
|
<h3 class="text-center display-4">Add a WorkType</h3>
|
||||||
|
<form method="POST">
|
||||||
|
<div class="mb-3">
|
||||||
|
<input type="text" required class="form-control" name="worktype" value="{{worktype}}" placeholder="WorkType here">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Submit</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{% extends "shared/base.html" %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
<title>WorkTypes</title>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% with msg=msg %}
|
||||||
|
{% include "components/alerts.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
<div class="container">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead><tr>
|
||||||
|
<th scope="col">Name</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for worktype in worktypes %}
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><a href="/comic/worktypes/{{worktype.id}}">{{worktype.name}}</a></th>
|
||||||
|
<td><a href="/comic/worktype/edit/{{worktype.id}}" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Edit</a></td>
|
||||||
|
<td><a href="/comic/worktype/delete/{{worktype.id}}" class="btn btn-outline-danger btn-sm active" role="button" aria-pressed="true">Delete</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<a href="/comic/worktype/add" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Add WorkType</a>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
{% if check == 1 %}
|
{% if check %}
|
||||||
<img src="{{ url_for('static', path='images/tick.png') }}" alt="" width="24" height="24">
|
<img src="{{ url_for('static', path='images/tick.png') }}" alt="" width="24" height="24">
|
||||||
{% else %}
|
{% else %}
|
||||||
<img src="{{ url_for('static', path='images/cross.png') }}" alt="" width="24" height="24">
|
<img src="{{ url_for('static', path='images/cross.png') }}" alt="" width="24" height="24">
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<h2>Footer</h2>
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
<nav class="navbar navbar-expand-lg navbar-light bg-light px-5">
|
<div class="topnav">
|
||||||
|
<a href="/">Kontor</a>
|
||||||
|
<a href="/comic/comics">Comics</a>
|
||||||
|
<a href="/media/files">Media</a>
|
||||||
|
<a style="float:right" href="/login/">Login</a></li>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <nav class="navbar navbar-expand-lg navbar-light bg-light px-5">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a class="navbar-brand" href="#">
|
<a class="navbar-brand" href="#">Kontor</a>
|
||||||
<img src="{{ url_for('static', path='images/logo.png') }}" alt="" width="30" height="24">
|
|
||||||
</a>
|
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
@@ -18,7 +24,7 @@
|
|||||||
<li><a class="dropdown-item" href="/comic/artists/">Artists</a></li>
|
<li><a class="dropdown-item" href="/comic/artists/">Artists</a></li>
|
||||||
<li><a class="dropdown-item" href="/comic/publishers/">Publishers</a></li>
|
<li><a class="dropdown-item" href="/comic/publishers/">Publishers</a></li>
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
<li><a class="dropdown-item" href="#">Something else here</a></li>
|
<li><a class="dropdown-item" href="/comic/worktypes">WorkTypes</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
@@ -26,6 +32,7 @@
|
|||||||
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
|
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||||
<li><a class="dropdown-item" href="/media/files/">MediaFiles</a></li>
|
<li><a class="dropdown-item" href="/media/files/">MediaFiles</a></li>
|
||||||
<li><a class="dropdown-item" href="/media/actors/">MediaActors</a></li>
|
<li><a class="dropdown-item" href="/media/actors/">MediaActors</a></li>
|
||||||
|
<li><a class="dropdown-item" href="/media/videos/">MediaVideos</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
@@ -42,18 +49,19 @@
|
|||||||
<li><a class="dropdown-item" href="/register/">Signup</a></li>
|
<li><a class="dropdown-item" href="/register/">Signup</a></li>
|
||||||
<li><a class="dropdown-item" href="/login/">Login</a></li>
|
<li><a class="dropdown-item" href="/login/">Login</a></li>
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
<li><a class="dropdown-item" href="#">Something else here</a></li>
|
<li><a class="dropdown-item" href="/admin/profiles">Profiles</a></li>
|
||||||
|
<li><a class="dropdown-item" href="/admin/permissions">Permissions</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="navbar-nav ml-auto mb-2 mb-lg-0">
|
<ul class="navbar-nav ml-auto mb-2 mb-lg-0">
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
Jobs
|
Media
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
|
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||||
<li><a class="dropdown-item" href="/comics/">Comics</a></li>
|
<li><a class="dropdown-item" href="/media/add-link/">Add Link</a></li>
|
||||||
<li><a class="dropdown-item" href="/comics/">Media</a></li>
|
<li><a class="dropdown-item" href="/media/add-file">Add MediaFile</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -63,4 +71,4 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav> -->
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
<title>Kontor</title>
|
<title>Kontor</title>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<h1>Kontor</h1>
|
||||||
|
{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% with msg=msg %}
|
{% with msg=msg %}
|
||||||
{% include "components/alerts.html" %}
|
{% include "components/alerts.html" %}
|
||||||
|
|||||||
@@ -20,6 +20,10 @@
|
|||||||
<th scope="row">Actor Name</th>
|
<th scope="row">Actor Name</th>
|
||||||
<td colspan="2">{{actor.name}}</td>
|
<td colspan="2">{{actor.name}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">URL</th>
|
||||||
|
<td colspan="2">{{actor.url}}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Works</th>
|
<th scope="row">Works</th>
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
@@ -41,5 +45,12 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div>
|
||||||
|
<a href="/media/actors" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Back to list</a>
|
||||||
|
<a href="/media/actor/edit/{{actor.id}}" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Edit</a>
|
||||||
|
<a href="/media/actor/delete/{{actor.id}}" class="btn btn-outline-danger btn-sm active" role="button" aria-pressed="true">Delete</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
{% for actor in actors %}
|
{% for actor in actors %}
|
||||||
<div class="col-lg-4 col-md-3 col-sm-10 mr-auto">
|
<div class="col-lg-4 col-md-3 col-sm-10 mr-auto">
|
||||||
{% with obj=actor %}
|
{% with obj=actor %}
|
||||||
{% include "components/actor_cards.html" %}
|
{% include "media/actor_cards.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
{% if loop.index %3 %}
|
{% if loop.index %3 %}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{% extends "shared/base.html" %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
<title>Add a Video Link</title>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="text-danger font-weight-bold">
|
||||||
|
{% for error in errors %}
|
||||||
|
<li>{{error}}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row my-5">
|
||||||
|
<h3 class="text-center display-4">Add a Video Link</h3>
|
||||||
|
<form method="POST">
|
||||||
|
<div class="mb-3">
|
||||||
|
<input type="text" required class="form-control" name="url" value="{{url}}" placeholder="Video Link here">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Submit</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
@@ -61,5 +61,12 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div>
|
||||||
|
<a href="/media/files" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Back to list</a>
|
||||||
|
<a href="/media/file/edit/{{mediafile.id}}" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Edit</a>
|
||||||
|
<a href="/media/file/delete/{{mediafile.id}}" class="btn btn-outline-danger btn-sm active" role="button" aria-pressed="true">Delete</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -4,26 +4,54 @@
|
|||||||
<title>MediaFiles List</title>
|
<title>MediaFiles List</title>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<h1>MediaFiles..</h1>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block subnav %}
|
||||||
|
{% include "media/media_nav.html" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% with msg=msg %}
|
{% with msg=msg %}
|
||||||
{% include "components/alerts.html" %}
|
{% include "components/alerts.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
<div class="container">
|
<div class="row">
|
||||||
<table class="table table-hover">
|
<form class="form-inline" action="/media/files">
|
||||||
<thead><tr>
|
<input type="text" placeholder="Search" name="query">
|
||||||
<th scope="col">Titel</th>
|
<label>
|
||||||
<th scope="col">URL</th>
|
<input type="checkbox" name="review" {% if request.query_params.get("review")=="on" %}checked{% endif %}>Review
|
||||||
<th scope="col">Cloudlink</th>
|
</label>
|
||||||
</tr></thead>
|
<label>
|
||||||
<tbody>
|
<input type="checkbox" name="download" {% if request.query_params.get("download")=="on" %}checked{% endif %}>Download
|
||||||
{% for mediafile in mediafiles %}
|
</label>
|
||||||
<tr>
|
<button type="submit">Search</button>
|
||||||
<th scope="row"><a href="/media/files/{{mediafile.id}}">{{mediafile.title}}</a></th>
|
<div class="pill-nav">
|
||||||
<td>{{mediafile.url}}</td>
|
<a href="/media/files/add">Add MediaFile</a>
|
||||||
<td>{{mediafile.cloud_link}}</td>
|
</div>
|
||||||
</tr>
|
</form>
|
||||||
{% endfor %}
|
</div>
|
||||||
</tbody>
|
<div class="row">
|
||||||
</table>
|
<div class="maincolumn">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead><tr>
|
||||||
|
<th scope="col">Titel</th>
|
||||||
|
<th scope="col">Review</th>
|
||||||
|
<th scope="col">Download</th>
|
||||||
|
<th colspan="2">Actions</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for mediafile in mediafiles %}
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><a href="/media/files/{{mediafile.id}}">{{mediafile.title}}</a></th>
|
||||||
|
<td>{% with check=mediafile.review %}{% include "components/check.html" %}{% endwith %}</td>
|
||||||
|
<td>{% with check=mediafile.should_download %}{% include "components/check.html" %}{% endwith %}</td>
|
||||||
|
<td><a href="/media/files/edit/{{mediafile.id}}" class="btn btn-outline-primary btn-sm active" role="button" aria-pressed="true">Edit</a></td>
|
||||||
|
<td><a href="/media/files/delete/{{mediafile.id}}" class="btn btn-outline-danger btn-sm active" role="button" aria-pressed="true">Delete</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user