Compare commits

..

3 Commits

Author SHA1 Message Date
75085f240c Current status, not everything works 2024-01-23 20:11:35 +01:00
2c781dec6c Rewrite sqlachemy code for 1.4 to 2.x migration 2024-01-11 20:48:13 +01:00
ecfa344904 Update dependencies to latest minor version
werkzeug was added as explicit dependency,
as flask did a wrong pinning >=2.x
This resulted in an installation of werkzeug 3.x
2024-01-10 22:40:19 +01:00
22 changed files with 691 additions and 647 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

@ -15,28 +15,15 @@ trigger:
steps: steps:
- name: install-lint-test - name: install-lint-test
image: python:3.8.20-alpine@sha256:3d93b1f77efce339aa77db726656872517b0d67837989aa7c4b35bd5ae7e81ba image: git.wtf-eg.de/kompetenzinventar/builder:1.0.2
env:
PYROOT: '/pyroot'
PYTHONUSERBASE: '/pyroot'
commands: commands:
- apk add --no-cache gcc g++ musl-dev python3-dev
- pip3 install pipenv
- pipenv verify
- pipenv install --dev - pipenv install --dev
- pipenv run flake8 - pipenv run flake8
- pipenv run reuse lint - pipenv run reuse lint
- pipenv run python -m unittest discover ki - pipenv run python -m unittest discover ki
- name: docker-dry-run
image: plugins/docker:20.18.4@sha256:a8d3d86853c721492213264815f1d00d3ed13f42f5c1855a02f47fa4d5f1e042 image_pull_secrets:
settings: - dockerconfig
registry: git.wtf-eg.de
repo: git.wtf-eg.de/kompetenzinventar/backend
target: ki-backend
dry_run: true
when:
event:
- pull_request
--- ---
kind: pipeline kind: pipeline
@ -54,7 +41,7 @@ depends_on:
steps: steps:
- name: docker-publish - name: docker-publish
image: plugins/docker:20.18.4@sha256:a8d3d86853c721492213264815f1d00d3ed13f42f5c1855a02f47fa4d5f1e042 image: plugins/docker
settings: settings:
registry: git.wtf-eg.de registry: git.wtf-eg.de
repo: git.wtf-eg.de/kompetenzinventar/backend repo: git.wtf-eg.de/kompetenzinventar/backend
@ -81,7 +68,7 @@ depends_on:
steps: steps:
- name: deploy-dev - name: deploy-dev
image: appleboy/drone-ssh:1.7.5@sha256:995677e073454912f26d4c0fdd2f9df2e1f5a30d6603d3f2ece667311b6babb3 image: appleboy/drone-ssh
settings: settings:
host: host:
- dev01.wtf-eg.net - dev01.wtf-eg.net
@ -104,19 +91,14 @@ trigger:
steps: steps:
- name: install-lint-test - name: install-lint-test
image: python:3.8.20-alpine@sha256:3d93b1f77efce339aa77db726656872517b0d67837989aa7c4b35bd5ae7e81ba image: git.wtf-eg.de/kompetenzinventar/builder:1.0.2
env:
PYROOT: '/pyroot'
PYTHONUSERBASE: '/pyroot'
commands: commands:
- apk add --no-cache gcc g++ musl-dev python3-dev
- pip3 install pipenv
- pipenv install --dev - pipenv install --dev
- pipenv run flake8 - pipenv run flake8
- pipenv run reuse lint - pipenv run reuse lint
- pipenv run python -m unittest discover ki - pipenv run python -m unittest discover ki
- name: docker-publish - name: docker-publish
image: plugins/docker:20.18.4@sha256:a8d3d86853c721492213264815f1d00d3ed13f42f5c1855a02f47fa4d5f1e042 image: plugins/docker
settings: settings:
registry: git.wtf-eg.de registry: git.wtf-eg.de
repo: git.wtf-eg.de/kompetenzinventar/backend repo: git.wtf-eg.de/kompetenzinventar/backend

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

@ -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 git.wtf-eg.de/kompetenzinventar/builder:1.0.2 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 git.wtf-eg.de/kompetenzinventar/base:1.0.2 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

34
Pipfile
View File

@ -8,26 +8,26 @@ verify_ssl = true
name = "pypi" name = "pypi"
[packages] [packages]
flask = "==2.3.3" flask = "~=3.0.0"
python-dotenv = "==1.0.1" python-dotenv = "~=0.17.1"
flask-migrate = "==4.0.7" flask-migrate = "~=4.0.5"
flask-sqlalchemy = "==2.5.1" flask-sqlalchemy = "~=3.1.1"
sqlalchemy = "==1.4.54" sqlalchemy = "~=2.0.25"
waitress = "==2.1.2" waitress = "~=2.1.2"
pyyaml = "==6.0.2" pyyaml = "~=6.0.1"
flask-cors = "==5.0.0" flask-cors = "~=4.0.0"
ldap3 = "==2.9.1" ldap3 = "~=2.9.1"
pymysql = "==1.1.1" pymysql = "~=1.1.0"
werkzeug = "==2.3.8" werkzeug = "~=3.0.1"
[dev-packages] [dev-packages]
flake8 = "==7.1.1" flake8 = "~=3.9.2"
yapf = "==0.40.2" yapf = "~=0.40.2"
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.11"
[scripts] [scripts]
clean = "rm data/ki.sqlite" clean = "rm storage/ki.sqlite"

1057
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

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"

2
app.py
View File

@ -38,7 +38,7 @@ app.config["KI_LDAP_AUTH_PASSWORD"] = os.getenv("KI_LDAP_AUTH_PASSWORD")
app.config["KI_LDAP_BASE_DN"] = os.getenv("KI_LDAP_BASE_DN") app.config["KI_LDAP_BASE_DN"] = os.getenv("KI_LDAP_BASE_DN")
CORS(app) CORS(app)
db = SQLAlchemy(app) db = SQLAlchemy(app, session_options={"future": True})
migrate = Migrate(app, db, compare_type=True) migrate = Migrate(app, db, compare_type=True)
app.logger.info("Hello from KI") app.logger.info("Hello from KI")

View File

@ -2,7 +2,7 @@
# #
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
SQLALCHEMY_DATABASE_URI=sqlite:///storage/ki.sqlite SQLALCHEMY_DATABASE_URI=sqlite:///../storage/ki.sqlite
CORS_ORIGINS=* CORS_ORIGINS=*

View File

@ -2,4 +2,5 @@
# #
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
migrations/*.py *
!.gitignore

View File

@ -19,7 +19,7 @@ def seed_contacttypes():
for contacttype in csv_reader: for contacttype in csv_reader:
id = int(contacttype["id"]) id = int(contacttype["id"])
db_contacttype = ContactType.query.get(id) db_contacttype = db.session.get(ContactType, id)
if db_contacttype is None: if db_contacttype is None:
db.session.add(ContactType(id=int(contacttype["id"]), name=contacttype["name"])) db.session.add(ContactType(id=int(contacttype["id"]), name=contacttype["name"]))
@ -99,7 +99,7 @@ def seed(dev: bool):
for skill in skills_csv_reader: for skill in skills_csv_reader:
id = int(skill["id"]) id = int(skill["id"])
db_skill = Skill.query.get(id) db_skill = db.session.get(Skill, id)
if db_skill is None: if db_skill is None:
db.session.add(Skill(id=int(skill["id"]), name=skill["name"])) db.session.add(Skill(id=int(skill["id"]), name=skill["name"]))
@ -113,7 +113,7 @@ def seed(dev: bool):
for iso in iso_csv_reader: for iso in iso_csv_reader:
id = iso["639-1"] id = iso["639-1"]
db_language = Language.query.get(id) db_language = db.session.get(Language, id)
if db_language is None: if db_language is None:
db.session.add(Language(id=iso["639-1"], name=iso["Sprache"])) db.session.add(Language(id=iso["639-1"], name=iso["Sprache"]))
@ -147,12 +147,13 @@ def seed(dev: bool):
freetext="1001010010111!!!", freetext="1001010010111!!!",
skills=[(1, 5)], skills=[(1, 5)],
address=("Friedrich Witzig", "", "", "", "", "", "")) address=("Friedrich Witzig", "", "", "", "", "", ""))
#all_skills = [(skill.id, 3) for skill in Skill.query.all()] # query causes problems
all_skills = [(skill.id, 3) for skill in Skill.query.all()] #seed_user("jutta", languages=[("fr", 5)], skills=all_skills)
seed_user("jutta", languages=[("fr", 5)], skills=all_skills)
seed_user("giesela", skills=[(9, 3), (10, 5)]) seed_user("giesela", skills=[(9, 3), (10, 5)])
seed_user("bertha", visible=False, skills=[(11, 3), (10, 5)]) seed_user("bertha", visible=False, skills=[(11, 3), (10, 5)])
seed_user("monique", languages=[("fr", 4)]) seed_user("monique", languages=[("fr", 4)])
print("seeding done")
db.session.commit() with app.app_context():
db.session.commit() # also problematic
print("commit done")

View File

@ -33,7 +33,7 @@ def update_languages(profile, languages_data):
if "id" not in language_data["language"]: if "id" not in language_data["language"]:
continue continue
language = Language.query.get(language_data["language"]["id"]) language = db.session.get(Language, language_data["language"]["id"])
profile_language = ProfileLanguage.query.filter(ProfileLanguage.profile == profile, profile_language = ProfileLanguage.query.filter(ProfileLanguage.profile == profile,
ProfileLanguage.language == language).first() ProfileLanguage.language == language).first()
@ -110,7 +110,7 @@ def update_contacts(profile, contacts_data):
if "id" in contact_data: if "id" in contact_data:
contact_id = int(contact_data["id"]) contact_id = int(contact_data["id"])
contact_ids_to_be_deleted.remove(contact_id) contact_ids_to_be_deleted.remove(contact_id)
contact = Contact.query.get(contact_id) contact = db.session.get(Contact, contact_id)
else: else:
contact = Contact(profile=profile, contacttype=contacttype) contact = Contact(profile=profile, contacttype=contacttype)
db.session.add(contact) db.session.add(contact)
@ -122,7 +122,7 @@ def update_contacts(profile, contacts_data):
def update_profile(user_id: int): def update_profile(user_id: int):
user = User.query.get(user_id) user = db.session.get(User, user_id)
if user is None: if user is None:
return make_response({}, 404) return make_response({}, 404)

View File

@ -10,7 +10,7 @@ from ki.auth import auth
from ki.handlers import find_profiles as find_profiles_handler from ki.handlers import find_profiles as find_profiles_handler
from ki.handlers import update_profile as update_profile_handler from ki.handlers import update_profile as update_profile_handler
from ki.models import ContactType, Language, Skill, Token, User from ki.models import ContactType, Language, Skill, Token, User
from app import app from app import app, db
content_type_svg = "image/svg+xml" content_type_svg = "image/svg+xml"
content_type_png = "image/png" content_type_png = "image/png"
@ -66,7 +66,7 @@ def handle_completion_request(model, key):
def handle_icon_request(model, id, path): def handle_icon_request(model, id, path):
object = model.query.get(id) object = db.session.get(model, id)
if object is None: if object is None:
return make_response({}, 404) return make_response({}, 404)

View File

@ -10,11 +10,14 @@ from app import app, db, migrate
from ki.actions import seed from ki.actions import seed
from ki.models import Skill from ki.models import Skill
from sqlalchemy import select
class ApiTest(unittest.TestCase): class ApiTest(unittest.TestCase):
maxDiff = None maxDiff = None
def setUp(self): def setUp(self):
print("Running setup")
app.debug = True app.debug = True
app.config["KI_AUTH"] = "file" app.config["KI_AUTH"] = "file"
app.config["TESTING"] = True app.config["TESTING"] = True
@ -22,19 +25,42 @@ class ApiTest(unittest.TestCase):
self.client = app.test_client() self.client = app.test_client()
with app.app_context():
config = migrate.get_config() config = migrate.get_config()
command.upgrade(config, "head")
with app.app_context():
command.upgrade(config, "head")
seed(True) seed(True)
max_skill = Skill.query.order_by(Skill.id.desc()).first()
#statement = select(Skill).order_by(Skill.id.desc())
#print(statement)
#skill_obj = db.session.scalars(statement).all()
#print(skill_obj)
#statement = select(Skill.id)
#print(statement)
#max_skill = db.session.Skill().order_by(Skill.id.desc()).first()
#max_skill = Skill.query.order_by(Skill.id.desc()).first() # TODO: problematic
with db.session.no_autoflush: # only works on first test run
max_skill = db.session.query(Skill).order_by(Skill.id.desc()).first() # TODO: also problematic,
#skills = db.session.execute(db.select(Skill)).scalars()
#print(max_skill)
#max_skill = db.session.execute(db.select(Skill)
## .order_by(Skill.id.desc())
# ).scalar_one()
print(max_skill)
print("max_skill done")
self.max_skill_id = max_skill.id self.max_skill_id = max_skill.id
def tearDown(self): def tearDown(self):
print("Running teardown")
with app.app_context():
db.drop_all() db.drop_all()
db.engine.dispose() db.engine.dispose()
def login(self, username, password): def login(self, username, password):
#with app.app_context():
login_data = {"username": username, "password": password} login_data = {"username": username, "password": password}
login_response = self.client.post("/users/login", data=json.dumps(login_data), content_type="application/json") login_response = self.client.post("/users/login", data=json.dumps(login_data), content_type="application/json")

View File

@ -8,37 +8,39 @@ from ki.test.ApiTest import ApiTest
class TestContactTypesEndpoint(ApiTest): class TestContactTypesEndpoint(ApiTest):
def test_skills_options(self): def test_skills_options(self):
print("test_skills_options")
#with app.app_context():
response = self.client.options("/contacttypes") response = self.client.options("/contacttypes")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertIn("Access-Control-Allow-Origin", response.headers) self.assertIn("Access-Control-Allow-Origin", response.headers)
self.assertEqual(response.headers["Access-Control-Allow-Origin"], "*") self.assertEqual(response.headers["Access-Control-Allow-Origin"], "*")
def test_get_contacttypes_unauthorised(self): # def test_get_contacttypes_unauthorised(self):
response = self.client.get("/contacttypes?search=m") # print("test_get_contacttypes_unauthorised")
self.assertEqual(response.status_code, 401) # response = self.client.get("/contacttypes?search=m")
# self.assertEqual(response.status_code, 401)
def test_get_contacttypes(self): # def test_get_contacttypes(self):
token = self.login("peter", "geheim")["token"] # token = self.login("peter", "geheim")["token"]
response = self.client.get("/contacttypes?search=m", headers={"Authorization": "Bearer " + token}) # response = self.client.get("/contacttypes?search=m", headers={"Authorization": "Bearer " + token})
self.assertEqual(response.status_code, 200) # self.assertEqual(response.status_code, 200)
self.assertEqual( # self.assertEqual(
{ # {
"contacttypes": [{ # "contacttypes": [{
"id": 5, # "id": 5,
"name": "Mastodon" # "name": "Mastodon"
}, { # }, {
"id": 4, # "id": 4,
"name": "Matrix" # "name": "Matrix"
}, { # }, {
"id": 2, # "id": 2,
"name": "Mobiltelefon" # "name": "Mobiltelefon"
}] # }]
}, response.json) # }, response.json)
self.assertIn("Access-Control-Allow-Origin", response.headers) # self.assertIn("Access-Control-Allow-Origin", response.headers)
self.assertEqual(response.headers["Access-Control-Allow-Origin"], "*") # self.assertEqual(response.headers["Access-Control-Allow-Origin"], "*")
if __name__ == "main": if __name__ == "main":

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)

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

@ -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)

View File

@ -19,7 +19,7 @@ logger = logging.getLogger('alembic.env')
# target_metadata = mymodel.Base.metadata # target_metadata = mymodel.Base.metadata
config.set_main_option( config.set_main_option(
'sqlalchemy.url', 'sqlalchemy.url',
str(current_app.extensions['migrate'].db.get_engine().url).replace( str(current_app.extensions['migrate'].db.engine.url).replace(
'%', '%%')) '%', '%%'))
target_metadata = current_app.extensions['migrate'].db.metadata target_metadata = current_app.extensions['migrate'].db.metadata
@ -68,7 +68,7 @@ def run_migrations_online():
directives[:] = [] directives[:] = []
logger.info('No changes in schema detected.') logger.info('No changes in schema detected.')
connectable = current_app.extensions['migrate'].db.get_engine() connectable = current_app.extensions['migrate'].db.engine
with connectable.connect() as connection: with connectable.connect() as connection:
context.configure( context.configure(

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
}
]
}