-
Notifications
You must be signed in to change notification settings - Fork 52
Feature/menstrual predictor #314
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
Open
Liad-n
wants to merge
26
commits into
PythonFreeCourse:develop
Choose a base branch
from
Liad-n:feature/menstrual-predictor
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
5ab86e3
Created menstrual model and partial join form
44d4d09
Fixed merge conflicts
b59eb57
merging develop
783493f
commit to pull develop
665e3a3
merging develop
64c889b
Menstrual predictor before tests
a1a5062
Menstrual predictor with part of tests
f928e94
Merge branch 'develop' of github.com:PythonFreeCourse/calendar into f…
6bef286
partial tests not working properly
d131ac6
Fixed merging conflicts
1f0ba12
Removed conftest from changes
16b0112
Added conftest
9e0d0ca
Fixed some issues with dependency injection
2a93d84
Added annotations in utils
a49878c
Fixed merging conflicts
85eee50
Fixed CR change requests by Yam
b74d3c9
Fixed merging conflicts
73def22
fixed wrong import caused by changed file in develop
9a8b92e
Fixed yam code review change requests
7375a5d
Merge branch 'develop' of github.com:PythonFreeCourse/calendar into f…
dfa0761
small change to re run check
91c69b9
Tried to add to settings
8470ff7
Fixed more CR change requests, improved coverage
103e18d
Merge branch 'develop' of github.com:PythonFreeCourse/calendar into f…
f66da5a
Fixed more CR change requests, improved coverage, retry because tests…
ac4ceca
Fixed CR requests, fixed merge conflicts
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
import datetime | ||
from datetime import timedelta | ||
from typing import List, Union | ||
|
||
from fastapi import Depends | ||
from loguru import logger | ||
from sqlalchemy import asc | ||
from sqlalchemy.exc import SQLAlchemyError | ||
from sqlalchemy.orm import Session | ||
|
||
from app.database.models import Event, UserMenstrualPeriodLength | ||
from app.dependencies import get_db | ||
from app.internal.security.dependencies import current_user | ||
from app.internal.security.schema import CurrentUser | ||
from app.routers.event import create_event | ||
|
||
MENSTRUAL_PERIOD_CATEGORY_ID = 111 | ||
N_MONTHS_GENERATED = 3 | ||
GAP_IN_CASE_NO_PERIODS = 30 | ||
|
||
|
||
def get_avg_period_gap(db: Session, user_id: int) -> int: | ||
period_days = get_all_period_days(db, user_id) | ||
gaps_list = [] | ||
|
||
if len(period_days) <= 1: | ||
return GAP_IN_CASE_NO_PERIODS | ||
|
||
for i in range(len(period_days) - 1): | ||
gap = get_date_diff(period_days[i].start, period_days[i + 1].start) | ||
gaps_list.append(gap.days) | ||
return get_list_avg(gaps_list) | ||
|
||
|
||
def get_date_diff(date_1: datetime, date_2: datetime) -> timedelta: | ||
return date_2 - date_1 | ||
|
||
|
||
def get_list_avg(received_list: List) -> int: | ||
return sum(received_list) // len(received_list) | ||
|
||
|
||
def remove_existing_period_dates(db: Session, user_id: int) -> None: | ||
( | ||
db.query(Event) | ||
.filter(Event.owner_id == user_id) | ||
.filter(Event.category_id == MENSTRUAL_PERIOD_CATEGORY_ID) | ||
.filter(Event.start > datetime.datetime.now()) | ||
.delete() | ||
) | ||
db.commit() | ||
logger.info("Removed all period predictions to create new ones") | ||
|
||
|
||
def generate_predicted_period_dates( | ||
db: Session, | ||
period_length: str, | ||
period_start_date: datetime, | ||
user_id: int, | ||
) -> Event: | ||
delta = datetime.timedelta(int(period_length)) | ||
period_end_date = period_start_date + delta | ||
period_event = create_event( | ||
db, | ||
"period", | ||
period_start_date, | ||
period_end_date, | ||
user_id, | ||
category_id=MENSTRUAL_PERIOD_CATEGORY_ID, | ||
) | ||
return period_event | ||
|
||
|
||
def add_n_month_predictions( | ||
db: Session, | ||
period_length: str, | ||
period_start_date: datetime, | ||
user_id: int, | ||
) -> List[Event]: | ||
avg_gap = get_avg_period_gap(db, user_id) | ||
avg_gap_delta = datetime.timedelta(avg_gap) | ||
generated_months = [] | ||
for _ in range(N_MONTHS_GENERATED + 1): | ||
generated_period = generate_predicted_period_dates( | ||
db, | ||
period_length, | ||
period_start_date, | ||
user_id, | ||
) | ||
generated_months.append(generated_period) | ||
period_start_date += avg_gap_delta | ||
logger.info(f"Generated predictions: {generated_months}") | ||
return generated_months | ||
|
||
|
||
def add_prediction_events_if_valid( | ||
period_start_date: datetime, | ||
db: Session = Depends(get_db), | ||
user: CurrentUser = Depends(current_user), | ||
) -> None: | ||
current_user_id = user.user_id | ||
user_period_length = is_user_signed_up_to_menstrual_predictor( | ||
db, | ||
current_user_id, | ||
) | ||
|
||
remove_existing_period_dates(db, current_user_id) | ||
if user_period_length: | ||
add_n_month_predictions( | ||
db, | ||
user_period_length, | ||
period_start_date, | ||
current_user_id, | ||
) | ||
|
||
|
||
def get_all_period_days(session: Session, user_id: int) -> List[Event]: | ||
"""Returns all period days filtered by user id.""" | ||
|
||
try: | ||
period_days = ( | ||
session.query(Event) | ||
.filter(Event.owner_id == user_id) | ||
.filter(Event.category_id == MENSTRUAL_PERIOD_CATEGORY_ID) | ||
.order_by(asc(Event.start)) | ||
.all() | ||
) | ||
|
||
except SQLAlchemyError as err: | ||
logger.exception(err) | ||
return [] | ||
|
||
return period_days | ||
|
||
|
||
def is_user_signed_up_to_menstrual_predictor( | ||
session: Session, | ||
user_id: int, | ||
) -> Union[bool, int]: | ||
user_menstrual_period_length = ( | ||
session.query(UserMenstrualPeriodLength) | ||
.filter(user_id == user_id) | ||
.first() | ||
) | ||
if user_menstrual_period_length: | ||
return user_menstrual_period_length.period_length | ||
return False |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import datetime | ||
|
||
from fastapi import APIRouter, Depends, HTTPException, Request | ||
from fastapi.responses import RedirectResponse, Response | ||
from loguru import logger | ||
from sqlalchemy.exc import SQLAlchemyError | ||
from sqlalchemy.orm import Session | ||
from starlette.status import HTTP_302_FOUND, HTTP_400_BAD_REQUEST | ||
|
||
from app.database.models import UserMenstrualPeriodLength | ||
from app.dependencies import get_db, templates | ||
from app.internal.menstrual_predictor_utils import ( | ||
add_prediction_events_if_valid, | ||
generate_predicted_period_dates, | ||
is_user_signed_up_to_menstrual_predictor, | ||
) | ||
from app.internal.security.dependencies import current_user | ||
from app.internal.security.schema import CurrentUser | ||
from app.internal.utils import create_model | ||
|
||
router = APIRouter( | ||
prefix="/menstrual-predictor", | ||
tags=["menstrual-predictor"], | ||
dependencies=[Depends(get_db)], | ||
) | ||
|
||
|
||
@router.get("/") | ||
def join_menstrual_predictor( | ||
request: Request, | ||
db: Session = Depends(get_db), | ||
user: CurrentUser = Depends(current_user), | ||
) -> Response: | ||
current_user_id = user.user_id | ||
|
||
if is_user_signed_up_to_menstrual_predictor(db, current_user_id): | ||
return RedirectResponse(url="/", status_code=HTTP_302_FOUND) | ||
|
||
return templates.TemplateResponse( | ||
"join_menstrual_predictor.html", | ||
{ | ||
"request": request, | ||
}, | ||
) | ||
|
||
|
||
@router.get("/add/{start_date}") | ||
def add_period_start( | ||
request: Request, | ||
start_date: str, | ||
db: Session = Depends(get_db), | ||
user: CurrentUser = Depends(current_user), | ||
) -> RedirectResponse: | ||
try: | ||
period_start_date = datetime.datetime.strptime(start_date, "%Y-%m-%d") | ||
except ValueError as err: | ||
logger.exception(err) | ||
raise HTTPException( | ||
status_code=HTTP_400_BAD_REQUEST, | ||
detail="The given date doesn't match a date format YYYY-MM-DD", | ||
) | ||
else: | ||
add_prediction_events_if_valid(period_start_date, db, user) | ||
logger.info("Adding menstrual start date") | ||
return RedirectResponse("/", status_code=HTTP_302_FOUND) | ||
|
||
|
||
@router.post("/") | ||
async def submit_join_form( | ||
request: Request, | ||
db: Session = Depends(get_db), | ||
user: CurrentUser = Depends(current_user), | ||
) -> RedirectResponse: | ||
|
||
data = await request.form() | ||
print(data) | ||
user_menstrual_period_length = { | ||
"user_id": user.user_id, | ||
"period_length": data["avg-period-length"], | ||
} | ||
last_period_date = datetime.datetime.strptime( | ||
data["last-period-date"], | ||
"%Y-%m-%d", | ||
) | ||
try: | ||
create_model( | ||
session=db, | ||
model_class=UserMenstrualPeriodLength, | ||
**user_menstrual_period_length, | ||
) | ||
except SQLAlchemyError: | ||
logger.info("Current user already signed up to the service, hurray!!") | ||
db.rollback() | ||
url = "/" | ||
generate_predicted_period_dates( | ||
db, | ||
data["avg-period-length"], | ||
last_period_date, | ||
user.user_id, | ||
) | ||
|
||
return RedirectResponse(url=url, status_code=HTTP_302_FOUND) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
function change_max_to_today_date(el) { | ||
const today = new Date(); | ||
const today_str = today.toISOString().substring(0, 10); | ||
el.max = el.dataset.maxDate = today_str; | ||
} | ||
function validate_date_older_than_today(received_date) { | ||
return received_date < new Date(); | ||
} | ||
|
||
document.addEventListener("DOMContentLoaded", () => { | ||
const last_period_date_element = document.getElementById("last-period-date"); | ||
change_max_to_today_date(last_period_date_element); | ||
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,23 +1,39 @@ | ||
document.addEventListener('DOMContentLoaded', () => { | ||
const tabBtn = document.getElementsByClassName("tab"); | ||
for (let i = 0; i < tabBtn.length; i++) { | ||
const btn = document.getElementById("tab" + i); | ||
btn.addEventListener('click', () => { | ||
tabClick(btn.id, tabBtn); | ||
}); | ||
} | ||
}); | ||
document.addEventListener("DOMContentLoaded", () => { | ||
const tabBtn = document.getElementsByClassName("tab"); | ||
for (let i = 0; i < tabBtn.length; i++) { | ||
const btn = document.getElementById("tab" + i); | ||
btn.addEventListener("click", () => { | ||
tabClick(btn.id, tabBtn); | ||
}); | ||
} | ||
|
||
const menstrualSubscriptionSwitch = document.getElementById("switch3"); | ||
menstrualSubscriptionSwitch.addEventListener("click", () => { | ||
const btnState = menstrualSubscriptionSwitch.checked; | ||
if (btnState) { | ||
fetch('/menstrual-predictor/') | ||
.then(response => { | ||
let subscriptionContainer = document.getElementById('menstrual-prediction-container'); | ||
subscriptionContainer.innerHTML = response; | ||
}) | ||
|
||
function tabClick(tab_id, tabBtn) { | ||
let shownTab = document.querySelector(".tab-show"); | ||
let selectedTabContent = document.querySelector(`#${tab_id}-content`); | ||
shownTab.classList.remove("tab-show"); | ||
shownTab.classList.add("tab-hide"); | ||
for (btn of tabBtn) { | ||
btn.children[0].classList.remove("active"); | ||
console.log(menstrualSubscriptionSwitch.checked); | ||
} | ||
document.getElementById(tab_id).classList.add("active"); | ||
selectedTabContent.classList.remove("tab-hide"); | ||
selectedTabContent.classList.add("tab-show"); | ||
}); | ||
}); | ||
async function loadSubscriptionPage(response){ | ||
const data = await response.text(); | ||
return data; | ||
} | ||
function tabClick(tab_id, tabBtn) { | ||
let shownTab = document.querySelector(".tab-show"); | ||
let selectedTabContent = document.querySelector(`#${tab_id}-content`); | ||
shownTab.classList.remove("tab-show"); | ||
shownTab.classList.add("tab-hide"); | ||
for (btn of tabBtn) { | ||
btn.children[0].classList.remove("active"); | ||
} | ||
document.getElementById(tab_id).classList.add("active"); | ||
selectedTabContent.classList.remove("tab-hide"); | ||
selectedTabContent.classList.add("tab-show"); | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.