Compare commits

..

No commits in common. "main" and "fix/flags" have entirely different histories.

27 changed files with 663 additions and 937 deletions

View File

@ -1,13 +0,0 @@
# SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
*
!Pipfile
!Pipfile.lock
!data/
!ki/
!LICENSES/
!migrations/
!app.py
!run_prod.py

View File

@ -4,125 +4,31 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
kind: pipeline kind: pipeline
type: docker type: docker
name: qa name: default
trigger:
event:
- push
- pull_request
branch:
- main
steps: steps:
- name: install-lint-test - name: qa
image: python:3.8.20-alpine@sha256:3d93b1f77efce339aa77db726656872517b0d67837989aa7c4b35bd5ae7e81ba image: registry.wtf-eg.net/ki-backend-builder:1.0.0
env: commands:
PYROOT: '/pyroot' - pipenv install --dev
PYTHONUSERBASE: '/pyroot' - pipenv run flake8
commands: - pipenv run reuse lint
- apk add --no-cache gcc g++ musl-dev python3-dev - pipenv run python -m unittest discover ki
- pip3 install pipenv
- pipenv verify
- pipenv install --dev
- pipenv run flake8
- pipenv run reuse lint
- pipenv run python -m unittest discover ki
- name: docker-dry-run
image: plugins/docker:20.18.4@sha256:a8d3d86853c721492213264815f1d00d3ed13f42f5c1855a02f47fa4d5f1e042
settings:
registry: git.wtf-eg.de
repo: git.wtf-eg.de/kompetenzinventar/backend
target: ki-backend
dry_run: true
when:
event:
- pull_request
--- - name: docker-publish
kind: pipeline image: plugins/docker
type: docker settings:
name: build registry: registry.wtf-eg.net
repo: registry.wtf-eg.net/ki-backend
target: ki-backend
auto_tag: true
username:
from_secret: "docker_username"
password:
from_secret: "docker_password"
when:
branch:
- main
trigger: image_pull_secrets:
event: - dockerconfig
- push
branch:
- main
depends_on:
- qa
steps:
- name: docker-publish
image: plugins/docker:20.18.4@sha256:a8d3d86853c721492213264815f1d00d3ed13f42f5c1855a02f47fa4d5f1e042
settings:
registry: git.wtf-eg.de
repo: git.wtf-eg.de/kompetenzinventar/backend
target: ki-backend
auto_tag: true
username:
from_secret: "docker_username"
password:
from_secret: "docker_password"
---
kind: pipeline
type: docker
name: deploy
trigger:
event:
- push
branch:
- main
depends_on:
- build
steps:
- name: deploy-dev
image: appleboy/drone-ssh:1.7.5@sha256:995677e073454912f26d4c0fdd2f9df2e1f5a30d6603d3f2ece667311b6babb3
settings:
host:
- dev01.wtf-eg.net
username: drone_deployment
key:
from_secret: "dev01_deployment_key"
command_timeout: 2m
script:
- echo "Executing forced command..."
---
kind: pipeline
type: docker
name: tag-release
trigger:
event:
- tag
steps:
- name: install-lint-test
image: python:3.8.20-alpine@sha256:3d93b1f77efce339aa77db726656872517b0d67837989aa7c4b35bd5ae7e81ba
env:
PYROOT: '/pyroot'
PYTHONUSERBASE: '/pyroot'
commands:
- apk add --no-cache gcc g++ musl-dev python3-dev
- pip3 install pipenv
- pipenv install --dev
- pipenv run flake8
- pipenv run reuse lint
- pipenv run python -m unittest discover ki
- name: docker-publish
image: plugins/docker:20.18.4@sha256:a8d3d86853c721492213264815f1d00d3ed13f42f5c1855a02f47fa4d5f1e042
settings:
registry: git.wtf-eg.de
repo: git.wtf-eg.de/kompetenzinventar/backend
target: ki-backend
auto_tag: true
username:
from_secret: "docker_username"
password:
from_secret: "docker_password"

View File

@ -1 +0,0 @@
3.8.20

12
.reuse/dep5 Normal file
View File

@ -0,0 +1,12 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: Kompetenzinventar
Upstream-Contact: Michael Weimann <mail@michael-weimann.eu>
Source: https://git.wtf-eg.de/kompetenzinventar/ki-backend
Files: data/imgs/flags/*
Copyright: 2013 Panayiotis Lipiridis <https://flagicons.lipis.dev/>
License: MIT
Files: Pipfile.lock migrations/*
Copyright: WTF Kooperative eG <https://wtf-eg.de/>
License: AGPL-3.0-or-later

View File

@ -1,5 +0,0 @@
# SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
migrations/*.py

View File

@ -2,17 +2,7 @@
# #
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
FROM python:3.8.20-alpine@sha256:3d93b1f77efce339aa77db726656872517b0d67837989aa7c4b35bd5ae7e81ba AS builder FROM registry.wtf-eg.net/ki-backend-builder:1.0.0 as builder
ENV PYROOT=/pyroot
ENV PYTHONUSERBASE=$PYROOT
RUN apk add --no-cache \
gcc \
g++ \
musl-dev \
python3-dev && \
pip3 install pipenv
COPY Pipfile* ./ COPY Pipfile* ./
@ -20,10 +10,7 @@ RUN PIP_USER=1 PIP_IGNORE_INSTALLED=1 pipenv install --system --deploy --ignore-
RUN pip3 uninstall --yes pipenv RUN pip3 uninstall --yes pipenv
FROM python:3.8.20-alpine@sha256:3d93b1f77efce339aa77db726656872517b0d67837989aa7c4b35bd5ae7e81ba AS ki-backend FROM registry.wtf-eg.net/ki-backend-base:1.0.0 as ki-backend
ENV PYROOT=/pyroot
ENV PYTHONUSERBASE=$PYROOT
# Install six explicitly. Otherwise Python complains about it missing. # Install six explicitly. Otherwise Python complains about it missing.
RUN pip3 install six RUN pip3 install six
@ -35,9 +22,4 @@ WORKDIR /app
COPY . . COPY . .
LABEL org.opencontainers.image.source=https://git.wtf-eg.de/kompetenzinventar/ki-backend.git
LABEL org.opencontainers.image.url=https://git.wtf-eg.de/kompetenzinventar/ki-backend
LABEL org.opencontainers.image.documentation=https://git.wtf-eg.de/kompetenzinventar/ki-backend#docker
LABEL org.opencontainers.image.vendor="WTF Kooperative eG"
CMD ["python3", "run_prod.py"] CMD ["python3", "run_prod.py"]

29
Pipfile
View File

@ -8,23 +8,22 @@ verify_ssl = true
name = "pypi" name = "pypi"
[packages] [packages]
flask = "==2.3.3" flask = "~=2.0.1"
python-dotenv = "==1.0.1" python-dotenv = "~=0.17.1"
flask-migrate = "==4.0.7" flask-migrate = "~=3.0.1"
flask-sqlalchemy = "==2.5.1" flask-sqlalchemy = "~=2.5.1"
sqlalchemy = "==1.4.54" sqlalchemy = "~=1.4.18"
waitress = "==2.1.2" waitress = "~=2.0.0"
pyyaml = "==6.0.2" pyyaml = "~=5.4.1"
flask-cors = "==5.0.0" flask-cors = "~=3.0.10"
ldap3 = "==2.9.1" ldap3 = "~=2.9"
pymysql = "==1.1.1" pymysql = "~=1.0.2"
werkzeug = "==2.3.8"
[dev-packages] [dev-packages]
flake8 = "==7.1.1" flake8 = "~=3.9.2"
yapf = "==0.40.2" yapf = "~=0.31.0"
pre-commit = "==2.21.0" pre-commit = "~=2.13.0"
reuse = "==4.0.3" reuse = "~=0.13.0"
[requires] [requires]
python_version = "3.8" python_version = "3.8"

959
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -9,32 +9,6 @@ SPDX-License-Identifier: AGPL-3.0-or-later
[![Build Status](https://drone.wtf-eg.de/api/badges/kompetenzinventar/ki-backend/status.svg?ref=refs/heads/main)](https://drone.wtf-eg.de/kompetenzinventar/ki-backend) [![Build Status](https://drone.wtf-eg.de/api/badges/kompetenzinventar/ki-backend/status.svg?ref=refs/heads/main)](https://drone.wtf-eg.de/kompetenzinventar/ki-backend)
[![REUSE status](https://api.reuse.software/badge/git.wtf-eg.de/kompetenzinventar/ki-backend)](https://api.reuse.software/info/git.wtf-eg.de/kompetenzinventar/ki-backend) [![REUSE status](https://api.reuse.software/badge/git.wtf-eg.de/kompetenzinventar/ki-backend)](https://api.reuse.software/info/git.wtf-eg.de/kompetenzinventar/ki-backend)
## Über
Dieses Repo enthält das Backend des Projekts Kompentenzinventar - einer Webapplikation zur Erfassung von Userprofilen für die WTF eG.
Implementiert ist das Backend mit Flask.
### Mitmachen
Du kannst gerne bei der Entwicklung des Kompetenzinventars mitmachen.
- Fehler oder fehlende Funktionen erfassen. Bitte direkt über die [Issues](https://git.wtf-eg.de/kompetenzinventar/ki-backend/issues) in Gitea.
- Dokumentation oder Implementierung verbessern. Bitte forke hierzu das Projekt, branche von `main` ab und erstelle dann einen [Pull Request](https://git.wtf-eg.de/kompetenzinventar/ki-backend/pulls).
### Kommunikation
Folgende Kanäle gibt es für die Kommunikation über das Kompetenzinventar:
- Die [Issues](https://git.wtf-eg.de/kompetenzinventar/ki-backend/issues) im WTF Gitea.
- Den Bereich [AG Entwicklung](https://forum.wtf-eg.de/c/interna/ag-entwicklung/21) im WTF Forum.
- Einen Raum in Matrix. Zutritt per Einladung, frlan lädt ein, eine einfache PN im Forum reicht.
### Repos
* **[ki-backend](https://git.wtf-eg.de/kompetenzinventar/ki-backend)** (dieses Repo) enthält das Backend
* [ki-frontend](https://git.wtf-eg.de/kompetenzinventar/ki-frontend) enthält das Frontend
* Weitere Repositories befinden sich in der Gitea Organisation [Kompetenzinventar](https://git.wtf-eg.de/kompetenzinventar).
## Entwicklung ## Entwicklung
### Abhängigkeiten ### Abhängigkeiten
@ -222,6 +196,25 @@ docker-compose up
Dann http://localhost:13337 aufrufen. Dann http://localhost:13337 aufrufen.
### Workaround, falls der Zugriff auf registry.wtf-eg.net nicht möglich ist
Voraussetzung:
[ki-backend-docker](https://git.wtf-eg.de/kompetenzinventar/ki-backend-docker) muss parallel zum `ki-backend` ausgecheckt sein.
```
cd ki-backend-docker
docker build . --target base -t ki-backend-base
docker build . --target builder -t ki-backend-builder
```
Ändern der 2 Einträge im `Dockerfile` des `ki-backend`:
- registry.wtf-eg.net/ki-backend-builder:1.0.0 -> ki-backend-builder
- registry.wtf-eg.net/ki-backend-base:1.0.0 -> ki-backend-base
Danach sollte `docker-compose up` funktionieren.
## Lizenzen ## Lizenzen
Dieses Projekt erfüllt die [REUSE](https://reuse.software/) Spezifikation. Dieses Projekt erfüllt die [REUSE](https://reuse.software/) Spezifikation.

View File

@ -1,24 +0,0 @@
# SPDX-FileCopyrightText: NONE
# SPDX-License-Identifier: CC0-1.0
version = 1
SPDX-PackageName = "Kompetenzinventar Backend"
SPDX-PackageDownloadLocation = "https://git.wtf-eg.de/kompetenzinventar/ki-backend"
[[annotations]]
path = "data/imgs/flags/**"
precedence = "aggregate"
SPDX-FileCopyrightText = "2013 Panayiotis Lipiridis <https://flagicons.lipis.dev/>"
SPDX-License-Identifier = "MIT"
[[annotations]]
path = ["Pipfile.lock", "migrations/**"]
precedence = "aggregate"
SPDX-FileCopyrightText = "WTF Kooperative eG <https://wtf-eg.de/>"
SPDX-License-Identifier = "AGPL-3.0-or-later"
[[annotations]]
path = ["renovate.json", ".python-version"]
precedence = "aggregate"
SPDX-FileCopyrightText = "WTF Kooperative eG <https://wtf-eg.de/>"
SPDX-License-Identifier = "AGPL-3.0-or-later"

15
app.py
View File

@ -8,23 +8,22 @@ import os
from dotenv import load_dotenv, find_dotenv from dotenv import load_dotenv, find_dotenv
from flask import Flask from flask import Flask
from flask_cors import CORS from flask_cors import CORS
from flask.logging import default_handler
from flask_migrate import Migrate from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from ldap3.utils.log import logger as ldap3_logger
from ldap3.utils.log import set_library_log_detail_level, BASIC from ldap3.utils.log import set_library_log_detail_level, BASIC
load_dotenv(find_dotenv()) load_dotenv(find_dotenv())
app = Flask(__name__) app = Flask(__name__)
# Configure logging
loglevel = os.getenv("KI_LOGLEVEL", logging.WARNING) loglevel = os.getenv("KI_LOGLEVEL", logging.WARNING)
loglevel = int(loglevel) loglevel = int(loglevel)
app.logger.setLevel(loglevel) app.logger.setLevel(loglevel)
app.logger.propagate = False # do not forward messages to the root logger logging.basicConfig(level=loglevel)
logging.basicConfig(level=loglevel,
format='[%(asctime)s] %(levelname)s [%(name)s] %(message)s') # configure root logger as fallback set_library_log_detail_level(BASIC)
logging.getLogger('werkzeug').propagate = False # werkzeug has its own ColorStreamHandler ldap3_logger.addHandler(default_handler)
set_library_log_detail_level(BASIC) # ldap3 has different verbosity levels internally
app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv("SQLALCHEMY_DATABASE_URI") app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv("SQLALCHEMY_DATABASE_URI")
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
@ -41,6 +40,6 @@ CORS(app)
db = SQLAlchemy(app) db = SQLAlchemy(app)
migrate = Migrate(app, db, compare_type=True) migrate = Migrate(app, db, compare_type=True)
app.logger.info("Hello from KI") logging.debug("Hello from KI")
from ki import module # noqa from ki import module # noqa

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="810" height="540">
<rect width="810" height="540" fill="#FCDD09"/>
<path stroke="#DA121A" stroke-width="60" d="M0,90H810m0,120H0m0,120H810m0,120H0"/>
</svg>

Before

Width:  |  Height:  |  Size: 242 B

View File

@ -3,6 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
import csv import csv
import logging
from app import app, db from app import app, db
from ki.models import Address, Contact, ContactType, Language, Skill, Profile, ProfileLanguage, ProfileSearchtopic, \ from ki.models import Address, Contact, ContactType, Language, Skill, Profile, ProfileLanguage, ProfileSearchtopic, \
@ -12,7 +13,7 @@ from ki.models import Address, Contact, ContactType, Language, Skill, Profile, P
def seed_contacttypes(): def seed_contacttypes():
contacttypes_seed_file_path = app.config["KI_DATA_DIR"] + "/seed_data/contacttypes.csv" contacttypes_seed_file_path = app.config["KI_DATA_DIR"] + "/seed_data/contacttypes.csv"
app.logger.info("importing contacttypes") logging.info("importing contacttypes")
with open(contacttypes_seed_file_path) as file: with open(contacttypes_seed_file_path) as file:
csv_reader = csv.DictReader(file) csv_reader = csv.DictReader(file)
@ -25,30 +26,22 @@ def seed_contacttypes():
db.session.add(ContactType(id=int(contacttype["id"]), name=contacttype["name"])) db.session.add(ContactType(id=int(contacttype["id"]), name=contacttype["name"]))
def seed_user(auth_id, def seed_user(nickname,
nickname=None, visible=False,
pronouns="", skills=[],
visible=True, languages=[],
volunteerwork="", volunteerwork="",
availability_status=False, availability_status=False,
freetext="", freetext="",
availability_text="", availability_text="",
availability_hours_per_week=42, availability_hours_per_week=42):
skills=[],
searchtopics=[],
languages=[],
address=None,
contacts=[]):
if not nickname:
nickname = auth_id
app.logger.info(f"seeding {nickname} \\o/") app.logger.info(f"seeding {nickname} \\o/")
user = User(auth_id=auth_id) user = User(auth_id=nickname)
db.session.add(user) db.session.add(user)
profile = Profile(nickname=nickname, profile = Profile(nickname=nickname,
pronouns=pronouns, pronouns="",
volunteerwork=volunteerwork, volunteerwork=volunteerwork,
availability_status=availability_status, availability_status=availability_status,
availability_text=availability_text, availability_text=availability_text,
@ -61,29 +54,10 @@ def seed_user(auth_id,
skill = ProfileSkill(profile=profile, skill_id=skill_data[0], level=skill_data[1]) skill = ProfileSkill(profile=profile, skill_id=skill_data[0], level=skill_data[1])
db.session.add(skill) db.session.add(skill)
for skill_id in searchtopics:
searchtopic = ProfileSearchtopic(profile=profile, skill_id=skill_id)
db.session.add(searchtopic)
for language_data in languages: for language_data in languages:
language = ProfileLanguage(profile=profile, language_id=language_data[0], level=language_data[1]) language = ProfileLanguage(profile=profile, language_id=language_data[0], level=language_data[1])
db.session.add(language) db.session.add(language)
if address:
_address = Address(name=address[0],
street=address[1],
house_number=address[2],
additional=address[3],
postcode=address[4],
city=address[5],
country=address[6],
profile=profile)
db.session.add(_address)
for contact_data in contacts:
contact = Contact(profile=profile, contacttype_id=contact_data[0], content=contact_data[1])
db.session.add(contact)
db.session.add(profile) db.session.add(profile)
@ -119,40 +93,75 @@ def seed(dev: bool):
db.session.add(Language(id=iso["639-1"], name=iso["Sprache"])) db.session.add(Language(id=iso["639-1"], name=iso["Sprache"]))
if dev: if dev:
seed_user("klaus", visible=False) app.logger.info("seeding peter :)")
for i in range(1, 20): peter = User(auth_id="peter")
seed_user(f"babsi{i}") db.session.add(peter)
seed_user("peter", peters_profile = Profile(nickname="peternichtlustig",
nickname="peternichtlustig", pronouns="Herr Dr. Dr.",
visible=False, volunteerwork="Gartenverein",
pronouns="Herr Dr. Dr.", availability_status=True,
volunteerwork="Gartenverein", availability_hours_per_week=42,
availability_status=True, availability_text="Immer",
availability_hours_per_week=42, freetext="Ich mag Kaffee",
availability_text="Immer", user=peter)
freetext="Ich mag Kaffee", db.session.add(peters_profile)
skills=[(3, 3), (1, 5)],
searchtopics=[3, 1], matrix_contact = Contact(profile=peters_profile, contacttype_id=4, content="@peter:wtf-eg.de")
languages=[("de", 5), ("fr", 3)], db.session.add(matrix_contact)
address=("Peter Nichtlustig", "Waldweg", "23i", "Hinterhaus", "13337", "Bielefeld", "Deutschland"),
contacts=[(4, "@peter:wtf-eg.de"), (1, "peter@wtf-eg.de")]) email_contact = Contact(profile=peters_profile, contacttype_id=1, content="peter@wtf-eg.de")
db.session.add(email_contact)
peters_address = Address(name="Peter Nichtlustig",
street="Waldweg",
house_number="23i",
additional="Hinterhaus",
postcode="13337",
city="Bielefeld",
country="Deutschland",
profile=peters_profile)
db.session.add(peters_address)
peters_python_skill = ProfileSkill(profile=peters_profile, skill_id=3, level=3)
db.session.add(peters_python_skill)
peters_php_skill = ProfileSkill(profile=peters_profile, skill_id=1, level=5)
db.session.add(peters_php_skill)
peters_python_searchtopic = ProfileSearchtopic(profile=peters_profile, skill_id=3)
db.session.add(peters_python_searchtopic)
peters_php_searchtopic = ProfileSearchtopic(profile=peters_profile, skill_id=1)
db.session.add(peters_php_searchtopic)
peter_de = ProfileLanguage(profile=peters_profile, language_id="de", level=5)
db.session.add(peter_de)
peter_fr = ProfileLanguage(profile=peters_profile, language_id="fr", level=3)
db.session.add(peter_fr)
seed_user("klaus")
seed_user("dirtydieter", seed_user("dirtydieter",
visible=True,
volunteerwork="Müll sammeln", volunteerwork="Müll sammeln",
availability_status=True, availability_status=True,
availability_hours_per_week=24, availability_hours_per_week=24,
availability_text="Nur Nachts!", availability_text="Nur Nachts!",
freetext="1001010010111!!!", freetext="1001010010111!!!",
skills=[(1, 5)], skills=[(Skill.skill_id_php, 5)])
address=("Friedrich Witzig", "", "", "", "", "", ""))
all_skills = [(skill.id, 3) for skill in Skill.query.all()] all_skills = Skill.query.all()
seed_user("jutta", languages=[("fr", 5)], skills=all_skills) all_profile_skills = []
for skill in all_skills:
all_profile_skills.append((skill.id, 3))
seed_user("giesela", skills=[(9, 3), (10, 5)]) seed_user("jutta", visible=True, languages=[("fr", 5)], skills=all_profile_skills)
seed_user("bertha", visible=False, skills=[(11, 3), (10, 5)]) seed_user("giesela", visible=True, skills=[(Skill.skill_id_mysql, 3), (Skill.skill_id_postgresql, 5)])
seed_user("monique", languages=[("fr", 4)]) seed_user("bertha", visible=False, skills=[(Skill.skill_id_sqlite, 3), (Skill.skill_id_postgresql, 5)])
seed_user("monique", visible=True, languages=[("fr", 4)])
db.session.commit() db.session.commit()

View File

@ -4,14 +4,11 @@
from flask import make_response, request from flask import make_response, request
from ki.models import Profile, ProfileSkill, Skill, ProfileLanguage, Language, Address from ki.models import Profile, ProfileSkill, Skill, ProfileLanguage, Language
def find_profiles(): def find_profiles():
try: page = int(request.args.get("page", 1))
page = int(request.args.get("page", 1))
except ValueError:
page = 1
if page < 1: if page < 1:
return make_response({"messages": {"page": "Die angefragte Seite muss mindestens 1 sein"}}, 400) return make_response({"messages": {"page": "Die angefragte Seite muss mindestens 1 sein"}}, 400)
@ -22,35 +19,27 @@ def find_profiles():
return make_response({"messages": {"page_size": "Die maximale Anzahl Einträge pro Seite beträgt 100"}}, 400) return make_response({"messages": {"page_size": "Die maximale Anzahl Einträge pro Seite beträgt 100"}}, 400)
query = Profile.query.distinct(Profile.id) \ query = Profile.query.distinct(Profile.id) \
.order_by(Profile.nickname) \
.filter(Profile.visible.is_(True)) \ .filter(Profile.visible.is_(True)) \
.join(Profile.skills, isouter=True).join(ProfileSkill.skill, isouter=True) \ .join(Profile.skills, isouter=True).join(ProfileSkill.skill, isouter=True) \
.join(Profile.languages, isouter=True).join(ProfileLanguage.language, isouter=True) \ .join(Profile.languages, isouter=True).join(ProfileLanguage.language, isouter=True)
.join(Address, isouter=True)
if "search" in request.args: if "search" in request.args:
terms = request.args["search"].split(" ") terms = request.args["search"].split(" ")
for term in terms: for term in terms:
query = query.filter( query = query.filter(
Profile.nickname.like(f"%{term}%") | # noqa: W504 Profile.nickname.like(f"%{term}%") | Skill.name.like(f"%{term}%") | Language.name.like(f"%{term}%"))
Skill.name.like(f"%{term}%") | # noqa: W504
Language.name.like(f"%{term}%") | # noqa: W504
Address.name.like(f"%{term}%"))
if "nickname" in request.args: if "nickname" in request.args:
nickname = request.args.get("nickname") nickname = request.args.get("nickname")
query = query.filter(Profile.nickname.like(f"%{nickname}%")) query = query.filter(Profile.nickname.like(f"%{nickname}%"))
paginated_result = query.paginate(page=page, per_page=page_size) count = query.count()
offset = (page - 1) * page_size
db_profiles = query.limit(page_size).offset(offset).all()
api_profiles = [] api_profiles = []
for db_profile in paginated_result.items: for db_profile in db_profiles:
api_profiles.append(db_profile.to_dict()) api_profiles.append(db_profile.to_dict())
return make_response({ return make_response({"total": count, "profiles": api_profiles})
"total": paginated_result.total,
"pages": paginated_result.pages,
"page": paginated_result.page,
"profiles": api_profiles
})

View File

@ -151,11 +151,11 @@ def update_profile(user_id: int):
profile.freetext = request.json.get("freetext", "") profile.freetext = request.json.get("freetext", "")
profile.visible = request.json.get("visible", False) profile.visible = request.json.get("visible", False)
update_address(profile, request.json.get("address")) update_address(profile, request.json.get("address", {}))
update_contacts(profile, request.json.get("contacts", [])) update_contacts(profile, request.json.get("contacts", {}))
update_skills(profile, request.json.get("skills", [])) update_skills(profile, request.json.get("skills", {}))
update_searchtopics(profile, request.json.get("searchtopics", [])) update_searchtopics(profile, request.json.get("searchtopics"))
update_languages(profile, request.json.get("languages", [])) update_languages(profile, request.json.get("languages", {}))
db.session.commit() db.session.commit()

View File

@ -139,6 +139,13 @@ class Address(db.Model):
class Skill(db.Model): class Skill(db.Model):
skill_id_php = 1
skill_id_python = 3
skill_id_sqlalchemy = 7
skill_id_mysql = 9
skill_id_postgresql = 10
skill_id_sqlite = 11
__tablename__ = "skill" __tablename__ = "skill"
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)

View File

@ -17,7 +17,6 @@ content_type_png = "image/png"
def token_auth(func): def token_auth(func):
@wraps(func) @wraps(func)
def _token_auth(*args, **kwargs): def _token_auth(*args, **kwargs):
auth_header = request.headers.get("Authorization") auth_header = request.headers.get("Authorization")

View File

@ -8,7 +8,6 @@ from ki.test.ApiTest import ApiTest
class TestContactTypesEndpoint(ApiTest): class TestContactTypesEndpoint(ApiTest):
def test_skills_options(self): def test_skills_options(self):
response = self.client.options("/contacttypes") response = self.client.options("/contacttypes")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)

View File

@ -8,7 +8,6 @@ from ki.test.ApiTest import ApiTest
class TestFindProfilesEndpoint(ApiTest): class TestFindProfilesEndpoint(ApiTest):
def test_find_profiles_options(self): def test_find_profiles_options(self):
response = self.client.options("/users/profiles") response = self.client.options("/users/profiles")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@ -21,33 +20,25 @@ class TestFindProfilesEndpoint(ApiTest):
response = self.client.get("/users/profiles?nickname=horsthorsthorst", response = self.client.get("/users/profiles?nickname=horsthorsthorst",
headers={"Authorization": "Bearer " + token}) headers={"Authorization": "Bearer " + token})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.json, {"total": 0, "page": 1, "pages": 0, "profiles": []}) self.assertEqual(response.json, {"total": 0, "profiles": []})
def test_find_sql_specialchars(self): def test_find_sql_specialchars(self):
token = self.login("peter", "geheim")["token"] token = self.login("peter", "geheim")["token"]
response = self.client.get("/users/profiles?nickname=%22%27%25", headers={"Authorization": "Bearer " + token}) response = self.client.get("/users/profiles?nickname=%22%27%25", headers={"Authorization": "Bearer " + token})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.json, {"total": 0, "page": 1, "pages": 0, "profiles": []}) self.assertEqual(response.json, {"total": 0, "profiles": []})
def test_find_all_page1(self): def test_find_all(self):
token = self.login("peter", "geheim")["token"] token = self.login("peter", "geheim")["token"]
response = self.client.get("/users/profiles", headers={"Authorization": "Bearer " + token}) response = self.client.get("/users/profiles", headers={"Authorization": "Bearer " + token})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertDictContainsSubset({"total": 23, "page": 1, "pages": 2}, response.json) self.assertDictContainsSubset({"total": 4}, response.json)
self.assertDictContainsSubset({"nickname": "babsi1"}, response.json["profiles"][0]) self.assertDictContainsSubset({"nickname": "dirtydieter"}, response.json["profiles"][0])
self.assertDictContainsSubset({"nickname": "dirtydieter"}, response.json["profiles"][19])
def test_find_all_page2(self):
token = self.login("peter", "geheim")["token"]
response = self.client.get("/users/profiles?page=2", headers={"Authorization": "Bearer " + token})
self.assertEqual(response.status_code, 200)
self.assertDictContainsSubset({"total": 23, "page": 2, "pages": 2}, response.json)
self.assertDictContainsSubset({"nickname": "giesela"}, response.json["profiles"][0])
self.assertDictContainsSubset({"nickname": "jutta"}, response.json["profiles"][1]) self.assertDictContainsSubset({"nickname": "jutta"}, response.json["profiles"][1])
self.assertDictContainsSubset({"nickname": "monique"}, response.json["profiles"][2]) self.assertDictContainsSubset({"nickname": "giesela"}, response.json["profiles"][2])
self.assertDictContainsSubset({"nickname": "monique"}, response.json["profiles"][3])
def test_find_dieter(self): def test_find_dieter(self):
token = self.login("peter", "geheim")["token"] token = self.login("peter", "geheim")["token"]
@ -71,8 +62,8 @@ class TestFindProfilesEndpoint(ApiTest):
response = self.client.get("/users/profiles?search=sql", headers={"Authorization": "Bearer " + token}) response = self.client.get("/users/profiles?search=sql", headers={"Authorization": "Bearer " + token})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertDictContainsSubset({"total": 2}, response.json) self.assertDictContainsSubset({"total": 2}, response.json)
self.assertDictContainsSubset({"nickname": "giesela"}, response.json["profiles"][0]) self.assertDictContainsSubset({"nickname": "jutta"}, response.json["profiles"][0])
self.assertDictContainsSubset({"nickname": "jutta"}, response.json["profiles"][1]) self.assertDictContainsSubset({"nickname": "giesela"}, response.json["profiles"][1])
def test_find_postgres(self): def test_find_postgres(self):
token = self.login("peter", "geheim")["token"] token = self.login("peter", "geheim")["token"]
@ -80,8 +71,8 @@ class TestFindProfilesEndpoint(ApiTest):
response = self.client.get("/users/profiles?search=post", headers={"Authorization": "Bearer " + token}) response = self.client.get("/users/profiles?search=post", headers={"Authorization": "Bearer " + token})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertDictContainsSubset({"total": 2}, response.json) self.assertDictContainsSubset({"total": 2}, response.json)
self.assertDictContainsSubset({"nickname": "giesela"}, response.json["profiles"][0]) self.assertDictContainsSubset({"nickname": "jutta"}, response.json["profiles"][0])
self.assertDictContainsSubset({"nickname": "jutta"}, response.json["profiles"][1]) self.assertDictContainsSubset({"nickname": "giesela"}, response.json["profiles"][1])
def test_find_php_franzosen(self): def test_find_php_franzosen(self):
token = self.login("peter", "geheim")["token"] token = self.login("peter", "geheim")["token"]
@ -100,14 +91,6 @@ class TestFindProfilesEndpoint(ApiTest):
self.assertDictContainsSubset({"nickname": "jutta"}, response.json["profiles"][0]) self.assertDictContainsSubset({"nickname": "jutta"}, response.json["profiles"][0])
self.assertDictContainsSubset({"nickname": "monique"}, response.json["profiles"][1]) self.assertDictContainsSubset({"nickname": "monique"}, response.json["profiles"][1])
def test_find_dieter_by_name(self):
token = self.login("peter", "geheim")["token"]
response = self.client.get("/users/profiles?search=friedrich", headers={"Authorization": "Bearer " + token})
self.assertEqual(response.status_code, 200)
self.assertDictContainsSubset({"total": 1}, response.json)
self.assertDictContainsSubset({"nickname": "dirtydieter"}, response.json["profiles"][0])
if __name__ == "main": if __name__ == "main":
unittest.main() unittest.main()

View File

@ -8,7 +8,6 @@ from ki.test.ApiTest import ApiTest
class TestLanguagesEndpoint(ApiTest): class TestLanguagesEndpoint(ApiTest):
def test_skills_options(self): def test_skills_options(self):
response = self.client.options("/languages") response = self.client.options("/languages")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)

View File

@ -10,7 +10,6 @@ from ki.test.ApiTest import ApiTest
class TestLoginEndpoint(ApiTest): class TestLoginEndpoint(ApiTest):
def test_login(self): def test_login(self):
response1_data = self.login("peter", "geheim") response1_data = self.login("peter", "geheim")
response2_data = self.login("peter", "geheim") response2_data = self.login("peter", "geheim")

View File

@ -20,8 +20,7 @@ class TestProfileEndpoint(ApiTest):
self.assertEqual(login_response.status_code, 200) self.assertEqual(login_response.status_code, 200)
self.assertIn("token", login_response.json) self.assertIn("token", login_response.json)
babsi = User.query.filter(User.auth_id == "babsi1").first() response = self.client.post("/users/1/profile",
response = self.client.post(f"/users/{babsi.id}/profile",
data=json.dumps({}), data=json.dumps({}),
content_type="application/json", content_type="application/json",
headers={"Authorization": "Bearer " + login_response.json["token"]}) headers={"Authorization": "Bearer " + login_response.json["token"]})
@ -103,15 +102,14 @@ class TestProfileEndpoint(ApiTest):
"level": 2 "level": 2
}] }]
} }
peter = User.query.filter(User.auth_id == "peter").first() response = self.client.post("/users/1/profile",
response = self.client.post(f"/users/{peter.id}/profile",
data=json.dumps(data), data=json.dumps(data),
content_type="application/json", content_type="application/json",
headers={"Authorization": "Bearer " + token}) headers={"Authorization": "Bearer " + token})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
with app.app_context(): with app.app_context():
user = User.query.filter(User.id == peter.id).first() user = User.query.filter(User.id == 1).first()
profile = user.profile profile = user.profile
self.assertEqual("Hebbert", profile.nickname) self.assertEqual("Hebbert", profile.nickname)
self.assertEqual("Monsieur", profile.pronouns) self.assertEqual("Monsieur", profile.pronouns)
@ -185,8 +183,7 @@ class TestProfileEndpoint(ApiTest):
def test_get_visible_proifle(self): def test_get_visible_proifle(self):
token = self.login("peter", "geheim")["token"] token = self.login("peter", "geheim")["token"]
babsi = User.query.filter(User.auth_id == "babsi1").first() response = self.client.get("/users/3/profile", headers={"Authorization": f"Bearer {token}"})
response = self.client.get(f"/users/{babsi.id}/profile", headers={"Authorization": f"Bearer {token}"})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@ -197,15 +194,14 @@ class TestProfileEndpoint(ApiTest):
self.assertEqual(login_response.status_code, 200) self.assertEqual(login_response.status_code, 200)
self.assertIn("token", login_response.json) self.assertIn("token", login_response.json)
peter = User.query.filter(User.auth_id == "peter").first() response = self.client.get("/users/1/profile",
response = self.client.get(f"/users/{peter.id}/profile",
headers={"Authorization": "Bearer " + login_response.json["token"]}) headers={"Authorization": "Bearer " + login_response.json["token"]})
profile_id = peter.profile.id
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertDictEqual( self.assertDictEqual(
response.json, { response.json, {
"profile": { "profile": {
"user_id": peter.id, "user_id": 1,
"nickname": "peternichtlustig", "nickname": "peternichtlustig",
"pronouns": "Herr Dr. Dr.", "pronouns": "Herr Dr. Dr.",
"availability_status": True, "availability_status": True,
@ -222,12 +218,12 @@ class TestProfileEndpoint(ApiTest):
"id": 1, "id": 1,
"name": "Peter Nichtlustig", "name": "Peter Nichtlustig",
"postcode": "13337", "postcode": "13337",
"profile_id": profile_id, "profile_id": 1,
"street": "Waldweg" "street": "Waldweg"
}, },
"contacts": [{ "contacts": [{
"id": 1, "id": 1,
"profile_id": profile_id, "profile_id": 1,
"contacttype": { "contacttype": {
"id": 4, "id": 4,
"name": "Matrix" "name": "Matrix"
@ -235,7 +231,7 @@ class TestProfileEndpoint(ApiTest):
"content": "@peter:wtf-eg.de" "content": "@peter:wtf-eg.de"
}, { }, {
"id": 2, "id": 2,
"profile_id": profile_id, "profile_id": 1,
"contacttype": { "contacttype": {
"id": 1, "id": 1,
"name": "E-Mail" "name": "E-Mail"
@ -243,7 +239,7 @@ class TestProfileEndpoint(ApiTest):
"content": "peter@wtf-eg.de" "content": "peter@wtf-eg.de"
}], }],
"skills": [{ "skills": [{
"profile_id": profile_id, "profile_id": 1,
"skill": { "skill": {
"id": 1, "id": 1,
"name": "PHP", "name": "PHP",
@ -251,7 +247,7 @@ class TestProfileEndpoint(ApiTest):
}, },
"level": 5 "level": 5
}, { }, {
"profile_id": profile_id, "profile_id": 1,
"skill": { "skill": {
"id": 3, "id": 3,
"name": "Python", "name": "Python",
@ -260,14 +256,14 @@ class TestProfileEndpoint(ApiTest):
"level": 3 "level": 3
}], }],
"searchtopics": [{ "searchtopics": [{
"profile_id": profile_id, "profile_id": 1,
"skill": { "skill": {
"id": 1, "id": 1,
"name": "PHP", "name": "PHP",
"icon_url": "/skills/1/icon" "icon_url": "/skills/1/icon"
} }
}, { }, {
"profile_id": profile_id, "profile_id": 1,
"skill": { "skill": {
"id": 3, "id": 3,
"name": "Python", "name": "Python",
@ -275,7 +271,7 @@ class TestProfileEndpoint(ApiTest):
} }
}], }],
"languages": [{ "languages": [{
"profile_id": profile_id, "profile_id": 1,
"language": { "language": {
"id": "de", "id": "de",
"name": "Deutsch", "name": "Deutsch",
@ -283,7 +279,7 @@ class TestProfileEndpoint(ApiTest):
}, },
"level": 5 "level": 5
}, { }, {
"profile_id": profile_id, "profile_id": 1,
"language": { "language": {
"id": "fr", "id": "fr",
"name": "Französisch", "name": "Französisch",

View File

@ -8,7 +8,6 @@ from ki.test.ApiTest import ApiTest
class TestSkillsEndpoint(ApiTest): class TestSkillsEndpoint(ApiTest):
def test_skills_options(self): def test_skills_options(self):
response = self.client.options("/skills") response = self.client.options("/skills")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)

50
migrations/alembic.ini Normal file
View File

@ -0,0 +1,50 @@
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic,flask_migrate
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[logger_flask_migrate]
level = INFO
handlers =
qualname = flask_migrate
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

View File

@ -11,6 +11,9 @@ from alembic import context
# access to the values within the .ini file in use. # access to the values within the .ini file in use.
config = context.config config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env') logger = logging.getLogger('alembic.env')
# add your model's MetaData object here # add your model's MetaData object here

View File

@ -1,18 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:best-practices",
":disableDependencyDashboard",
":maintainLockFilesMonthly",
":pinVersions",
":separateMultipleMajorReleases"
],
"packageRules": [
{
"matchDepNames": ["python"],
"groupName": "Python",
"separateMinorPatch": true,
"separateMultipleMinor": true
}
]
}

View File

@ -11,4 +11,4 @@ with app.app_context():
config = migrate.get_config() config = migrate.get_config()
command.upgrade(config, "head") command.upgrade(config, "head")
serve(app, host="0.0.0.0", port=5000, threads=20) serve(app, host="0.0.0.0", port=5000)