diff --git a/app/config.py.example b/app/config.py.example
index d296d02e..628c1c23 100644
--- a/app/config.py.example
+++ b/app/config.py.example
@@ -40,6 +40,7 @@ WEBSITE_LANGUAGE = "en"
# Get a free API KEY for Astronomy feature @ www.weatherapi.com/signup.aspx
ASTRONOMY_API_KEY = os.getenv('ASTRONOMY_API_KEY')
WEATHER_API_KEY = os.getenv('WEATHER_API_KEY')
+RAWG_API_KEY = os.getenv("RAWG_API_KEY")
# https://developers.google.com/calendar/quickstart/python -
# follow instracions and make an env variable with the path to the file.
diff --git a/app/internal/game_releases_utils.py b/app/internal/game_releases_utils.py
new file mode 100644
index 00000000..c06b645b
--- /dev/null
+++ b/app/internal/game_releases_utils.py
@@ -0,0 +1,165 @@
+from __future__ import annotations
+
+from collections import defaultdict
+from datetime import datetime
+from functools import lru_cache
+from typing import TYPE_CHECKING, Any, DefaultDict, Dict, List
+
+import httpx
+from loguru import logger
+from sqlalchemy.orm import Session
+
+from app import config
+from app.database.models import UserSettings
+
+if TYPE_CHECKING:
+ from app.routers.calendar_grid import Day, Week
+
+
+def is_user_signed_up_for_game_releases(
+ session: Session,
+ current_user_id: int,
+) -> bool:
+ is_signed_up = bool(
+ session.query(UserSettings)
+ .filter(UserSettings.user_id == current_user_id)
+ .filter(UserSettings.video_game_releases.is_(True))
+ .first(),
+ )
+
+ return is_signed_up
+
+
+def add_game_events_to_weeks(
+ weeks: List["Week"],
+ is_active: bool = True,
+) -> List["Week"]:
+ if not is_active:
+ return weeks
+ first_week: Week = weeks[0]
+ last_week: Week = weeks[-1]
+ first_day: Day = first_week.days[0]
+ last_day: Day = last_week.days[-1]
+ first_day_str = datetime.strptime(first_day.set_id(), "%d-%B-%Y")
+ last_day_str = datetime.strptime(last_day.set_id(), "%d-%B-%Y")
+
+ output = get_games_data_by_dates_from_api(
+ start_date=first_day_str.strftime("%Y-%m-%d"),
+ end_date=last_day_str.strftime("%Y-%m-%d"),
+ )
+ if not output["success"]:
+ logger.exception("Unsuccessful RAWG API call")
+ return weeks
+ games_by_dates = output["results"]
+
+ unformatted_games_by_dates = get_games_data_separated_by_dates(
+ games_by_dates,
+ )
+ formatted_games = get_formatted_games_in_days(unformatted_games_by_dates)
+
+ return insert_formatted_games_to_weeks(weeks, formatted_games)
+
+
+def insert_formatted_games_to_weeks(
+ weeks: List["Week"],
+ formatted_games: DefaultDict[List[str]],
+) -> List["Week"]:
+ for week in weeks:
+ for day in week.days:
+ if day.set_id() in formatted_games.keys():
+ for game in formatted_games[day.set_id()]:
+ day.dailyevents.append(
+ (
+ f"GR!- {(game)[:10]}",
+ (game),
+ ),
+ )
+ return weeks
+
+
+@lru_cache(maxsize=128)
+def get_games_data_by_dates_from_api(
+ start_date: str,
+ end_date: str,
+) -> Dict[str, Any]:
+ API = "https://api.rawg.io/api/games"
+ NO_API_RESPONSE = "The RAWG server did not response"
+ input_query_string = {
+ "dates": f"{start_date},{end_date}",
+ "key": config.RAWG_API_KEY,
+ }
+
+ output: Dict[str, Any] = {}
+ try:
+ response = httpx.get(
+ API,
+ params=input_query_string,
+ )
+ except httpx.HTTPError:
+ output["success"] = False
+ output["error"] = NO_API_RESPONSE
+ return output
+
+ if response.status_code != httpx.codes.OK:
+ output["success"] = False
+ output["error"] = NO_API_RESPONSE
+ return output
+
+ output["success"] = True
+ try:
+ output.update(response.json())
+ return output
+ except KeyError:
+ output["success"] = False
+ output["error"] = response.json()["error"]["message"]
+ return output
+
+
+def get_games_data_separated_by_dates(
+ api_data: Dict[str, Any],
+) -> DefaultDict[List]:
+ games_data = defaultdict(list)
+ for result in api_data:
+ current = {
+ "name": result["name"],
+ "platforms": [],
+ }
+ if result["platforms"]:
+ for platform in result["platforms"]:
+ current["platforms"].append(platform["platform"]["name"])
+ ybd_release_date = translate_ymd_date_to_dby(result["released"])
+ games_data[ybd_release_date].append(current)
+ return games_data
+
+
+def get_formatted_games_in_days(
+ separated_games_dict: DefaultDict[List],
+ with_platforms: bool = False,
+) -> DefaultDict[List[str]]:
+ formatted_games = defaultdict(list)
+
+ for date, game_data in separated_games_dict.items():
+ for game in game_data:
+ formatted_game_str = format_single_game(game, with_platforms)
+ formatted_games[date].append(formatted_game_str)
+ return formatted_games
+
+
+def format_single_game(raw_game: Dict, with_platforms: bool = False) -> str:
+ formatted_game_str = ""
+ formatted_game_str += raw_game["name"]
+ if with_platforms:
+ formatted_game_str += "-Platforms-
"
+ for platform in raw_game["platforms"]:
+ formatted_game_str += f"{platform},"
+ return formatted_game_str
+
+
+def translate_ymd_date_to_dby(ymd_str: str) -> str:
+ ymd_time = datetime.strptime(ymd_str, "%Y-%m-%d")
+ return ymd_time.strftime("%d-%B-%Y")
+
+
+def translate_dby_date_to_ymd(dby_str: str) -> str:
+ dby_time = datetime.strptime(dby_str, "%d-%B-%Y")
+ return dby_time.strftime("%Y-%m-%d")
diff --git a/app/main.py b/app/main.py
index ccf4c30c..1a86d298 100644
--- a/app/main.py
+++ b/app/main.py
@@ -73,6 +73,7 @@ def create_tables(engine, psql_environment):
features,
four_o_four,
friendview,
+ game_release_dates_service,
google_connect,
joke,
login,
@@ -145,6 +146,7 @@ async def swagger_ui_redirect():
weekview.router,
weight.router,
whatsapp.router,
+ game_release_dates_service.router,
]
for router in routers_to_include:
diff --git a/app/routers/calendar.py b/app/routers/calendar.py
index dc1aeefa..2eed55d6 100644
--- a/app/routers/calendar.py
+++ b/app/routers/calendar.py
@@ -1,8 +1,7 @@
from http import HTTPStatus
from fastapi import APIRouter, Request
-from fastapi.responses import HTMLResponse
-from starlette.responses import Response
+from fastapi.responses import HTMLResponse, Response
from app.dependencies import templates
from app.routers import calendar_grid as cg
@@ -11,7 +10,7 @@
prefix="/calendar/month",
tags=["calendar"],
responses={404: {"description": "Not found"}},
- include_in_schema=False
+ include_in_schema=False,
)
@@ -25,18 +24,21 @@ async def calendar(request: Request) -> Response:
"request": request,
"day": day,
"week_days": cg.Week.DAYS_OF_THE_WEEK,
- "weeks_block": cg.get_month_block(day)
- }
+ "weeks_block": cg.get_month_block(day),
+ },
)
@router.get("/add/{date}")
async def update_calendar(
- request: Request, date: str, days: int
+ request: Request,
+ date: str,
+ days: int,
) -> HTMLResponse:
last_day = cg.Day.convert_str_to_date(date)
next_weeks = cg.create_weeks(cg.get_n_days(last_day, days))
template = templates.get_template(
- 'partials/calendar/monthly_view/add_week.html')
+ "partials/calendar/monthly_view/add_week.html",
+ )
content = template.render(weeks_block=next_weeks)
return HTMLResponse(content=content, status_code=HTTPStatus.OK)
diff --git a/app/routers/calendar_grid.py b/app/routers/calendar_grid.py
index 9ef5202c..e43ad04b 100644
--- a/app/routers/calendar_grid.py
+++ b/app/routers/calendar_grid.py
@@ -6,6 +6,8 @@
import pytz
+from app.internal.game_releases_utils import add_game_events_to_weeks
+
MONTH_BLOCK: int = 6
locale.setlocale(locale.LC_ALL, "en_US.UTF-8")
@@ -190,7 +192,9 @@ def create_weeks(
"""Return lists of Weeks objects."""
ndays: List[Day] = list(days)
num_days: int = len(ndays)
- return [Week(ndays[i : i + length]) for i in range(0, num_days, length)]
+ _weeks = [Week(ndays[i : i + length]) for i in range(0, num_days, length)]
+
+ return add_game_events_to_weeks(_weeks, is_active=True)
def get_month_block(day: Day, n: int = MONTH_BLOCK) -> List[Week]:
diff --git a/app/routers/game_release_dates_service.py b/app/routers/game_release_dates_service.py
new file mode 100644
index 00000000..1db2c942
--- /dev/null
+++ b/app/routers/game_release_dates_service.py
@@ -0,0 +1,144 @@
+import datetime
+from typing import Dict, List
+
+import requests
+from fastapi import APIRouter, Depends, Request
+from fastapi.responses import RedirectResponse, Response
+from sqlalchemy.orm import Session
+from starlette.status import HTTP_302_FOUND
+
+from app.database.models import UserSettings
+from app.dependencies import get_db, templates
+from app.internal.game_releases_utils import (
+ is_user_signed_up_for_game_releases,
+)
+from app.internal.security.dependencies import current_user
+from app.internal.security.schema import CurrentUser
+from app.internal.utils import create_model
+from app.routers.profile import router as profile_router
+
+router = APIRouter(
+ prefix="/game-releases",
+ tags=["game-releases"],
+ responses={404: {"description": "Not found"}},
+)
+
+
+@router.post("/get_releases_by_dates")
+async def fetch_released_games(
+ request: Request,
+ session=Depends(get_db),
+) -> Response:
+ data = await request.form()
+
+ from_date = data["from-date"]
+ to_date = data["to-date"]
+
+ games = get_games_data(from_date, to_date)
+
+ return templates.TemplateResponse(
+ "partials/calendar/feature_settings/games_list.html",
+ {"request": request, "games": games},
+ )
+
+
+@router.get("/next-month")
+def get_game_releases_month(request: Request) -> List:
+ today = datetime.datetime.today()
+ delta = datetime.timedelta(days=30)
+ today_str = today.strftime("%Y-%m-%d")
+ in_month_str = (today + delta).strftime("%Y-%m-%d")
+
+ return get_games_data(today_str, in_month_str)
+
+
+def get_games_data(start_date: datetime, end_date: datetime) -> List[Dict]:
+ API = "https://api.rawg.io/api/games"
+
+ current_day_games = requests.get(
+ f"{API}?dates={start_date},{end_date}",
+ )
+ current_day_games = current_day_games.json()["results"]
+ games_data = []
+ for result in current_day_games:
+ current = {
+ "name": result["name"],
+ "slug": result["slug"],
+ "platforms": [],
+ }
+
+ for platform in result["platforms"]:
+ current["platforms"].append(platform["platform"]["name"])
+ current["release_date"] = result["released"]
+ games_data.append(current)
+
+ return games_data
+
+
+@router.post("/subscribe")
+async def subscribe_game_release_service(
+ request: Request,
+ session: Session = Depends(get_db),
+ user: CurrentUser = Depends(current_user),
+) -> Response:
+ if is_user_signed_up_for_game_releases(session, user.user_id):
+ return RedirectResponse(
+ profile_router.url_path_for("profile"),
+ status_code=HTTP_302_FOUND,
+ )
+ games_setting_true_for_model = {
+ "user_id": user.user_id,
+ "video_game_releases": True,
+ }
+ current_user_settings = session.query(UserSettings).filter(
+ UserSettings.user_id == user.user_id,
+ )
+ if current_user_settings:
+ # TODO:
+ # If all users are created with a UserSettings entry -
+ # unnecessary check
+ current_user_settings.update(games_setting_true_for_model)
+ session.commit()
+ else:
+ create_model(session, UserSettings, **games_setting_true_for_model)
+ return RedirectResponse(
+ profile_router.url_path_for("profile"),
+ status_code=HTTP_302_FOUND,
+ )
+
+
+@router.post("/unsubscribe")
+async def unsubscribe_game_release_service(
+ request: Request,
+ session: Session = Depends(get_db),
+ user: CurrentUser = Depends(current_user),
+) -> RedirectResponse:
+ current_user_id = user.user_id
+
+ if not is_user_signed_up_for_game_releases(session, current_user_id):
+ return RedirectResponse(
+ profile_router.url_path_for("profile"),
+ status_code=HTTP_302_FOUND,
+ )
+ else:
+ games_setting_false_for_model = {
+ "user_id": str(current_user_id),
+ "video_game_releases": False,
+ }
+ current_user_settings = session.query(UserSettings).filter(
+ UserSettings.user_id == current_user_id,
+ )
+ if current_user_settings:
+ # TODO:
+ # If all users are created with a UserSettings entry -
+ # unnecessary check
+ current_user_settings.update(games_setting_false_for_model)
+ session.commit()
+ else:
+ create_model(
+ session, UserSettings, **games_setting_false_for_model
+ )
+ return RedirectResponse(
+ profile_router.url_path_for("profile"),
+ status_code=HTTP_302_FOUND,
+ )
diff --git a/app/static/game_releases.css b/app/static/game_releases.css
new file mode 100644
index 00000000..ee6bc0e9
--- /dev/null
+++ b/app/static/game_releases.css
@@ -0,0 +1,7 @@
+.game-title {
+ font-size: 1.1em;
+ font-weight: bolder;
+}
+.secondary-title {
+ font-weight: bold;
+}
diff --git a/app/static/js/game_releases.js b/app/static/js/game_releases.js
new file mode 100644
index 00000000..a1130ac0
--- /dev/null
+++ b/app/static/js/game_releases.js
@@ -0,0 +1,19 @@
+document.addEventListener("DOMContentLoaded", () => {
+ get_games_btn = document.getElementById("get-games");
+ get_games_btn.addEventListener("click", function (e) {
+ const formData = new FormData();
+ formData.append("from-date", document.getElementById("from-date").value);
+ formData.append("to-date", document.getElementById("to-date").value);
+
+ fetch("/game-releases/get_releases_by_dates", {
+ method: "post",
+ body: formData,
+ })
+ .then(function (response) {
+ return response.text();
+ })
+ .then(function (body) {
+ document.querySelector("#content-div").innerHTML = body;
+ });
+ });
+});
diff --git a/app/templates/partials/calendar/calendar_base.html b/app/templates/partials/calendar/calendar_base.html
index a0090af6..e5621eee 100644
--- a/app/templates/partials/calendar/calendar_base.html
+++ b/app/templates/partials/calendar/calendar_base.html
@@ -18,6 +18,7 @@
{% endblock content %}
+{% include 'partials/calendar/feature_settings/game_release_modal.html' %}
diff --git a/app/templates/partials/calendar/feature_settings/game_release_modal.html b/app/templates/partials/calendar/feature_settings/game_release_modal.html
new file mode 100644
index 00000000..9a9b7083
--- /dev/null
+++ b/app/templates/partials/calendar/feature_settings/game_release_modal.html
@@ -0,0 +1,55 @@
+
+
+
+