Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

release 1.0.1 #35

Merged
merged 36 commits into from
Oct 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
43ab824
feat: change axios requests
Oct 5, 2024
c7d3a30
fix: change page login
Oct 8, 2024
d7de656
feat: add context, getNetologyCourse, bulkEvents for calendar
Oct 8, 2024
2cbb57e
fix: fetchCourseAndEvents email, password
Oct 8, 2024
91ae9d4
styled: add calendar component
Oct 8, 2024
2a1f6bf
Merge remote-tracking branch 'origin/frontend' into frontend
Oct 8, 2024
e70f064
Merge branch 'frontend' of github.com:depocoder/YetAnotherCalendar in…
Oct 8, 2024
207b8c5
chore: del folders, files,
Oct 8, 2024
5f35176
Merge branch 'dev' of github.com:depocoder/YetAnotherCalendar into fr…
Oct 8, 2024
1ea789c
styled: add calendar component
Oct 10, 2024
fb8793c
Merge remote-tracking branch 'origin/frontend' into frontend
Oct 10, 2024
0d2d0af
fix change route calendar
Oct 10, 2024
d315737
fix ci
depocoder Oct 11, 2024
af5c122
fix ci
depocoder Oct 11, 2024
1d68c9f
Merge pull request #34 from depocoder/dev
depocoder Oct 11, 2024
96c1cc9
fix ci
depocoder Oct 11, 2024
dc94469
Merge remote-tracking branch 'origin/main'
depocoder Oct 11, 2024
929b22d
fix api/docs
depocoder Oct 11, 2024
6f5cefe
Merge branch 'dev'
depocoder Oct 11, 2024
f4df161
fix: add varable .env
AlexandrKarpovich Oct 11, 2024
67846b1
Merge remote-tracking branch 'origin/frontend' into frontend
AlexandrKarpovich Oct 11, 2024
252ba6f
fix api/docs
depocoder Oct 11, 2024
bd0d4e5
fix api/docs
depocoder Oct 11, 2024
c99493b
Merge branch 'frontend'
depocoder Oct 11, 2024
1da5954
fix frontend issues
depocoder Oct 11, 2024
a50326d
fix: rename varable .env
AlexandrKarpovich Oct 11, 2024
474a49d
Merge remote-tracking branch 'origin/frontend' into frontend
AlexandrKarpovich Oct 11, 2024
2ee0dcc
Timezone big update
depocoder Oct 11, 2024
6a0cbce
Merge branch 'dev'
depocoder Oct 11, 2024
d1fc6bc
linter fixes
depocoder Oct 11, 2024
f5ca2ca
fix workers count
depocoder Oct 11, 2024
37abae0
refactor env vars, add docker-compose.prod.yaml
depocoder Oct 12, 2024
73b6376
refactor docker compose
depocoder Oct 12, 2024
28f9d32
refactor docker compose
depocoder Oct 12, 2024
f5d25f5
refactor docker compose
depocoder Oct 12, 2024
5ff45c4
Delete backend/requirements.txt
depocoder Oct 12, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ jobs:
password: ${{ env.REGISTRY_PASSWORD }}
- name: Build & Publish backend to Github Container registry
run: |
docker build ./backend --tag $REGISTRY_URL/$REGISTRY_USERNAME/calendarit-backend:latest \
--tag $REGISTRY_URL/$REGISTRY_USERNAME/calendarit-backend:$IMAGE_TAG
docker push $REGISTRY_URL/$REGISTRY_USERNAME/calendarit-backend:latest
docker push $REGISTRY_URL/$REGISTRY_USERNAME/calendarit-backend:$IMAGE_TAG
docker build ./backend --tag $REGISTRY_URL/$REGISTRY_USERNAME/yet_another_calendar_backend:latest \
--tag $REGISTRY_URL/$REGISTRY_USERNAME/yet_another_calendar_backend:$IMAGE_TAG
docker push $REGISTRY_URL/$REGISTRY_USERNAME/yet_another_calendar_backend:latest
docker push $REGISTRY_URL/$REGISTRY_USERNAME/yet_another_calendar_backend:$IMAGE_TAG

- name: Build & Publish frontend to Github Container registry
run: |
docker build ./frontend --tag $REGISTRY_URL/$REGISTRY_USERNAME/calendarit-frontend:latest \
--tag $REGISTRY_URL/$REGISTRY_USERNAME/calendarit-frontend:$IMAGE_TAG
docker push $REGISTRY_URL/$REGISTRY_USERNAME/calendarit-frontend:latest
docker push $REGISTRY_URL/$REGISTRY_USERNAME/calendarit-frontend:$IMAGE_TAG
docker build ./frontend --tag $REGISTRY_URL/$REGISTRY_USERNAME/yet_another_calendar_frontend:latest \
--tag $REGISTRY_URL/$REGISTRY_USERNAME/yet_another_calendar_frontend:$IMAGE_TAG
docker push $REGISTRY_URL/$REGISTRY_USERNAME/yet_another_calendar_frontend:latest
docker push $REGISTRY_URL/$REGISTRY_USERNAME/yet_another_calendar_frontend:$IMAGE_TAG
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ celerybeat.pid
*.sage.py

# Environments
.env
frontend/.env
.venv
env/
venv/
Expand Down
4 changes: 3 additions & 1 deletion backend/.env.dist
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
MODEUS_USERNAME=test_username
MODEUS_PASSWORD=test_password
MODEUS_PASSWORD=test_password
YET_ANOTHER_CALENDAR_WORKERS_COUNT=10
YET_ANOTHER_CALENDAR_DEBUG=False
5 changes: 0 additions & 5 deletions backend/requirements.txt

This file was deleted.

4 changes: 1 addition & 3 deletions backend/yet_another_calendar/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@ class Settings(BaseSettings):
port: int = 8000
# quantity of workers for uvicorn
workers_count: int = 1
# Enable uvicorn reloading
reload: bool = env.bool("YET_ANOTHER_CALENDAR_RELOAD", False)

# Enable uvicorn reloading, debug and docs
debug: bool = env.bool("YET_ANOTHER_CALENDAR_DEBUG", False)

log_level: LogLevel = LogLevel.INFO
Expand Down
34 changes: 19 additions & 15 deletions backend/yet_another_calendar/web/api/bulk/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,22 @@


def create_ics_event(title: str, starts_at: datetime.datetime, ends_at: datetime.datetime,
lesson_id: Any, timezone: datetime.tzinfo, description: Optional[str] = None,
lesson_id: Any, description: Optional[str] = None,
webinar_url: Optional[str] = None) -> icalendar.Event:
event = icalendar.Event()
dt_now = datetime.datetime.now(tz=timezone)
dt_now = datetime.datetime.now()
event.add('summary', title)
event.add('location', webinar_url)
event.add('dtstart', starts_at.astimezone(timezone))
event.add('dtend', ends_at.astimezone(timezone))
event.add('dtstart', starts_at)
event.add('dtend', ends_at)
event.add('dtstamp', dt_now)
event.add('uid', lesson_id)
if description:
event.add('description', description)
return event


def export_to_ics(calendar: schema.CalendarResponse, timezone: str) -> Iterable[bytes]:
try:
tz = pytz.timezone(timezone)
except pytz.exceptions.UnknownTimeZoneError:
raise HTTPException(detail="Wrong timezone", status_code=status.HTTP_400_BAD_REQUEST) from None
def export_to_ics(calendar: schema.CalendarResponse) -> Iterable[bytes]:
ics_calendar = icalendar.Calendar()
ics_calendar.add('version', '2.0')
ics_calendar.add('prodid', 'yet_another_calendar')
Expand All @@ -50,12 +46,12 @@ def export_to_ics(calendar: schema.CalendarResponse, timezone: str) -> Iterable[
continue
event = create_ics_event(title=f"Netology: {netology_lesson.title}", starts_at=netology_lesson.starts_at,
ends_at=netology_lesson.ends_at, lesson_id=netology_lesson.id,
timezone=tz, webinar_url=netology_lesson.webinar_url)
webinar_url=netology_lesson.webinar_url)
ics_calendar.add_component(event)
for modeus_lesson in calendar.modeus:
event = create_ics_event(title=f"Modeus: {modeus_lesson.name}",
starts_at=modeus_lesson.start_time, ends_at=modeus_lesson.end_time,
lesson_id=modeus_lesson.id, timezone=tz,
lesson_id=modeus_lesson.id,
description=modeus_lesson.description)
ics_calendar.add_component(event)
yield ics_calendar.to_ical()
Expand All @@ -66,11 +62,12 @@ async def refresh_events(
jwt_token: str,
calendar_id: int,
cookies: netology_schema.NetologyCookies,
timezone: str,
) -> schema.RefreshedCalendarResponse:
"""Clear events cache."""
cached_json = await get_cached_calendar(body, jwt_token, calendar_id, cookies)
cached_json = await get_cached_calendar(body, jwt_token, calendar_id, cookies, timezone)
cached_calendar = schema.CalendarResponse.model_validate(cached_json)
calendar = await get_calendar(body, jwt_token, calendar_id, cookies)
calendar = await get_calendar(body, jwt_token, calendar_id, cookies, timezone)
changed = cached_calendar.get_hash() != calendar.get_hash()
try:
cache_key = default_key_builder(get_cached_calendar, args=(body, jwt_token, calendar_id, cookies), kwargs={})
Expand All @@ -93,13 +90,19 @@ async def get_calendar(
jwt_token: str,
calendar_id: int,
cookies: netology_schema.NetologyCookies,
timezone: str,
) -> schema.CalendarResponse:
try:
tz = pytz.timezone(timezone)
except pytz.exceptions.UnknownTimeZoneError:
raise HTTPException(detail="Wrong timezone", status_code=status.HTTP_400_BAD_REQUEST) from None

async with asyncio.TaskGroup() as tg:
netology_response = tg.create_task(netology_views.get_calendar(body, calendar_id, cookies))
modeus_response = tg.create_task(modeus_views.get_calendar(body, jwt_token))
return schema.CalendarResponse.model_validate(
{"netology": netology_response.result(), "modeus": modeus_response.result()},
)
).change_timezone(tz)


@cache(expire=settings.redis_events_time_live)
Expand All @@ -108,5 +111,6 @@ async def get_cached_calendar(
jwt_token: str,
calendar_id: int,
cookies: netology_schema.NetologyCookies,
timezone: str,
) -> schema.CalendarResponse:
return await get_calendar(body, jwt_token, calendar_id, cookies)
return await get_calendar(body, jwt_token, calendar_id, cookies, timezone)
13 changes: 13 additions & 0 deletions backend/yet_another_calendar/web/api/bulk/schema.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import datetime
import hashlib
from typing import Self

from pydantic import BaseModel, Field

Expand All @@ -11,6 +12,18 @@ class BulkResponse(BaseModel):
netology: netology_schema.SerializedEvents
modeus: list[modeus_schema.FullEvent]

def change_timezone(self, timezone: datetime.tzinfo) -> Self:
for homework in self.netology.homework:
if homework.deadline:
homework.deadline = homework.deadline.astimezone(timezone)
for webinar in self.netology.webinars:
webinar.starts_at = webinar.validate_starts_at(webinar.starts_at, timezone)
webinar.ends_at = webinar.validate_ends_at(webinar.ends_at, timezone)

for event in self.modeus:
event.start_time = event.validate_starts_at(event.start_time, timezone)
event.end_time = event.validate_end_time(event.end_time, timezone)
return self

class CalendarResponse(BulkResponse):
cached_at: datetime.datetime = Field(default_factory=datetime.datetime.now, alias="cached_at")
Expand Down
10 changes: 6 additions & 4 deletions backend/yet_another_calendar/web/api/bulk/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ async def get_calendar(
cookies: Annotated[netology_schema.NetologyCookies, Depends(netology_schema.get_cookies_from_headers)],
jwt_token: Annotated[str, Depends(modeus_schema.get_cookies_from_headers)],
calendar_id: int = settings.netology_default_course_id,
time_zone: str = "Europe/Moscow",
) -> schema.CalendarResponse:
"""
Get events from Netology and Modeus, cached.
"""

cached_calendar = await integration.get_cached_calendar(body, jwt_token, calendar_id, cookies)
cached_calendar = await integration.get_cached_calendar(body, jwt_token, calendar_id, cookies, time_zone)
return schema.CalendarResponse.model_validate(cached_calendar)


Expand All @@ -36,12 +37,13 @@ async def refresh_calendar(
cookies: Annotated[netology_schema.NetologyCookies, Depends(netology_schema.get_cookies_from_headers)],
jwt_token: Annotated[str, Depends(modeus_schema.get_cookies_from_headers)],
calendar_id: int = settings.netology_default_course_id,
time_zone: str = "Europe/Moscow",
) -> schema.RefreshedCalendarResponse:
"""
Refresh events in redis.
"""

return await integration.refresh_events(body, jwt_token, calendar_id, cookies)
return await integration.refresh_events(body, jwt_token, calendar_id, cookies, time_zone)


@router.post("/export_ics/")
Expand All @@ -55,5 +57,5 @@ async def export_ics(
"""
Export into .ics format
"""
calendar = await integration.get_calendar(body, jwt_token, calendar_id, cookies)
return StreamingResponse(integration.export_to_ics(calendar, time_zone))
calendar = await integration.get_calendar(body, jwt_token, calendar_id, cookies, time_zone)
return StreamingResponse(integration.export_to_ics(calendar))
6 changes: 3 additions & 3 deletions backend/yet_another_calendar/web/api/docs/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ async def swagger_ui_html(request: Request) -> HTMLResponse:
openapi_url=request.app.openapi_url,
title=f"{title} - Swagger UI",
oauth2_redirect_url=str(request.url_for("swagger_ui_redirect")),
swagger_js_url="/static/docs/swagger-ui-bundle.js",
swagger_css_url="/static/docs/swagger-ui.css",
swagger_js_url="/static_backend/docs/swagger-ui-bundle.js",
swagger_css_url="/static_backend/docs/swagger-ui.css",
)


Expand All @@ -49,5 +49,5 @@ async def redoc_html(request: Request) -> HTMLResponse:
return get_redoc_html(
openapi_url=request.app.openapi_url,
title=f"{title} - ReDoc",
redoc_js_url="/static/docs/redoc.standalone.js",
redoc_js_url="/static_backend/docs/redoc.standalone.js",
)
28 changes: 28 additions & 0 deletions backend/yet_another_calendar/web/api/modeus/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ class ModeusCreds(BaseModel):
username: str
password: str = Field(repr=False)


class ModeusTimeBody(BaseModel):
time_min: datetime.datetime = Field(alias="timeMin", examples=["2024-09-23T00:00:00+03:00"])
time_max: datetime.datetime = Field(alias="timeMax", examples=["2024-09-29T23:59:59+03:00"])


# noinspection PyNestedDecorators
class ModeusEventsBody(ModeusTimeBody):
"""Modeus search events body."""
Expand Down Expand Up @@ -84,6 +86,18 @@ class Event(BaseModel):
end_time: datetime.datetime = Field(alias="end")
id: uuid.UUID

@field_validator("start_time")
@classmethod
def validate_starts_at(cls, start_time: datetime.datetime,
timezone: datetime.tzinfo = datetime.timezone.utc) -> datetime.datetime:
return start_time.astimezone(timezone)

@field_validator("end_time")
@classmethod
def validate_end_time(cls, end_time: datetime.datetime,
timezone: datetime.tzinfo = datetime.timezone.utc) -> datetime.datetime:
return end_time.astimezone(timezone)


class Href(BaseModel):
href: str
Expand Down Expand Up @@ -157,6 +171,20 @@ class StudentsSpeciality(BaseModel):
specialty_name: Optional[str] = Field(alias="specialtyName")
specialty_profile: Optional[str] = Field(alias="specialtyProfile")

@field_validator("learning_start_date")
@classmethod
def validate_starts_at(cls, learning_start_date: Optional[datetime.datetime]) -> Optional[datetime.datetime]:
if not learning_start_date:
return learning_start_date
return learning_start_date.astimezone(datetime.timezone.utc)

@field_validator("learning_end_date")
@classmethod
def validate_learning_end_date(cls, learning_end_date: Optional[datetime.datetime]) -> Optional[datetime.datetime]:
if not learning_end_date:
return learning_end_date
return learning_end_date.astimezone(datetime.timezone.utc)


class ExtendedPerson(StudentsSpeciality, ShortPerson):
pass
Expand Down
47 changes: 40 additions & 7 deletions backend/yet_another_calendar/web/api/netology/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from urllib.parse import urljoin

from fastapi import Header
from pydantic import BaseModel, Field, computed_field
from pydantic import BaseModel, Field, computed_field, field_validator, model_validator, ConfigDict

from yet_another_calendar.settings import settings
from yet_another_calendar.web.api.modeus.schema import ModeusTimeBody
Expand Down Expand Up @@ -64,6 +64,22 @@ class LessonWebinar(BaseLesson):
video_url: Optional[str] = None
webinar_url: Optional[str] = None

@field_validator("starts_at")
@classmethod
def validate_starts_at(cls, starts_at: Optional[datetime.datetime],
timezone: datetime.tzinfo = datetime.timezone.utc) -> Optional[datetime.datetime]:
if not starts_at:
return starts_at
return starts_at.astimezone(timezone)

@field_validator("ends_at")
@classmethod
def validate_ends_at(cls, ends_at: Optional[datetime.datetime],
timezone: datetime.tzinfo = datetime.timezone.utc) -> Optional[datetime.datetime]:
if not ends_at:
return ends_at
return ends_at.astimezone(timezone)

def is_suitable_time(self, time_min: datetime.datetime, time_max: datetime.datetime) -> bool:
"""Check if lesson have suitable time"""
if not self.starts_at or time_min > self.starts_at:
Expand All @@ -75,21 +91,27 @@ def is_suitable_time(self, time_min: datetime.datetime, time_max: datetime.datet

# noinspection PyNestedDecorators
class LessonTask(BaseLesson):
model_config = ConfigDict(arbitrary_types_allowed=True)

path: str
deadline: Optional[datetime.datetime] = Field(default=None)

@computed_field # type: ignore
@property
def url(self) -> str:
return urljoin(settings.netology_url, self.path)

@computed_field # type: ignore
@property
def deadline(self) -> Optional[datetime.datetime]:
match = re.search(_DATE_PATTERN, self.title)
@model_validator(mode='before')
@classmethod
def deadline_validation(cls, data: Any) -> Any:
if not isinstance(data, dict):
return data
match = re.search(_DATE_PATTERN, data.get('title', ''))
if not match:
return None
return data
date = match.group(0)
return datetime.datetime.strptime(date, "%d.%m.%y").replace(tzinfo=datetime.timezone.utc)
data['deadline'] = datetime.datetime.strptime(date, "%d.%m.%y").astimezone(datetime.timezone.utc)
return data

def is_suitable_time(self, time_min: datetime.datetime, time_max: datetime.datetime) -> bool:
"""Check if lesson have suitable time"""
Expand Down Expand Up @@ -158,6 +180,16 @@ class DetailedProgram(BaseModel):
start_date: datetime.datetime
finish_date: datetime.datetime

@field_validator("start_date")
@classmethod
def validate_start_date(cls, start_date: datetime.datetime) -> datetime.datetime:
return start_date.astimezone(datetime.timezone.utc)

@field_validator("finish_date")
@classmethod
def validate_finish_date(cls, finish_date: datetime.datetime) -> datetime.datetime:
return finish_date.astimezone(datetime.timezone.utc)


class Program(BaseModel):
detailed_program: DetailedProgram = Field(alias='program')
Expand All @@ -173,6 +205,7 @@ def get_lesson_ids(self) -> set[int]:
program_ids.add(program.detailed_program.id)
return program_ids


class SerializedEvents(BaseModel):
"""Structure for displaying frontend."""
homework: list[LessonTask]
Expand Down
4 changes: 3 additions & 1 deletion backend/yet_another_calendar/web/api/router.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from fastapi.routing import APIRouter

from yet_another_calendar.settings import settings
from yet_another_calendar.web.api import docs, monitoring, netology, modeus, bulk

api_router = APIRouter()
api_router.include_router(monitoring.router)
api_router.include_router(docs.router)
if settings.debug:
api_router.include_router(docs.router)
api_router.include_router(netology.router, prefix="/netology", tags=["netology"])
api_router.include_router(modeus.router, prefix="/modeus", tags=["modeus"])
api_router.include_router(bulk.router, prefix="/bulk", tags=["bulk"])
2 changes: 1 addition & 1 deletion backend/yet_another_calendar/web/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,6 @@ def get_app() -> FastAPI:
app.include_router(router=api_router, prefix="/api")
# Adds static directory.
# This directory is used to access swagger files.
app.mount("/static", StaticFiles(directory=APP_ROOT / "static"), name="static")
app.mount("/static_backend", StaticFiles(directory=APP_ROOT / "static"), name="static")

return app
Loading
Loading