166 Commits

Author SHA1 Message Date
Thomas Peetz 2cfaaa4d87 remove obsolete kontor.py
fixes #15
2025-04-30 17:28:09 +02:00
Thomas Peetz 5850b1cfb0 rename User to Profile and Role to Permission. refs #15 2025-04-30 17:22:40 +02:00
Thomas Peetz 8833d2ca6a add db-password.txt in preparation of switching to PostgreSQL 2025-04-29 14:48:31 +02:00
Thomas Peetz 1290a45fb5 kontor-api: Add details pages for Comics, Artists and MediaFiles 2025-04-28 15:47:07 +02:00
Thomas Peetz 3afbbf900d add life of MediaFile 2025-04-27 22:11:56 +02:00
tpeetz f71419d37b Merge branch 'feature/12-import-templates-from-bottle' into 'develop/0.1.0'
Resolve "import templates from  bottle"

Closes #12

See merge request tpeetz/kontor!16
2025-04-27 14:53:34 +02:00
Thomas Peetz f5f673fd4e add comic artist details page 2025-04-27 14:52:07 +02:00
Thomas Peetz 8ec0784490 import templates from bottle and django. Use bootstrap for HTML content 2025-04-27 14:14:56 +02:00
Thomas Peetz 13faff8e01 add dependency to mariadb for kontor and kontor-api 2025-04-26 22:57:52 +02:00
Thomas Peetz 0a4f6bb0f9 remove kontor-cli 2025-04-26 22:17:09 +02:00
Thomas Peetz 3b4e5634b1 remove scriptsa.bak 2025-04-26 00:40:00 +02:00
Thomas Peetz 8c98e6de26 kontor-spring: change IssueView to use StatusIcon 2025-04-25 16:32:31 +02:00
Thomas Peetz 525d82cbda remove conflict marker 2025-04-25 08:13:33 +02:00
Thomas Peetz e26aaa1420 fix deprecatino warning in build.gradle 2025-04-25 07:47:29 +02:00
tpeetz ec7f05d8fc Merge branch 'feature/10-improve-docker-image' into 'develop/0.1.0'
Feature/10 improve docker image

See merge request tpeetz/kontor!14
2025-04-25 01:21:16 +02:00
Thomas Peetz 727e95e732 setup kontor-schema 2025-04-25 01:19:59 +02:00
tpeetz 75d3bf6ffb Resolve "evaluate uv" 2025-04-25 01:19:59 +02:00
Thomas Peetz 94782fc468 relocate sources 2025-04-25 01:19:59 +02:00
tpeetz 101ece44d8 Resolve "evaluate uv" 2025-04-25 01:19:59 +02:00
Thomas Peetz e880594a5b change schema back to use BIT(1) for boolean 2025-04-25 01:19:59 +02:00
Thomas Peetz c222d4cd7a copy sources from branch develop/0.1.0 2025-04-25 01:19:59 +02:00
Thomas Peetz cb0fa3f728 change schema 2025-04-25 01:19:59 +02:00
Thomas Peetz 4709e431b7 add schema from kontor-schema 2025-04-25 01:19:59 +02:00
Thomas Peetz bfccca72a1 copy schema to kontor-api 2025-04-25 01:19:59 +02:00
Thomas Peetz 8a3eebaab5 move schema to separate uv project 2025-04-25 01:19:47 +02:00
Thomas Peetz 6716103d0c setup kontor-schema 2025-04-25 01:19:37 +02:00
Thomas Peetz ee78af1abe relocate sources 2025-04-25 01:19:28 +02:00
tpeetz 1fc726ee4b Resolve "evaluate uv" 2025-04-25 01:19:28 +02:00
Thomas Peetz 882f48de0b relocate sources 2025-04-25 01:19:14 +02:00
tpeetz 097a3efd4a Merge branch 'feature/11-evaluate-uv' into 'develop/0.1.0'
Resolve "evaluate uv"

Closes #11

See merge request tpeetz/kontor!15
2025-04-21 17:39:21 +02:00
tpeetz 934ef826c9 Resolve "evaluate uv" 2025-04-21 17:39:20 +02:00
Thomas Peetz bb049441b4 extend check_kontor.py for comparing json file with db 2025-04-21 00:55:50 +02:00
tpeetz 2bdbef8f8a change boolean fields in schema from Bit(1) to Boolean 2025-04-19 01:40:13 +02:00
tpeetz 20f29d88fa add script to copy from json to mariadb 2025-04-18 22:55:40 +02:00
Thomas Peetz 1997957069 remove .itlab-ci.yml and call of pytest to Makefile 2025-04-18 00:52:00 +02:00
Thomas Peetz 116bb77e4c update check_kontor.py to call Kontor API 2025-04-18 00:09:30 +02:00
Thomas Peetz a6eeea6c1f add testing to fastapi 2025-04-17 02:21:14 +02:00
Thomas Peetz 4a61d6a727 refactor scripts to work wit api 2025-04-16 05:08:59 +02:00
Thomas Peetz 98e3d91edd reorganize python projects 2025-04-15 01:29:08 +02:00
tpeetz a169f6a6c1 Merge branch 'feature/9-evaluate-sqlmodel' into 'develop/0.1.0'
evaluate sqlmodel

See merge request tpeetz/kontor!13
2025-04-13 16:16:10 +02:00
tpeetz b14a267b5b evaluate sqlmodel 2025-04-13 16:16:10 +02:00
Thomas Peetz a43e2c806c add virtualenv to scripts 2025-04-08 12:14:20 +02:00
Thomas Peetz 10834df92b add script for import data 2025-04-08 11:49:52 +02:00
Thomas Peetz 3d97c3360a add MetaDataColumn import 2025-04-07 22:26:33 +02:00
Thomas Peetz 76d91dd506 add import view 2025-04-07 19:34:08 +02:00
Thomas Peetz 84aff2b7d7 add data for cards 2025-04-06 09:41:53 +02:00
Thomas Peetz 2f8c10a692 add package repository and refactor classes 2025-04-03 22:31:11 +02:00
Thomas Peetz c5f74e007a organize imports 2025-04-01 23:13:08 +02:00
Thomas Peetz 243f693f9d ignore project files 2025-04-01 23:08:54 +02:00
Thomas Peetz 976b4f3f47 extract ColumnToggleContextMenu and createStatusIcon to separate classes 2025-04-01 22:09:41 +02:00
Thomas Peetz 1b18dae311 add scripts from repository python-scripts 2025-04-01 08:12:53 +02:00
Thomas Peetz 38e77b25b1 change display of boolean in ComicView and add docker tasks to build.gradle 2025-03-31 22:41:08 +02:00
tpeetz bbc08863f0 Merge branch 'feature/8-create-docker-build' into 'develop/0.1.0'
Resolve "Create docker build"

Closes #8

See merge request tpeetz/kontor!6
2025-03-31 18:40:40 +02:00
tpeetz d58f18d206 Merge branch 'develop/0.1.0' into 'feature/8-create-docker-build'
# Conflicts:
#   springboot/Dockerfile
2025-03-31 18:30:11 +02:00
Thomas Peetz fd5bc54eee import repositories kontor-java and kontor-ee 2025-03-30 15:57:09 +02:00
Thomas Peetz cbc57d22f8 add Docker build and domcker-compose file 2025-03-30 04:17:58 +02:00
Thomas Peetz a5cdf8867a move Git repository kontor-docker to directory bottle-docker 2025-03-29 19:05:31 +01:00
tpeetz 45971518ee change ModuleData view 2025-03-17 18:43:55 +01:00
Thomas Peetz 7db244f599 add length vor varchar for _id fields 2025-02-12 18:07:48 +01:00
Thomas Peetz 41e2b11da2 add details view 2025-02-11 16:16:38 +01:00
Thomas Peetz 0d1b2e416e add tab witg table and details 2025-02-11 10:39:07 +01:00
Thomas Peetz d8eecb4dab fix NPE in MediaActorView 2025-02-10 15:56:32 +01:00
Thomas Peetz 3bb3b22cea add MediaActor and MediaActorFile to kontor-schema 2025-02-10 12:54:59 +01:00
Thomas Peetz 8ffd421c1b add views for MediaActor and adapt MediaFile 2025-02-10 12:25:39 +01:00
Thomas Peetz 6d54c7f315 add MediaActor 2025-02-10 00:27:23 +01:00
Thomas Peetz f5fb743503 refactor kontor-schema 2025-02-08 20:22:21 +01:00
tpeetz ca9022d289 Merge branch 'table_sortable' into 'develop/0.1.0'
Table sortable

See merge request tpeetz/kontor!11
2025-02-06 15:59:29 +00:00
Thomas Peetz ed0e108599 refactor project by using enums for recurring strings 2025-02-06 16:48:41 +01:00
Thomas Peetz 7dc18b10cb refactor kontor-schema by moving classes to seprate modules 2025-02-06 16:44:14 +01:00
Thomas Peetz 171bc1676a refactor project by using enums for recurring strings 2025-02-06 16:33:15 +01:00
Thomas Peetz 1a5cd6ffe8 refactor kontor-schema by moving classes to seprate modules 2025-02-06 11:09:09 +01:00
Thomas Peetz 71ecfaff1f make tables sortable 2025-02-03 23:28:35 +01:00
Thomas Peetz f33aaadce7 fix problem with closing subwindows and define field length for id in schema 2025-02-03 17:37:19 +01:00
Thomas Peetz 591171b223 add meta data subwindow 2025-02-02 21:38:16 +01:00
Thomas Peetz da453d642d rename kontor.py to main.py for pyside6-deploy 2025-01-28 23:19:23 +01:00
Thomas Peetz e733fa21e6 moved update and download functionality to kontor-schema 2025-01-28 15:10:10 +01:00
Thomas Peetz c61e49720e update title 2025-01-27 22:42:32 +01:00
Thomas Peetz d01489b1fa reformat python file 2025-01-27 15:21:51 +01:00
Thomas Peetz 93c7498a83 setting return value of adding link to db 2025-01-27 15:20:35 +01:00
Thomas Peetz fe89cc6e0f add Mixin for videos 2025-01-27 13:34:15 +01:00
Thomas Peetz 1e9ca7c1a4 add Enum for Videotype 2025-01-25 03:53:47 +01:00
Thomas Peetz 400aff6524 Add MediaWindow 2025-01-24 08:58:59 +01:00
Thomas Peetz 845f085a76 remove data.json 2025-01-24 05:31:20 +01:00
Thomas Peetz 88c623edb7 subclass VideoFile 2025-01-24 05:02:08 +01:00
Thomas Peetz 0cc73c09aa remove pyproject.toml 2025-01-21 20:14:27 +01:00
Thomas Peetz e8660faa06 Refactor GUI to use Multi Document Interface 2025-01-21 20:12:50 +01:00
Thomas Peetz 4e884fdbe5 fix exporting and importing from file 2025-01-21 16:46:12 +01:00
Thomas Peetz 4c330a1138 remove kontor-cli.backup 2025-01-21 09:42:33 +01:00
Thomas Peetz 65288a53a1 complete MetaDataTable and MetaDataColumn 2025-01-20 15:40:45 +01:00
Thomas Peetz 60fba0d3e9 add dependencies to kontor-schema and kontor-video in rquirements.txt 2025-01-19 23:39:53 +01:00
Thomas Peetz ada723dc48 separate cli and gui application in own python packages. provide database schema as python package. 2025-01-19 23:36:52 +01:00
Thomas Peetz f07c7b74ee prepare progress bar for download 2025-01-19 17:49:13 +01:00
Thomas Peetz f3c59c11ba add progress in statusbar 2025-01-18 01:28:27 +01:00
Thomas Peetz 3c4d3ad326 add check files command 2025-01-17 17:19:31 +01:00
Thomas Peetz 917e287784 fix missing argument for KontorDB in import_cmd and export 2025-01-17 13:38:16 +01:00
Thomas Peetz 611d6b9e61 add update and download for MediaFile 2025-01-17 13:31:49 +01:00
Thomas Peetz 58263cb854 use sqlalchemy session only as short as possible 2025-01-16 23:03:30 +01:00
Thomas Peetz 33dcbc4413 add command to add link 2025-01-16 16:50:14 +01:00
Thomas Peetz f3cf1a17f3 add import functionality 2025-01-15 14:16:35 +01:00
Thomas Peetz 3466c10a88 add commands and subcommands 2025-01-14 22:58:15 +01:00
Thomas Peetz 4cea554116 add missing schema classes 2025-01-14 17:53:05 +01:00
tpeetz 332e09b283 Merge branch 'feature/5-add-token-management' into 'develop/0.1.0'
Resolve "Add token management"

Closes #5

See merge request tpeetz/kontor!3
2025-01-14 14:55:54 +00:00
tpeetz d3c720f995 Merge branch 'develop/0.1.0' into 'feature/5-add-token-management'
# Conflicts:
#   qt/gui/main_window.py
2025-01-14 14:44:46 +00:00
Thomas Peetz 453ad2af7b merged command line and gui app in one command 2025-01-14 15:39:07 +01:00
Thomas Peetz 54bc17ee7d add cli app and fix relationship typos 2025-01-14 15:39:06 +01:00
Thomas Peetz 89b7b87b8c add export to json 2025-01-14 15:39:03 +01:00
Thomas Peetz 3f0a37ff19 create files with abstract model class 2025-01-14 15:38:30 +01:00
Thomas Peetz 954dab289a extend MetaDataColumn by adding column for name of column for referenced tables 2025-01-14 15:38:30 +01:00
Thomas Peetz 5affa16505 add tysc schema 2025-01-14 15:38:30 +01:00
Thomas Peetz d2b1cb999a reorganize files for Qt application 2025-01-14 15:38:27 +01:00
Thomas Peetz f1d36acff7 add sqlalchemy as orm tool 2025-01-14 15:35:53 +01:00
Thomas Peetz 7e8a6b3c91 add repo kontor-quarkus 2025-01-14 15:35:53 +01:00
Thomas Peetz b424e95e05 import other kontor repos into directories 2025-01-14 15:35:53 +01:00
Thomas Peetz 28746adfbb import from kontor-flask branch develop/0.1.0 2025-01-14 15:35:53 +01:00
Thomas Peetz 0d2f27f771 import from kontor-flask 2025-01-14 15:35:53 +01:00
Thomas Peetz d6410e2584 implement generic table model
implement generic table model which reads table info from db and
constructs table view
2025-01-14 15:21:03 +01:00
Thomas Peetz ce1514f20a fix errors when displaying empty table 2025-01-14 15:18:49 +01:00
Thomas Peetz c8ea7c4188 refresh table when updated 2025-01-14 15:18:46 +01:00
Thomas Peetz 245ee2378e display tick and cross for boolean values 2025-01-14 15:17:41 +01:00
Thomas Peetz afb3ac88c8 first implementation to show Comics and MediaFiles 2025-01-14 15:17:37 +01:00
Thomas Peetz 3a0a0055a6 Add Taskjuggler project file 2025-01-14 15:13:59 +01:00
tpeetz 283577df9b Merge branch 'add-cli-app' into 'develop/0.1.0'
Add cli app

See merge request tpeetz/kontor!9
2025-01-14 12:22:53 +00:00
Thomas Peetz 276302570f merged command line and gui app in one command 2025-01-14 13:10:24 +01:00
Thomas Peetz f74c07af9a add cli app and fix relationship typos 2025-01-13 22:54:25 +01:00
Thomas Peetz d0eae1980a add export to json 2025-01-13 16:18:13 +01:00
Thomas Peetz 820ae3d374 create files with abstract model class 2025-01-13 00:26:42 +01:00
Thomas Peetz bf14c9a020 extend MetaDataColumn by adding column for name of column for referenced tables 2025-01-12 02:41:40 +01:00
Thomas Peetz a3652cc9b8 add tysc schema 2025-01-11 19:00:58 +01:00
Thomas Peetz 8f2e99195a reorganize files for Qt application 2025-01-11 01:54:05 +01:00
Thomas Peetz 2ae11e24ef add sqlalchemy as orm tool 2025-01-10 17:39:54 +01:00
Thomas Peetz a1e8474149 add repo kontor-quarkus 2025-01-09 22:28:46 +01:00
Thomas Peetz 9b329c0ac4 import other kontor repos into directories 2025-01-09 19:28:50 +01:00
tpeetz 8120c92b32 Merge branch 'feature/4-import-kontor-flask-into-directory-flask' into 'develop/0.1.0'
Import kontor-flask into directory flask

Closes #4

See merge request tpeetz/kontor!2
2025-01-08 21:44:47 +00:00
Thomas Peetz 5c261529fc import from kontor-flask branch develop/0.1.0 2025-01-08 22:34:21 +01:00
Thomas Peetz 6923b3e5ee import from kontor-flask 2025-01-08 22:31:20 +01:00
Thomas Peetz fc4110b11d fix problem in string formatting 2025-01-08 22:25:44 +01:00
tpeetz e078f43cc6 set correct project id in build.gradle 2025-01-08 22:25:44 +01:00
Thomas Peetz 24e4c0b58e import export dialog by adding selection of export type 2025-01-08 22:25:44 +01:00
Thomas Peetz 8d31a92692 improve view of meta data by using icons for boolean and add search 2025-01-08 22:25:44 +01:00
Thomas Peetz c619745a4a prevent NPE 2025-01-08 22:25:44 +01:00
Thomas Peetz c769115331 implement general data tab view 2025-01-08 22:25:44 +01:00
Thomas Peetz 1a7da0ab9f add column header info 2025-01-08 22:25:44 +01:00
tpeetz 78632e0e12 Import kontor-spring into directory springboot 2025-01-08 22:25:44 +01:00
Thomas Peetz 3aed8af868 implement generic table model
implement generic table model which reads table info from db and
constructs table view
2025-01-08 22:25:44 +01:00
Thomas Peetz c6d1e4d7e7 fix errors when displaying empty table 2025-01-08 22:25:44 +01:00
Thomas Peetz d98dc79cf8 refresh table when updated 2025-01-08 22:25:44 +01:00
Thomas Peetz 9dcc09c586 display tick and cross for boolean values 2025-01-08 22:25:44 +01:00
Thomas Peetz 57e7b9e999 first implementation to show Comics and MediaFiles 2025-01-08 22:25:44 +01:00
Thomas Peetz d97145b629 add first implementation of Dockerfile 2025-01-08 21:57:13 +01:00
Thomas Peetz 8ba23b6365 add first example of a Dockerfile for a Spring Boot Application 2025-01-08 21:41:20 +01:00
Thomas Peetz 66d61e2c1f add entity token 2025-01-08 21:08:15 +01:00
Thomas Peetz 83f645d4c6 fix problem in string formatting 2025-01-08 15:34:11 +01:00
tpeetz 7d84e341d6 set correct project id in build.gradle 2025-01-08 14:28:18 +00:00
Thomas Peetz c3ec0977c0 import export dialog by adding selection of export type 2025-01-08 14:15:29 +01:00
Thomas Peetz d51bc47ffe improve view of meta data by using icons for boolean and add search 2025-01-08 10:39:52 +01:00
Thomas Peetz faa1d73eea prevent NPE 2025-01-08 08:05:09 +01:00
Thomas Peetz fe407d6d89 implement general data tab view 2025-01-07 21:18:14 +01:00
Thomas Peetz 0759c99c1d add column header info 2025-01-07 01:48:03 +01:00
tpeetz 77bb86a9fa Merge branch 'feature/2-import-kontor-spring-into-directory-springboot' into 'develop/0.1.0'
Import kontor-spring into directory springboot

Closes #2

See merge request tpeetz/kontor!1
2025-01-06 17:09:38 +00:00
tpeetz 4d7d7391c3 Import kontor-spring into directory springboot 2025-01-06 17:09:38 +00:00
Thomas Peetz c5aa3b32ef implement generic table model
implement generic table model which reads table info from db and
constructs table view
2025-01-06 17:07:20 +01:00
Thomas Peetz 4ff999a74e fix errors when displaying empty table 2025-01-05 23:44:29 +01:00
Thomas Peetz bdf2b2ba43 refresh table when updated 2025-01-05 21:47:27 +01:00
Thomas Peetz 04dfe483e4 display tick and cross for boolean values 2025-01-05 14:13:15 +01:00
Thomas Peetz 4a279738e3 first implementation to show Comics and MediaFiles 2025-01-05 14:10:15 +01:00
Thomas Peetz 3f65ec55fc Add Taskjuggler project file 2024-12-05 20:40:13 +01:00
303 changed files with 4772 additions and 26606 deletions
+13 -5
View File
@@ -1,8 +1,16 @@
.vscode/
.idea/
.angular/
.theia/
.vscode/
__pycache__/
node_modules/
.editorconfig
bonus/
icons/
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
couchdb-password.txt
+3 -6
View File
@@ -1,12 +1,9 @@
kontor_api := kontor-api
kontor_spring := kontor-spring
kontor_angular := kontor-angular
TARGET=docker
.PHONY: all $(kontor_spring) $(kontor_api) $(kontor_angular)
.PHONY: all $(kontor_spring) $(kontor_api)
all: $(kontor_spring) $(kontor_api)
all: $(kontor_spring) $(kontor_api) $(kontor_angular)
$(kontor_spring) $(kontor_api) $(kontor_angular):
$(kontor_spring) $(kontor_api):
$(MAKE) --directory=$@ $(TARGET)
-22
View File
@@ -1,22 +0,0 @@
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
-35
View File
@@ -1,35 +0,0 @@
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
+17 -71
View File
@@ -1,98 +1,44 @@
include:
- ./compose-postgres.yaml
services:
activemq:
image: apache/activemq-artemis:latest-alpine
mariadb:
image: mariadb
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: kontor
MYSQL_USER: kontor
MYSQL_PASSWORD: kontor
MYSQL_DATABASE: kontor
ports:
- 61616:61616
- 8161:8161
- 5672:5672
- 3316:3306
networks:
- integration
- frontend
- database
volumes:
- activemq-data:/var/lib/artemis-instance
- mariadb-storage:/var/lib/mysql:rw
kontor:
build:
context: ./kontor-spring
dockerfile: Dockerfile
tags:
- kontor:0.2.0-SNAPSHOT
image: kontor:0.2.0-SNAPSHOT
image: kontor
restart: unless-stopped
networks:
- database
- integration
- frontend
ports:
- 8000:8000
volumes:
- images-data:/data/images
depends_on:
postgres:
condition: service_healthy
- mariadb
kontor-api:
build:
context: ./kontor-api
dockerfile: Dockerfile
tags:
- kontor-api:0.2.0-SNAPSHOT
image: kontor-api:0.2.0-SNAPSHOT
image: kontor-api
restart: unless-stopped
networks:
- database
- integration
- frontend
ports:
- 8800:8800
volumes:
- images-data:/data/images
depends_on:
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
- mariadb
networks:
database:
integration:
name: integration
frontend:
volumes:
activemq-data:
images-data:
volumes:
mariadb-storage:
-3
View File
@@ -1,3 +0,0 @@
dist
node_modules
-42
View File
@@ -1,42 +0,0 @@
# 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
-14
View File
@@ -1,14 +0,0 @@
### 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
-8
View File
@@ -1,8 +0,0 @@
.PHONY: docker dev
docker:
docker build -t kontor-angular:0.2.0-SNAPSHOT .
dev:
ng serve
-59
View File
@@ -1,59 +0,0 @@
# 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.
-94
View File
@@ -1,94 +0,0 @@
{
"$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
}
}
-37
View File
@@ -1,37 +0,0 @@
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;
}
}
-9522
View File
File diff suppressed because it is too large Load Diff
-45
View File
@@ -1,45 +0,0 @@
{
"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.

Before

Width:  |  Height:  |  Size: 15 KiB

-12
View File
@@ -1,12 +0,0 @@
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)
]
};
-342
View File
@@ -1,342 +0,0 @@
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<!-- * * * * * * * * * * * 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 />
-3
View File
@@ -1,3 +0,0 @@
import { Routes } from '@angular/router';
export const routes: Routes = [];
View File
-25
View File
@@ -1,25 +0,0 @@
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');
});
});
-12
View File
@@ -1,12 +0,0 @@
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');
}
-13
View File
@@ -1,13 +0,0 @@
<!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>
-6
View File
@@ -1,6 +0,0 @@
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));
-1
View File
@@ -1 +0,0 @@
/* You can add global styles to this file, and also import other style files */
-15
View File
@@ -1,15 +0,0 @@
/* 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"
]
}
-34
View File
@@ -1,34 +0,0 @@
/* 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"
}
]
}
-14
View File
@@ -1,14 +0,0 @@
/* 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.
-3
View File
@@ -1,4 +1 @@
.env
.coverage
app.log
+3 -1
View File
@@ -1,7 +1,8 @@
## ------------------------------- Builder Stage ------------------------------ ##
FROM python:3.13-bookworm AS builder
RUN apt-get update && apt-get install --no-install-recommends -y build-essential && \
RUN apt-get update && apt-get install --no-install-recommends -y \
build-essential libmariadb-dev && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# Download the latest installer, install it and then remove it
@@ -41,6 +42,7 @@ WORKDIR /app
COPY /src src
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
ENV PATH="/app/.venv/bin:$PATH"
+3 -3
View File
@@ -4,11 +4,11 @@ clean:
find . -name '*.py[co]' -delete
test:
DB_SERVER=localhost uv run pytest -v --cov --cov-report=term --cov-report=html:coverage-report
DB_HOST=localhost uv run pytest -v --cov --cov-report=term --cov-report=html:coverage-report
docker: clean
docker build --target=production -t kontor-api:0.2.0-SNAPSHOT .
docker build --target=production -t kontor-api -t kontor-api:0.1.0-SNAPSHOT .
dev:
DB_SERVER=127.0.0.1 uv run fastapi dev src/main.py --port 8008
MARIADB_SERVER=localhost uv run fastapi dev src/main.py --port 8008
+1 -7
View File
@@ -8,6 +8,7 @@ dependencies = [
"beautifulsoup4>=4.13.4",
"fastapi[standard]>=0.115.12",
"httpx==0.24.1",
"mariadb>=1.1.12",
"sqlalchemy>=2.0.40",
"platformdirs>=4.3.7",
"pathlib>=1.0.1",
@@ -21,11 +22,4 @@ dependencies = [
"python-jose>=3.4.0",
"python-multipart>=0.0.20",
"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",
]
+2 -4
View File
@@ -1,10 +1,8 @@
from fastapi import APIRouter
from src.apis.version1 import comic, mediaactor, mediafile, tysc, admin
from src.apis.version1 import comic, media, tysc
api_router = APIRouter(prefix="/api")
api_router.include_router(comic.router, prefix="/comics", tags=["comics"])
api_router.include_router(mediafile.router, prefix="/media", tags=["media"])
api_router.include_router(mediaactor.router, prefix="/media", tags=["media"])
api_router.include_router(media.router, prefix="/media", tags=["media"])
api_router.include_router(tysc.router, prefix="/tysc", tags=["tysc"])
api_router.include_router(admin.router, prefix="/login", tags=["login"])
-40
View File
@@ -1,13 +1,4 @@
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 sqlalchemy.orm import Session
@@ -15,34 +6,3 @@ from sqlalchemy.orm import Session
from src.db.session import 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
-69
View File
@@ -1,69 +0,0 @@
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
+13 -18
View File
@@ -1,28 +1,31 @@
from typing import List, AnyStr
from uuid import UUID
from typing import List
from fastapi import APIRouter, HTTPException, status
from sqlalchemy import select
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.artist import ArtistCreation, ArtistDetailResponse, ArtistResponse
from src.db.models.comic import Comic, Artist, Issue
from src.schema.comics.issue import IssueDetailsResponse
from src.schema.comics.artist import ArtistCreation, ArtistDetailResponse, ArtistResponse, get_artist_details
from src.db.models.comic import Comic, Artist
router = APIRouter()
router = APIRouter(
prefix="/comic",
tags=["comics"],
responses={404: {"description": "Not found"}},
)
@router.get("/comics")
def get_all_comics(db: SessionDep) -> List[ComicResponse]:
results: List[ComicResponse] = []
comics = list_comics(db)
comics = db.scalars(select(Comic)).all()
for comic in comics:
response = get_short_info(comic)
results.append(response)
return results
@router.get("/comics/{comic_id}", response_model=ComicDetailsResponse)
def get_comic(comic_id: AnyStr, db: SessionDep) -> ComicDetailsResponse:
def get_comic(comic_id: UUID, db: SessionDep) -> ComicDetailsResponse:
comic = db.get(Comic, comic_id)
if comic is None:
raise HTTPException(status_code=404, detail="Comic could not be found")
@@ -38,7 +41,7 @@ def get_all_artists(db: SessionDep) -> List[ArtistResponse]:
return results
@router.get("/artists/{artist_id}", response_model=ArtistDetailResponse)
def get_artist(artist_id: AnyStr, db: SessionDep) -> ArtistDetailResponse:
def get_artist(artist_id: UUID, db: SessionDep) -> ArtistDetailResponse:
artist = db.get(Artist, artist_id)
if artist is None:
raise HTTPException(status_code=404, detail="Artist could not be found")
@@ -57,11 +60,3 @@ def add_artist(db: SessionDep, artist_creation: ArtistCreation) -> ArtistRespons
response = ArtistResponse(id=artist.id, name=artist.name)
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
+76
View File
@@ -0,0 +1,76 @@
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
@@ -1,21 +0,0 @@
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
-112
View File
@@ -1,112 +0,0 @@
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 -1
View File
@@ -5,7 +5,11 @@ from src.apis.utils import SessionDep
from src.schema.tysc.sport import SportResponse
from src.db.models.tysc import Sport
router = APIRouter()
router = APIRouter(
prefix="/tysc",
tags=["tysc"],
responses={404: {"description": "Not found"}},
)
@router.get("/sports")
def get_all_sports(db: SessionDep) -> List[SportResponse]:
+8 -10
View File
@@ -9,17 +9,15 @@ load_dotenv(dotenv_path=env_path)
class Settings:
PROJECT_NAME: str = "Kontor"
PROJECT_VERSION: str = "0.2.0"
PROJECT_VERSION: str = "0.1.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()
-44
View File
@@ -1,44 +0,0 @@
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__)
-21
View File
@@ -1,21 +0,0 @@
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
+24 -20
View File
@@ -1,6 +1,7 @@
from datetime import datetime
from sqlalchemy import Column, ForeignKey, Integer, String, Boolean
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.dialects.mysql import BIT
from sqlalchemy.orm import relationship, mapped_column, Mapped
from src.db.models.base import Base, BaseMixin
@@ -8,12 +9,12 @@ from src.db.models.base import Base, BaseMixin
class Profile(Base, BaseMixin):
__tablename__ = 'profile'
first_name = Column(String)
last_name = Column(String)
user_name = Column(String, nullable=False)
email = Column(String)
password = Column(String)
enabled = Column(Boolean)
first_name = Column(String(255))
last_name = Column(String(255))
user_name = Column(String(255), nullable=False)
email = Column(String(255))
password = Column(String(255))
enabled = Column(BIT(1))
assignments = relationship("Assignment")
tokens = relationship("Token")
@@ -27,23 +28,20 @@ class Profile(Base, BaseMixin):
full_name += self.last_name
return full_name
def __str__(self):
return f"Profile({self.id} {self.user_name}, {self.email})"
class Token(Base, BaseMixin):
__tablename__ = "token"
token = Column(String, nullable=False, unique=True)
name = Column(String)
token = Column(String(255), nullable=False, unique=True)
name = Column(String(255))
last_used_date: Mapped[datetime] = mapped_column()
enabled = Column(Boolean)
profile_id = Column(String, ForeignKey("profile.id"), nullable=False)
enabled = Column(BIT(1))
profile_id = Column(String(255), ForeignKey("profile.id"), nullable=False)
profile = relationship("Profile", back_populates="tokens")
class Permission(Base, BaseMixin):
__tablename__ = "permission"
name = Column(String, nullable=False)
name = Column(String(255), nullable=False)
assignments = relationship("Assignment")
@@ -55,14 +53,20 @@ class Assignment(Base, BaseMixin):
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):
__tablename__ = "mail_account"
host = Column(String)
host = Column(String(255))
port = Column(Integer)
protocol = Column(String)
user_name = Column(String)
password = Column(String)
start_tls = Column(Boolean)
protocol = Column(String(255))
user_name = Column(String(255))
password = Column(String(255))
start_tls = Column(BIT(1))
class Mail(Base, BaseMixin):
+11 -10
View File
@@ -1,7 +1,8 @@
import uuid
from datetime import datetime
from sqlalchemy import func, Column, String, Boolean
from sqlalchemy import func, Column, String
from sqlalchemy.dialects.mysql import BIT
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
@@ -10,8 +11,8 @@ class Base(DeclarativeBase):
class BaseMixin:
#id = Column(String, primary_key=True, default=uuid.uuid4)
id: Mapped[str] = mapped_column(primary_key=True, default=str(uuid.uuid4()))
id = Column(String(255), primary_key=True, default=uuid.uuid4())
# id: Mapped[str] = mapped_column(primary_key=True, default=uuid.uuid4())
# created_date = Column(DateTime)
created_date: Mapped[datetime] = mapped_column(default=func.now())
# last_modified_date = Column(DateTime)
@@ -21,10 +22,10 @@ class BaseMixin:
class BaseVideoMixin:
cloud_link = Column(String, nullable=True)
file_name = Column(String, nullable=True)
path = Column(String)
review = Column(Boolean)
title = Column(String)
url = Column(String, nullable=True)
should_download = Column(Boolean)
cloud_link = Column(String(255))
file_name = Column(String(255))
path = Column(String(255))
review = Column(BIT(1))
title = Column(String(255))
url = Column(String(255), unique=True)
should_download = Column(BIT(1))
+6 -6
View File
@@ -6,28 +6,28 @@ from src.db.models.base import Base, BaseMixin
class Article(Base, BaseMixin):
__tablename__ = 'article'
title = Column(String, unique=True)
title = Column(String(length=255), unique=True)
article_authors = relationship("ArticleAuthor")
class Author(Base, BaseMixin):
__tablename__ = 'author'
first_name = Column(String)
last_name = Column(String)
first_name = Column(String(255))
last_name = Column(String(255))
article_authors = relationship("ArticleAuthor")
book_authors = relationship("BookAuthor")
class BookshelfPublisher(Base, BaseMixin):
__tablename__ = 'bookshelf_publisher'
name = Column(String, unique=True)
name = Column(String(length=255), unique=True)
books = relationship("Book")
class Book(Base, BaseMixin):
__tablename__ = 'book'
isbn = Column(String, unique=True)
title = Column(String)
isbn = Column(String(255), unique=True)
title = Column(String(255))
year = Column(Integer, nullable=False)
publisher_id = Column(String, ForeignKey('bookshelf_publisher.id'), nullable=False)
publisher = relationship('BookshelfPublisher', back_populates="books")
+23 -84
View File
@@ -1,24 +1,15 @@
import uuid
from datetime import datetime
from typing import AnyStr, Dict, List, Optional, Any
from typing import Dict, List
from natsort import natsorted
from sqlalchemy import Column, ForeignKey, Integer, String, Boolean, func
from sqlalchemy.orm import relationship, Mapped, mapped_column
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.dialects.mysql import BIT
from sqlalchemy.orm import relationship
from src.db.models.base import Base, BaseMixin
class Publisher(Base):
class Publisher(Base, BaseMixin):
__tablename__ = "publisher"
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")
name = Column(String(length=255), unique=True)
comics = relationship("Comic")
def __repr__(self):
@@ -30,12 +21,11 @@ class Publisher(Base):
class Comic(Base, BaseMixin):
__tablename__ = 'comic'
title = Column(String, unique=True)
title = Column(String(length=255), unique=True)
publisher_id = Column(String, ForeignKey('publisher.id'), nullable=False)
publisher = relationship("Publisher", back_populates="comics")
current_order = Column(Boolean)
completed = Column(Boolean)
weblink = Column(String, nullable=True)
current_order = Column(BIT(1))
completed = Column(BIT(1))
issues = relationship("Issue", order_by="Issue.issue_number")
story_arcs = relationship("StoryArc")
trade_paperbacks = relationship("TradePaperback")
@@ -48,10 +38,10 @@ class Comic(Base, BaseMixin):
def __str__(self):
return f'{self.title}({self.id})'
def get_artists(self) -> Dict[Any, List[Any]]:
works: Dict[Any, List[Any]] = {}
def get_artists(self) -> Dict[str, List[str]]:
works: Dict[str, List[str]] = {}
for work in self.comic_works:
work_type = work.work_type
work_type = work.work_type.name
artist = work.artist
if work_type in works:
works[work_type].append(artist)
@@ -66,16 +56,15 @@ class Comic(Base, BaseMixin):
class Volume(Base, BaseMixin):
__tablename__ = "volume"
name = Column(String, nullable=False)
name = Column(String(length=255), nullable=False)
comic_id = Column(String, ForeignKey("comic.id"), nullable=False)
comic = relationship("Comic", back_populates="volumes")
story_arcs = relationship("StoryArc")
issues = relationship("Issue")
class TradePaperback(Base, BaseMixin):
__tablename__ = "trade_paperback"
name = Column(String, nullable=False)
name = Column(String(length=255), nullable=False)
issue_start = Column(Integer)
issue_end = Column(Integer)
comic_id = Column(String, ForeignKey("comic.id"), nullable=False)
@@ -84,58 +73,31 @@ class TradePaperback(Base, BaseMixin):
class StoryArc(Base, BaseMixin):
__tablename__ = "story_arc"
name = Column(String, nullable=False)
name = Column(String(length=255), nullable=False)
comic_id = Column(String, ForeignKey("comic.id"), nullable=False)
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):
__tablename__ = "issue"
issue_number = Column(String)
title = Column(String, nullable=True)
published_on: Mapped[datetime] = mapped_column(nullable=True)
in_stock = Column(Boolean)
is_read = Column(Boolean)
issue_number = Column(String(255))
in_stock = Column(BIT(1))
is_read = Column(BIT(1))
comic_id = Column(String, ForeignKey("comic.id"), nullable=False)
comic = relationship("Comic", back_populates="issues")
volume_id = Column(String, ForeignKey("volume.id"), nullable=True)
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):
__tablename__ = "artist"
name = Column(String, nullable=False)
weblink = Column(String, nullable=True)
name = Column(String(length=255), nullable=False)
comic_works = relationship("ComicWork")
issue_works = relationship("IssueWork")
def get_comics(self) -> Dict[Any, List[Comic]]:
works: Dict[Any, List[Comic]] = {}
def get_comics(self) -> Dict[str, List[str]]:
works: Dict[str, List[str]] = {}
for work in self.comic_works:
work_type = work.work_type
work_type = work.work_type.name
comic = work.comic
if work_type in works:
works[work_type].append(comic)
@@ -146,20 +108,8 @@ class Artist(Base, BaseMixin):
class WorkType(Base, BaseMixin):
__tablename__ = "worktype"
name = Column(String, nullable=False, unique=True)
name = Column(String(length=255), nullable=False, unique=True)
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):
return f'Worktype({self.id} {self.version} {self.name} {len(self.comic_works)})'
@@ -176,14 +126,3 @@ class ComicWork(Base, BaseMixin):
artist = relationship("Artist", back_populates="comic_works")
work_type_id = Column(String, ForeignKey("worktype.id"), nullable=False)
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")
+7 -6
View File
@@ -1,9 +1,10 @@
import json
import logging
import uuid
from datetime import datetime
from enum import Enum, auto
from pathlib import Path
from typing import Any, List
from typing import Any
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
@@ -12,7 +13,7 @@ from sqlalchemy.orm import sessionmaker
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.bookshelf import ArticleAuthor, BookAuthor, BookshelfPublisher, Article, Book, Author
from src.db.models.admin import Mail, MailAccount, ModuleData, Token, Assignment, Permission, Profile
from src.db.models.admin import Mail, MailAccount, ModuleData, Role, User, Token, AuthorizationMatrix
from src.db.models.metadata import MetaDataTable, MetaDataColumn
from src.db.models.media import MediaVideo, MediaArticle, MediaFile, MediaActor, MediaActorFile
@@ -78,10 +79,10 @@ class KontorDB:
self.registry[MediaVideo.__tablename__] = MediaVideo
self.registry[MetaDataColumn.__tablename__] = MetaDataColumn
self.registry[MetaDataTable.__tablename__] = MetaDataTable
self.registry[Assignment.__tablename__] = Assignment
self.registry[AuthorizationMatrix.__tablename__] = AuthorizationMatrix
self.registry[Token.__tablename__] = Token
self.registry[Profile.__tablename__] = Profile
self.registry[Permission.__tablename__] = Permission
self.registry[User.__tablename__] = User
self.registry[Role.__tablename__] = Role
self.registry[ModuleData.__tablename__] = ModuleData
self.registry[MailAccount.__tablename__] = MailAccount
self.registry[Mail.__tablename__] = Mail
@@ -359,7 +360,7 @@ class KontorDB:
update_list[link.id] = link.title
return update_list
def get_download_list(self) -> List[str]:
def get_download_list(self) -> list[uuid.UUID]:
download_list = []
__session__ = sessionmaker(self.engine)
_filter = { 'should_download': True}
+20 -34
View File
@@ -6,7 +6,8 @@ from pathlib import Path
import requests
from bs4 import BeautifulSoup
from sqlalchemy import Column, String, ForeignKey, Boolean
from sqlalchemy import Column, String, ForeignKey
from sqlalchemy.dialects.mysql import BIT
from sqlalchemy.orm import relationship
from src.db.models.base import Base, BaseMixin, BaseVideoMixin
@@ -29,10 +30,10 @@ class MediaFile(Base, BaseMixin, BaseVideoMixin):
soup = BeautifulSoup(r.content, "html.parser")
title = soup.title.string
self.title = title
self.review = False
self.review = 0
except:
self.title = None
self.review = True
self.review = 1
self.last_modified_date = datetime.now()
def download_file(self, download_dir: str, dl_tool: str):
@@ -44,12 +45,12 @@ class MediaFile(Base, BaseMixin, BaseVideoMixin):
lines_list = output.splitlines()
file_name = self.__parse_output__(lines_list)
if file_name is None:
self.review = True
self.should_download = True
self.review = 1
self.should_download = 1
self.file_name = None
else:
download_file = Path(file_name)
self.should_download = False
self.should_download = 0
self.file_name = download_file.name
self.cloud_link = str(download_file.absolute())
self.last_modified_date = datetime.now()
@@ -70,46 +71,31 @@ class MediaFile(Base, BaseMixin, BaseVideoMixin):
class MediaActor(Base, BaseMixin):
__tablename__ = 'media_actor'
name = Column(String)
url = Column(String, unique=True, nullable=True)
name = Column(String(255))
media_actor_files = relationship("MediaActorFile")
class MediaActorFile(Base, BaseMixin):
__tablename__ = 'media_actor_file'
media_actor_id = Column(String, ForeignKey("media_actor.id"), nullable=False)
media_actor_id = Column(String(255), ForeignKey("media_actor.id"), nullable=False)
media_actor = relationship("MediaActor", back_populates="media_actor_files")
media_file_id = Column(String, ForeignKey("media_file.id"), nullable=True)
media_file_id = Column(String(255), ForeignKey("media_file.id"), nullable=True)
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):
__tablename__ = 'media_article'
review = Column(Boolean)
title = Column(String)
url = Column(String, unique=True)
review = Column(BIT(1))
title = Column(String(255))
url = Column(String(255), unique=True)
class MediaVideo(Base, BaseMixin):
__tablename__ = 'media_video'
cloud_link = Column(String)
file_name = Column(String)
path = Column(String)
review = Column(Boolean)
title = Column(String)
url = Column(String, unique=True)
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})'
cloud_link = Column(String(255))
file_name = Column(String(255))
path = Column(String(255))
review = Column(BIT(1))
title = Column(String(255))
url = Column(String(255), unique=True)
should_download = Column(BIT(1))
+42
View File
@@ -0,0 +1,42 @@
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})'
+13 -12
View File
@@ -1,4 +1,5 @@
from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint, Boolean
from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint
from sqlalchemy.dialects.mysql import BIT
from sqlalchemy.orm import relationship
from src.db.models.base import Base, BaseMixin
@@ -9,15 +10,15 @@ class Sport(Base, BaseMixin):
__table_args__ = (
UniqueConstraint("name"),
)
name = Column(String, nullable=False, index=True, unique=True)
name = Column(String(255), nullable=False, index=True, unique=True)
teams = relationship("Team")
positions = relationship("FieldPosition")
class Team(Base, BaseMixin):
__tablename__ = "team"
name = Column(String, nullable=False, index=True, unique=True)
short_name = Column(String, nullable=False, )
name = Column(String(255), nullable=False, index=True, unique=True)
short_name = Column(String(255), nullable=False, )
sport_id = Column(String, ForeignKey("sport.id"), nullable=False)
sport = relationship("Sport", back_populates="teams")
roosters = relationship("Rooster")
@@ -29,8 +30,8 @@ class FieldPosition(Base, BaseMixin):
UniqueConstraint("name", "sport_id"),
UniqueConstraint("short_name", "sport_id"),
)
name = Column(String, nullable=False, index=True)
short_name = Column(String, nullable=False)
name = Column(String(255), nullable=False, index=True)
short_name = Column(String(255), nullable=False)
sport_id = Column(String, ForeignKey("sport.id"), nullable=False, index=True)
sport = relationship("Sport", back_populates="positions")
roosters = relationship("Rooster")
@@ -41,8 +42,8 @@ class Player(Base, BaseMixin):
__table_args__ = (
UniqueConstraint("first_name", "last_name"),
)
first_name = Column(String, nullable=False, index=True)
last_name = Column(String, nullable=False, index=True)
first_name = Column(String(255), nullable=False, index=True)
last_name = Column(String(255), nullable=False, index=True)
roosters = relationship("Rooster")
def get_full_name(self) -> str:
@@ -66,7 +67,7 @@ class Rooster(Base, BaseMixin):
class Vendor(Base, BaseMixin):
__tablename__ = "vendor"
name = Column(String, nullable=False, unique=True, index=True)
name = Column(String(255), nullable=False, unique=True, index=True)
card_sets = relationship("CardSet")
cards = relationship("Card")
@@ -76,9 +77,9 @@ class CardSet(Base, BaseMixin):
__table_args__ = (
UniqueConstraint("name", "vendor_id"),
)
name = Column(String, index=True)
parallel_set = Column(Boolean)
insert_set = Column(Boolean)
name = Column(String(255), index=True)
parallel_set = Column(BIT(1))
insert_set = Column(BIT(1))
vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False, index=True)
vendor = relationship("Vendor", back_populates="card_sets")
cards = relationship("Card")
-10
View File
@@ -1,10 +0,0 @@
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
@@ -1,19 +0,0 @@
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
@@ -1,31 +0,0 @@
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
@@ -1,32 +0,0 @@
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
-53
View File
@@ -1,53 +0,0 @@
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
+2 -2
View File
@@ -1,7 +1,7 @@
from typing import Generator
from typing import Generator, Annotated
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import sessionmaker, Session
from src.core.config import settings
-28
View File
@@ -1,28 +0,0 @@
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
+5 -17
View File
@@ -1,25 +1,12 @@
import logging
import logging.config
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
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.utils import check_db_connected, check_db_disconnected
from src.webapps.base import api_router as web_app_router
from src.core.config import settings
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):
app.include_router(api_router)
app.include_router(web_app_router)
@@ -30,12 +17,13 @@ def configure_static(app: FastAPI):
def create_tables():
Base.metadata.create_all(bind=engine)
def start_application(log):
log.info(f"using database: {settings.DATABASE_URL}")
app = FastAPI(title=settings.PROJECT_NAME, version=settings.PROJECT_VERSION, lifespan=lifespan)
def start_application():
app = FastAPI(title=settings.PROJECT_NAME, version=settings.PROJECT_VERSION)
include_router(app)
configure_static(app)
create_tables()
return app
kontor = start_application(logger)
kontor = start_application()
-8
View File
@@ -1,8 +0,0 @@
from typing import Optional
from pydantic import BaseModel
class Token(BaseModel):
access_token: str
token_type: str
+21 -4
View File
@@ -1,19 +1,36 @@
from typing import List, Dict
from uuid import UUID
from pydantic import BaseModel
from src.db.models.comic import Artist
class ArtistCreation(BaseModel):
id: str
name: str
class ArtistResponse(BaseModel):
id: str
id: UUID
name: str
class ArtistDetailResponse(BaseModel):
id: str
id: UUID
name: str
weblink: 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
+8 -17
View File
@@ -1,35 +1,26 @@
from typing import List, Dict, Optional
from typing import List, Dict
from uuid import UUID
from pydantic import BaseModel, AnyUrl
from pydantic import BaseModel
from src.db.models.comic import Comic
class ComicResponse(BaseModel):
id: str
id: UUID
title: str
completed: bool
class ComicDetailsResponse(BaseModel):
id: str
id: UUID
created: str
title: str
completed : bool
current_order : bool
weblink: str
publisher: str
volumes: 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:
response = ComicResponse(
id=comic.id,
@@ -55,11 +46,11 @@ def get_comic_details(comic: Comic) -> ComicDetailsResponse | None:
id=comic.id,
created=str(comic.created_date),
title=comic.title,
completed=comic.completed,
current_order=comic.current_order,
weblink=comic.weblink,
completed=(comic.completed == 1),
current_order=(comic.current_order == 1),
publisher=comic.publisher.name,
volumes=volumes,
works=works
)
return response
-10
View File
@@ -1,10 +0,0 @@
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
-4
View File
@@ -1,4 +0,0 @@
from pydantic import BaseModel
class AddWorkType(BaseModel):
worktype: str
-10
View File
@@ -1,10 +0,0 @@
from datetime import datetime
from src.db.models.media import MediaActor
from pydantic import BaseModel
class MediaActorResponse(BaseModel):
id: str
name: str
url: str
-10
View File
@@ -1,10 +0,0 @@
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
+14 -7
View File
@@ -1,29 +1,30 @@
from datetime import datetime
from uuid import UUID
from src.db.models.media import MediaFile
from pydantic import BaseModel
class MediaFileResponse(BaseModel):
id: str
id: UUID
title: str | None = None
file_name: str | None = None
cloud_link: str | None = None
url: str | None = None
url: str
review: bool = False
should_download: bool = False
class Link(BaseModel):
url: str
def get_file_details(mediafile: MediaFile) -> MediaFileResponse:
def get_file_details(mediafile: MediaFile) -> MediaFileResponse | None:
response = MediaFileResponse(id=mediafile.id,
title=mediafile.title,
file_name=mediafile.file_name,
cloud_link=mediafile.cloud_link,
url=str(mediafile.url),
review=mediafile.review,
should_download=mediafile.should_download)
review=(mediafile.review == 1),
should_download=(mediafile.should_download == 1))
#print(f"id: {mediafile.id}: review: {response.review} <- {mediafile.review}")
#print(f"id: {mediafile.id}: download: {response.should_download} <- {mediafile.should_download}")
return response
@@ -34,5 +35,11 @@ def set_file(model: MediaFileResponse, mediafile: MediaFile) -> None:
mediafile.url = model.url
mediafile.title = model.title
mediafile.last_modified_date = datetime.now()
mediafile.review = model.review
mediafile.should_download = model.should_download
if model.review:
mediafile.review = 1
else:
mediafile.review = 0
if model.should_download:
mediafile.should_download = 1
else:
mediafile.should_download = 0
-5
View File
@@ -1,5 +0,0 @@
from pydantic import BaseModel
class AddLink(BaseModel):
url: str
+2 -2
View File
@@ -1,8 +1,8 @@
from uuid import UUID
from typing import AnyStr
from pydantic import BaseModel
class SportResponse(BaseModel):
id: AnyStr
id: UUID
name: str
-172
View File
@@ -1,172 +0,0 @@
* {
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%;
}
}
@@ -1,38 +0,0 @@
{% 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 %}
@@ -1,43 +0,0 @@
{% 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 %}
-40
View File
@@ -1,40 +0,0 @@
{% 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 %}
@@ -20,16 +20,12 @@
<th scope="row">Artist Name</th>
<td colspan="2">{{artist.name}}</td>
</tr>
<tr>
<th scope="row">Link</th>
<td colspan="2">{{artist.weblink}}</td>
</tr>
<tr>
<th scope="row">Works</th>
<td colspan="2">
{% for work in artist.get_comics() %}
<p>
<a href="/comic/worktypes/{{work.id}}">{{work.name}}</a>
{{work}}:
<ul>
{% for comic in artist.get_comics()[work] %}
<li><a href="/comic/comics/{{comic.id}}">{{comic.title}}</a></li>
@@ -47,20 +43,8 @@
<th scope="row">Data Modified</th>
<td colspan="2">{{artist.last_modified_date}}</td>
</tr>
<tr>
<th scope="row">Data Version</th>
<td colspan="2">{{artist.version}}</td>
</tr>
</tbody>
</table>
</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>
{% endblock %}
@@ -1,32 +0,0 @@
{% 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 %}
+1 -1
View File
@@ -18,7 +18,7 @@
{% for artist in artists %}
<div class="col-lg-4 col-md-3 col-sm-10 mr-auto">
{% with obj=artist %}
{% include "comic/artist_cards.html" %}
{% include "components/artist_cards.html" %}
{% endwith %}
{% if loop.index %3 %}
@@ -27,17 +27,12 @@
{% endwith %}
</td>
</tr>
<tr>
<th scope="row">Link</th>
<td colspan="2">{{comic.weblink}}</td>
</tr>
{% if comic.get_artists()|length > 0 %}
<tr>
<th scope="row">Works</th>
<td colspan="2">
{% for work in comic.get_artists() %}
<p>
<a href="/comic/worktypes/{{work.id}}">{{work.name}}</a>
{{work}}:
<ul>
{% for artist in comic.get_artists()[work] %}
<li><a href="/comic/artists/{{artist.id}}">{{artist.name}}</a></li>
@@ -47,29 +42,6 @@
{% endfor %}
</td>
</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>
<th scope="row">Data Created</th>
<td colspan="2">{{comic.created_date}}</td>
@@ -82,15 +54,18 @@
<th scope="row">Data Version</th>
<td colspan="2">{{comic.version}}</td>
</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>
</table>
</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>
{% endblock %}
@@ -1,56 +0,0 @@
{% 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 %}
+17 -47
View File
@@ -4,58 +4,28 @@
<title>Comic List</title>
{% 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 %}
{% with msg=msg %}
{% include "components/alerts.html" %}
{% endwith %}
<div class="container">
<div class="row">
<form class="form-inline" action="/comic/comics">
<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 class="col">
<h1 class="display-5">Find Jobs..</h1>
</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 %}
@@ -1,92 +0,0 @@
{% 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,24 +20,6 @@
<th scope="row">Publisher Name</th>
<td colspan="2">{{publisher.name}}</td>
</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>
<th scope="row">Comics</th>
<td colspan="2">
@@ -59,12 +41,5 @@
</tbody>
</table>
</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>
{% endblock %}
@@ -18,7 +18,7 @@
{% for publisher in publishers %}
<div class="col-lg-4 col-md-3 col-sm-10 mr-auto">
{% with obj=publisher %}
{% include "comic/publisher_cards.html" %}
{% include "components/publisher_cards.html" %}
{% endwith %}
{% if loop.index %3 %}
</div>
@@ -1,60 +0,0 @@
{% 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 %}
@@ -1,28 +0,0 @@
{% 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 %}
@@ -1,28 +0,0 @@
{% 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,7 +1,6 @@
<div class="card shadow p-3 mb-2 bg-body rounded" style="width: 18rem;">
<div class="card-body">
<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>
</div>
</div>
@@ -1,4 +1,4 @@
{% if check %}
{% if check == 1 %}
<img src="{{ url_for('static', path='images/tick.png') }}" alt="" width="24" height="24">
{% else %}
<img src="{{ url_for('static', path='images/cross.png') }}" alt="" width="24" height="24">
@@ -1 +0,0 @@
<h2>Footer</h2>
+10 -18
View File
@@ -1,14 +1,8 @@
<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">
<nav class="navbar navbar-expand-lg navbar-light bg-light px-5">
<div class="container-fluid">
<a class="navbar-brand" href="#">Kontor</a>
<a class="navbar-brand" href="#">
<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">
<span class="navbar-toggler-icon"></span>
</button>
@@ -24,7 +18,7 @@
<li><a class="dropdown-item" href="/comic/artists/">Artists</a></li>
<li><a class="dropdown-item" href="/comic/publishers/">Publishers</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="/comic/worktypes">WorkTypes</a></li>
<li><a class="dropdown-item" href="#">Something else here</a></li>
</ul>
</li>
<li class="nav-item dropdown">
@@ -32,7 +26,6 @@
<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/actors/">MediaActors</a></li>
<li><a class="dropdown-item" href="/media/videos/">MediaVideos</a></li>
</ul>
</li>
<li class="nav-item">
@@ -49,19 +42,18 @@
<li><a class="dropdown-item" href="/register/">Signup</a></li>
<li><a class="dropdown-item" href="/login/">Login</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="/admin/profiles">Profiles</a></li>
<li><a class="dropdown-item" href="/admin/permissions">Permissions</a></li>
<li><a class="dropdown-item" href="#">Something else here</a></li>
</ul>
</li>
</ul>
<ul class="navbar-nav ml-auto mb-2 mb-lg-0">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Media
Jobs
</a>
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
<li><a class="dropdown-item" href="/media/add-link/">Add Link</a></li>
<li><a class="dropdown-item" href="/media/add-file">Add MediaFile</a></li>
<li><a class="dropdown-item" href="/comics/">Comics</a></li>
<li><a class="dropdown-item" href="/comics/">Media</a></li>
</ul>
</li>
</ul>
@@ -71,4 +63,4 @@
</form>
</div>
</div>
</nav> -->
</nav>
-3
View File
@@ -5,9 +5,6 @@
<title>Kontor</title>
{% endblock %}
{% block header %}
<h1>Kontor</h1>
{% endblock %}
{% block content %}
{% with msg=msg %}
{% include "components/alerts.html" %}
@@ -20,10 +20,6 @@
<th scope="row">Actor Name</th>
<td colspan="2">{{actor.name}}</td>
</tr>
<tr>
<th scope="row">URL</th>
<td colspan="2">{{actor.url}}</td>
</tr>
<tr>
<th scope="row">Works</th>
<td colspan="2">
@@ -45,12 +41,5 @@
</tbody>
</table>
</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>
{% endblock %}
+1 -1
View File
@@ -18,7 +18,7 @@
{% for actor in actors %}
<div class="col-lg-4 col-md-3 col-sm-10 mr-auto">
{% with obj=actor %}
{% include "media/actor_cards.html" %}
{% include "components/actor_cards.html" %}
{% endwith %}
{% if loop.index %3 %}
@@ -1,29 +0,0 @@
{% 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,12 +61,5 @@
</tbody>
</table>
</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>
{% endblock %}
+17 -45
View File
@@ -4,54 +4,26 @@
<title>MediaFiles List</title>
{% endblock %}
{% block header %}
<h1>MediaFiles..</h1>
{% endblock %}
{% block subnav %}
{% include "media/media_nav.html" %}
{% endblock %}
{% block content %}
{% with msg=msg %}
{% include "components/alerts.html" %}
{% endwith %}
<div class="row">
<form class="form-inline" action="/media/files">
<input type="text" placeholder="Search" name="query">
<label>
<input type="checkbox" name="review" {% if request.query_params.get("review")=="on" %}checked{% endif %}>Review
</label>
<label>
<input type="checkbox" name="download" {% if request.query_params.get("download")=="on" %}checked{% endif %}>Download
</label>
<button type="submit">Search</button>
<div class="pill-nav">
<a href="/media/files/add">Add MediaFile</a>
</div>
</form>
</div>
<div class="row">
<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 class="container">
<table class="table table-hover">
<thead><tr>
<th scope="col">Titel</th>
<th scope="col">URL</th>
<th scope="col">Cloudlink</th>
</tr></thead>
<tbody>
{% for mediafile in mediafiles %}
<tr>
<th scope="row"><a href="/media/files/{{mediafile.id}}">{{mediafile.title}}</a></th>
<td>{{mediafile.url}}</td>
<td>{{mediafile.cloud_link}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
@@ -1,5 +0,0 @@
<div class="subnav">
<a href="/media/files/">MediaFiles</a>
<a href="/media/actors/">MediaActors</a>
<a href="/media/videos/">MediaVideos</a>
</div>

Some files were not shown because too many files have changed in this diff Show More