Skip to content

Commit

Permalink
Merge pull request #17 from albertsgarde/6-add-tests
Browse files Browse the repository at this point in the history
6 add tests
  • Loading branch information
albertsgarde authored Oct 5, 2024
2 parents 1ebec82 + de7613f commit 2973747
Show file tree
Hide file tree
Showing 28 changed files with 2,259 additions and 699 deletions.
26 changes: 18 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,24 @@ jobs:
- name: Validate YAML files
run: yamllint . --strict

- name: Ruff
uses: chartboost/ruff-action@v1
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
src: eadk_discord
version: 0.4.18

- name: Mypy
uses: jashparekh/mypy-action@v2
- name: Set up Python
uses: actions/setup-python@v5
with:
mypy_version: 1.11.2
requirement_files: requirements.txt requirements_dev.txt
python_version: '3.12'
python-version-file: pyproject.toml

- name: Install the project
run: uv sync --all-extras --dev

- name: Ruff
run: uv run ruff check

- name: Mypy
run: uv run mypy .

- name: Run pytest
run: uv run pytest --cov --cov-report=term-missing
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

db.json
db*.json

.vscode/
.ruff_cache/
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,4 @@ repos:
- types-python-dateutil~=2.9.0
- discord.py~=2.4.0
- pydantic~=2.9.1
- beartype~=0.19.0
11 changes: 11 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.PHONY: test launch t

test:
uv run pytest
t: test

cov:
uv run pytest --cov --cov-report=term-missing

launch:
. ./.env && uv run eadk_discord
3 changes: 2 additions & 1 deletion eadk_discord/__main__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# pragma: coverage exclude file
import logging
import os
from pathlib import Path

import discord
from discord.abc import Snowflake

from . import bot_setup
from eadk_discord import bot_setup

if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG)
Expand Down
212 changes: 212 additions & 0 deletions eadk_discord/bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
from zoneinfo import ZoneInfo

import discord
from beartype import beartype
from discord.app_commands import AppCommandError
from pydantic import BaseModel, Field

from eadk_discord import dates, fmt
from eadk_discord.database import Database
from eadk_discord.database.event import BookDesk, Event, MakeFlex, MakeOwned, UnbookDesk
from eadk_discord.database.event_errors import EventError

TIME_ZONE = ZoneInfo("Europe/Copenhagen")


class CommandInfo(BaseModel):
now: datetime = Field()
format_user: Callable[[int], str] = Field()
author_id: int = Field()

@beartype
@staticmethod
def from_interaction(interaction: discord.Interaction) -> "CommandInfo":
return CommandInfo(
now=datetime.now(TIME_ZONE),
format_user=lambda user: fmt.user(interaction, user),
author_id=interaction.user.id,
)


@dataclass
class Response:
message: str
ephemeral: bool
embed: discord.Embed | None

@beartype
def __init__(self, message: str = "", ephemeral: bool = False, embed: discord.Embed | None = None) -> None:
self.message = message
self.ephemeral = ephemeral
self.embed = embed

@beartype
async def send(self, interaction: discord.Interaction) -> None: # pragma: no cover
if self.embed is None:
await interaction.response.send_message(self.message, ephemeral=self.ephemeral)
else:
await interaction.response.send_message(self.message, ephemeral=self.ephemeral, embed=self.embed)


class EADKBot:
_database: Database

@beartype
def __init__(self, database: Database) -> None:
self._database = database

@property
def database(self) -> Database:
return self._database

@beartype
def info(self, info: CommandInfo, date_str: str | None) -> Response:
booking_date = dates.get_booking_date(date_str, info.now)
booking_day, _ = self._database.state.day(booking_date)

desk_numbers_str = "\n".join(str(i + 1) for i in range(len(booking_day.desks)))
desk_bookers_str = "\n".join(
info.format_user(desk.booker) if desk.booker else "**Free**" for desk in booking_day.desks
)
desk_owners_str = "\n".join(
info.format_user(desk.owner) if desk.owner else "**Flex**" for desk in booking_day.desks
)

return Response(
message="",
ephemeral=True,
embed=discord.Embed(title="Desk availability", description=f"{booking_date.strftime('%A %Y-%m-%d')}")
.add_field(name="Desk", value=desk_numbers_str, inline=True)
.add_field(name="Booked by", value=desk_bookers_str, inline=True)
.add_field(name="Owner", value=desk_owners_str, inline=True),
)

@beartype
def book(self, info: CommandInfo, date_str: str | None, user_id: int | None, desk_num: int | None) -> Response:
if user_id is None:
user_id = info.author_id

booking_date = dates.get_booking_date(date_str, info.now)
booking_day, _ = self._database.state.day(booking_date)
date_str = fmt.date(booking_date)

if booking_date < info.now.date():
return Response(
message=f"Date {date_str} not available for booking. Desks cannot be unbooked in the past.",
ephemeral=True,
)

if desk_num is not None:
desk_index = desk_num - 1
else:
desk_index_option = booking_day.get_available_desk()
if desk_index_option is not None:
desk_index = desk_index_option
desk_num = desk_index + 1
else:
return Response(message=f"No more desks are available for booking on {date_str}.", ephemeral=True)
self._database.handle_event(
Event(
author=info.author_id,
time=datetime.now(),
event=BookDesk(date=booking_date, desk_index=desk_index, user=user_id),
)
)
return Response(message=f"Desk {desk_num} has been booked for {info.format_user(user_id)} on {date_str}.")

@beartype
def unbook(self, info: CommandInfo, date_str: str | None, user_id: int | None, desk_num: int | None) -> Response:
booking_date = dates.get_booking_date(date_str, info.now)
booking_day, _ = self._database.state.day(booking_date)
date_str = fmt.date(booking_date)

if booking_date < info.now.date():
return Response(
message=f"Date {date_str} not available for booking. Desks cannot be unbooked in the past.",
ephemeral=True,
)

if desk_num is not None:
desk_index = desk_num - 1
if user_id is not None:
if user_id != booking_day.desk(desk_index).booker:
return Response(
message=f"Desk {desk_num} is not booked by {info.format_user(user_id)} on {date_str}.",
ephemeral=True,
)
else:
if user_id is None:
user_id = info.author_id
desk_indices = booking_day.booked_desks(user_id)
if desk_indices:
desk_index = desk_indices[0]
desk_num = desk_index + 1
else:
return Response(
message=f"{info.format_user(user_id)} already has no desks booked for {date_str}.", ephemeral=True
)

desk_booker = booking_day.desk(desk_index).booker
if desk_booker is not None:
self._database.handle_event(
Event(
author=info.author_id,
time=datetime.now(),
event=UnbookDesk(date=booking_date, desk_index=desk_index),
)
)
return Response(
message=f"Desk {desk_num} is no longer booked for {info.format_user(desk_booker)} on {date_str}."
)
else:
return Response(message=f"Desk {desk_num} is already free on {date_str}.", ephemeral=True)

@beartype
def makeowned(self, info: CommandInfo, start_date_str: str, user_id: int | None, desk_num: int) -> Response:
booking_date = dates.get_booking_date(start_date_str, info.now)
date_str = fmt.date(booking_date)

desk_index = desk_num - 1

if user_id is None:
user_id = info.author_id
self._database.handle_event(
Event(
author=info.author_id,
time=datetime.now(),
event=MakeOwned(start_date=booking_date, desk_index=desk_index, user=user_id),
)
)
return Response(message=f"Desk {desk_num} is now owned by {info.format_user(user_id)} from {date_str} onwards.")

@beartype
def makeflex(self, info: CommandInfo, start_date_str: str, desk_num: int) -> Response:
booking_date = dates.get_booking_date(start_date_str, info.now)
date_str = fmt.date(booking_date)

desk_index = desk_num - 1

self._database.handle_event(
Event(
author=info.author_id,
time=datetime.now(),
event=MakeFlex(start_date=booking_date, desk_index=desk_index),
)
)
return Response(message=f"Desk {desk_num} is now a flex desk from {date_str} onwards.")

@beartype
def handle_error(self, info: CommandInfo, error: AppCommandError) -> Response: # pragma: no cover
match error:
case discord.app_commands.errors.MissingAnyRole() | discord.app_commands.errors.MissingRole():
return Response(message="You do not have permission to run this command.", ephemeral=True)
case discord.app_commands.errors.CheckFailure():
return Response(message="This command can only be used in the office channel.", ephemeral=True)
case discord.app_commands.errors.CommandInvokeError() as error:
match error.__cause__:
case EventError() as event_error:
return Response(message=event_error.message(info.format_user), ephemeral=True)
raise error
Loading

0 comments on commit 2973747

Please sign in to comment.