Compare commits

...

7 Commits

Author SHA1 Message Date
1b58c65666
fix migrations
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2021-07-06 21:19:58 +02:00
2f83e206e1
implement searchtopics 2021-07-06 21:18:47 +02:00
8e087198a4 Merge pull request 'Profilsuche' (#31) from feature-profile-search into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #31
2021-07-06 21:17:00 +02:00
85b8e638a7
add reuse headers to profile search files
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2021-07-06 21:14:41 +02:00
020edffec7
implement profile search
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2021-07-06 21:04:47 +02:00
980415bd20 Merge pull request 'availability Feld' (#33) from feature-availability into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #33
2021-07-06 21:03:04 +02:00
a401c6d4a7
add availability
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2021-07-05 20:16:50 +02:00
12 changed files with 193 additions and 17 deletions

View File

@ -6,7 +6,8 @@ import csv
import logging import logging
from app import app, db from app import app, db
from ki.models import Address, Contact, ContactType, Language, Skill, Profile, ProfileLanguage, ProfileSkill, User from ki.models import Address, Contact, ContactType, Language, Skill, Profile, ProfileLanguage, ProfileSearchtopic, \
ProfileSkill, User
def seed_contacttypes(): def seed_contacttypes():
@ -65,6 +66,7 @@ def seed(dev: bool):
peters_profile = Profile(nickname="peternichtlustig", peters_profile = Profile(nickname="peternichtlustig",
pronouns="Herr Dr. Dr.", pronouns="Herr Dr. Dr.",
volunteerwork="Gartenverein", volunteerwork="Gartenverein",
availability="Immer",
freetext="Ich mag Kaffee", freetext="Ich mag Kaffee",
user=peter) user=peter)
db.session.add(peters_profile) db.session.add(peters_profile)
@ -91,6 +93,12 @@ def seed(dev: bool):
peters_php_skill = ProfileSkill(profile=peters_profile, skill_id=1, level=5) peters_php_skill = ProfileSkill(profile=peters_profile, skill_id=1, level=5)
db.session.add(peters_php_skill) 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) peter_de = ProfileLanguage(profile=peters_profile, language_id="de", level=5)
db.session.add(peter_de) db.session.add(peter_de)

View File

@ -2,4 +2,5 @@
# #
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
from ki.handlers.find_profiles import find_profiles # noqa
from ki.handlers.update_profile import update_profile # noqa from ki.handlers.update_profile import update_profile # noqa

View File

@ -0,0 +1,32 @@
# SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
from flask import make_response, request
from ki.models import Profile
def find_profiles():
page = int(request.args.get("page", 1))
if page < 1:
return make_response({"messages": {"page": "Die angefragte Seite muss mindestens 1 sein"}}, 400)
page_size = int(request.args.get("page_size", 20))
if page_size > 100:
return make_response({"messages": {"page_size": "Die maximale Anzahl Einträge pro Seite beträgt 100"}}, 400)
offset = (page - 1) * page_size
query = Profile.query.filter(Profile.visible is True)
count = query.count()
db_profiles = query.limit(page_size).offset(offset).all()
api_profiles = []
for db_profile in db_profiles:
api_profiles.append(db_profile.to_dict())
return make_response({"total": count, "profiles": api_profiles})

View File

@ -5,7 +5,8 @@
from flask import make_response, request from flask import make_response, request
from sqlalchemy import not_ from sqlalchemy import not_
from ki.models import Address, Contact, ContactType, Language, User, Profile, ProfileLanguage, ProfileSkill, Skill from ki.models import Address, Contact, ContactType, Language, User, Profile, ProfileLanguage, ProfileSearchtopic, \
ProfileSkill, Skill
from app import db from app import db
@ -71,6 +72,30 @@ def update_skills(profile, skills_data):
not_(ProfileSkill.skill_id.in_(profile_skill_ids))).delete() not_(ProfileSkill.skill_id.in_(profile_skill_ids))).delete()
def update_searchtopics(profile, searchtopics_data):
profile_searchtopics_ids = []
for searchtopic_data in searchtopics_data:
skill_name = searchtopic_data["skill"]["name"]
skill = Skill.query.filter(Skill.name == skill_name).first()
if (skill is None):
skill = Skill(name=skill_name)
db.session.add(skill)
profile_searchtopic = ProfileSearchtopic.query.filter(ProfileSearchtopic.profile == profile,
ProfileSearchtopic.skill == skill).first()
if (profile_searchtopic is None):
profile_searchtopic = ProfileSearchtopic(profile=profile, skill=skill)
db.session.add(profile_searchtopic)
profile_searchtopics_ids.append(skill.id)
ProfileSearchtopic.query.filter(ProfileSearchtopic.profile == profile,
not_(ProfileSearchtopic.skill_id.in_(profile_searchtopics_ids))).delete()
def update_contacts(profile, contacts_data): def update_contacts(profile, contacts_data):
contact_ids_to_be_deleted = list(map(lambda c: c.id, profile.contacts)) contact_ids_to_be_deleted = list(map(lambda c: c.id, profile.contacts))
@ -110,12 +135,14 @@ def update_profile(user_id: int):
profile.pronouns = request.json.get("pronouns", "") profile.pronouns = request.json.get("pronouns", "")
profile.volunteerwork = request.json.get("volunteerwork", "") profile.volunteerwork = request.json.get("volunteerwork", "")
profile.availability = request.json.get("availability", "")
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_languages(profile, request.json.get("languages", {})) update_languages(profile, request.json.get("languages", {}))
db.session.commit() db.session.commit()

View File

@ -32,6 +32,7 @@ class Profile(db.Model):
pronouns = Column(String(25), default="") pronouns = Column(String(25), default="")
volunteerwork = Column(String(4000), default="") volunteerwork = Column(String(4000), default="")
freetext = Column(String(4000), default="") freetext = Column(String(4000), default="")
availability = Column(String(4000), default="")
visible = Column(Boolean, nullable=False, default=False) visible = Column(Boolean, nullable=False, default=False)
created = Column(DateTime, nullable=False, default=datetime.now) created = Column(DateTime, nullable=False, default=datetime.now)
updated = Column(DateTime, onupdate=datetime.now, nullable=False, default=datetime.now) updated = Column(DateTime, onupdate=datetime.now, nullable=False, default=datetime.now)
@ -40,6 +41,7 @@ class Profile(db.Model):
contacts = relationship("Contact") contacts = relationship("Contact")
address = relationship("Address", uselist=False, back_populates="profile") address = relationship("Address", uselist=False, back_populates="profile")
skills = relationship("ProfileSkill", back_populates="profile") skills = relationship("ProfileSkill", back_populates="profile")
searchtopics = relationship("ProfileSearchtopic", back_populates="profile")
languages = relationship("ProfileLanguage", back_populates="profile") languages = relationship("ProfileLanguage", back_populates="profile")
def to_dict(self): def to_dict(self):
@ -48,11 +50,13 @@ class Profile(db.Model):
"nickname": self.nickname, "nickname": self.nickname,
"pronouns": self.pronouns, "pronouns": self.pronouns,
"volunteerwork": self.volunteerwork, "volunteerwork": self.volunteerwork,
"availability": self.availability,
"freetext": self.freetext, "freetext": self.freetext,
"visible": self.visible, "visible": self.visible,
"address": self.address.to_dict(), "address": self.address.to_dict(),
"contacts": list(map(lambda contact: contact.to_dict(), self.contacts)), "contacts": list(map(lambda contact: contact.to_dict(), self.contacts)),
"skills": list(map(lambda skill: skill.to_dict(), self.skills)), "skills": list(map(lambda skill: skill.to_dict(), self.skills)),
"searchtopics": list(map(lambda searchtopic: searchtopic.to_dict(), self.searchtopics)),
"languages": list(map(lambda language: language.to_dict(), self.languages)) "languages": list(map(lambda language: language.to_dict(), self.languages))
} }
@ -135,6 +139,7 @@ class Skill(db.Model):
name = Column(String(25), unique=True, nullable=False) name = Column(String(25), unique=True, nullable=False)
profiles = relationship("ProfileSkill", back_populates="skill") profiles = relationship("ProfileSkill", back_populates="skill")
searchtopics = relationship("ProfileSearchtopic", back_populates="skill")
def to_dict(self): def to_dict(self):
return {"id": self.id, "name": self.name, "icon_url": "/skills/{}/icon".format(self.id)} return {"id": self.id, "name": self.name, "icon_url": "/skills/{}/icon".format(self.id)}
@ -154,6 +159,19 @@ class ProfileSkill(db.Model):
return {"profile_id": self.profile_id, "skill": self.skill.to_dict(), "level": self.level} return {"profile_id": self.profile_id, "skill": self.skill.to_dict(), "level": self.level}
class ProfileSearchtopic(db.Model):
__tablename__ = "profile_searchtopic"
profile_id = Column(Integer, ForeignKey("profile.id"), primary_key=True)
skill_id = Column(Integer, ForeignKey("skill.id"), primary_key=True)
profile = relationship("Profile", back_populates="searchtopics")
skill = relationship("Skill", back_populates="searchtopics")
def to_dict(self):
return {"profile_id": self.profile_id, "skill": self.skill.to_dict()}
class Language(db.Model): class Language(db.Model):
__tablename__ = "language" __tablename__ = "language"

View File

@ -7,6 +7,7 @@ from flask import g, make_response, request, send_file
from functools import wraps from functools import wraps
from ki.auth import auth from ki.auth import auth
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
@ -143,6 +144,12 @@ def get_contacttypes():
return handle_completion_request(ContactType, "contacttypes") return handle_completion_request(ContactType, "contacttypes")
@app.route("/users/profiles")
@token_auth
def find_profiles():
return find_profiles_handler()
@app.route("/skills") @app.route("/skills")
@token_auth @token_auth
def get_skills(): def get_skills():

View File

@ -0,0 +1,43 @@
# SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
import unittest
from ki.test.ApiTest import ApiTest
class TestSkillsEndpoint(ApiTest):
def test_skills_options(self):
response = self.client.options("/skills")
self.assertEqual(response.status_code, 200)
self.assertIn("Access-Control-Allow-Origin", response.headers)
self.assertEqual(response.headers["Access-Control-Allow-Origin"], "*")
def test_get_skills1(self):
token = self.login("peter", "geheim")["token"]
response = self.client.get("/skills?search=p", headers={"Authorization": "Bearer " + token})
self.assertEqual(response.status_code, 200)
self.assertEqual(
{
"skills": [{
"id": 1,
"name": "PHP",
"icon_url": "/skills/1/icon"
}, {
"id": 10,
"name": "PostgreSQL",
"icon_url": "/skills/10/icon"
}, {
"id": 3,
"name": "Python",
"icon_url": "/skills/3/icon"
}]
}, response.json)
self.assertIn("Access-Control-Allow-Origin", response.headers)
self.assertEqual(response.headers["Access-Control-Allow-Origin"], "*")
if __name__ == "main":
unittest.main()

View File

@ -33,6 +33,7 @@ class TestProfileEndpoint(ApiTest):
data = { data = {
"pronouns": "Monsieur", "pronouns": "Monsieur",
"volunteerwork": "ja", "volunteerwork": "ja",
"availability": "Nie",
"freetext": "Hallo", "freetext": "Hallo",
"visible": True, "visible": True,
"address": { "address": {
@ -70,6 +71,19 @@ class TestProfileEndpoint(ApiTest):
}, },
"level": 5 "level": 5
}], }],
"searchtopics": [{
"profile_id": 1,
"skill": {
"id": 3,
"name": "Python",
"icon_url": "/skills/3/icon"
}
}, {
"profile_id": 1,
"skill": {
"name": "Assembler"
}
}],
"languages": [{ "languages": [{
"id": 1, "id": 1,
"language": { "language": {
@ -96,6 +110,7 @@ class TestProfileEndpoint(ApiTest):
profile = user.profile profile = user.profile
self.assertEqual("Monsieur", profile.pronouns) self.assertEqual("Monsieur", profile.pronouns)
self.assertEqual("ja", profile.volunteerwork) self.assertEqual("ja", profile.volunteerwork)
self.assertEqual("Nie", profile.availability)
self.assertEqual("Hallo", profile.freetext) self.assertEqual("Hallo", profile.freetext)
self.assertTrue(profile.visible) self.assertTrue(profile.visible)
@ -132,6 +147,17 @@ class TestProfileEndpoint(ApiTest):
self.assertEqual(second_skill.skill.name, "Tschunkproduktion") self.assertEqual(second_skill.skill.name, "Tschunkproduktion")
self.assertEqual(second_skill.level, 5) self.assertEqual(second_skill.level, 5)
searchtopics = profile.searchtopics
self.assertEqual(len(searchtopics), 2)
first_searchtopic = searchtopics[0]
self.assertEqual(first_searchtopic.skill.id, 3)
self.assertEqual(first_searchtopic.skill.name, "Python")
second_searchtopic = searchtopics[1]
self.assertEqual(second_searchtopic.skill.id, 14)
self.assertEqual(second_searchtopic.skill.name, "Assembler")
languages = profile.languages languages = profile.languages
self.assertEqual(len(languages), 2) self.assertEqual(len(languages), 2)
@ -165,6 +191,7 @@ class TestProfileEndpoint(ApiTest):
"user_id": 1, "user_id": 1,
"nickname": "peternichtlustig", "nickname": "peternichtlustig",
"pronouns": "Herr Dr. Dr.", "pronouns": "Herr Dr. Dr.",
"availability": "Immer",
"freetext": "Ich mag Kaffee", "freetext": "Ich mag Kaffee",
"volunteerwork": "Gartenverein", "volunteerwork": "Gartenverein",
"visible": False, "visible": False,
@ -213,6 +240,21 @@ class TestProfileEndpoint(ApiTest):
}, },
"level": 3 "level": 3
}], }],
"searchtopics": [{
"profile_id": 1,
"skill": {
"id": 1,
"name": "PHP",
"icon_url": "/skills/1/icon"
}
}, {
"profile_id": 1,
"skill": {
"id": 3,
"name": "Python",
"icon_url": "/skills/3/icon"
}
}],
"languages": [{ "languages": [{
"profile_id": 1, "profile_id": 1,
"language": { "language": {

1
migrations/README Normal file
View File

@ -0,0 +1 @@
Generic single-database configuration.

View File

@ -1,7 +1,3 @@
; SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
;
; SPDX-License-Identifier: AGPL-3.0-or-later
# A generic, single database configuration. # A generic, single database configuration.
[alembic] [alembic]

View File

@ -1,7 +1,3 @@
# SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
from __future__ import with_statement from __future__ import with_statement
import logging import logging

View File

@ -1,12 +1,8 @@
# SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Initial migration. """Initial migration.
Revision ID: ebb2dd1fb371 Revision ID: 44b45a772abd
Revises: Revises:
Create Date: 2021-07-02 16:20:18.160228 Create Date: 2021-07-06 21:19:44.217722
""" """
from alembic import op from alembic import op
@ -14,7 +10,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = 'ebb2dd1fb371' revision = '44b45a772abd'
down_revision = None down_revision = None
branch_labels = None branch_labels = None
depends_on = None depends_on = None
@ -38,6 +34,7 @@ def upgrade():
sa.Column('pronouns', sa.String(length=25), nullable=True), sa.Column('pronouns', sa.String(length=25), nullable=True),
sa.Column('volunteerwork', sa.String(length=4000), nullable=True), sa.Column('volunteerwork', sa.String(length=4000), nullable=True),
sa.Column('freetext', sa.String(length=4000), nullable=True), sa.Column('freetext', sa.String(length=4000), nullable=True),
sa.Column('availability', sa.String(length=4000), nullable=True),
sa.Column('visible', sa.Boolean(), nullable=False), sa.Column('visible', sa.Boolean(), nullable=False),
sa.Column('created', sa.DateTime(), nullable=False), sa.Column('created', sa.DateTime(), nullable=False),
sa.Column('updated', sa.DateTime(), nullable=False), sa.Column('updated', sa.DateTime(), nullable=False),
@ -80,6 +77,13 @@ def upgrade():
sa.ForeignKeyConstraint(['profile_id'], ['profile.id'], ), sa.ForeignKeyConstraint(['profile_id'], ['profile.id'], ),
sa.PrimaryKeyConstraint('profile_id', 'language_id') sa.PrimaryKeyConstraint('profile_id', 'language_id')
) )
op.create_table('profile_searchtopic',
sa.Column('profile_id', sa.Integer(), nullable=False),
sa.Column('skill_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['profile_id'], ['profile.id'], ),
sa.ForeignKeyConstraint(['skill_id'], ['skill.id'], ),
sa.PrimaryKeyConstraint('profile_id', 'skill_id')
)
op.create_table('profile_skill', op.create_table('profile_skill',
sa.Column('profile_id', sa.Integer(), nullable=False), sa.Column('profile_id', sa.Integer(), nullable=False),
sa.Column('skill_id', sa.Integer(), nullable=False), sa.Column('skill_id', sa.Integer(), nullable=False),
@ -111,6 +115,7 @@ def downgrade():
op.drop_table('token') op.drop_table('token')
op.drop_table('user') op.drop_table('user')
op.drop_table('profile_skill') op.drop_table('profile_skill')
op.drop_table('profile_searchtopic')
op.drop_table('profile_language') op.drop_table('profile_language')
op.drop_table('contact') op.drop_table('contact')
op.drop_table('address') op.drop_table('address')