Python backend for a Slack's kudos plugin.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: added plaza models

+238 -42
+1
kefi/config/__init__.py
··· 13 13 SLACK_TEAM_ID: str 14 14 RECHARGE_KEFIS_AMOUNT: int = 550 15 15 REDIS_HOST: str = "redis" 16 + PLAZA_PRICE: int = 10 16 17 17 18 class Config: 18 19 env_file = ".env"
+1
kefi/migrations/env.py
··· 18 18 from kefi.config import settings 19 19 from kefi.models.core.models import Transaction, User 20 20 from kefi.models.kudos.models import Action 21 + from kefi.models.plazas.models import Attendance, Plaza 21 22 22 23 config.set_main_option("sqlalchemy.url", settings.db_url()) 23 24 target_metadata = SQLModel.metadata
+32
kefi/migrations/versions/b7773310aaa7_updated_transaction.py
··· 1 + """updated transaction 2 + 3 + Revision ID: b7773310aaa7 4 + Revises: f8d0e71fac20 5 + Create Date: 2022-11-07 17:38:07.069101 6 + 7 + """ 8 + import sqlalchemy as sa 9 + import sqlmodel 10 + from alembic import op 11 + 12 + # revision identifiers, used by Alembic. 13 + revision = "b7773310aaa7" 14 + down_revision = "f8d0e71fac20" 15 + branch_labels = None 16 + depends_on = None 17 + 18 + 19 + def upgrade(): 20 + # ### commands auto generated by Alembic - please adjust! ### 21 + op.add_column( 22 + "transaction", sa.Column("attendance_id", sa.Integer(), nullable=True) 23 + ) 24 + op.create_foreign_key(None, "transaction", "attendance", ["attendance_id"], ["id"]) 25 + # ### end Alembic commands ### 26 + 27 + 28 + def downgrade(): 29 + # ### commands auto generated by Alembic - please adjust! ### 30 + op.drop_constraint(None, "transaction", type_="foreignkey") 31 + op.drop_column("transaction", "attendance_id") 32 + # ### end Alembic commands ###
+51
kefi/migrations/versions/f8d0e71fac20_added_plaza_models.py
··· 1 + """added plaza models 2 + 3 + Revision ID: f8d0e71fac20 4 + Revises: d3a04e3f4a1f 5 + Create Date: 2022-11-07 17:22:29.725246 6 + 7 + """ 8 + import sqlalchemy as sa 9 + import sqlmodel 10 + from alembic import op 11 + 12 + # revision identifiers, used by Alembic. 13 + revision = "f8d0e71fac20" 14 + down_revision = "d3a04e3f4a1f" 15 + branch_labels = None 16 + depends_on = None 17 + 18 + 19 + def upgrade(): 20 + # ### commands auto generated by Alembic - please adjust! ### 21 + op.create_table( 22 + "plaza", 23 + sa.Column("id", sa.Integer(), nullable=False), 24 + sa.Column("date", sa.DateTime(), nullable=False), 25 + sa.PrimaryKeyConstraint("id"), 26 + ) 27 + op.create_index(op.f("ix_plaza_date"), "plaza", ["date"], unique=True) 28 + op.create_table( 29 + "attendance", 30 + sa.Column("id", sa.Integer(), nullable=False), 31 + sa.Column("plaza_id", sa.Integer(), nullable=False), 32 + sa.Column("user_id", sa.Integer(), nullable=False), 33 + sa.ForeignKeyConstraint( 34 + ["plaza_id"], 35 + ["plaza.id"], 36 + ), 37 + sa.ForeignKeyConstraint( 38 + ["user_id"], 39 + ["user.id"], 40 + ), 41 + sa.PrimaryKeyConstraint("id"), 42 + ) 43 + # ### end Alembic commands ### 44 + 45 + 46 + def downgrade(): 47 + # ### commands auto generated by Alembic - please adjust! ### 48 + op.drop_table("attendance") 49 + op.drop_index(op.f("ix_plaza_date"), table_name="plaza") 50 + op.drop_table("plaza") 51 + # ### end Alembic commands ###
+6
kefi/models/core/models.py
··· 5 5 6 6 if TYPE_CHECKING: 7 7 from kefi.models.kudos.models import Action 8 + from kefi.models.plazas.models import Attendance, Plaza 8 9 9 10 10 11 class User(SQLModel, table=True): ··· 28 29 sa_relationship_kwargs=dict(foreign_keys="[Transaction.receiver_id]"), 29 30 ) 30 31 is_admin: bool = False 32 + attendances: list["Attendance"] = Relationship(back_populates="user") 31 33 32 34 def get_short_name(self): 33 35 """Gets the short name of the user.""" ··· 51 53 # Action, the reference of the action used to send the transaction 52 54 action_id: int | None = Field(default=None, foreign_key="action.id") 53 55 action: Optional["Action"] = Relationship(back_populates="transactions") 56 + 57 + # Attendance, the reference of the attendance used to send the transaction 58 + attendance_id: int | None = Field(default=None, foreign_key="attendance.id") 59 + attendance: Optional["Attendance"] = Relationship(back_populates="transactions") 54 60 55 61 # Sender user, if not defined, is a system transaction 56 62 sender_id: int | None = Field(default=None, foreign_key="user.id")
kefi/models/kefinect/__init__.py kefi/models/plazas/__init__.py
-8
kefi/models/kefinect/helpers.py
··· 1 - # import datetime 2 - 3 - # from sqlmodel import Session, func, or_, select 4 - 5 - 6 - # def get_or_create_current_meeting_session(session: Session): 7 - # now = datetime.datetime.now() 8 - # query = select(MeetingSession).filter(MeetingSession.date == command.user_id)
-33
kefi/models/kefinect/models.py
··· 1 - import datetime 2 - from typing import TYPE_CHECKING, Optional 3 - 4 - from sqlalchemy import Column, String 5 - from sqlmodel import Field, Relationship, SQLModel 6 - 7 - if TYPE_CHECKING: 8 - from kefi.models.core.models import User 9 - from kefi.models.kudos.models import Action 10 - 11 - 12 - class MeetingSession(SQLModel, table=True): 13 - """A meeting session that represents a moment scheduled to create a group call with 14 - the different groups of attendees.""" 15 - 16 - date = datetime.datetime # When the meetings are going to be created 17 - 18 - 19 - class Attendance(SQLModel, table=True): 20 - """An attendance is a user who has spent kefis to be part of a meetings session.""" 21 - 22 - # Meeting session that the attendance makes reference 23 - meeting_session_id: int = Field(default=None, foreign_key="meeting_session.id") 24 - meeting_session: MeetingSession = Relationship( 25 - back_populates="attendances", 26 - sa_relationship_kwargs=dict(foreign_keys="[Attendance.meeting_session_id]"), 27 - ) 28 - # User who is going to attend to the meeting session 29 - user_id: int = Field(foreign_key="user.id") 30 - user: User = Relationship( 31 - back_populates="attendances", 32 - sa_relationship_kwargs=dict(foreign_keys="[Attendance.user_id]"), 33 - )
+48
kefi/models/plazas/helpers.py
··· 1 + import datetime 2 + 3 + import pytz 4 + from sqlmodel import Session, select 5 + 6 + from kefi.config import settings 7 + from kefi.models.core.helpers import available_balance 8 + from kefi.models.core.models import Transaction, User 9 + from kefi.models.plazas.models import Attendance, Plaza 10 + 11 + 12 + def next_plaza_appointment() -> datetime.datetime: 13 + """Gets the next plaza date and time appointment.""" 14 + today = datetime.datetime.today() 15 + next_date = (today + datetime.timedelta((4 - today.weekday()) % 7)).date() 16 + default_time = datetime.time(hour=10, minute=0) 17 + return datetime.datetime.combine(date=next_date, time=default_time).astimezone( 18 + pytz.timezone("Europe/Madrid") 19 + ) 20 + 21 + 22 + def get_or_create_current_plaza(session: Session) -> Plaza: 23 + """Gets the current available plaza.""" 24 + now = datetime.datetime.now() 25 + query = select(Plaza).filter(Plaza.date >= now).order_by("date") 26 + results = session.exec(query) 27 + first = results.first() 28 + if first: 29 + return first 30 + plaza = Plaza(date=next_plaza_appointment()) 31 + session.add(plaza) 32 + return plaza 33 + 34 + 35 + def create_attendance(user: User, session: Session) -> Attendance: 36 + """Creates the transaction that spends the kefis and also creates the attendance 37 + for the next plaza. 38 + """ 39 + plaza = get_or_create_current_plaza(session=session) 40 + balance = available_balance(user=user, session=session) 41 + price = settings.PLAZA_PRICE 42 + if balance < price: 43 + raise ValueError("The user doesn't have enough balance") 44 + attendance = Attendance(plaza=plaza, user=user) 45 + session.add(attendance) 46 + transaction = Transaction(amount=-price, user=user, attendance=attendance) 47 + session.add(transaction) 48 + return attendance
+43
kefi/models/plazas/models.py
··· 1 + import datetime 2 + from typing import TYPE_CHECKING 3 + 4 + from sqlmodel import Field, Relationship, SQLModel 5 + 6 + from kefi.models.core.models import User 7 + 8 + if TYPE_CHECKING: 9 + 10 + from kefi.models.core.models import Transaction 11 + 12 + 13 + class Plaza(SQLModel, table=True): 14 + """A plaza is a meeting session that represents a moment scheduled to create a 15 + group call with the different groups of attendees. 16 + """ 17 + 18 + id: int | None = Field(default=None, primary_key=True) 19 + date: datetime.datetime = Field( 20 + unique=True, index=True 21 + ) # When the meetings are going to be created 22 + attendances: list["Attendance"] = Relationship(back_populates="plaza") 23 + 24 + 25 + class Attendance(SQLModel, table=True): 26 + """An attendance is a user who has spent kefis to be part of a meetings session in 27 + the plaza. 28 + """ 29 + 30 + id: int | None = Field(default=None, primary_key=True) 31 + # Meeting session that the attendance makes reference 32 + plaza_id: int = Field(foreign_key="plaza.id") 33 + plaza: Plaza = Relationship( 34 + back_populates="attendances", 35 + sa_relationship_kwargs=dict(foreign_keys="[Attendance.plaza_id]"), 36 + ) 37 + # User who is going to attend to the meeting session 38 + user_id: int = Field(foreign_key="user.id") 39 + user: User = Relationship( 40 + back_populates="attendances", 41 + sa_relationship_kwargs=dict(foreign_keys="[Attendance.user_id]"), 42 + ) 43 + transactions: list["Transaction"] = Relationship(back_populates="attendance")
+45
kefi/tests/test_models.py
··· 1 + import datetime 2 + 1 3 from sqlmodel import Session 2 4 3 5 from kefi.config import settings ··· 10 12 from kefi.models.core.models import Transaction, User 11 13 from kefi.models.kudos.helpers import send_action 12 14 from kefi.models.kudos.models import Action 15 + from kefi.models.plazas.helpers import create_attendance, get_or_create_current_plaza 16 + from kefi.models.plazas.models import Attendance, Plaza 13 17 14 18 15 19 def test_available_balance(session: Session): ··· 87 91 available_balance(user=user, session=session) 88 92 == settings.RECHARGE_KEFIS_AMOUNT 89 93 ) 94 + 95 + 96 + def test_get_or_create_current_plaza(session: Session): 97 + first_plaza = get_or_create_current_plaza(session=session) 98 + assert isinstance(first_plaza, Plaza) 99 + second_plaza = get_or_create_current_plaza(session=session) 100 + assert isinstance(second_plaza, Plaza) 101 + assert first_plaza.id == second_plaza.id 102 + 103 + 104 + def test_get_or_create_current_plaza_with_future_and_past(session: Session): 105 + first_plaza = get_or_create_current_plaza(session=session) 106 + session.add(Plaza(date=first_plaza.date + datetime.timedelta(days=3))) 107 + session.add(Plaza(date=datetime.datetime.now() - datetime.timedelta(days=3))) 108 + assert isinstance(first_plaza, Plaza) 109 + second_plaza = get_or_create_current_plaza(session=session) 110 + assert isinstance(second_plaza, Plaza) 111 + assert first_plaza.id == second_plaza.id 112 + 113 + 114 + def test_create_attendance_no_balance(session: Session): 115 + user = User(slack_user_id="user_1") 116 + session.add(user) 117 + try: 118 + create_attendance(user=user, session=session) 119 + exception = False 120 + except ValueError: 121 + exception = True 122 + assert exception 123 + 124 + 125 + def test_create_attendance(session: Session): 126 + user = User(slack_user_id="user_1") 127 + session.add(user) 128 + recharge_wallets(settings.RECHARGE_KEFIS_AMOUNT, session) 129 + initial_balance = available_balance(user=user, session=session) 130 + attendance = create_attendance(user=user, session=session) 131 + assert isinstance(attendance, Attendance) 132 + assert initial_balance - settings.PLAZA_PRICE == available_balance( 133 + user=user, session=session 134 + )
+10 -1
poetry.lock
··· 950 950 six = ">=1.4.0" 951 951 952 952 [[package]] 953 + name = "pytz" 954 + version = "2022.6" 955 + description = "World timezone definitions, modern and historical" 956 + category = "main" 957 + optional = false 958 + python-versions = "*" 959 + 960 + [[package]] 953 961 name = "pyyaml" 954 962 version = "6.0" 955 963 description = "YAML parser and emitter for Python" ··· 1280 1288 [metadata] 1281 1289 lock-version = "1.1" 1282 1290 python-versions = "^3.10" 1283 - content-hash = "03d1f89f3e03ac02036ab4c15f42186818ac2b356a2908e00e503b56717910cd" 1291 + content-hash = "8e47557b60b0276276fafe2e8917adc19d5c353755f7bc00d80424a0fa618ed0" 1284 1292 1285 1293 [metadata.files] 1286 1294 aiodns = [ ··· 1719 1727 python-multipart = [ 1720 1728 {file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"}, 1721 1729 ] 1730 + pytz = [] 1722 1731 pyyaml = [ 1723 1732 {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, 1724 1733 {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
+1
pyproject.toml
··· 21 21 psycopg2-binary = "^2.9.1" 22 22 aiohttp = {version = "^3.8.0", extras = ["speedups"]} 23 23 single-source = "^0.3.0" 24 + pytz = "^2022.6" 24 25 25 26 [tool.poetry.dev-dependencies] 26 27 black = "^22.10.0"