From 0f95dd83f82dbd91e420d00b098c0438993cd2c1 Mon Sep 17 00:00:00 2001 From: kobyfogel <71118404+kobyfogel@users.noreply.github.com> Date: Thu, 18 Feb 2021 18:27:44 +0200 Subject: [PATCH 01/46] Feature/login (#293) --- app/internal/security/dependancies.py | 55 ++++++++- app/internal/security/ouath2.py | 24 ++-- app/internal/security/schema.py | 15 ++- app/static/style.css | 11 ++ git | 0 tests/security_testing_routes.py | 51 ++++++-- tests/test_login.py | 161 ++++++++++++++++++++------ 7 files changed, 247 insertions(+), 70 deletions(-) create mode 100644 git diff --git a/app/internal/security/dependancies.py b/app/internal/security/dependancies.py index 7f2a0795..584235dd 100644 --- a/app/internal/security/dependancies.py +++ b/app/internal/security/dependancies.py @@ -1,8 +1,13 @@ +from fastapi import Depends, HTTPException +from starlette.status import HTTP_401_UNAUTHORIZED from starlette.requests import Request +from app.database.models import User from app.dependencies import get_db from app.internal.security.ouath2 import ( - Depends, Session, check_jwt_token, get_authorization_cookie) + Session, get_jwt_token, get_authorization_cookie +) +from app.internal.security import schema async def is_logged_in( @@ -11,7 +16,7 @@ async def is_logged_in( """ A dependency function protecting routes for only logged in user """ - await check_jwt_token(db, jwt) + await get_jwt_token(db, jwt) return True @@ -21,5 +26,47 @@ async def is_manager( """ A dependency function protecting routes for only logged in manager """ - await check_jwt_token(db, jwt, manager=True) - return True + jwt_payload = await get_jwt_token(db, jwt) + if jwt_payload.get("is_manager"): + return True + raise HTTPException( + status_code=HTTP_401_UNAUTHORIZED, + headers=request.url.path, + detail="You don't have a permition to enter this page") + + +async def current_user_from_db( + request: Request, + db: Session = Depends(get_db), + jwt: str = Depends(get_authorization_cookie), +) -> User: + """ + Returns logged in User object. + A dependency function protecting routes for only logged in user. + """ + jwt_payload = await get_jwt_token(db, jwt) + username = jwt_payload.get("sub") + user_id = jwt_payload.get("user_id") + db_user = await User.get_by_username(db, username=username) + if db_user and db_user.id == user_id: + return db_user + else: + raise HTTPException( + status_code=HTTP_401_UNAUTHORIZED, + headers=request.url.path, + detail="Your token is incorrect. Please log in again") + + +async def current_user( + request: Request, + db: Session = Depends(get_db), + jwt: str = Depends(get_authorization_cookie), +) -> schema: + """ + Returns logged in User object. + A dependency function protecting routes for only logged in user. + """ + jwt_payload = await get_jwt_token(db, jwt) + username = jwt_payload.get("sub") + user_id = jwt_payload.get("user_id") + return schema.CurrentUser(user_id=user_id, username=username) diff --git a/app/internal/security/ouath2.py b/app/internal/security/ouath2.py index fd96973f..d520c4cc 100644 --- a/app/internal/security/ouath2.py +++ b/app/internal/security/ouath2.py @@ -63,28 +63,18 @@ def create_jwt_token( return jwt_token -async def check_jwt_token( +async def get_jwt_token( db: Session, - token: str = Depends(oauth_schema), - path: bool = None, - manager: bool = False, -) -> User: + token: str = Depends(oauth_schema), + path: Union[bool, str] = None) -> User: """ Check whether JWT token is correct. Returns jwt payloads if correct. Raises HTTPException if fails to decode. """ try: - jwt_payload = jwt.decode(token, JWT_KEY, algorithms=JWT_ALGORITHM) - if not manager: - return True - if jwt_payload.get("is_manager"): - return True - raise HTTPException( - status_code=HTTP_401_UNAUTHORIZED, - headers=path, - detail="You don't have a permition to enter this page", - ) + jwt_payload = jwt.decode( + token, JWT_KEY, algorithms=JWT_ALGORITHM) except InvalidSignatureError: raise HTTPException( status_code=HTTP_401_UNAUTHORIZED, @@ -101,8 +91,8 @@ async def check_jwt_token( raise HTTPException( status_code=HTTP_401_UNAUTHORIZED, headers=path, - detail="Your token is incorrect. Please log in again", - ) + detail="Your token is incorrect. Please log in again") + return jwt_payload async def get_authorization_cookie(request: Request) -> str: diff --git a/app/internal/security/schema.py b/app/internal/security/schema.py index 31a009e7..95645ac0 100644 --- a/app/internal/security/schema.py +++ b/app/internal/security/schema.py @@ -3,15 +3,22 @@ from pydantic import BaseModel -class LoginUser(BaseModel): +class CurrentUser(BaseModel): """ Validating fields types - Returns a User object for signing in. + Returns a user details as a class. """ user_id: Optional[int] - is_manager: Optional[bool] username: str - password: str class Config: orm_mode = True + + +class LoginUser(CurrentUser): + """ + Validating fields types + Returns a User object for signing in. + """ + is_manager: Optional[bool] + password: str diff --git a/app/static/style.css b/app/static/style.css index c9dd9f26..8c527fc5 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -96,11 +96,22 @@ p { border: none; background-color: whitesmoke; } + +.error-message { + line-height: 0; + color: red; + padding-left: 12.5rem; +} + +.subtitle { + font-size: 1.25rem; +} .error-message { line-height: 0; color: red; padding-left: 12.5rem; + margin-bottom: 1em; } .input-upload-file { diff --git a/git b/git new file mode 100644 index 00000000..e69de29b diff --git a/tests/security_testing_routes.py b/tests/security_testing_routes.py index 83ce6f92..4df73e8b 100644 --- a/tests/security_testing_routes.py +++ b/tests/security_testing_routes.py @@ -1,6 +1,10 @@ -from app.internal.security.dependancies import (is_manager, is_logged_in) from fastapi import APIRouter, Depends, Request +from app.internal.security.dependancies import ( + current_user, current_user_from_db, + is_logged_in, is_manager, User +) + """ These routes are for security testing. @@ -14,20 +18,43 @@ ) -@router.get('/protected') -async def protected_route( +@router.get('/is_logged_in') +async def is_logged_in( request: Request, user: bool = Depends(is_logged_in)): - # This is how to protect route for logged in user only. - # Dependency will return True. - # if user not looged-in, will be redirected to login route. + """This is how to protect route for logged in user only. + Dependency will return True. + if user not looged-in, will be redirected to login route. + """ return {"user": user} -@router.get('/manager') -async def manager_route( +@router.get('/is_manager') +async def is_manager( request: Request, user: bool = Depends(is_manager)): - # This is how to protect route for logged in manager only. - # Dependency will return True. - # if user not looged-in, or have no manager permission, - # will be redirected to login route. + """This is how to protect route for logged in manager only. + Dependency will return True. + if user not looged-in, or have no manager permission, + will be redirected to login route. + """ return {"manager": user} + + +@router.get('/current_user_from_db') +async def current_user_from_db( + request: Request, user: User = Depends(current_user_from_db)): + """This is how to protect route for logged in user only. + Dependency will return User object. + if user not looged-in, will be redirected to login route. + """ + return {"user": user.username} + + +@router.get('/current_user') +async def current_user( + request: Request, user: User = Depends(current_user)): + """This is how to protect route for logged in user only. + Dependency will return schema.CurrentUser object, + contains user_id and username. + if user not looged-in, will be redirected to login route. + """ + return {"user": user.username} diff --git a/tests/test_login.py b/tests/test_login.py index e9818737..11432738 100644 --- a/tests/test_login.py +++ b/tests/test_login.py @@ -31,92 +31,187 @@ def test_login_route_ok(security_test_client): 'username': 'incorrect_user', 'password': 'correct_password'} -def test_register_user(session, security_test_client): - security_test_client.post('/register', data=REGISTER_DETAIL) - - @pytest.mark.parametrize( "username, password, expected_response", LOGIN_WRONG_DETAILS) def test_login_fails( session, security_test_client, username, password, expected_response): - security_test_client.post('/register', data=REGISTER_DETAIL) + security_test_client.post( + security_test_client.app.url_path_for('register'), + data=REGISTER_DETAIL) data = {'username': username, 'password': password} - data = security_test_client.post('/login', data=data).content + data = security_test_client.post( + security_test_client.app.url_path_for('login'), + data=data).content assert expected_response in data def test_login_successfull(session, security_test_client): - security_test_client.post('/register', data=REGISTER_DETAIL) - res = security_test_client.post('/login', data=LOGIN_DATA) + security_test_client.post( + security_test_client.app.url_path_for('register'), + data=REGISTER_DETAIL) + res = security_test_client.post( + security_test_client.app.url_path_for('login'), + data=LOGIN_DATA) assert res.status_code == HTTP_302_FOUND def test_is_logged_in_dependency_with_logged_in_user( session, security_test_client): - security_test_client.post('/register', data=REGISTER_DETAIL) - security_test_client.post('/login', data=LOGIN_DATA) - res = security_test_client.get('/protected') + security_test_client.post( + security_test_client.app.url_path_for('register'), + data=REGISTER_DETAIL) + security_test_client.post( + security_test_client.app.url_path_for('login'), + data=LOGIN_DATA) + res = security_test_client.get( + security_test_client.app.url_path_for('is_logged_in')) assert res.json() == {"user": True} def test_is_logged_in_dependency_without_logged_in_user( session, security_test_client): - res = security_test_client.get('/logout') - res = security_test_client.get('/protected') + res = security_test_client.get( + security_test_client.app.url_path_for('logout')) + res = security_test_client.get( + security_test_client.app.url_path_for('is_logged_in')) assert b'Please log in' in res.content def test_is_manager_in_dependency_with_logged_in_regular_user( session, security_test_client): - security_test_client.post('/register', data=REGISTER_DETAIL) - security_test_client.post('/login', data=LOGIN_DATA) - res = security_test_client.get('/manager') + security_test_client.post( + security_test_client.app.url_path_for('register'), + data=REGISTER_DETAIL) + security_test_client.post( + security_test_client.app.url_path_for('login'), + data=LOGIN_DATA) + res = security_test_client.get( + security_test_client.app.url_path_for('is_manager')) assert b"have a permition" in res.content def test_is_manager_in_dependency_with_logged_in_manager( session, security_test_client): - security_test_client.post('/register', data=REGISTER_DETAIL) + security_test_client.post( + security_test_client.app.url_path_for('register'), + data=REGISTER_DETAIL) manager = session.query(User).filter( User.username == 'correct_user').first() manager.is_manager = True session.commit() - security_test_client.post('/login', data=LOGIN_DATA) - res = security_test_client.get('/manager') + security_test_client.post( + security_test_client.app.url_path_for('login'), data=LOGIN_DATA) + res = security_test_client.get( + security_test_client.app.url_path_for('is_manager')) assert res.json() == {"manager": True} def test_logout(session, security_test_client): - res = security_test_client.get('/logout') + res = security_test_client.get( + security_test_client.app.url_path_for('logout')) assert b'Login' in res.content def test_incorrect_secret_key_in_token(session, security_test_client): user = LoginUser(**LOGIN_DATA) incorrect_token = create_jwt_token(user, jwt_key="wrong secret key") - security_test_client.post('/register', data=REGISTER_DETAIL) - url = f"/login?existing_jwt={incorrect_token}" - security_test_client.post(f'{url}', data=LOGIN_DATA) - res = security_test_client.get('/protected') + security_test_client.post( + security_test_client.app.url_path_for('register'), + data=REGISTER_DETAIL) + params = f"?existing_jwt={incorrect_token}" + security_test_client.post( + security_test_client.app.url_path_for('login') + f'{params}', + data=LOGIN_DATA) + res = security_test_client.get( + security_test_client.app.url_path_for('is_logged_in')) assert b'Your token is incorrect' in res.content def test_expired_token(session, security_test_client): - security_test_client.get('/logout') + security_test_client.get( + security_test_client.app.url_path_for('logout')) user = LoginUser(**LOGIN_DATA) incorrect_token = create_jwt_token(user, jwt_min_exp=-1) - security_test_client.post('/register', data=REGISTER_DETAIL) - url = f"/login?existing_jwt={incorrect_token}" - security_test_client.post(f'{url}', data=LOGIN_DATA) - res = security_test_client.get('/protected') + security_test_client.post( + security_test_client.app.url_path_for('register'), + data=REGISTER_DETAIL) + params = f"?existing_jwt={incorrect_token}" + security_test_client.post( + security_test_client.app.url_path_for('login') + f'{params}', + data=LOGIN_DATA) + res = security_test_client.get( + security_test_client.app.url_path_for('is_logged_in')) assert b'expired' in res.content def test_corrupted_token(session, security_test_client): user = LoginUser(**LOGIN_DATA) incorrect_token = create_jwt_token(user) + "s" - security_test_client.post('/register', data=REGISTER_DETAIL) - url = f"/login?existing_jwt={incorrect_token}" - security_test_client.post(f'{url}', data=LOGIN_DATA) - res = security_test_client.get('/protected') + security_test_client.post( + security_test_client.app.url_path_for('register'), + data=REGISTER_DETAIL) + params = f"?existing_jwt={incorrect_token}" + security_test_client.post( + security_test_client.app.url_path_for('login') + f'{params}', + data=LOGIN_DATA) + res = security_test_client.get( + security_test_client.app.url_path_for('is_logged_in')) + assert b'Your token is incorrect' in res.content + + +def test_current_user_from_db_dependency_ok(session, security_test_client): + security_test_client.post( + security_test_client.app.url_path_for('register'), + data=REGISTER_DETAIL) + security_test_client.post( + security_test_client.app.url_path_for('login'), data=LOGIN_DATA) + res = security_test_client.get( + security_test_client.app.url_path_for('current_user_from_db')) + assert res.json() == {"user": 'correct_user'} + + +def test_current_user_from_db_dependency_not_logged_in( + session, security_test_client): + security_test_client.get( + security_test_client.app.url_path_for('logout')) + res = security_test_client.get( + security_test_client.app.url_path_for('current_user_from_db')) + assert b'Please log in' in res.content + + +def test_current_user_from_db_dependency_wrong_details( + session, security_test_client): + security_test_client.get( + security_test_client.app.url_path_for('logout')) + security_test_client.post( + security_test_client.app.url_path_for('register'), + data=REGISTER_DETAIL) + user = LoginUser(**WRONG_LOGIN_DATA) + incorrect_token = create_jwt_token(user) + params = f"?existing_jwt={incorrect_token}" + security_test_client.post( + security_test_client.app.url_path_for('login') + f'{params}', + data=LOGIN_DATA) + res = security_test_client.get( + security_test_client.app.url_path_for('current_user_from_db')) assert b'Your token is incorrect' in res.content + + +def test_current_user_dependency_ok(session, security_test_client): + security_test_client.post( + security_test_client.app.url_path_for('register'), + data=REGISTER_DETAIL) + security_test_client.post( + security_test_client.app.url_path_for('login'), data=LOGIN_DATA) + res = security_test_client.get( + security_test_client.app.url_path_for('current_user')) + assert res.json() == {"user": 'correct_user'} + + +def test_current_user_dependency_not_logged_in( + session, security_test_client): + security_test_client.get( + security_test_client.app.url_path_for('logout')) + res = security_test_client.get( + security_test_client.app.url_path_for('current_user')) + assert b'Please log in' in res.content From 10489565b3891ff7ce2f711df7ef2897545d934f Mon Sep 17 00:00:00 2001 From: Gonzom Date: Thu, 18 Feb 2021 20:23:18 +0200 Subject: [PATCH 02/46] fix: type bug fix (#323) --- tests/user_fixture.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/tests/user_fixture.py b/tests/user_fixture.py index 09fa5af7..2befa98d 100644 --- a/tests/user_fixture.py +++ b/tests/user_fixture.py @@ -1,4 +1,4 @@ -from collections import Generator +from typing import Generator import pytest from sqlalchemy.orm import Session @@ -10,10 +10,11 @@ @pytest.fixture def user(session: Session) -> Generator[User, None, None]: mock_user = create_model( - session, User, - username='test_username', - password='test_password', - email='test.email@gmail.com', + session, + User, + username="test_username", + password="test_password", + email="test.email@gmail.com", language_id=1, ) yield mock_user @@ -23,10 +24,11 @@ def user(session: Session) -> Generator[User, None, None]: @pytest.fixture def sender(session: Session) -> Generator[User, None, None]: mock_user = create_model( - session, User, - username='sender_username', - password='sender_password', - email='sender.email@gmail.com', + session, + User, + username="sender_username", + password="sender_password", + email="sender.email@gmail.com", language_id=1, ) yield mock_user From 72d059661dc8a7d600f6ec93b1d2a2acd0ebccd7 Mon Sep 17 00:00:00 2001 From: fandomario <71139801+fandomario@users.noreply.github.com> Date: Sat, 20 Feb 2021 02:44:29 +0200 Subject: [PATCH 03/46] Feature/getjoke (#143) --- app/database/models.py | 7 + app/internal/jokes.py | 24 ++ app/internal/json_data_loader.py | 11 +- app/main.py | 3 +- app/resources/jokes.json | 368 +++++++++++++++++++++++++++++++ app/routers/joke.py | 12 + app/static/joke.js | 14 ++ app/templates/base.html | 6 + tests/conftest.py | 1 + tests/jokes_fixture.py | 20 ++ tests/test_joke.py | 17 ++ tests/test_joke_route.py | 4 + 12 files changed, 484 insertions(+), 3 deletions(-) create mode 100644 app/internal/jokes.py create mode 100644 app/resources/jokes.json create mode 100644 app/routers/joke.py create mode 100644 app/static/joke.js create mode 100644 tests/jokes_fixture.py create mode 100644 tests/test_joke.py create mode 100644 tests/test_joke_route.py diff --git a/app/database/models.py b/app/database/models.py index 0e4f4aea..20df5947 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -382,6 +382,13 @@ def __repr__(self): ) +class Joke(Base): + __tablename__ = "jokes" + + id = Column(Integer, primary_key=True, index=True) + text = Column(String, nullable=False) + + # insert language data # Credit to adrihanu https://stackoverflow.com/users/9127249/adrihanu diff --git a/app/internal/jokes.py b/app/internal/jokes.py new file mode 100644 index 00000000..d8de9456 --- /dev/null +++ b/app/internal/jokes.py @@ -0,0 +1,24 @@ +from typing import Dict, Optional + +from sqlalchemy.orm import Session +from sqlalchemy.sql.expression import func + +from app.database.models import Joke + + +def get_joke(joke_: Dict[str, Optional[str]]) -> Joke: + """Returns a Joke object from the dictionary data. + + Args: + joke_: A dictionary joke related information. + + Returns: + A new Joke object. + """ + return Joke( + text=joke_['text'], + ) + + +def get_a_joke(session: Session): + return session.query(Joke).order_by(func.random()).first() diff --git a/app/internal/json_data_loader.py b/app/internal/json_data_loader.py index d67443e6..4e9d83e7 100644 --- a/app/internal/json_data_loader.py +++ b/app/internal/json_data_loader.py @@ -5,8 +5,8 @@ from loguru import logger from sqlalchemy.orm import Session -from app.database.models import Base, Quote, Zodiac -from app.internal import daily_quotes, zodiac +from app.database.models import Base, Joke, Quote, Zodiac +from app.internal import daily_quotes, jokes, zodiac def load_to_database(session: Session) -> None: @@ -35,6 +35,13 @@ def load_to_database(session: Session) -> None: daily_quotes.get_quote, ) + _insert_into_database( + session, + 'app/resources/jokes.json', + Joke, + jokes.get_joke, + ) + def _insert_into_database( session: Session, diff --git a/app/main.py b/app/main.py index 39e24cdc..e6e4af34 100644 --- a/app/main.py +++ b/app/main.py @@ -42,7 +42,7 @@ def create_tables(engine, psql_environment): from app.routers import ( # noqa: E402 about_us, agenda, calendar, categories, celebrity, credits, currency, dayview, email, event, export, four_o_four, friendview, - google_connect, invitation, login, logout, profile, + google_connect, invitation, joke, login, logout, profile, register, search, telegram, user, weekview, whatsapp, ) @@ -82,6 +82,7 @@ async def swagger_ui_redirect(): four_o_four.router, google_connect.router, invitation.router, + joke.router, login.router, logout.router, profile.router, diff --git a/app/resources/jokes.json b/app/resources/jokes.json new file mode 100644 index 00000000..4950d6c3 --- /dev/null +++ b/app/resources/jokes.json @@ -0,0 +1,368 @@ +[ + {"text": "Chuck Norris uses ribbed condoms inside out, so he gets the pleasure."}, + {"text": "MacGyver can build an airplane out of gum and paper clips. Chuck Norris can kill him and take it."}, + {"text": "Chuck Norris doesn't read books. He stares them down until he gets the information he wants."}, + {"text": "Chuck Norris lost his virginity before his dad did."}, + {"text": "Since 1940, the year Chuck Norris was born, roundhouse kick related deaths have increased 13,000 percent."}, + {"text": "Chuck Norris sheds his skin twice a year."}, + {"text": "When Chuck Norris goes to donate blood, he declines the syringe, and instead requests a hand gun and a bucket."}, + {"text": "Chuck Norris does not teabag the ladies. He potato-sacks them."}, + {"text": "In an average living room there are 1,242 objects Chuck Norris could use to kill you, including the room itself."}, + {"text": "Chuck Norris doesn't shower, he only takes blood baths."}, + {"text": "Time waits for no man. Unless that man is Chuck Norris."}, + {"text": "In the Bible, Jesus turned water into wine. But then Chuck Norris turned that wine into beer."}, + {"text": "Chuck Norris is not hung like a horse. Horses are hung like Chuck Norris."}, + {"text": "Chuck Norris has two speeds: Walk and Kill."}, + {"text": "Fool me once, shame on you. Fool Chuck Norris once and he will roundhouse kick you in the face."}, + {"text": "If you spell Chuck Norris in Scrabble, you win. Forever."}, + {"text": "Someone once videotaped Chuck Norris getting pissed off. It was called Walker: Texas Chain Saw Masacre."}, + {"text": "Chuck Norris will attain statehood in 2009. His state flower will be the Magnolia."}, + {"text": "Chuck Norris doesn't wash his clothes. He disembowels them."}, + {"text": "Chuck Norris doesn't churn butter. He roundhouse kicks the cows and the butter comes straight out."}, + {"text": "Police label anyone attacking Chuck Norris as a Code 45-11.... A suicide."}, + {"text": "Chuck Norris is the only man to ever defeat a brick wall in a game of tennis."}, + {"text": "What was going through the minds of all of Chuck Norris' victims before they died? His shoe."}, + {"text": "Chuck Norris once ate three 72 oz. steaks in one hour. He spent the first 45 minutes having sex with his waitress."}, + {"text": "There is no theory of evolution, just a list of creatures Chuck Norris allows to live."}, + {"text": "Chuck Norris can win a game of Connect Four in only three moves."}, + {"text": "The quickest way to a man's heart is with Chuck Norris' fist."}, + {"text": "Chuck Norris drives an ice cream truck covered in human skulls."}, + {"text": "Most people have 23 pairs of chromosomes. Chuck Norris has 72... and they're all poisonous."}, + {"text": "The Great Wall of China was originally created to keep Chuck Norris out. It failed miserably."}, + {"text": "Chuck Norris is ten feet tall, weighs two-tons, breathes fire, and could eat a hammer and take a shotgun blast standing."}, + {"text": "Crop circles are Chuck Norris' way of telling the world that sometimes corn needs to lie down."}, + {"text": "When Chuck Norris calls 1-900 numbers, he doesn't get charged. He holds up the phone and money falls out."}, + {"text": "Chuck Norris once ate a whole cake before his friends could tell him there was a stripper in it."}, + {"text": "Some people like to eat frogs' legs. Chuck Norris likes to eat lizard legs. Hence, snakes."}, + {"text": "There are no races, only countries of people Chuck Norris has beaten to different shades of black and blue."}, + {"text": "A Chuck Norris-delivered Roundhouse Kick is the preferred method of execution in 16 states."}, + {"text": "When Chuck Norris falls in water, Chuck Norris doesn't get wet. Water gets Chuck Norris."}, + {"text": "Chuck Norris' house has no doors, only walls that he walks through."}, + {"text": "When Chuck Norris has sex with a man, it won't be because he is gay. It will be because he has run out of women."}, + {"text": "How much wood would a woodchuck chuck if a woodchuck could Chuck Norris? All of it."}, + {"text": "Chuck Norris doesn't actually write books, the words assemble themselves out of fear."}, + {"text": "Chuck Norris can believe it's not butter."}, + {"text": "If tapped, a Chuck Norris roundhouse kick could power the country of Australia for 44 minutes."}, + {"text": "Chuck Norris can divide by zero."}, + {"text": "Chuck Norris invented his own type of karate. It's called Chuck-Will-Kill."}, + {"text": "While urinating, Chuck Norris is easily capable of welding titanium."}, + {"text": "When Chuck Norris talks, everybody listens. And dies."}, + {"text": "When Steven Seagal kills a ninja, he only takes its hide. When Chuck Norris kills a ninja, he uses every part."}, + {"text": "Contrary to popular belief, there is indeed enough Chuck Norris to go around."}, + {"text": "Chuck Norris doesnt shave; he kicks himself in the face. The only thing that can cut Chuck Norris is Chuck Norris."}, + {"text": "For some, the left testicle is larger than the right one. For Chuck Norris, each testicle is larger than the other one."}, + {"text": "Chuck Norris always knows the EXACT location of Carmen SanDiego."}, + {"text": "Chuck Norris invented black. In fact, he invented the entire spectrum of visible light. Except pink. Tom Cruise invented pink."}, + {"text": "When you're Chuck Norris, anything + anything is equal to 1. One roundhouse kick to the face."}, + {"text": "On his birthday, Chuck Norris randomly selects one lucky child to be thrown into the sun."}, + {"text": "Nobody doesn't like Sara Lee. Except Chuck Norris."}, + {"text": "Chuck Norris doesn't throw up if he drinks too much. Chuck Norris throws down!"}, + {"text": "Chuck Norris has 12 moons. One of those moons is the Earth."}, + {"text": "Chuck Norris grinds his coffee with his teeth and boils the water with his own rage."}, + {"text": "Chuck Norris ordered a Big Mac at Burger King, and got one."}, + {"text": "Chuck Norris can drink an entire gallon of milk in thirty-seven seconds."}, + {"text": "Chuck Norris doesn't bowl strikes, he just knocks down one pin and the other nine faint."}, + {"text": "It takes Chuck Norris 20 minutes to watch 60 Minutes."}, + {"text": "Chuck Norris has a deep and abiding respect for human life... unless it gets in his way."}, + {"text": "The Bermuda Triangle used to be the Bermuda Square, until Chuck Norris Roundhouse kicked one of the corners off."}, + {"text": "Chuck Norris doesn't believe in Germany."}, + {"text": "When Chuck Norris is in a crowded area, he doesn't walk around people. He walks through them."}, + {"text": "Chuck Norris once ate an entire bottle of sleeping pills. They made him blink."}, + {"text": "Chuck Norris can touch MC Hammer."}, + {"text": "Chuck Norris played Russian Roulette with a fully loaded gun and won."}, + {"text": "It takes 14 puppeteers to make Chuck Norris smile, but only 2 to make him destroy an orphanage."}, + {"text": "Some people wear Superman pajamas. Superman wears Chuck Norris pajamas."}, + {"text": "Simply by pulling on both ends, Chuck Norris can stretch diamonds back into coal."}, + {"text": "When Chuck Norris does a pushup, he isn't lifting himself up, he's pushing the Earth down."}, + {"text": "Chuck Norris invented the bolt-action rifle, liquor, sexual intercourse, and football-- in that order."}, + {"text": "A high tide means Chuck Norris is flying over your coast. The tide is caused by God pissing his pants."}, + {"text": "There is in fact an 'I' in Norris, but there is no 'team'. Not even close."}, + {"text": "An anagram for Walker Texas Ranger is KARATE WRANGLER SEX. I don't know what that is, but it sounds AWESOME."}, + {"text": "Chuck Norris doesn't stub his toes. He accidentally destroys chairs, bedframes, and sidewalks."}, + {"text": "Chuck Norris does not own a stove, oven, or microwave , because revenge is a dish best served cold."}, + {"text": "Chuck Norris can slam a revolving door."}, + {"text": "Chuck Norris built a better mousetrap, but the world was too frightened to beat a path to his door."}, + {"text": "Hellen Keller's favorite color is Chuck Norris."}, + {"text": "If, by some incredible space-time paradox, Chuck Norris would ever fight himself, he'd win. Period."}, + {"text": "Chuck Norris is currently suing myspace for taking the name of what he calls everything around you."}, + {"text": "Science Fact: Roundhouse kicks are comprised primarily of an element called Chucktanium."}, + {"text": "Chuck Norris proved that we are alone in the universe. We weren't before his first space expedition."}, + {"text": "Superman once watched an episode of Walker, Texas Ranger. He then cried himself to sleep."}, + {"text": "Chuck Norris doesn't step on toes. Chuck Norris steps on necks."}, + {"text": "There is no such thing as global warming. Chuck Norris was cold, so he turned the sun up."}, + {"text": "A study showed the leading causes of death in the United States are: 1. Heart disease, 2. Chuck Norris, 3. Cancer"}, + {"text": "Chuck Norris did in fact, build Rome in a day."}, + {"text": "Along with his black belt, Chuck Norris often chooses to wear brown shoes. No one has DARED call him on it. Ever."}, + {"text": "Once you go Norris, you are physically unable to go back."}, + {"text": "Ninjas want to grow up to be just like Chuck Norris. But usually they grow up just to be killed by Chuck Norris."}, + {"text": "The last thing you hear before Chuck Norris gives you a roundhouse kick? No one knows because dead men tell no tales."}, + {"text": "Chuck Norris doesn't play god. Playing is for children."}, + {"text": "Chuck Norris is the only person in the world that can actually email a roundhouse kick."}, + {"text": "Chuck Norris won super bowls VII and VIII singlehandedly before unexpectedly retiring to pursue a career in ass-kicking."}, + {"text": "Chuck Norris can set ants on fire with a magnifying glass. At night."}, + {"text": "Some kids play Kick the can. Chuck Norris played Kick the keg."}, + {"text": "'Icy-Hot' is too weak for Chuck Norris. After a workout, Chuck Norris rubs his muscles down with liquid-hot MAGMA."}, + {"text": "Chuck Norris cannot love, he can only not kill."}, + {"text": "When Chuck Norris was a baby, he didn't suck his mother's breast. His mother served him whiskey, straight out of the bottle."}, + {"text": "According to Einstein's theory of relativity, Chuck Norris can actually roundhouse kick you yesterday."}, + {"text": "Chuck Norris once pulled out a single hair from his beard and skewered three men through the heart with it."}, + {"text": "Chuck Norris? favourite cut of meat is the roundhouse."}, + {"text": "Chuck Norris recently had the idea to sell his urine as a canned beverage. We know this beverage as Red Bull."}, + {"text": "If at first you don't succeed, you're not Chuck Norris."}, + {"text": "If Chuck Norris were a calendar, every month would be named Chucktober, and every day he'd kick your ass."}, + {"text": "# Chuck Norris's show is called Walker: Texas Ranger, because Chuck Norris doesn't run."}, + {"text": "Behind every successful man, there is a woman. Behind every dead man, there is Chuck Norris."}, + {"text": "Chuck Norris brushes his teeth with a mixture of iron shavings, industrial paint remover, and wood-grain alcohol."}, + {"text": "The easiest way to determine Chuck Norris' age is to cut him in half and count the rings."}, + {"text": "There is endless debate about the existence of the human soul. Well it does exist and Chuck Norris finds it delicious."}, + {"text": "Most boots are made for walkin'. Chuck Norris' boots ain't that merciful."}, + {"text": "Chuck Norris wears a live rattlesnake as a condom."}, + {"text": "Chuck Norris began selling the Total Gym as an ill-fated attempt to make his day-to-day opponents less laughably pathetic."}, + {"text": "Do you know why Baskin Robbins only has 31 flavors? Because Chuck Norris doesn't like Fudge Ripple."}, + {"text": "Chuck Norris was what Willis was talkin' about."}, + {"text": "Google won't search for Chuck Norris because it knows you don't find Chuck Norris, he finds you."}, + {"text": "Chuck Norris can lead a horse to water AND make it drink."}, + {"text": "Nagasaki never had a bomb dropped on it. Chuck Norris jumped out of a plane and punched the ground"}, + {"text": "Chuck Norris destroyed the periodic table, because Chuck Norris only recognizes the element of surprise."}, + {"text": "It is believed dinosaurs are extinct due to a giant meteor. That's true if you want to call Chuck Norris a giant meteor."}, + {"text": "Chuck Norris shot the sheriff, but he round house kicked the deputy."}, + {"text": "That's not Chuck Norris doing push-ups -- that's Chuck Norris moving the Earth away from the path of a deadly asteroid."}, + {"text": "Chuck Norris can judge a book by its cover."}, + {"text": "How many Chuck Norris' does it take to change a light bulb? None, Chuck Norris prefers to kill in the dark."}, + {"text": "Crime does not pay - unless you are an undertaker following Walker, Texas Ranger, on a routine patrol."}, + {"text": "Chuck Norris invented the internet? just so he had a place to store his porn."}, + {"text": "Chuck Norris does not own a house. He walks into random houses and people move."}, + {"text": "It is better to give than to receive. This is especially true of a Chuck Norris roundhouse kick."}, + {"text": "Chuck Norris is the only person to ever win a staring contest against Ray Charles and Stevie Wonder."}, + {"text": "Industrial logging isn't the cause of deforestation. Chuck Norris needs toothpicks."}, + {"text": "Chuck Norris smells what the Rock is cooking... because the Rock is Chuck Norris' personal chef."}, + {"text": "Chuck Norris is the reason why Waldo is hiding."}, + {"text": "Chuck Norris does not eat. Food understands that the only safe haven from Chuck Norris' fists is inside his own body."}, + {"text": "One day Chuck Norris walked down the street with a massive erection. There were no survivors."}, + {"text": "Chuck Norris uses a night light. Not because Chuck Norris is afraid of the dark, but the dark is afraid of Chuck Norris."}, + {"text": "When Bruce Banner gets mad, he turns into the Hulk. When the Hulk gets mad, he turns into Chuck Norris."}, + {"text": "Chuck Norris once kicked a horse in the chin. Its descendants are known today as Giraffes."}, + {"text": "Sticks and stones may break your bones, but a Chuck Norris glare will liquefy your kidneys."}, + {"text": "Chuck Norris once went skydiving, but promised never to do it again. One Grand Canyon is enough."}, + {"text": "In a fight between Batman and Darth Vader, the winner would be Chuck Norris."}, + {"text": "Everybody loves Raymond. Except Chuck Norris."}, + {"text": "Chuck Norris got his drivers license at the age of 16. Seconds."}, + {"text": "Chuck Norris can win at solitaire with only 18 cards."}, + {"text": "Chuck Norris once shat blood - the blood of 11,940 natives he had killed and eaten."}, + {"text": "The truth will set you free. Unless Chuck Norris has you, in which case, forget it buddy!"}, + {"text": "Chuck Norris doesn't look both ways before he crosses the street... he just roundhouses any cars that get too close."}, + {"text": "How many roundhouse kicks does it take to get to the center of a tootsie pop? Just one. From Chuck Norris."}, + {"text": "Chuck Norris doesnt wear a watch, HE decides what time it is."}, + {"text": "When Chuck Norris does division, there are no remainders."}, + {"text": "Never look a gift Chuck Norris in the mouth, because he will bite your damn eyes off."}, + {"text": "Chuck Norris? roundhouse kick is so powerful, it can be seen from outer space by the naked eye."}, + {"text": "Ozzy Osbourne bites the heads off of bats. Chuck Norris bites the heads off of Siberian Tigers."}, + {"text": "He who lives by the sword, dies by the sword. He who lives by Chuck Norris, dies by the roundhouse kick."}, + {"text": "The best-laid plans of mice and men often go awry. Even the worst-laid plans of Chuck Norris come off without a hitch."}, + {"text": "Chuck Norris can taste lies."}, + {"text": "One time, Chuck Norris accidentally stubbed his toe. It destroyed the entire state of Ohio."}, + {"text": "Little Miss Muffet sat on her tuffet, until Chuck Norris roundhouse kicked her into a glacier."}, + {"text": "Chuck Norris can blow bubbles with beef jerky."}, + {"text": "Chuck Norris does, in fact, live in a round house."}, + {"text": "When Chuck Norris works out on the Total Gym, the Total Gym feels like it's been raped."}, + {"text": "Chuck Norris can skeletize a cow in two minutes."}, + {"text": "The only sure things are Death and Taxes and when Chuck Norris goes to work for the IRS, they'll be the same thing."}, + {"text": "Chuck Norris' first job was as a paperboy. There were no survivors."}, + {"text": "With the rising cost of gasoline, Chuck Norris is beginning to worry about his drinking habit."}, + {"text": "The square root of Chuck Norris is pain. Do not try to square Chuck Norris, the result is death."}, + {"text": "To be or not to be? That is the question. The answer? Chuck Norris."}, + {"text": "Chuck Norris has never been in a fight, ever. Do you call one roundhouse kick to the face a fight?"}, + {"text": "There are two types of people in the world... people that suck, and Chuck Norris."}, + {"text": "Chuck Norris never wet his bed as a child. The bed wet itself out of fear."}, + {"text": "Chuck Norris uses 8'x10' sheets of plywood as toilet paper."}, + {"text": "Noah was the only man notified before Chuck Norris relieved himself in the Atlantic Ocean."}, + {"text": "Chuck Norris eats steak for every single meal. Most times he forgets to kill the cow."}, + {"text": "The First Law of Thermodynamics states that energy can neither be created nor destroyed... unless it meets Chuck Norris."}, + {"text": "Fact: Chuck Norris doesn't consider it sex if the woman survives."}, + {"text": "Chuck Norris knows everything there is to know - Except for the definition of mercy."}, + {"text": "Chuck Norris never has to wax his skis because they're always slick with blood."}, + {"text": "182,000 Americans die from Chuck Norris-related accidents every year."}, + {"text": "Paper beats rock, rock beats scissors, and scissors beats paper, but Chuck Norris beats all 3 at the same time."}, + {"text": "Jesus can walk on water, but Chuck Norris can walk on Jesus."}, + {"text": "All roads lead to Chuck Norris. And by the transitive property, a roundhouse kick to the face."}, + {"text": "July 4th is Independence day. And the day Chuck Norris was born. Coincidence? I think not."}, + {"text": "Chuck Norris was once in a knife fight, and the knife lost."}, + {"text": "If you work in an office with Chuck Norris, don't ask him for his three-hole-punch."}, + {"text": "The First rule of Chuck Norris is: you do not talk about Chuck Norris."}, + {"text": "When Chuck Norris plays Monopoly, it affects the actual world economy."}, + {"text": "Chuck Norris drinks napalm to quell his heartburn."}, + {"text": "As an infant, Chuck Norris' parents gave him a toy hammer. He gave the world Stonehenge."}, + {"text": "Chuck Norris once ordered a steak in a restaurant. The steak did what it was told."}, + {"text": "There are only two things that can cut diamonds: other diamonds, and Chuck Norris."}, + {"text": "President Roosevelt once rode his horse 100 miles. Chuck Norris carried his the same distance in half the time."}, + {"text": "Chuck Norris qualified with a top speed of 324 mph at the Daytona 500, without a car."}, + {"text": "Chuck Norris likes his coffee half and half: half coffee grounds, half wood-grain alcohol."}, + {"text": "Chuck Norris uses tabasco sauce instead of visine."}, + {"text": "Chuck Norris' credit cards have no limit. Last weekend, he maxed them out."}, + {"text": "Think of a hot woman. Chuck Norris did her."}, + {"text": "Chuck Norris sleeps with a pillow under his gun."}, + {"text": "Chuck Norris doesn't chew gum. Chuck Norris chews tin foil."}, + {"text": "Aliens DO indeed exist. They just know better than to visit a planet that Chuck Norris is on."}, + {"text": "Some people ask for a Kleenex when they sneeze, Chuck Norris asks for a body bag."}, + {"text": "There?s an order to the universe: space, time, Chuck Norris.... Just kidding, Chuck Norris is first."}, + {"text": "Chuck Norris doesn't see dead people. He makes people dead."}, + {"text": "For undercover police work, Chuck Norris pins his badge underneath his shirt, directly into his chest."}, + {"text": "We live in an expanding universe. All of it is trying to get away from Chuck Norris."}, + {"text": "The word 'Kill' was invented by Chuck Norris. Other words were 'Die', 'Beer', and 'What'."}, + {"text": "Chuck Norris is his own line at the DMV."}, + {"text": "Two wrongs don't make a right. Unless you're Chuck Norris. Then two wrongs make a roundhouse kick to the face."}, + {"text": "Who let the dogs out? Chuck Norris let the dogs out... and then roundhouse kicked them through an Oldsmobile."}, + {"text": "When Chuck Norris goes to out to eat, he orders a whole chicken, but he only eats its soul."}, + {"text": "Chuck Norris has never won an Academy Award for acting... because he's not acting."}, + {"text": "If Chuck Norris wants your opinion, he'll beat it into you."}, + {"text": "Not everyone that Chuck Norris is mad at gets killed. Some get away. They are called astronauts."}, + {"text": "Godzilla is a Japanese rendition of Chuck Norris' first visit to Tokyo."}, + {"text": "They once made a Chuck Norris toilet paper, but there was a problem-- It wouldn't take shit from anybody."}, + {"text": "Chuck Norris once rode a nine foot grizzly bear through an automatic car wash, instead of taking a shower."}, + {"text": "Chuck Norris' sperm can be seen with the naked eye. Each one is the size of a quarter."}, + {"text": "Chuck Norris doesn't daydream. He's too busy giving other people nightmares."}, + {"text": "There are no such things as tornados. Chuck Norris just hates trailer parks."}, + {"text": "Chuck Norris' penis is a third degree blackbelt, and an honorable 32nd-degree mason."}, + {"text": "Chuck Norris once participated in the running of the bulls. He walked."}, + {"text": "The Drummer for Def Leppard's only got one arm. Chuck Norris needed a back scratcher."}, + {"text": "Chuck Norris once rode a bull, and nine months later it had a calf."}, + {"text": "For Spring Break '05, Chuck Norris drove to Madagascar, riding a chariot pulled by two electric eels."}, + {"text": "Chuck Norris has banned rainbows from the state of North Dakota."}, + {"text": "Divide Chuck Norris by zero and you will in fact get one........one bad-ass that is."}, + {"text": "TNT was originally developed by Chuck Norris to cure indigestion."}, + {"text": "Chuck Norris runs on batteries. Specifically, Die Hards."}, + {"text": "Chuck Norris will never have a heart attack. His heart isn't nearly foolish enough to attack him."}, + {"text": "Only Chuck Norris can prevent forest fires."}, + {"text": "When Chuck Norris makes a burrito, its main ingredient is real toes."}, + {"text": "Chuck Norris is not Irish. His hair is soaked in the blood of his victims."}, + {"text": "They say curiosity killed the cat. This is false. Chuck Norris killed the cat. Every single one of them."}, + {"text": "There is no such thing as a lesbian, just a woman who has never met Chuck Norris."}, + {"text": "Chuck Norris crossed the road. No one has ever dared question his motives."}, + {"text": "One time, at band camp, Chuck Norris ate a percussionist."}, + {"text": "Love does not hurt. Chuck Norris does."}, + {"text": "Chuck Norris once round-house kicked a salesman. Over the phone."}, + {"text": "The pen is mightier than the sword, but only if the pen is held by Chuck Norris."}, + {"text": "Chuck Norris knows the last digit of pi."}, + {"text": "The air around Chuck Norris is always a balmy 78 degrees."}, + {"text": "When Chuck Norris wants an egg, he cracks open a chicken."}, + {"text": "Chuck Norris plays racquetball with a waffle iron and a bowling ball."}, + {"text": "Chuck Norris doesn't believe in ravioli. He stuffs a live turtle with beef and smothers it in pig's blood."}, + {"text": "Count from one to ten. That's how long it would take Chuck Norris to kill you...Forty seven times."}, + {"text": "Chuck Norris is not Politically Correct. He is just Correct. Always."}, + {"text": "Mr. T pities the fool. Chuck Norris rips the fool's head off."}, + {"text": "Chuck Norris had to stop washing his clothes in the ocean. The tsunamis were killing people."}, + {"text": "Chuck Norris has volunteered to remain on earth after the Rapture; he will spend his time fighting the Anti-Christ."}, + {"text": "Chuck Norris is the only known mammal in history to have an opposable thumb. On his penis."}, + {"text": "Chuck Norris' favorite cereal is Kellogg's Nails 'N' Gravel."}, + {"text": "Chuck Norris does not wear a condom. Because there is no such thing as protection from Chuck Norris."}, + {"text": "Rules of fighting: 1) Don't bring a knife to a gun fight. 2) Don't bring a gun to a Chuck Norris fight."}, + {"text": "Chuck Norris is the only man who has, literally, beaten the odds. With his fists."}, + {"text": "Chuck Norris wipes his ass with chain mail and sandpaper."}, + {"text": "Chuck Norris likes his ice like he likes his skulls: crushed."}, + {"text": "Chuck Norris can kick through all 6 degrees of separation, hitting anyone, anywhere, in the face, at any time."}, + {"text": "Most tough men eat nails for breakfast. Chuck Norris does all of his grocery shopping at Home Depot."}, + {"text": "Everything King Midas touches turnes to gold. Everything Chuck Norris touches turns up dead."}, + {"text": "When Chuck Norris throws exceptions, it's across the room."}, + {"text": "All arrays Chuck Norris declares are of infinite size, because Chuck Norris knows no bounds."}, + {"text": "Chuck Norris doesn't have disk latency because the hard drive knows to hurry the hell up."}, + {"text": "Chuck Norris writes code that optimizes itself."}, + {"text": "Chuck Norris can't test for equality because he has no equal."}, + {"text": "Chuck Norris doesn't need garbage collection because he doesn't call .Dispose(), he calls .DropKick()."}, + {"text": "Chuck Norris's first program was kill -9."}, + {"text": "Chuck Norris burst the dot com bubble."}, + {"text": "All browsers support the hex definitions #chuck and #norris for the colors black and blue."}, + {"text": "MySpace actually isn't your space, it's Chuck's (he just lets you use it)."}, + {"text": "Chuck Norris can write infinite recursion functions and have them return."}, + {"text": "Chuck Norris can solve the Towers of Hanoi in one move."}, + {"text": "The only pattern Chuck Norris knows is God Object."}, + {"text": "Chuck Norris finished World of Warcraft."}, + {"text": "Project managers never ask Chuck Norris for estimations... ever."}, + {"text": "Chuck Norris doesn't use web standards as the web will conform to him."}, + {"text": "Whiteboards are white because Chuck Norris scared them that way."}, + {"text": "Chuck Norris can delete the Recycling Bin."}, + {"text": "Chuck Norris can unit test entire applications with a single assert."}, + {"text": "Chuck Norris doesn't bug hunt as that signifies a probability of failure, he goes bug killing."}, + {"text": "Chuck Norris's keyboard doesn't have a Ctrl key because nothing controls Chuck Norris."}, + {"text": "Chuck Norris doesn't need a debugger, he just stares down the bug until the code confesses."}, + {"text": "Chuck Norris can access private methods."}, + {"text": "The class object inherits from Chuck Norris"}, + {"text": "Bill Gates thinks he's Chuck Norris. Chuck Norris actually laughed. Once."}, + {"text": "No statement can catch the ChuckNorrisException."}, + {"text": "Chuck Norris can write multi-threaded applications with a single thread."}, + {"text": "Chuck Norris doesn't need to use AJAX because pages are too afraid to postback anyways."}, + {"text": "Chuck Norris doesn't use reflection, reflection asks politely for his help."}, + {"text": "There is no Esc key on Chuck Norris' keyboard, because no one escapes Chuck Norris."}, + {"text": "Chuck Norris can binary search unsorted data."}, + {"text": "Chuck Norris doesn't needs try-catch, exceptions are too afraid to raise."}, + {"text": "Chuck Norris went out of an infinite loop."}, + {"text": "If Chuck Norris writes code with bugs, the bugs fix themselves."}, + {"text": "Chuck Norris hosting is 101% uptime guaranteed."}, + {"text": "Chuck Norris's keyboard has the Any key."}, + {"text": "Chuck Norris can access the DB from the UI."}, + {"text": "Chuck Norris' programs never exit, they terminate."}, + {"text": "Chuck Norris protocol design method has no status, requests or responses, only commands."}, + {"text": "Chuck Norris programs occupy 150% of CPU, even when they are not executing."}, + {"text": "Chuck Norris programs do not accept input."}, + {"text": "Chuck Norris doesn't need an OS."}, + {"text": "Chuck Norris can compile syntax errors."}, + {"text": "Every SQL statement that Chuck Norris codes has an implicit COMMIT in its end."}, + {"text": "Chuck Norris does not code in cycles, he codes in strikes."}, + {"text": "Chuck Norris doesn't use a computer because a computer does everything slower than Chuck Norris."}, + {"text": "Chuck Norris compresses his files by doing a flying round house kick to the hard drive."}, + {"text": "Chuck Norris doesn't cheat death. He wins fair and square."}, + {"text": "Chuck Norris once won a game of connect four in 3 moves."}, + {"text": "Chuck Norris can do a wheelie on a unicycle."}, + {"text": "Chuck Norris can win in a game of Russian roulette with a fully loaded gun."}, + {"text": "No one has ever pair-programmed with Chuck Norris and lived to tell about it."}, + {"text": "No one has ever spoken during review of Chuck Norris' code and lived to tell about it."}, + {"text": "Chuck Norris doesn't use Oracle, he is the Oracle."}, + {"text": "Jesus can walk on water, but Chuck Norris can swim through land."}, + {"text": "A diff between your code and Chuck Norris's is infinite."}, + {"text": "The Chuck Norris Eclipse plugin made alien contact."}, + {"text": "Chuck Norris is the ultimate mutex, all threads fear him."}, + {"text": "Don't worry about tests, Chuck Norris's test cases cover your code too."}, + {"text": "When Chuck Norris break the build, you can't fix it, because there is not a single line of code left."}, + {"text": "Chuck Norris types with one finger. He points it at the keyboard and the keyboard does the rest."}, + {"text": "Chuck Norris's brain waves are suspected to be harmful to cell phones."}, + {"text": "Chuck Norris does infinite loops in 4 seconds."}, + {"text": "Product Owners never ask Chuck Norris for more features. They ask for mercy."}, + {"text": "Chuck Norris killed two stones with one bird."}, + {"text": "Chuck Norris can speak Braille."}, + {"text": "Chuck Norris knows the value of NULL, and he can sort by it too."}, + {"text": "Chuck Norris can install a 64 bit OS on 32 bit machines."}, + {"text": "Chuck Norris can write to an output stream."}, + {"text": "Chuck Norris can read from an input stream."}, + {"text": "Chuck Norris never has to build his program to machine code. Machines have learnt to interpret Chuck Norris code."}, + {"text": "Chuck Norris' unit tests don't run. They die."}, + {"text": "Chuck Norris sits at the stand-up."}, + {"text": "Chuck Norris doesn't need an account. He just logs in."}, + {"text": "Code runs faster when Chuck Norris watches it."}, + {"text": "Chuck Norris does not need a watch, he decides what time it is."}, + {"text": "Chuck Norris already went to Moon and Mars, that's why there are no signs of life."}, + {"text": "Once a police officer caught Chuck Norris, the cop was lucky enough to escape with a warning."}, + {"text": "Chuck Norris knows Victoria's secret."}, + {"text": "Dark spots on the Moon are the result of Chuck Norris' shooting practice."}, + {"text": "Chuck Norris died before 20 years, Death doesn't have the courage to tell him yet."}, + {"text": "There is no April 1st in Chuck Norris' calendar, because no one can fool him."}, + {"text": "Chuck Norris can make onions cry."}, + {"text": "Chuck Norris can watch the radio."}, + {"text": "Chuck Norris built the hospital he was born in."}, + {"text": "Once Chuck Norris signed a cheque and the bank bounced."}, + {"text": "Chuck Norris can drown a fish."}, + {"text": "Once death had a near Chuck Norris experience."}, + {"text": "Once Chuck Norris and Superman had a competition. The loser had to wear his underwear over his pants."}, + {"text": "Chuck Norris can make fire using two ice cubes."}, + {"text": "Chuck Norris tears can cure the cancer, but the sad thing is Chuck Norris never cries."}, + {"text": "Chuck Norris can remember the future."}, + {"text": "Chuck Norris doesn't age, because time cannot keep up with him."}, + {"text": "Ghosts are actually caused by Chuck Norris killing people faster than Death can process them."}, + {"text": "Chuck Norris doesn't need a keyboard he tells the computer to write something and it does."}, + {"text": "Chuck Norris plays pool with comets and astroids. He shoots them into black holes."}, + {"text": "There was never anything wrong with Achilles' heel until he got mad and decided to kick Chuck Norris."}, + {"text": "Tornados occur when Chuck Norris sneezes."}, + {"text": "Chuck Norris once sold eBay to eBay on eBay."}, + {"text": "Chuck Norris can build a snowman out of rain."}, + {"text": "Chuck Norris made the sun by rubbing his hands together."}, + {"text": "Chuck Norris puts sunglasses on to protect the sun from his eyes."}, + {"text": "Chuck Norris can lock a safe and keep the key inside it."} +] diff --git a/app/routers/joke.py b/app/routers/joke.py new file mode 100644 index 00000000..07b7b453 --- /dev/null +++ b/app/routers/joke.py @@ -0,0 +1,12 @@ +from fastapi import APIRouter, Depends, Request +from app.internal import jokes +from sqlalchemy.orm import Session +from app.dependencies import get_db + + +router = APIRouter() + + +@router.get("/joke") +async def joke(request: Request, db: Session = Depends(get_db)): + return jokes.get_a_joke(db) diff --git a/app/static/joke.js b/app/static/joke.js new file mode 100644 index 00000000..4cfd223b --- /dev/null +++ b/app/static/joke.js @@ -0,0 +1,14 @@ +function makejoke() { + fetch('/joke') + .then(response => response.json()) + .then(data => Swal.fire(data.text)); +} + + +function addEventsAfterPageLoaded() { + const element = document.getElementById("a-joke"); + element.addEventListener("click", makejoke, false); +} + + +document.addEventListener("DOMContentLoaded", addEventsAfterPageLoaded); \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html index c7431c71..95ebdf52 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -55,6 +55,9 @@ + @@ -75,6 +78,9 @@ + + + \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 4923a614..1d8a21d2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,6 +20,7 @@ 'smtpdfix', 'tests.quotes_fixture', 'tests.zodiac_fixture', + 'tests.jokes_fixture', 'tests.comment_fixture', ] diff --git a/tests/jokes_fixture.py b/tests/jokes_fixture.py new file mode 100644 index 00000000..d7e3258c --- /dev/null +++ b/tests/jokes_fixture.py @@ -0,0 +1,20 @@ +import pytest +from sqlalchemy.orm import Session + +from app.database.models import Joke +from app.internal.utils import create_model, delete_instance + + +def add_joke(session: Session, id_joke: int, text: str) -> Joke: + joke = create_model(session, Joke, id=id_joke, text=text) + yield joke + delete_instance(session, joke) + + +@pytest.fixture +def joke(session: Session) -> Joke: + yield from add_joke( + session=session, + id_joke=1, + text='Chuck Norris can slam a revolving door.', + ) diff --git a/tests/test_joke.py b/tests/test_joke.py new file mode 100644 index 00000000..df165802 --- /dev/null +++ b/tests/test_joke.py @@ -0,0 +1,17 @@ +from app.database.models import Joke +from app.internal import jokes + + +def get_jokes_amount(session): + return session.query(Joke).count() + + +def test_get_a_joke(session, joke): + assert jokes.get_a_joke(session).text == joke.text + + +def test_jokes_not_load_twice_to_db(session): + jokes.get_a_joke(session) + first_load_amount = get_jokes_amount(session) + jokes.get_a_joke(session) + assert first_load_amount == get_jokes_amount(session) diff --git a/tests/test_joke_route.py b/tests/test_joke_route.py new file mode 100644 index 00000000..6b2e3d02 --- /dev/null +++ b/tests/test_joke_route.py @@ -0,0 +1,4 @@ +def test_joke(client, session): + resp = client.get('/joke') + assert resp.ok + assert resp.json From 919f2b198ecec2a8222f4bdffc24670122d3786d Mon Sep 17 00:00:00 2001 From: Gonzom Date: Sat, 20 Feb 2021 03:00:28 +0200 Subject: [PATCH 04/46] fix: add extend-ignore = E203 (#324) * fix: add extend-ignore = E203 This is needed as we are now using black code style which may raise E203. * fix: grammar --- tox.ini | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 77e3612e..34198fa9 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -skipsdist=True +skipsdist = True envlist = cov, flake8 [testenv] @@ -24,7 +24,7 @@ commands = flake8 tests -# pytest Configuration +# Pytest configuration [pytest] junit_family = xunit2 testpaths = tests @@ -32,8 +32,12 @@ filterwarnings = ignore:.*'collections'.*'collections.abc'.*:DeprecationWarning ignore:Task.all_tasks() is deprecated, use asyncio.all_tasks().*:PendingDeprecationWarning -# Flake8 Configuration + +# Flake8 configuration [flake8] # gettext() adds _() to the global namespace. This lets flake recognize it. builtins = _, + +# https://github.com/psf/black/blob/master/docs/the_black_code_style.md#slices +extend-ignore = E203 From b8f94b2060a3e2ae7808d0a89a97dfa183103c81 Mon Sep 17 00:00:00 2001 From: Ode Date: Sat, 20 Feb 2021 13:00:15 +0200 Subject: [PATCH 05/46] Resolved #325: Profile Page (#326) * fixed bugs in the user profile page * class and id name fixes * dev: fixed an issue in edit_event_details_tab.html due to conflicts * dev: fixed an issue in calendar_day_view.html due to conflicts --- app/templates/calendar_day_view.html | 32 +- app/templates/dayview.html | 75 ---- .../partials/edit_event_details_tab.html | 86 ---- .../event/edit_event_details_tab.html | 144 ++++--- .../partials/user_profile/middle_content.html | 13 + .../middle_content/event_card.html | 15 + .../middle_content/event_settings.html | 10 + .../middle_content/update_event_modal.html | 16 + .../partials/user_profile/sidebar_left.html | 12 + .../sidebar_left/daily_horoscope.html | 13 + .../sidebar_left/features_card.html | 33 ++ .../sidebar_left/profile_card.html | 8 + .../sidebar_left/profile_card/modals.html | 17 + .../modals/calendar_privacy_modal.html | 22 + .../modals/description_modal.html | 20 + .../profile_card/modals/email_modal.html | 20 + .../profile_card/modals/name_modal.html | 20 + .../profile_card/modals/telegram_modal.html | 21 + .../modals/upload_photo_modal.html | 21 + .../profile_card/profile_update_menu.html | 46 +++ .../profile_card/user_details.html | 14 + .../partials/user_profile/sidebar_right.html | 15 + .../user_profile/sidebar_right/meetups.html | 7 + .../user_profile/sidebar_right/new_card.html | 7 + .../user_profile/sidebar_right/new_card2.html | 7 + .../sidebar_right/on_this_day.html | 5 + app/templates/profile.html | 376 +----------------- 27 files changed, 472 insertions(+), 603 deletions(-) delete mode 100644 app/templates/dayview.html delete mode 100644 app/templates/event/partials/edit_event_details_tab.html create mode 100644 app/templates/partials/user_profile/middle_content.html create mode 100644 app/templates/partials/user_profile/middle_content/event_card.html create mode 100644 app/templates/partials/user_profile/middle_content/event_settings.html create mode 100644 app/templates/partials/user_profile/middle_content/update_event_modal.html create mode 100644 app/templates/partials/user_profile/sidebar_left.html create mode 100644 app/templates/partials/user_profile/sidebar_left/daily_horoscope.html create mode 100644 app/templates/partials/user_profile/sidebar_left/features_card.html create mode 100644 app/templates/partials/user_profile/sidebar_left/profile_card.html create mode 100644 app/templates/partials/user_profile/sidebar_left/profile_card/modals.html create mode 100644 app/templates/partials/user_profile/sidebar_left/profile_card/modals/calendar_privacy_modal.html create mode 100644 app/templates/partials/user_profile/sidebar_left/profile_card/modals/description_modal.html create mode 100644 app/templates/partials/user_profile/sidebar_left/profile_card/modals/email_modal.html create mode 100644 app/templates/partials/user_profile/sidebar_left/profile_card/modals/name_modal.html create mode 100644 app/templates/partials/user_profile/sidebar_left/profile_card/modals/telegram_modal.html create mode 100644 app/templates/partials/user_profile/sidebar_left/profile_card/modals/upload_photo_modal.html create mode 100644 app/templates/partials/user_profile/sidebar_left/profile_card/profile_update_menu.html create mode 100644 app/templates/partials/user_profile/sidebar_left/profile_card/user_details.html create mode 100644 app/templates/partials/user_profile/sidebar_right.html create mode 100644 app/templates/partials/user_profile/sidebar_right/meetups.html create mode 100644 app/templates/partials/user_profile/sidebar_right/new_card.html create mode 100644 app/templates/partials/user_profile/sidebar_right/new_card2.html create mode 100644 app/templates/partials/user_profile/sidebar_right/on_this_day.html diff --git a/app/templates/calendar_day_view.html b/app/templates/calendar_day_view.html index 745864de..ba681c9f 100644 --- a/app/templates/calendar_day_view.html +++ b/app/templates/calendar_day_view.html @@ -20,19 +20,24 @@ {{day}} / {{month}} {% endif %} +
+ {% for event in all_day_events %} +

{{ event.title }}

+ {% endfor %} +
- {% for hour in range(25)%} -
-
- {% if view == 'day'%} - {% set hour = hour|string() %} - {{hour.zfill(2)}}:00 - {% endif %} -
-
-
- {% endfor %} + {% for hour in range(25)%} +
+
+ {% if view == 'day'%} + {% set hour = hour|string() %} + {{hour.zfill(2)}}:00 + {% endif %} +
+
+
+ {% endfor %}
{% for event, attr in events %} @@ -56,14 +61,15 @@
{% endfor %}
- + {% if view == 'day'%} {% endif %} -{% if view == 'day'%}
{% endif %} +{% if view == 'day'%} +
{% endif %} diff --git a/app/templates/dayview.html b/app/templates/dayview.html deleted file mode 100644 index 8a5a2a16..00000000 --- a/app/templates/dayview.html +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - dayview - - -
- {% if view == 'day' %} - - {{month}} - {{day}} - {% if zodiac %} -
- zodiac sign -
- {% endif %} - {% else %} - {{day}} / {{month}} - {% endif %} -
-
- {% for event in all_day_events %} -

{{ event.title }}

- {% endfor %} -
-
-
- {% if view == 'day'%} - {% for hour in range(24)%} -
- {% set hour = hour|string() %} - {{hour.zfill(2)}}:00 -
- {% endfor %} - {% endif %} -
-
- {% for event, attr in events %} -
-

{{ event.title }}

- {% if attr.total_time_visible %} -

{{attr.total_time}}

- {% endif %} -
- {% endfor %} -
-
- {% for i in range(25)%} -
---
- {% endfor %} -
-
- - {% for event, attr in events %} -
- - - -
- {% endfor %} -
-
-
-
-
Have a nice day
- - - - diff --git a/app/templates/event/partials/edit_event_details_tab.html b/app/templates/event/partials/edit_event_details_tab.html deleted file mode 100644 index 871b1eab..00000000 --- a/app/templates/event/partials/edit_event_details_tab.html +++ /dev/null @@ -1,86 +0,0 @@ -
- - -
- -
- - -
-
-
-
- - - -
-
- - - -
-
- - - -
-
-
- - - - -
-
- - - - -
-
- - - -
-
- -
-
- - -
-
-
- - -
-
- - - - - - - -
-
diff --git a/app/templates/partials/calendar/event/edit_event_details_tab.html b/app/templates/partials/calendar/event/edit_event_details_tab.html index 4e27dadf..e9fde613 100644 --- a/app/templates/partials/calendar/event/edit_event_details_tab.html +++ b/app/templates/partials/calendar/event/edit_event_details_tab.html @@ -1,75 +1,95 @@
- - -
-
- -
- - -
-
-
-
- - - -
-
- - - -
-
- - - -
+ + +
+
+
+ + +
+
+ + + +
+
+ + + +
+
+ + + +
+
- - - - + + + +
+
- - - - + + + +
+
- - - + + +
+
- + +
+ +
+ +
+
-
- - -
-
- - - - -
+
+ + +
+ +
+ + + + + + + + + +
diff --git a/app/templates/partials/user_profile/middle_content.html b/app/templates/partials/user_profile/middle_content.html new file mode 100644 index 00000000..193dc351 --- /dev/null +++ b/app/templates/partials/user_profile/middle_content.html @@ -0,0 +1,13 @@ +
+
+
+ +
+ {% for event in events %} + + {% include 'partials/user_profile/middle_content/event_card.html' %} + + {% include 'partials/user_profile/middle_content/update_event_modal.html' %} + {% endfor %} +
+
\ No newline at end of file diff --git a/app/templates/partials/user_profile/middle_content/event_card.html b/app/templates/partials/user_profile/middle_content/event_card.html new file mode 100644 index 00000000..9c86560e --- /dev/null +++ b/app/templates/partials/user_profile/middle_content/event_card.html @@ -0,0 +1,15 @@ +
+
+ {{ gettext("Upcoming event on (date)", date=event.start) }} + + {% include 'partials/user_profile/middle_content/event_settings.html' %} +
+ +
+

+ The Event {{ event }} - description ... +

+
+ Last updated {{time}} ago +
+
\ No newline at end of file diff --git a/app/templates/partials/user_profile/middle_content/event_settings.html b/app/templates/partials/user_profile/middle_content/event_settings.html new file mode 100644 index 00000000..b9d7f048 --- /dev/null +++ b/app/templates/partials/user_profile/middle_content/event_settings.html @@ -0,0 +1,10 @@ + +
+ + + +
+
\ No newline at end of file diff --git a/app/templates/partials/user_profile/middle_content/update_event_modal.html b/app/templates/partials/user_profile/middle_content/update_event_modal.html new file mode 100644 index 00000000..a6966e28 --- /dev/null +++ b/app/templates/partials/user_profile/middle_content/update_event_modal.html @@ -0,0 +1,16 @@ + \ No newline at end of file diff --git a/app/templates/partials/user_profile/sidebar_left.html b/app/templates/partials/user_profile/sidebar_left.html new file mode 100644 index 00000000..92c48acb --- /dev/null +++ b/app/templates/partials/user_profile/sidebar_left.html @@ -0,0 +1,12 @@ +
+ + +{% include 'partials/user_profile/sidebar_left/profile_card.html' %} + + + {% include 'partials/user_profile/sidebar_left/features_card.html' %} + + + {% include 'partials/user_profile/sidebar_left/daily_horoscope.html' %} + +
\ No newline at end of file diff --git a/app/templates/partials/user_profile/sidebar_left/daily_horoscope.html b/app/templates/partials/user_profile/sidebar_left/daily_horoscope.html new file mode 100644 index 00000000..29bea347 --- /dev/null +++ b/app/templates/partials/user_profile/sidebar_left/daily_horoscope.html @@ -0,0 +1,13 @@ +
+
+ + +
+
\ No newline at end of file diff --git a/app/templates/partials/user_profile/sidebar_left/features_card.html b/app/templates/partials/user_profile/sidebar_left/features_card.html new file mode 100644 index 00000000..3015edcc --- /dev/null +++ b/app/templates/partials/user_profile/sidebar_left/features_card.html @@ -0,0 +1,33 @@ +
+
+ +

+ Explore more features +

+ + +
+
\ No newline at end of file diff --git a/app/templates/partials/user_profile/sidebar_left/profile_card.html b/app/templates/partials/user_profile/sidebar_left/profile_card.html new file mode 100644 index 00000000..b4dbaec2 --- /dev/null +++ b/app/templates/partials/user_profile/sidebar_left/profile_card.html @@ -0,0 +1,8 @@ +
+ + {% include 'partials/user_profile/sidebar_left/profile_card/profile_update_menu.html' %} + + {% include 'partials/user_profile/sidebar_left/profile_card/modals.html' %} + + {% include 'partials/user_profile/sidebar_left/profile_card/user_details.html' %} +
\ No newline at end of file diff --git a/app/templates/partials/user_profile/sidebar_left/profile_card/modals.html b/app/templates/partials/user_profile/sidebar_left/profile_card/modals.html new file mode 100644 index 00000000..cde0ca1e --- /dev/null +++ b/app/templates/partials/user_profile/sidebar_left/profile_card/modals.html @@ -0,0 +1,17 @@ + +{% include 'partials/user_profile/sidebar_left/profile_card/modals/name_modal.html' %} + + +{% include 'partials/user_profile/sidebar_left/profile_card/modals/email_modal.html' %} + + +{% include 'partials/user_profile/sidebar_left/profile_card/modals/description_modal.html' %} + + +{% include 'partials/user_profile/sidebar_left/profile_card/modals/upload_photo_modal.html' %} + + +{% include 'partials/user_profile/sidebar_left/profile_card/modals/telegram_modal.html' %} + + +{% include 'partials/user_profile/sidebar_left/profile_card/modals/calendar_privacy_modal.html' %} \ No newline at end of file diff --git a/app/templates/partials/user_profile/sidebar_left/profile_card/modals/calendar_privacy_modal.html b/app/templates/partials/user_profile/sidebar_left/profile_card/modals/calendar_privacy_modal.html new file mode 100644 index 00000000..b56b1572 --- /dev/null +++ b/app/templates/partials/user_profile/sidebar_left/profile_card/modals/calendar_privacy_modal.html @@ -0,0 +1,22 @@ + \ No newline at end of file diff --git a/app/templates/partials/user_profile/sidebar_left/profile_card/modals/description_modal.html b/app/templates/partials/user_profile/sidebar_left/profile_card/modals/description_modal.html new file mode 100644 index 00000000..ad13a645 --- /dev/null +++ b/app/templates/partials/user_profile/sidebar_left/profile_card/modals/description_modal.html @@ -0,0 +1,20 @@ + \ No newline at end of file diff --git a/app/templates/partials/user_profile/sidebar_left/profile_card/modals/email_modal.html b/app/templates/partials/user_profile/sidebar_left/profile_card/modals/email_modal.html new file mode 100644 index 00000000..49055e94 --- /dev/null +++ b/app/templates/partials/user_profile/sidebar_left/profile_card/modals/email_modal.html @@ -0,0 +1,20 @@ + \ No newline at end of file diff --git a/app/templates/partials/user_profile/sidebar_left/profile_card/modals/name_modal.html b/app/templates/partials/user_profile/sidebar_left/profile_card/modals/name_modal.html new file mode 100644 index 00000000..264c9c3a --- /dev/null +++ b/app/templates/partials/user_profile/sidebar_left/profile_card/modals/name_modal.html @@ -0,0 +1,20 @@ + \ No newline at end of file diff --git a/app/templates/partials/user_profile/sidebar_left/profile_card/modals/telegram_modal.html b/app/templates/partials/user_profile/sidebar_left/profile_card/modals/telegram_modal.html new file mode 100644 index 00000000..bf9be4fc --- /dev/null +++ b/app/templates/partials/user_profile/sidebar_left/profile_card/modals/telegram_modal.html @@ -0,0 +1,21 @@ + \ No newline at end of file diff --git a/app/templates/partials/user_profile/sidebar_left/profile_card/modals/upload_photo_modal.html b/app/templates/partials/user_profile/sidebar_left/profile_card/modals/upload_photo_modal.html new file mode 100644 index 00000000..d1e44547 --- /dev/null +++ b/app/templates/partials/user_profile/sidebar_left/profile_card/modals/upload_photo_modal.html @@ -0,0 +1,21 @@ + \ No newline at end of file diff --git a/app/templates/partials/user_profile/sidebar_left/profile_card/profile_update_menu.html b/app/templates/partials/user_profile/sidebar_left/profile_card/profile_update_menu.html new file mode 100644 index 00000000..df643cf1 --- /dev/null +++ b/app/templates/partials/user_profile/sidebar_left/profile_card/profile_update_menu.html @@ -0,0 +1,46 @@ +
+ +
\ No newline at end of file diff --git a/app/templates/partials/user_profile/sidebar_left/profile_card/user_details.html b/app/templates/partials/user_profile/sidebar_left/profile_card/user_details.html new file mode 100644 index 00000000..5f85416f --- /dev/null +++ b/app/templates/partials/user_profile/sidebar_left/profile_card/user_details.html @@ -0,0 +1,14 @@ +Profile image +
+
{{ user.full_name }}
+

+ + Settings + +

+
+

+ {{ user.description }} +

+
\ No newline at end of file diff --git a/app/templates/partials/user_profile/sidebar_right.html b/app/templates/partials/user_profile/sidebar_right.html new file mode 100644 index 00000000..c84dedb8 --- /dev/null +++ b/app/templates/partials/user_profile/sidebar_right.html @@ -0,0 +1,15 @@ +
+ + + {% include 'partials/user_profile/sidebar_right/meetups.html' %} + + + {% include 'partials/user_profile/sidebar_right/on_this_day.html' %} + + + {% include 'partials/user_profile/sidebar_right/new_card.html' %} + + + {% include 'partials/user_profile/sidebar_right/new_card2.html' %} + +
\ No newline at end of file diff --git a/app/templates/partials/user_profile/sidebar_right/meetups.html b/app/templates/partials/user_profile/sidebar_right/meetups.html new file mode 100644 index 00000000..bc9ee2c4 --- /dev/null +++ b/app/templates/partials/user_profile/sidebar_right/meetups.html @@ -0,0 +1,7 @@ +
+
+

+ {{ gettext("Explore MeetUps near you") }} +

+
+
\ No newline at end of file diff --git a/app/templates/partials/user_profile/sidebar_right/new_card.html b/app/templates/partials/user_profile/sidebar_right/new_card.html new file mode 100644 index 00000000..ff6350f7 --- /dev/null +++ b/app/templates/partials/user_profile/sidebar_right/new_card.html @@ -0,0 +1,7 @@ +
+
+

+ {{ gettext("Your Card") }} +

+
+
\ No newline at end of file diff --git a/app/templates/partials/user_profile/sidebar_right/new_card2.html b/app/templates/partials/user_profile/sidebar_right/new_card2.html new file mode 100644 index 00000000..20529588 --- /dev/null +++ b/app/templates/partials/user_profile/sidebar_right/new_card2.html @@ -0,0 +1,7 @@ +
+
+

+ Your Card +

+
+
\ No newline at end of file diff --git a/app/templates/partials/user_profile/sidebar_right/on_this_day.html b/app/templates/partials/user_profile/sidebar_right/on_this_day.html new file mode 100644 index 00000000..71b9fe42 --- /dev/null +++ b/app/templates/partials/user_profile/sidebar_right/on_this_day.html @@ -0,0 +1,5 @@ +
+
+ {% include "on_this_day.html" %} +
+
\ No newline at end of file diff --git a/app/templates/profile.html b/app/templates/profile.html index b12f25ee..900739ec 100644 --- a/app/templates/profile.html +++ b/app/templates/profile.html @@ -1,377 +1,19 @@ -{% extends "base.html" %} {% include "partials/calendar/event/text_editor_partial_head.html" %} {% block content %} +{% extends "base.html" %} +{% include "partials/calendar/event/text_editor_partial_head.html" %} +{% block content %}
-
- -
+ {% include 'partials/user_profile/sidebar_left.html' %} - -
- - - - - - - - - - - - - - - -
- - -
- {% for event in events %} - -
-
- {{ gettext("Upcoming event on (date)", date=event.start) }} - - -
- - - - -
-
- - -
-
-

- The Event {{ event }} - description ... -

-
- Last updated 3 mins ago -
-
-
-

- {{ gettext("The Event (event)", event=event.title) }} -

-
- {{ gettext("Last updated (time) ago") }} -
- - {% endfor %} -
- -
-
-
-
+ + {% include 'partials/user_profile/middle_content.html' %} + {% include 'partials/user_profile/sidebar_right.html' %} -
- - -
-
-

- {{ gettext("Explore MeetUps near you") }} -

-
- - -
-
-

- {{ gettext("Your Card") }} -

-
- - - -
-
-

- Your Card -

- -
-
- {% include "on_this_day.html" %} -
-
-
-
-
-
-
-{% include "partials/calendar/event/text_editor_partial_body.html" %} {% endblock content %} \ No newline at end of file +{% include "partials/calendar/event/text_editor_partial_body.html" %} +{% endblock content %} \ No newline at end of file From 44563e89cf6374016cdb6f907f2ed9db2d12f55e Mon Sep 17 00:00:00 2001 From: ADI-projects30 <70817810+ADI-projects30@users.noreply.github.com> Date: Sat, 20 Feb 2021 13:50:32 +0200 Subject: [PATCH 06/46] feat: add color to category (#228) --- app/routers/categories.py | 65 +++++++++++-------- app/routers/event.py | 11 +++- app/static/categories_style.css | 32 +++++++++ app/static/event/eventedit.css | 6 +- app/templates/base.html | 3 + app/templates/categories.html | 30 +++++++++ .../event/edit_event_details_tab.html | 12 ++++ app/templates/partials/index/navigation.html | 3 + tests/client_fixture.py | 8 ++- tests/test_categories.py | 52 +++++++++------ 10 files changed, 171 insertions(+), 51 deletions(-) create mode 100644 app/static/categories_style.css create mode 100644 app/templates/categories.html diff --git a/app/routers/categories.py b/app/routers/categories.py index 45522ccd..525350ea 100644 --- a/app/routers/categories.py +++ b/app/routers/categories.py @@ -1,15 +1,18 @@ import re -from typing import Any, Dict, List +from typing import Dict, List -from fastapi import APIRouter, Depends, HTTPException, Request +from fastapi import APIRouter, Depends, Form, HTTPException, Request from pydantic import BaseModel from sqlalchemy.exc import IntegrityError, SQLAlchemyError from sqlalchemy.orm import Session from starlette import status from starlette.datastructures import ImmutableMultiDict +from starlette.templating import _TemplateResponse + from app.database.models import Category from app.dependencies import get_db +from app.dependencies import templates HEX_COLOR_FORMAT = r"^(?:[0-9a-fA-F]{3}){1,2}$" @@ -35,7 +38,7 @@ class Config: # TODO(issue#29): get current user_id from session -@router.get("/", include_in_schema=False) +@router.get("/user", include_in_schema=False) def get_categories(request: Request, db_session: Session = Depends(get_db)) -> List[Category]: if validate_request_params(request.query_params): @@ -46,39 +49,39 @@ def get_categories(request: Request, f"unallowed params.") -@router.get("/list") -def get_all_categories( - db_session: Session = Depends(get_db)) -> List[Category]: - return db_session.query(Category).all() - - @router.get("/") -def get_categories_by_user_id( - user_id: int, db_session: Session = Depends(get_db) -) -> List[Category]: - return get_user_categories(db_session, user_id) +def category_color_insert(request: Request) -> _TemplateResponse: + return templates.TemplateResponse("categories.html", { + "request": request + }) # TODO(issue#29): get current user_id from session @router.post("/") -async def set_category(category: CategoryModel, - db_sess: Session = Depends(get_db)) -> Dict[str, Any]: - if not validate_color_format(category.color): +async def set_category(request: Request, + name: str = Form(None), + color: str = Form(None), + db_sess: Session = Depends(get_db)): + + message = "" + user_id = 1 # until issue#29 will get current user_id from session + color = color.replace('#', '') + if not validate_color_format(color): raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Color {category.color} if not from " + detail=f"Color {color} if not from " f"expected format.") try: - cat = Category.create(db_sess, - name=category.name, - color=category.color, - user_id=category.user_id) + Category.create(db_sess, name=name, color=color, user_id=user_id) except IntegrityError: db_sess.rollback() - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, - detail=f"category is already exists for " - f"user {category.user_id}.") - else: - return {"category": cat.to_dict()} + message = "Category already exists" + return templates.TemplateResponse("categories.html", + dictionary_req(request, message, + name, color)) + message = f"Congratulation! You have created a new category: {name}" + return templates.TemplateResponse("categories.html", + dictionary_req(request, message, + name, color)) def validate_request_params(query_params: ImmutableMultiDict) -> bool: @@ -120,3 +123,13 @@ def get_user_categories(db_session: Session, return [] else: return categories + + +def dictionary_req(request, message, name, color) -> Dict: + dictionary_tamplates = { + "request": request, + "message": message, + "name": name, + "color": color, + } + return dictionary_tamplates diff --git a/app/routers/event.py b/app/routers/event.py index 124ff64e..2557a4cc 100644 --- a/app/routers/event.py +++ b/app/routers/event.py @@ -23,6 +23,7 @@ from app.internal import comment as cmt from app.internal.emotion import get_emotion from app.internal.utils import create_model, get_current_user +from app.routers.categories import get_user_categories EVENT_DATA = Tuple[Event, List[Dict[str, str]], str] @@ -41,6 +42,7 @@ "category_id": (int, type(None)), } + router = APIRouter( prefix="/event", tags=["event"], @@ -80,8 +82,13 @@ async def create_event_api(event: EventModel, session=Depends(get_db)): @router.get("/edit", include_in_schema=False) @router.get("/edit") -async def eventedit(request: Request) -> Response: - return templates.TemplateResponse("eventedit.html", {"request": request}) +async def eventedit(request: Request, + db_session: Session = Depends(get_db)) -> Response: + user_id = 1 # until issue#29 will get current user_id from session + categories_list = get_user_categories(db_session, user_id) + return templates.TemplateResponse("eventedit.html", + {"request": request, + "categories_list": categories_list}) @router.post("/edit", include_in_schema=False) diff --git a/app/static/categories_style.css b/app/static/categories_style.css new file mode 100644 index 00000000..70afe5d7 --- /dev/null +++ b/app/static/categories_style.css @@ -0,0 +1,32 @@ + + +body { + color: #333; + margin: auto; + font: 1.2em / 1.2 Arial, Helvetica, sans-serif; +} + +h1 { + color: black; + font-size: 150%; + text-align: center; +} + +form { + text-align: center; +} + +input { + border-radius: 0.5em; + color: plum; + padding: 0.5em; +} + +input[type="color"].custom { + padding: 0; + border: none; + height: 1.875em; + width: 9.375em; + vertical-align: middle; + border-radius: 0.5em; +} \ No newline at end of file diff --git a/app/static/event/eventedit.css b/app/static/event/eventedit.css index a8a0e92e..9c5d3fda 100644 --- a/app/static/event/eventedit.css +++ b/app/static/event/eventedit.css @@ -29,12 +29,14 @@ form { .form_row, .form_row_start, -.form_row_end { +.form_row_end, +.user_categories { display: flex } .form_row_start, -.form_row_end { +.form_row_end, +.user_categories { flex: 1; } diff --git a/app/templates/base.html b/app/templates/base.html index 95ebdf52..a7699566 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -58,6 +58,9 @@ + + Create Categories + diff --git a/app/templates/categories.html b/app/templates/categories.html new file mode 100644 index 00000000..c70e8b1e --- /dev/null +++ b/app/templates/categories.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} + +{% block head %} +{{ super() }} + + +{% endblock %} + +{% block content %} +
+

It's time to make some decisions

+

+ Here you can create your unique categories and choose your favorite color +

+ +
+ +

+ +

+

+
+
+{% if message %} +
+

{{ message }}

+
+{% endif %} + +{% endblock %} \ No newline at end of file diff --git a/app/templates/partials/calendar/event/edit_event_details_tab.html b/app/templates/partials/calendar/event/edit_event_details_tab.html index e9fde613..d5980f5f 100644 --- a/app/templates/partials/calendar/event/edit_event_details_tab.html +++ b/app/templates/partials/calendar/event/edit_event_details_tab.html @@ -84,6 +84,18 @@ + + + - - + + + @@ -104,4 +105,3 @@
- diff --git a/app/templates/partials/user_profile/sidebar_left/profile_card/modals/calendar_privacy_modal.html b/app/templates/partials/user_profile/sidebar_left/profile_card/modals/calendar_privacy_modal.html index b56b1572..0a2a7e7c 100644 --- a/app/templates/partials/user_profile/sidebar_left/profile_card/modals/calendar_privacy_modal.html +++ b/app/templates/partials/user_profile/sidebar_left/profile_card/modals/calendar_privacy_modal.html @@ -7,10 +7,10 @@ - \ No newline at end of file + diff --git a/app/templates/profile.html b/app/templates/profile.html index 900739ec..9e3afc64 100644 --- a/app/templates/profile.html +++ b/app/templates/profile.html @@ -16,4 +16,4 @@ {% include "partials/calendar/event/text_editor_partial_body.html" %} -{% endblock content %} \ No newline at end of file +{% endblock content %} diff --git a/tests/test_emotion.py b/tests/test_emotion.py index f16c88d8..644dfe14 100644 --- a/tests/test_emotion.py +++ b/tests/test_emotion.py @@ -7,7 +7,8 @@ is_emotion_above_significance, get_dominant_emotion, get_emotion, - get_html_emoticon) + get_html_emoticon, +) from app.routers.event import create_event @@ -20,47 +21,71 @@ emotion_tests = [ - (HAPPY_MESSAGE, HAPPY_MESSAGE, Emoticon('Happy', 1.0, '😃')), - (SAD_MESSAGE, SAD_MESSAGE, Emoticon('Sad', 1.0, '🙁')), - (ANGRY_MESSAGE, ANGRY_MESSAGE, Emoticon('Angry', 1.0, '😠')), - (FEAR_MESSAGE, FEAR_MESSAGE, Emoticon('Fear', 1.0, '😱')), - (SURPRISE_MESSAGE, SURPRISE_MESSAGE, - Emoticon('Surprise', 1.0, '😮')), - (SURPRISE_MESSAGE, None, Emoticon('Surprise', 1.0, '😮')), - (SURPRISE_MESSAGE, "", Emoticon('Surprise', 1.0, '😮')), - (HAPPY_MESSAGE, SAD_MESSAGE, Emoticon('Happy', 0.6, '😃')), + (HAPPY_MESSAGE, HAPPY_MESSAGE, Emoticon("Happy", 1.0, "😃")), + (SAD_MESSAGE, SAD_MESSAGE, Emoticon("Sad", 1.0, "🙁")), + (ANGRY_MESSAGE, ANGRY_MESSAGE, Emoticon("Angry", 1.0, "😠")), + (FEAR_MESSAGE, FEAR_MESSAGE, Emoticon("Fear", 1.0, "😱")), + ( + SURPRISE_MESSAGE, + SURPRISE_MESSAGE, + Emoticon("Surprise", 1.0, "😮"), + ), + (SURPRISE_MESSAGE, None, Emoticon("Surprise", 1.0, "😮")), + (SURPRISE_MESSAGE, "", Emoticon("Surprise", 1.0, "😮")), + (HAPPY_MESSAGE, SAD_MESSAGE, Emoticon("Happy", 0.6, "😃")), ] emotion_significance_tests = [ - (Emoticon('Happy', 1.0, '😃'), None, True), - (Emoticon('Happy', 0.6, '😃'), None, True), - (Emoticon('Happy', 0.4, '😃'), None, False), - (Emoticon('Happy', 0.4, '😃'), 0.3, True), - (Emoticon('Happy', 0.7, '😃'), 0.8, False) + (Emoticon("Happy", 1.0, "😃"), None, True), + (Emoticon("Happy", 0.6, "😃"), None, True), + (Emoticon("Happy", 0.4, "😃"), None, False), + (Emoticon("Happy", 0.4, "😃"), 0.3, True), + (Emoticon("Happy", 0.7, "😃"), 0.8, False), ] get_html_emoticon_tests = [ - (Emoticon('Happy', 1.0, '😃'), '😃'), - (Emoticon('Happy', 1.0, None), None) + (Emoticon("Happy", 1.0, "😃"), "😃"), + (Emoticon("Happy", 1.0, None), None), ] get_emotion_tests = [ - Emoticon(HAPPY_MESSAGE, HAPPY_MESSAGE, '😃'), - Emoticon(SAD_MESSAGE, SAD_MESSAGE, '🙁'), - Emoticon(HAPPY_MESSAGE, SAD_MESSAGE, '😃'), - Emoticon(" ", " ", None) + Emoticon(HAPPY_MESSAGE, HAPPY_MESSAGE, "😃"), + Emoticon(SAD_MESSAGE, SAD_MESSAGE, "🙁"), + Emoticon(HAPPY_MESSAGE, SAD_MESSAGE, "😃"), + Emoticon(" ", " ", None), ] create_event_tests = [ - (HAPPY_MESSAGE, datetime.datetime(2019, 5, 21, 0, 0), - datetime.datetime(2019, 5, 22, 0, 0), False, 1, HAPPY_MESSAGE, "location", - "😃"), - (SAD_MESSAGE, datetime.datetime(2019, 5, 21, 0, 0), - datetime.datetime(2019, 5, 22, 0, 0), False, 1, HAPPY_MESSAGE, "location", - "🙁"), - (" ", datetime.datetime(2019, 5, 21, 0, 0), - datetime.datetime(2019, 5, 22, 0, 0), False, 1, " ", "location", - None) + ( + HAPPY_MESSAGE, + datetime.datetime(2019, 5, 21, 0, 0), + datetime.datetime(2019, 5, 22, 0, 0), + False, + 1, + HAPPY_MESSAGE, + "location", + "😃", + ), + ( + SAD_MESSAGE, + datetime.datetime(2019, 5, 21, 0, 0), + datetime.datetime(2019, 5, 22, 0, 0), + False, + 1, + HAPPY_MESSAGE, + "location", + "🙁", + ), + ( + " ", + datetime.datetime(2019, 5, 21, 0, 0), + datetime.datetime(2019, 5, 22, 0, 0), + False, + 1, + " ", + "location", + None, + ), ] @@ -69,14 +94,18 @@ def test_dominant_emotion(title, content, result): assert get_dominant_emotion(title, content) == result -@pytest.mark.parametrize("dominant_emotion, significance, result", - emotion_significance_tests) +@pytest.mark.parametrize( + "dominant_emotion, significance, result", + emotion_significance_tests, +) def test_is_emotion_above_significance(dominant_emotion, significance, result): if significance is None: assert is_emotion_above_significance(dominant_emotion) == result else: - assert is_emotion_above_significance(dominant_emotion, - significance) == result + assert ( + is_emotion_above_significance(dominant_emotion, significance) + == result + ) @pytest.mark.parametrize("dominant_emotion, result", get_html_emoticon_tests) @@ -89,10 +118,29 @@ def test_get_emotion(title, content, result): assert get_emotion(title, content) == result -@pytest.mark.parametrize("title, start, end, all_day, owner_id, content, " + - "location, result", create_event_tests) -def test_create_event(title, start, end, all_day, - owner_id, content, location, result, session): - event = create_event(session, title, start, - end, all_day, owner_id, content, location) +@pytest.mark.parametrize( + "title, start, end, all_day, owner_id, content, " + "location, result", + create_event_tests, +) +def test_create_event( + title, + start, + end, + all_day, + owner_id, + content, + location, + result, + session, +): + event = create_event( + session, + title, + start, + end, + owner_id, + all_day, + content, + location, + ) assert event.emotion == result diff --git a/tests/test_event.py b/tests/test_event.py index 914b2350..81f0aae2 100644 --- a/tests/test_event.py +++ b/tests/test_event.py @@ -4,17 +4,18 @@ from fastapi import HTTPException, Request from fastapi.testclient import TestClient +from sqlalchemy.sql.elements import Null from sqlalchemy.orm.session import Session from starlette import status from app.database.models import Comment, Event from app.dependencies import get_db +from app.internal.privacy import PrivacyKinds from app.internal.utils import delete_instance from app.main import app - from app.routers import event as evt - +from app.routers.event import event_to_show CORRECT_EVENT_FORM_DATA = { "title": "test title", @@ -27,7 +28,7 @@ "description": "content", "color": "red", "availability": "True", - "privacy": "public", + "privacy": PrivacyKinds.Public.name, "invited": "a@a.com,b@b.com", "event_type": "on", } @@ -43,7 +44,7 @@ "description": "content", "color": "red", "availability": "busy", - "privacy": "public", + "privacy": PrivacyKinds.Public.name, "event_type": "on", "is_google_event": "False", } @@ -59,7 +60,7 @@ "description": "content", "color": "red", "availability": "True", - "privacy": "public", + "privacy": PrivacyKinds.Public.name, "invited": "a@a.com,b@b.com", "event_type": "on", "is_google_event": "False", @@ -76,7 +77,7 @@ "description": "content", "color": "red", "availability": "busy", - "privacy": "public", + "privacy": PrivacyKinds.Public.name, "invited": "a@a.com,b@b.com,ccc", "event_type": "on", "is_google_event": "False", @@ -93,7 +94,7 @@ "description": "content", "color": "red", "availability": "busy", - "privacy": "public", + "privacy": PrivacyKinds.Public.name, "event_type": "on", "invited": "a@a.com,b@b.com", "is_google_event": "False", @@ -110,7 +111,7 @@ "description": "content", "color": "red", "availability": "busy", - "privacy": "public", + "privacy": PrivacyKinds.Public.name, "invited": "a@a.com,b@b.com", "event_type": "on", "is_google_event": "False", @@ -526,6 +527,38 @@ def test_deleting_an_event_does_not_exist(event_test_client, event): assert response.status_code == status.HTTP_404_NOT_FOUND +def test_can_show_event_public(event, session, user): + assert event_to_show(event, session) == event + assert event_to_show(event, session, user) == event + + +def test_can_show_event_hidden(event, session, user): + event.privacy = PrivacyKinds.Hidden.name + assert event_to_show(event, session, user) is None + assert event_to_show(event, session) == event + + +def test_can_show_event_private(event, session, user): + event.privacy = PrivacyKinds.Private.name + private_event = event_to_show(event, session, user) + private_attributes = [ + private_event.title, + private_event.location, + private_event.content, + private_event.invitees, + ] + null_attributes = [ + private_event.color, + private_event.emotion, + private_event.category_id, + ] + is_private_attributes = [ + attr == event.privacy for attr in private_attributes + ] + is_null_attributes = [attr is Null for attr in null_attributes] + assert all(is_private_attributes) and all(is_null_attributes) + + def test_get_tamplate_to_share_event(event, session): html_template = evt.get_template_to_share_event( event_id=1, From ab1d671e952b368e5ac5d83afb75216500cee14c Mon Sep 17 00:00:00 2001 From: fandomario <71139801+fandomario@users.noreply.github.com> Date: Sat, 20 Feb 2021 14:47:22 +0200 Subject: [PATCH 09/46] Feature/weightt (#277) --- app/database/models.py | 1 + app/main.py | 7 ++-- app/routers/weight.py | 69 +++++++++++++++++++++++++++++++++++++++ app/static/weight.css | 13 ++++++++ app/templates/base.html | 3 ++ app/templates/weight.html | 20 ++++++++++++ tests/client_fixture.py | 7 +++- tests/test_weight.py | 42 ++++++++++++++++++++++++ tests/user_fixture.py | 1 + 9 files changed, 159 insertions(+), 4 deletions(-) create mode 100644 app/routers/weight.py create mode 100644 app/static/weight.css create mode 100644 app/templates/weight.html create mode 100644 tests/test_weight.py diff --git a/app/database/models.py b/app/database/models.py index bff6134d..f06f6d54 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -48,6 +48,7 @@ class User(Base): privacy = Column(String, default="Private", nullable=False) is_manager = Column(Boolean, default=False) language_id = Column(Integer, ForeignKey("languages.id")) + target_weight = Column(Float, nullable=True) owned_events = relationship( "Event", diff --git a/app/main.py b/app/main.py index e6e4af34..203ce0a4 100644 --- a/app/main.py +++ b/app/main.py @@ -43,7 +43,7 @@ def create_tables(engine, psql_environment): about_us, agenda, calendar, categories, celebrity, credits, currency, dayview, email, event, export, four_o_four, friendview, google_connect, invitation, joke, login, logout, profile, - register, search, telegram, user, weekview, whatsapp, + register, search, telegram, user, weekview, weight, whatsapp, ) json_data_loader.load_to_database(next(get_db())) @@ -74,12 +74,11 @@ async def swagger_ui_redirect(): credits.router, currency.router, dayview.router, - friendview.router, - weekview.router, email.router, event.router, export.router, four_o_four.router, + friendview.router, google_connect.router, invitation.router, joke.router, @@ -91,6 +90,8 @@ async def swagger_ui_redirect(): search.router, telegram.router, user.router, + weekview.router, + weight.router, whatsapp.router, ] diff --git a/app/routers/weight.py b/app/routers/weight.py new file mode 100644 index 00000000..058f72d6 --- /dev/null +++ b/app/routers/weight.py @@ -0,0 +1,69 @@ +from typing import Union + +from fastapi import APIRouter, Depends, Request +from sqlalchemy.orm import Session +from starlette.responses import RedirectResponse + +from app.database.models import User +from app.dependencies import get_db +from app.dependencies import templates + + +router = APIRouter(tags=["weight"],) + + +@router.get("/weight") +async def get_weight( + request: Request, + session: Session = Depends(get_db), + target: Union[float, None] = None, + current_weight: Union[float, None] = None, + ): + + # TODO Waiting for user registration + user_id = 1 + user = session.query(User).filter_by(id=user_id).first() + target = user.target_weight + if current_weight: + return RedirectResponse(url='/') + return templates.TemplateResponse("weight.html", { + "request": request, + "target": target, + "current_weight": current_weight, + } + ) + + +@router.post("/weight") +async def weight( + request: Request, + session: Session = Depends(get_db)): + user_id = 1 + user = session.query(User).filter_by(id=user_id).first() + data = await request.form() + target = data['target'] + current_weight = data['current_weight'] + if target: + user.target_weight = target + session.commit() + else: + target = user.target_weight + if not target: + target = current_weight + way_to_go = float(current_weight) - float(target) + way_to_go = round(way_to_go, 2) + if way_to_go > 0: + way_message = f"Weight to lose: {way_to_go} Kg" + elif way_to_go < 0: + way_to_go = abs(way_to_go) + way_message = f"Weight to add: {way_to_go} Kg" + else: + way_message = f"Great! You have reached your goal: {target} Kg" + + return templates.TemplateResponse("weight.html", { + "request": request, + "target": target, + "current_weight": current_weight, + "way_message": way_message + } + ) diff --git a/app/static/weight.css b/app/static/weight.css new file mode 100644 index 00000000..5704115a --- /dev/null +++ b/app/static/weight.css @@ -0,0 +1,13 @@ + +.weight { + display: block; + margin-top: 1rem; + margin-left: 1rem; + } + + +.weight-message { + display: block; + margin-top: 1rem; + margin-left: 1rem; + } diff --git a/app/templates/base.html b/app/templates/base.html index a7699566..dc93d52c 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -61,6 +61,9 @@ Create Categories + diff --git a/app/templates/weight.html b/app/templates/weight.html new file mode 100644 index 00000000..ccb41e50 --- /dev/null +++ b/app/templates/weight.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} +{% block content %} + +

+
+
+
+ + +
+ +
+
+ {% if way_message %} + {{ way_message }} + {% endif %} +
+
+ +{% endblock %} diff --git a/tests/client_fixture.py b/tests/client_fixture.py index 64db26a3..c40b9fb8 100644 --- a/tests/client_fixture.py +++ b/tests/client_fixture.py @@ -8,7 +8,7 @@ from app.database.models import Base, User from app.routers import ( agenda, categories, event, friendview, google_connect, - invitation, profile + invitation, profile, weight, ) from app.routers.salary import routes as salary from tests import security_testing_routes @@ -54,6 +54,11 @@ def friendview_test_client() -> Generator[TestClient, None, None]: yield from create_test_client(friendview.get_db) +@pytest.fixture(scope="session") +def weight_test_client() -> Generator[TestClient, None, None]: + yield from create_test_client(weight.get_db) + + @pytest.fixture(scope="session") def event_test_client() -> Generator[TestClient, None, None]: yield from create_test_client(event.get_db) diff --git a/tests/test_weight.py b/tests/test_weight.py new file mode 100644 index 00000000..71b3b673 --- /dev/null +++ b/tests/test_weight.py @@ -0,0 +1,42 @@ +class TestWeight: + WEIGHT = "/weight" + WAY_TO_GO = b'15.5' + """Target weight set to 60""" + + @staticmethod + def test_weight_ok(weight_test_client, user, session): + new_weight = {'target': user.target_weight, 'current_weight': 70} + resp = weight_test_client.post(TestWeight.WEIGHT, data=new_weight) + assert resp.ok + + @staticmethod + def test_need_to_lose_weight(weight_test_client, user): + new_weight = {'target': user.target_weight, 'current_weight': 80} + resp = weight_test_client.post(TestWeight.WEIGHT, data=new_weight) + assert b"Weight to lose" in resp.content + + @staticmethod + def test_need_to_gain_weight(weight_test_client, user): + new_weight = {'target': user.target_weight, 'current_weight': 50} + resp = weight_test_client.post(TestWeight.WEIGHT, data=new_weight) + assert b"Weight to add" in resp.content + + @staticmethod + def test_reached_the_goal(weight_test_client, user): + new_weight = {'target': user.target_weight, 'current_weight': 60} + resp = weight_test_client.post(TestWeight.WEIGHT, data=new_weight) + assert b"reached your goal" in resp.content + + @staticmethod + def test_way_to_go(weight_test_client, user): + new_weight = {'target': user.target_weight, 'current_weight': 75.5} + resp = weight_test_client.post(TestWeight.WEIGHT, data=new_weight) + assert TestWeight.WAY_TO_GO in resp.content + + @staticmethod + def test_no_target_entered(weight_test_client, user): + """In this case, the target set to current weight""" + + new_weight = {'target': '', 'current_weight': 60} + resp = weight_test_client.post(TestWeight.WEIGHT, data=new_weight) + assert b"reached your goal" in resp.content diff --git a/tests/user_fixture.py b/tests/user_fixture.py index 2befa98d..b50fb900 100644 --- a/tests/user_fixture.py +++ b/tests/user_fixture.py @@ -16,6 +16,7 @@ def user(session: Session) -> Generator[User, None, None]: password="test_password", email="test.email@gmail.com", language_id=1, + target_weight=60, ) yield mock_user delete_instance(session, mock_user) From fe4cab998fbc82bb69c3a5203351889c851fd6dc Mon Sep 17 00:00:00 2001 From: leddest <46251307+leddest@users.noreply.github.com> Date: Sun, 21 Feb 2021 11:37:53 +0200 Subject: [PATCH 10/46] Telegram v2 (#328) --- app/config.py.example | 2 +- app/media/user1.png | Bin 3556 -> 0 bytes .../user_profile/sidebar_left/features_card.html | 4 ++-- requirements.txt | 2 +- tests/test_a_telegram_asyncio.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) delete mode 100644 app/media/user1.png diff --git a/app/config.py.example b/app/config.py.example index 4556dcca..e7d927ca 100644 --- a/app/config.py.example +++ b/app/config.py.example @@ -7,7 +7,7 @@ from starlette.templating import Jinja2Templates class Settings(BaseSettings): - app_name: str = "PyLander" + app_name: str = "PyLendar" bot_api: str = "BOT_API" webhook_url: str = "WEBHOOK_URL" diff --git a/app/media/user1.png b/app/media/user1.png deleted file mode 100644 index bd856aaa02ed8fc75fb310971827e4734b8785d5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3556 zcmb7Hc{Ei2-yfP7HADtumo4GT*lOxaWp9cQ!&tvV2-%e=iIF8^DH^_E$Ox(Ip(q;L zgv1zs`$|4e%r-X$qqvmDZg=MdLBxwl+=+Tu^3xCQ#d3fmnGRuf~@FAzZs%q3rvo>tw zH1ovj`8TSa{hY&lNQPN(XO}uSh?t0+llJuT>bQMK9^Hm>bfos? zp3p*}V#>;nMZ9%Gh=>$cS8Ek+2~ov6-@Su4IXfRomd9o7r-k50+V#L)m?h(}uLQ!> z5YaKcM53G$(nkD`LY4(ss~f>9Q>H0tX(e?so}y%Br9y26=1Ci zeGQFQA@$mAp!kemJzJ%t_QCIa%Z0z=s zj#)$TkRp20n>YG{LqjZ*gPUP^_*#9BFqF&I))px)PGvIXmd2ZSF&WF7oXVryC(dQu zm5pBKii?Yz=Z=?yJ*})Xx3y&?d~c|$D=aPLRqrv%@`A=0_O|j^GSD}cq;}WY>HITq zeox#2k6OK|sK&wBY&KF_I-bU+e)s^V(4TH*&~?cYRJsqMA#l8PrYBoVODon#_dtUC zpvHwr+hI}oi*R3p96mf;G$0`0>*AtmSeTBB#&CW2uLyl~*eu+rP!J5>)CQH?i@8Cf z;ss9^Sfx@xYhdw4&e1nQLWIC*C{Hb&r@Olk5U7u5b!-qq_e`dqW&gObpPS32DV+aA z&_p|^#@^n35L^2T${jlmoqT&sZ0;kujr{kGL3>{mQVz!L>6k#pLueuIdwb)%JdM;v z+kX7eZe^jMhLD~bJdy|A_Kv;6%nfR$mKH=O$MGK8y)yFP928XKT`LYbz9=JX7#f~Gb)@GAA^pU${b#Wda8gIabHNFJ``NI1@D+epO{Fi zuRjiX&Z&4e?;O6q?5?Y;`}oNdlK@AKmPBdJ3Atai;MHGI#6&?QPJhq zzB6p1W^)V`pVV+O{h@K90IR>y7!m{c5F5*-uBrKaPoqcM&(Cjpim0jV0^qc~wSz<= zA#JGpihMkAT)YBOXI)*TT{OfN!kbF6S2m(v1+%y{TpJNBctgQ{24f#4Lzg`$8T2N0 zc6K%(Jr>XbK=%khOJif>;H?m;#KOWO*KXW6s>c^O=&whd@aK`%JRhWlIlT6LGQD9k zS`v%JI!8|iHn0cFY*CLa?p>!NOnptdYf@TUTVEYT!`mL0mGLPkC@gP8%}1EqAWoe+ zW$o;oYhBea0cr%?^YQm@zgqdWzC1s*#8+2OuQ;$!fxdjPX*^BAJk4j~T26_Uj!xX} z?yem+XjmR|I5aG51x$+Fdtx%>>Sv1YDINP7&;oolZcyXmJ>De_=K@BoA@_loH~`ev z+Tw+;n<;`WI9$^9c9f;&I~2T=MjNvHNfY-fRu$4V3bwZw&5xMk>s5>|@dYhy-3E`hBH^|GlbqA)g}zWmWH$J%o!ZeDg%&I4)|%@3Cn-NJJtxrx#gY;eA7lGyFsZZrRlkiH>Hv`_6sU)kYs(uO0^{*Xs26DH$ z5&p8GDK$lKY;=@}M?LkWW+<|l)VM0A@?5t}5-I-03-w-^P+f_R#nA>?40*Vy{Z`Og zljzJVEvRFCS%6%~O!oY2tF`($$YqkrkTsm;x&)ipGf?Xo>7 zx~XOg7Rs4sPZco$jeL_jB2P?EZKzk*IimV9RxZP)@l+YU{Bk_iz3k z$1tw$NS>miqCfJMe{1Qal)+7eKqC7e*@wd%Q)g$%vA~Ae8x?Y};8SFD^!9p77RCmV zCjEE9?r!wCj3;YyQnWv6tl!55RNm`zj6h3ue*TR7GCB$@fJ<>Aqo=zY7+vY^i8j<9 zl?Qg4ckznRZ;PylGs;@yLtzthXOsY!(yqd;HT_J6-4X8 zg8I_f>wOKQsVgfXpw!pq@`Il~O$`)Zz~OM#3=DVdA1kMijin3>445}h%|z zy?xe2cXt$xJqwYRmi{_FPoVqQT^R?;HWz6kFJz45GRFo7@Exr86S#kZHpNy&zf0e)fGIeuvGn#2PJ`X)ei0SM!!>Nn9G)IZA zuCC6IT0+h!1v}Y4OKO@mAQgD9uW~pRxq*}=g)YFoq>Dj_Lg)N)r>BFU04{a z@@&LPe6)Y_bKnBN+N;o+1Dji0APf!ZI^pYhZGJ=yx7EDJnS62gkYsA--KMpN*XiBH z)uW3Mobra?h0j_R78XnpApRwjO;Jb_6!I<=A2Z~Km0jB1*=pD>m(&E7R&aYwe0FXw zWNR$|Bs-A2falbXz%#p`xxEP>nkvJ3Hguec7Vz@&@)IS?!I+V5mfI7@ zRFaK=A0{U!5B4cyzN~#%XSF3tI$h8fRsbKJpZBwKa0udT0a%F=l~_MMt-epw_H;a6 zR`f0NNPd9SJ!wp=ITh@W9k8q z2Rym{f#fA_|6I>2^WC+x&`Ng@O57?P7=+&oBXN0odAh^uAxTWVZ9vx~v!WcwRa+|q z(wa^|W-M&?Ow?)FKR|ZW(?-HacOx^)1gSXJvGo<2td@a6dA)u9oQ7Wl)(g45)B zgE2eGRcg24WBtC%(a|ztVPXH~<+V>wD}vFL$}~OA(-S9lSX*1iq@UjO8BTwDfRArN z{j34wzL)qPS}iQxo&0XDD**!Gyg+8nb4AQyk7^`3t*uA1B_;Ey73J|`x$z+@@sNWh zDhKSJV-3=^$7y_l@inpES0Z#r1G=k;6cwWk#m|f353{q2nww=rL_`{Uf>WGPJvBbQ zzBa#a(`YoH^BKF7kC&ju#l;}%<-iWdQ6a<*?ovisf{4;h6MA4wMkY)Q)CB4WF)^O; z&BW5uQk9dX3ldn%-tONqLjh>uy#xX`P-WM*%~FRjd~QVbo<`2Z&$D0lP4fMH!TaNV n*s-|(yjA%RyO{s~(%c=neK`Ysl~63$y+JUh7fmXR@Pz*Yw5`1? diff --git a/app/templates/partials/user_profile/sidebar_left/features_card.html b/app/templates/partials/user_profile/sidebar_left/features_card.html index 3015edcc..dbff7721 100644 --- a/app/templates/partials/user_profile/sidebar_left/features_card.html +++ b/app/templates/partials/user_profile/sidebar_left/features_card.html @@ -8,8 +8,8 @@
    {% if not user.telegram_id %}
  • - - Try PyLander bot + + Try PyLendar bot
  • {% endif %} diff --git a/requirements.txt b/requirements.txt index 0079077f..5bbdca17 100644 --- a/requirements.txt +++ b/requirements.txt @@ -148,4 +148,4 @@ win32-setctime==1.0.3 word-forms==2.1.0 wsproto==1.0.0 yapf==0.30.0 -zipp==3.4.0 +zipp==3.4.0 \ No newline at end of file diff --git a/tests/test_a_telegram_asyncio.py b/tests/test_a_telegram_asyncio.py index ec58f989..faf99d98 100644 --- a/tests/test_a_telegram_asyncio.py +++ b/tests/test_a_telegram_asyncio.py @@ -140,7 +140,7 @@ async def test_user_not_registered(telegram_client): assert response.status_code == status.HTTP_200_OK assert b'Hello, Moshe!' in response.content assert b'To use PyLendar Bot you have to register' \ - in response.content + in response.content @staticmethod @pytest.mark.asyncio From d7450b6eeadd124580fe0f40443789a4c7e78dd8 Mon Sep 17 00:00:00 2001 From: Ode Date: Sun, 21 Feb 2021 21:03:52 +0200 Subject: [PATCH 11/46] Fixes to Profile and Calendar Month View Templates (#327) * merged and deleted old calendar template files * merged and deleted old calendar template files * added missing export calendar feature to profile page --- app/templates/calendar/add_week.html | 21 ----- app/templates/calendar/calendar.html | 19 ----- app/templates/calendar/layout.html | 81 ------------------- app/templates/index.html | 68 ++++++++-------- .../partials/calendar/calendar_base.html | 35 ++++---- .../calendar/monthly_view/add_week.html | 36 ++++----- .../calendar/monthly_view/monthly_grid.html | 40 ++++----- .../partials/calendar/navigation.html | 72 +++++++++-------- .../sidebar_left/features_card.html | 15 +++- .../features_card/export_calendar.html | 15 ++++ 10 files changed, 152 insertions(+), 250 deletions(-) delete mode 100644 app/templates/calendar/add_week.html delete mode 100644 app/templates/calendar/calendar.html delete mode 100644 app/templates/calendar/layout.html create mode 100644 app/templates/partials/user_profile/sidebar_left/features_card/export_calendar.html diff --git a/app/templates/calendar/add_week.html b/app/templates/calendar/add_week.html deleted file mode 100644 index 702ee572..00000000 --- a/app/templates/calendar/add_week.html +++ /dev/null @@ -1,21 +0,0 @@ -{% for week in weeks_block %} -
    - {% for day in week.days %} -
    -
    -
    {{day}}
    - -
    - {% for devent in day.dailyevents %} -
    -
    {{devent[0]}}
    -
    {{devent[1]}}
    -
    - {% endfor %} - {% for event in day.events %} -
    {{event[0]}} {{event[1]}}
    - {% endfor %} -
    - {% endfor %} -
    -{% endfor %} \ No newline at end of file diff --git a/app/templates/calendar/calendar.html b/app/templates/calendar/calendar.html deleted file mode 100644 index d5d95c15..00000000 --- a/app/templates/calendar/calendar.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends 'calendar/layout.html' %} - -{% block main %} -
    -
    - {% for d in week_days %} - {% if d == day.sday %} -
    {{ d.upper() }}
    - {% else %} -
    {{ d.upper() }}
    - {% endif %} - {% endfor %} -
    -
    - {% include 'calendar/add_week.html' %} -
    -
    -
    -{% endblock %} \ No newline at end of file diff --git a/app/templates/calendar/layout.html b/app/templates/calendar/layout.html deleted file mode 100644 index 90a320ec..00000000 --- a/app/templates/calendar/layout.html +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - - - - - - - - - - - Calendar - - - -
    - -
    -
    FEATURE NAME
    -
    -
    -
    -
    -
    {{day.display()}}
    -
    Location 0oc 00:00
    -
    - -
    -
    - {% block main %} {% endblock %} -
    -
    -
    - - - \ No newline at end of file diff --git a/app/templates/index.html b/app/templates/index.html index 32a492b5..3bc1d8fc 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -3,42 +3,42 @@ {% block content %}
    -
    -
    -

    - PyLendar -
    -

    -

    - Open Source Calendar built with Python -

    -
    -
    +
    +
    +

    + PyLendar +
    +

    +

    + Open Source Calendar built with Python +

    +
    +
    - {% if quote %} - {% if not quote.author%} -

    "{{ quote.text }}"

    - {% else %} -

    "{{ quote.text }}"   \ {{quote.author}} -

    - {% endif %} - {% endif %} -
    -
    - + {% if quote %} + {% if not quote.author%} +

    "{{ quote.text }}"

    + {% else %} +

    "{{ quote.text }}"   \ {{quote.author}} +

    + {% endif %} + {% endif %}
    -
    -
    - calendar image +
    +
    +
    +
    + calendar image +
    -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/partials/calendar/calendar_base.html b/app/templates/partials/calendar/calendar_base.html index 740add68..c70ce366 100644 --- a/app/templates/partials/calendar/calendar_base.html +++ b/app/templates/partials/calendar/calendar_base.html @@ -1,25 +1,22 @@ {% extends "./partials/base.html" %} {% block head %} - {{super()}} - - - - +{{super()}} + + {% endblock head %} {% block page_name %}Month View{% endblock page_name %} {% block body %} -
    - {% include 'partials/calendar/navigation.html' %} -
    - {% include 'partials/calendar/feature_settings/example.html' %} -
    -
    - {% block content %} - {% endblock content %} -
    +
    + {% include 'partials/calendar/navigation.html' %} +
    + {% include 'partials/calendar/feature_settings/example.html' %}
    - - - - -{% endblock body %} \ No newline at end of file +
    + {% block content %} + {% endblock content %} +
    +
    + + + +{% endblock body %} diff --git a/app/templates/partials/calendar/monthly_view/add_week.html b/app/templates/partials/calendar/monthly_view/add_week.html index a53a7f4a..2f821846 100644 --- a/app/templates/partials/calendar/monthly_view/add_week.html +++ b/app/templates/partials/calendar/monthly_view/add_week.html @@ -1,21 +1,21 @@ {% for week in weeks_block %} -
    - {% for day in week.days %} -
    -
    -
    {{ day }}
    - -
    - {% for devent in day.dailyevents %} -
    -
    {{ devent[0] }}
    -
    {{ devent[1] }}
    -
    - {% endfor %} - {% for event in day.events %} -
    {{ event[0] }} {{ event[1] }}
    - {% endfor %} -
    +
    + {% for day in week.days %} +
    +
    +
    {{ day }}
    + +
    + {% for devent in day.dailyevents %} +
    +
    {{ devent[0] }}
    +
    {{ devent[1] }}
    +
    + {% endfor %} + {% for event in day.events %} +
    {{ event[0] }} {{ event[1] }}
    {% endfor %}
    -{% endfor %} \ No newline at end of file + {% endfor %} +
    +{% endfor %} diff --git a/app/templates/partials/calendar/monthly_view/monthly_grid.html b/app/templates/partials/calendar/monthly_view/monthly_grid.html index 41d70b65..1a2df047 100644 --- a/app/templates/partials/calendar/monthly_view/monthly_grid.html +++ b/app/templates/partials/calendar/monthly_view/monthly_grid.html @@ -1,11 +1,11 @@
    {% for d in week_days %} - {% if d == day.sday %} -
    {{ d.upper() }}
    - {% else %} -
    {{ d.upper() }}
    - {% endif %} + {% if d == day.sday %} +
    {{ d.upper() }}
    + {% else %} +
    {{ d.upper() }}
    + {% endif %} {% endfor %}
    @@ -13,21 +13,21 @@
    -
    -
    - - TODAY - - - - - - - - - - -
    +
    +
    + + TODAY + + + + + + + + + +
    +
    diff --git a/app/templates/partials/calendar/navigation.html b/app/templates/partials/calendar/navigation.html index 84a1303b..ab65b8eb 100644 --- a/app/templates/partials/calendar/navigation.html +++ b/app/templates/partials/calendar/navigation.html @@ -1,36 +1,40 @@ \ No newline at end of file + +
    +
    + +
    +
    + +
    + +
    + +
    + diff --git a/app/templates/partials/user_profile/sidebar_left/features_card.html b/app/templates/partials/user_profile/sidebar_left/features_card.html index dbff7721..946d1040 100644 --- a/app/templates/partials/user_profile/sidebar_left/features_card.html +++ b/app/templates/partials/user_profile/sidebar_left/features_card.html @@ -6,6 +6,7 @@

    -
    \ No newline at end of file + diff --git a/app/templates/partials/user_profile/sidebar_left/features_card/export_calendar.html b/app/templates/partials/user_profile/sidebar_left/features_card/export_calendar.html new file mode 100644 index 00000000..3a34dade --- /dev/null +++ b/app/templates/partials/user_profile/sidebar_left/features_card/export_calendar.html @@ -0,0 +1,15 @@ + + +
    +
    +
    +
    +
    +
    + +
    +
    From 46e0deb0b01e16b6c8f7a0b514f2dfe90abc8a31 Mon Sep 17 00:00:00 2001 From: imimouni <71105447+imimouni@users.noreply.github.com> Date: Mon, 22 Feb 2021 01:47:30 +0200 Subject: [PATCH 12/46] fix: event backend and frontend creation (#329) --- app/database/models.py | 2 +- app/internal/event.py | 67 ++++++++++++------- app/routers/event.py | 6 +- .../event/edit_event_details_tab.html | 31 +++++---- 4 files changed, 63 insertions(+), 43 deletions(-) diff --git a/app/database/models.py b/app/database/models.py index f06f6d54..b6f63bde 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -93,7 +93,7 @@ class Event(Base): content = Column(String) location = Column(String, nullable=True) is_google_event = Column(Boolean, default=False) - vc_link = Column(String) + vc_link = Column(String, nullable=True) color = Column(String, nullable=True) all_day = Column(Boolean, default=False) invitees = Column(String) diff --git a/app/internal/event.py b/app/internal/event.py index 5207761e..57b29d27 100644 --- a/app/internal/event.py +++ b/app/internal/event.py @@ -9,46 +9,55 @@ from app.database.models import Event -ZOOM_REGEX = re.compile(r'https://.*?\.zoom.us/[a-z]/.[^.,\b\s]+') +ZOOM_REGEX = re.compile(r"https://.*?\.zoom.us/[a-z]/.[^.,\b\s]+") def raise_if_zoom_link_invalid(vc_link): if ZOOM_REGEX.search(vc_link) is None: - raise HTTPException(status_code=HTTP_400_BAD_REQUEST, - detail="VC type with no valid zoom link") + raise HTTPException( + status_code=HTTP_400_BAD_REQUEST, + detail="VC type with no valid zoom link", + ) def get_invited_emails(invited_from_form: str) -> List[str]: invited_emails = [] - for invited_email in invited_from_form.split(','): + if not invited_from_form: + return [""] + for invited_email in invited_from_form.split(","): invited_email = invited_email.strip() try: validate_email(invited_email, check_deliverability=False) except EmailSyntaxError: - logging.exception(f'{invited_email} is not a valid email address') - continue - invited_emails.append(invited_email) + logging.exception( + f"{invited_email} is not a valid email address", + ) + else: + invited_emails.append(invited_email) return invited_emails -def get_uninvited_regular_emails(session: Session, - owner_id: int, - title: str, - invited_emails: List[str]) -> Set[str]: +def get_uninvited_regular_emails( + session: Session, + owner_id: int, + title: str, + invited_emails: List[str], +) -> Set[str]: invitees_query = session.query(Event).with_entities(Event.invitees) - similar_events_invitees = invitees_query.filter(Event.owner_id == owner_id, - Event.title == title).all() + similar_events_invitees = invitees_query.filter( + Event.owner_id == owner_id, + Event.title == title, + ).all() regular_invitees = set() for record in similar_events_invitees: if record: - regular_invitees.update(record[0].split(',')) + regular_invitees.update(record[0].split(",")) return regular_invitees - set(invited_emails) -def check_diffs(checked_event: Event, - all_events: List[Event]): +def check_diffs(checked_event: Event, all_events: List[Event]): """Returns the repeated events and the week difference""" diffs = [] for event in all_events: @@ -65,22 +74,30 @@ def check_diffs(checked_event: Event, def find_pattern(session, event): - all_events_with_same_name = session.query(Event).filter( - Event.owner_id == event.owner_id, Event.title == event.title).all() + all_events_with_same_name = ( + session.query(Event) + .filter(Event.owner_id == event.owner_id, Event.title == event.title) + .all() + ) return check_diffs(event, all_events_with_same_name) -def get_messages(session: Session, - event: Event, - uninvited_contacts: Set[str]) -> List[str]: +def get_messages( + session: Session, + event: Event, + uninvited_contacts: Set[str], +) -> List[str]: messages = [] if uninvited_contacts: - messages.append(f'Forgot to invite ' - f'{", ".join(uninvited_contacts)} maybe?') + messages.append( + f"Forgot to invite " f'{", ".join(uninvited_contacts)} maybe?', + ) pattern = find_pattern(session, event) for weeks_diff in pattern: - messages.append(f'Same event happened {weeks_diff} weeks before too. ' - f'Want to create another one {weeks_diff} after too?') + messages.append( + f"Same event happened {weeks_diff} weeks before too. " + f"Want to create another one {weeks_diff} after too?", + ) return messages diff --git a/app/routers/event.py b/app/routers/event.py index a8e290bc..647e124a 100644 --- a/app/routers/event.py +++ b/app/routers/event.py @@ -72,7 +72,7 @@ async def create_event_api(event: EventModel, session=Depends(get_db)): db=session, title=event.title, start=event.start, - end=event.start, + end=event.end, content=event.content, owner_id=event.owner_id, location=event.location, @@ -117,7 +117,7 @@ async def create_new_event( location = data["location"] all_day = data["event_type"] and data["event_type"] == "on" - vc_link = data["vc_link"] + vc_link = data.get("vc_link") category_id = data.get("category_id") privacy = data["privacy"] privacy_kinds = [kind.name for kind in PrivacyKinds] @@ -132,7 +132,7 @@ async def create_new_event( invited_emails, ) - if vc_link is not None: + if vc_link: raise_if_zoom_link_invalid(vc_link) event = create_event( diff --git a/app/templates/partials/calendar/event/edit_event_details_tab.html b/app/templates/partials/calendar/event/edit_event_details_tab.html index 50a35296..7d538bd8 100644 --- a/app/templates/partials/calendar/event/edit_event_details_tab.html +++ b/app/templates/partials/calendar/event/edit_event_details_tab.html @@ -40,25 +40,22 @@
    - - - + +
    -
    - +
    + +
    - - + + +
    + +
    +
    @@ -72,6 +69,12 @@
    + + + + + + +
    + {% for song in songs %} +
    + + {% endfor %} +
    + + +
    + + + + + +
    + + + + + + +
    + +{% endblock %} diff --git a/app/templates/base.html b/app/templates/base.html index dc93d52c..fd313c61 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -46,6 +46,9 @@ + @@ -84,9 +87,12 @@ + + + - \ No newline at end of file + diff --git a/app/templates/home.html b/app/templates/home.html index 9405d7e7..f7d8d0d2 100644 --- a/app/templates/home.html +++ b/app/templates/home.html @@ -1,4 +1,4 @@ -{% extends "partials/index/index_base.html" %} +{% extends "base.html" %} {% block content %} @@ -6,6 +6,12 @@
    +
    +
    +
    diff --git a/app/templates/index.html b/app/templates/index.html index 3bc1d8fc..15488dcc 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -15,14 +15,29 @@

    - {% if quote %} - {% if not quote.author%} -

    "{{ quote.text }}"

    - {% else %} -

    "{{ quote.text }}"   \ {{quote.author}} -

    - {% endif %} - {% endif %} + {% if quote %} + {% if not quote.author%} +

    "{{ quote.text }}"

    + {% else %} +

    "{{ quote.text }}"   \ {{quote.author}} +

    + {% endif %} + {% endif %} +
    +
    + +

    diff --git a/app/templates/partials/base.html b/app/templates/partials/base.html index 4338cacd..790a617d 100644 --- a/app/templates/partials/base.html +++ b/app/templates/partials/base.html @@ -28,6 +28,11 @@ integrity="sha512-d9xgZrVZpmmQlfonhQUvTR7lMPtO7NkZMkA0ABN3PHCbKA5nqylQ/yWlFAyY6hYgdF1Qh6nYiuADWwKB4C2WSw==" crossorigin="anonymous"> + + + + + {% endblock head %} {% block title %} Pylendar{% if self.page_name() %} - {% endif %}{% block page_name %}{% endblock %} @@ -37,4 +42,4 @@ {% block body %} {% endblock %} - + \ No newline at end of file diff --git a/app/templates/partials/index/navigation.html b/app/templates/partials/index/navigation.html index c36d2343..d9394182 100644 --- a/app/templates/partials/index/navigation.html +++ b/app/templates/partials/index/navigation.html @@ -23,6 +23,9 @@ place will change later according to the web design --> Agenda + @@ -41,4 +44,4 @@
- \ No newline at end of file + diff --git a/tests/client_fixture.py b/tests/client_fixture.py index c40b9fb8..465cfe8d 100644 --- a/tests/client_fixture.py +++ b/tests/client_fixture.py @@ -7,8 +7,15 @@ from app import main from app.database.models import Base, User from app.routers import ( - agenda, categories, event, friendview, google_connect, - invitation, profile, weight, + agenda, + audio, + categories, + event, + friendview, + google_connect, + invitation, + profile, + weight, ) from app.routers.salary import routes as salary from tests import security_testing_routes @@ -19,12 +26,12 @@ def get_test_placeholder_user() -> User: return User( - username='fake_user', - email='fake@mail.fake', - password='123456fake', - full_name='FakeName', + username="fake_user", + email="fake@mail.fake", + password="123456fake", + full_name="FakeName", language_id=1, - telegram_id='666666', + telegram_id="666666", ) @@ -84,7 +91,8 @@ def profile_test_client() -> Generator[Session, None, None]: Base.metadata.create_all(bind=test_engine) main.app.dependency_overrides[profile.get_db] = get_test_db main.app.dependency_overrides[ - profile.get_placeholder_user] = get_test_placeholder_user + profile.get_placeholder_user + ] = get_test_placeholder_user with TestClient(main.app) as client: yield client @@ -93,6 +101,11 @@ def profile_test_client() -> Generator[Session, None, None]: Base.metadata.drop_all(bind=test_engine) +@pytest.fixture(scope="session") +def audio_test_client() -> Iterator[TestClient]: + yield from create_test_client(audio.get_db) + + @pytest.fixture(scope="session") def security_test_client(): yield from create_test_client(event.get_db) diff --git a/tests/test_audio.py b/tests/test_audio.py new file mode 100644 index 00000000..f7040b9b --- /dev/null +++ b/tests/test_audio.py @@ -0,0 +1,56 @@ +from app.routers.audio import router + +AUDIO_SETTINGS_URL = router.url_path_for("audio_settings") +GET_CHOICES_URL = router.url_path_for("get_choices") +START_AUDIO_URL = router.url_path_for("start_audio") + + +def test_get_settings(audio_test_client): + response = audio_test_client.get(url=AUDIO_SETTINGS_URL) + assert response.ok + assert b"Audio Settings" in response.content + + +def test_start_audio_default(audio_test_client): + response = audio_test_client.get(START_AUDIO_URL) + assert response.ok + + +def test_choices_Off(audio_test_client): + data = {"music_on": False, "sfx_on": False} + response = audio_test_client.post(url=GET_CHOICES_URL, data=data) + assert response.ok + + +def test_choices_On(audio_test_client): + data = { + "music_on": True, + "music_choices": ["GASTRONOMICA.mp3"], + "music_vol": 50, + "sfx_on": True, + "sfx_choice": "click_1.wav", + "sfx_vol": 50, + } + response = audio_test_client.post(url=GET_CHOICES_URL, data=data) + assert response.ok + + +def test_start_audio(audio_test_client): + data = { + "music_on": True, + "music_choices": ["GASTRONOMICA.mp3"], + "music_vol": 50, + "sfx_on": True, + "sfx_choice": "click_1.wav", + "sfx_vol": 50, + } + audio_test_client.post(url=GET_CHOICES_URL, data=data) + response = audio_test_client.get(url=START_AUDIO_URL) + assert response.ok + + +def test_start_audio_sfx_off(audio_test_client): + data = {"music_on_off": "Off", "sfx_on_off": "Off"} + audio_test_client.post(url=GET_CHOICES_URL, data=data) + response = audio_test_client.get(url=START_AUDIO_URL) + assert response.ok From 190a2742e4ab321fee77083c9268e9016996e14f Mon Sep 17 00:00:00 2001 From: efratush <70986341+efratush@users.noreply.github.com> Date: Mon, 22 Feb 2021 02:58:32 +0200 Subject: [PATCH 14/46] Feature/waze + Fix event (#313) * add waze link and fix edit_event_datails_tab.hatml * Add documentation to the 'get_waze_link' function. * Fix: vc_link can be null * Fix CR: block title and + Change the order of the rows in the get_waze_link function Co-authored-by: Yam Mesicka --- app/database/models.py | 2 +- app/routers/event.py | 22 +++ app/static/images/icons/waze.svg | 172 ++++++++++++++++++ app/templates/eventview.html | 13 +- .../event/edit_event_details_tab.html | 24 +-- .../event/view_event_details_tab.html | 9 +- tests/test_event.py | 13 ++ 7 files changed, 240 insertions(+), 15 deletions(-) create mode 100644 app/static/images/icons/waze.svg diff --git a/app/database/models.py b/app/database/models.py index ede86caa..048fa53f 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -92,8 +92,8 @@ class Event(Base): end = Column(DateTime, nullable=False) content = Column(String) location = Column(String, nullable=True) - is_google_event = Column(Boolean, default=False) vc_link = Column(String, nullable=True) + is_google_event = Column(Boolean, default=False) color = Column(String, nullable=True) all_day = Column(Boolean, default=False) invitees = Column(String) diff --git a/app/routers/event.py b/app/routers/event.py index 647e124a..d87e206e 100644 --- a/app/routers/event.py +++ b/app/routers/event.py @@ -2,6 +2,7 @@ import json from operator import attrgetter from typing import Any, Dict, List, Optional, Tuple +import urllib from fastapi import APIRouter, Depends, HTTPException, Request from pydantic import BaseModel @@ -160,6 +161,23 @@ async def create_new_event( ) +def get_waze_link(event: Event) -> str: + """Get a waze navigation link to the event location. + + Returns: + If there are coordinates, waze will navigate to the exact location. + Otherwise, waze will look for the address that appears in the location. + If there is no address, an empty string will be returned.""" + + if not event.location: + return "" + # if event.latitude and event.longitude: + # coordinates = f"{event.latitude},{event.longitude}" + # return f"https://waze.com/ul?ll={coordinates}&navigate=yes" + url_location = urllib.parse.quote(event.location) + return f"https://waze.com/ul?q={url_location}&navigate=yes" + + def raise_for_nonexisting_event(event_id: int) -> None: error_message = f"Event ID does not exist. ID: {event_id}" logger.exception(error_message) @@ -180,6 +198,7 @@ async def eventview( if event.all_day: start_format = "%A, %d/%m/%Y" end_format = "" + waze_link = get_waze_link(event) event_considering_privacy = event_to_show(event, db) if not event_considering_privacy: raise_for_nonexisting_event(event.id) @@ -188,6 +207,7 @@ async def eventview( "eventview.html", { "request": request, + "waze_link": waze_link, "event": event_considering_privacy, "comments": comments, "start_format": start_format, @@ -597,11 +617,13 @@ async def view_comments( This essentially the same as `eventedit`, only with comments tab auto showed.""" event, comments, end_format = get_event_data(db, event_id) + waze_link = get_waze_link(event) return templates.TemplateResponse( "eventview.html", { "request": request, "event": event, + "waze_link": waze_link, "comments": comments, "comment": True, "start_format": START_FORMAT, diff --git a/app/static/images/icons/waze.svg b/app/static/images/icons/waze.svg new file mode 100644 index 00000000..d2c6330b --- /dev/null +++ b/app/static/images/icons/waze.svg @@ -0,0 +1,172 @@ + +image/svg+xml diff --git a/app/templates/eventview.html b/app/templates/eventview.html index 3d219975..602f290c 100644 --- a/app/templates/eventview.html +++ b/app/templates/eventview.html @@ -1,4 +1,14 @@ - +{% extends 'base.html' %} + +{% block head %} + {{ super() }} + {% block title %} + View Event + {% endblock %} + +{% endblock head %} + +{% block content %}
--> + {% endblock content %} diff --git a/app/templates/partials/calendar/event/edit_event_details_tab.html b/app/templates/partials/calendar/event/edit_event_details_tab.html index 7d538bd8..714484f5 100644 --- a/app/templates/partials/calendar/event/edit_event_details_tab.html +++ b/app/templates/partials/calendar/event/edit_event_details_tab.html @@ -1,9 +1,9 @@
- - -
+ + +
+
@@ -40,18 +40,18 @@
- - + +
- - + +
- - + +
@@ -77,8 +77,8 @@ diff --git a/app/templates/partials/calendar/event/view_event_details_tab.html b/app/templates/partials/calendar/event/view_event_details_tab.html index a886cc1a..98478652 100644 --- a/app/templates/partials/calendar/event/view_event_details_tab.html +++ b/app/templates/partials/calendar/event/view_event_details_tab.html @@ -22,11 +22,18 @@

{{ event.title }}

+{% if event.location %}
- ICON + icon wase
{{ event.location }}
+
+{% endif %} +{% if event.vc_link %} +
+ ICON
VC linkVC URL
+{% endif %} {% if event.invitees %}
diff --git a/tests/test_event.py b/tests/test_event.py index 81f0aae2..4c118669 100644 --- a/tests/test_event.py +++ b/tests/test_event.py @@ -164,6 +164,19 @@ def test_eventview_with_id(event_test_client, session, event): response = event_test_client.get(f"/event/{event_id}") assert response.ok assert b"Event Details" in response.content + assert b"View Event" in response.content + assert b"Some random location" in response.content + waze_link = b"https://waze.com/ul?q=Some%20random%20location" + assert waze_link in response.content + assert b'VC link' not in response.content + + +def test_eventview_without_location(event_test_client, session, event): + event_id = event.id + event.location = None + session.commit() + response = event_test_client.get(f"/event/{event_id}") + assert b"https://waze.com/ul" not in response.content def test_all_day_eventview_with_id(event_test_client, session, all_day_event): From 9c9c499d1f06a59e526f63b8c3721918feb498b7 Mon Sep 17 00:00:00 2001 From: nadav pesach <71080776+nadav-pesach@users.noreply.github.com> Date: Mon, 22 Feb 2021 11:28:57 +0200 Subject: [PATCH 15/46] feat: busiest day of the week graph (#243) --- app/internal/agenda_events.py | 27 ++++++++++++++++++- app/routers/agenda.py | 8 +++++- app/static/graph.js | 49 +++++++++++++++++++++++++++++++++++ app/templates/agenda.html | 9 +++++++ app/templates/base.html | 1 + tests/test_agenda_internal.py | 23 ++++++++++++++++ tests/test_agenda_route.py | 2 +- 7 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 app/static/graph.js diff --git a/app/internal/agenda_events.py b/app/internal/agenda_events.py index a7053a46..ccd4bb23 100644 --- a/app/internal/agenda_events.py +++ b/app/internal/agenda_events.py @@ -1,6 +1,6 @@ import datetime from datetime import date, timedelta -from typing import Iterator, List, Optional, Union +from typing import Dict, Iterator, List, Optional, Tuple, Union import arrow from sqlalchemy.orm import Session @@ -89,3 +89,28 @@ def get_events_in_time_frame( """Yields all user's events in a time frame.""" events = get_all_user_events(db, user_id) yield from filter_dates(events, start_date, end_date) + + +def get_events_for_the_week(db: Session, user_id: int + ) -> Tuple[Union[Iterator[Event], list], Dict]: + WEEK_DAYS = 7 + start_date = date.today() + end_date = start_date + timedelta(days=WEEK_DAYS - 1) + + events_this_week = get_events_per_dates( + db, user_id, start_date, end_date + ) + events_for_graph = { + str(start_date + timedelta(i)): 0 for i in range(WEEK_DAYS) + } + return events_this_week, events_for_graph + + +def make_dict_for_graph_data(db: Session, user_id: int) -> Dict[str, int]: + """create a dict with number of events per day for current week""" + events_this_week, events_for_graph = get_events_for_the_week(db, user_id) + + for event_obj in events_this_week: + event_date = event_obj.start.date() + events_for_graph[str(event_date)] += 1 + return events_for_graph diff --git a/app/routers/agenda.py b/app/routers/agenda.py index c25012ce..6cc5a7af 100644 --- a/app/routers/agenda.py +++ b/app/routers/agenda.py @@ -1,5 +1,6 @@ from collections import defaultdict from datetime import date, timedelta +import json from typing import Optional, Tuple from fastapi import APIRouter, Depends, Request @@ -38,6 +39,7 @@ def agenda( """Route for the agenda page, using dates range or exact amount of days.""" user_id = 1 # there is no user session yet, so I use user id- 1. + start_date, end_date = calc_dates_range_for_agenda( start_date, end_date, days ) @@ -46,15 +48,19 @@ def agenda( db, user_id, start_date, end_date ) events = defaultdict(list) + for event_obj in events_objects: event_duration = agenda_events.get_time_delta_string( event_obj.start, event_obj.end ) events[event_obj.start.date()].append((event_obj, event_duration)) - + events_for_graph = json.dumps( + agenda_events.make_dict_for_graph_data(db, user_id) + ) return templates.TemplateResponse("agenda.html", { "request": request, "events": events, "start_date": start_date, "end_date": end_date, + "events_for_graph": events_for_graph, }) diff --git a/app/static/graph.js b/app/static/graph.js new file mode 100644 index 00000000..0a2b7daf --- /dev/null +++ b/app/static/graph.js @@ -0,0 +1,49 @@ +function busiestDayOfTheWeekGraph(events) { + events = JSON.parse(events); + + const data = Object.values(events); + const labels = Object.keys(events); + const ctx = document.getElementById("myChart"); + ctx.style.backgroundColor = "rgba(255, 255, 255, 1)"; + const myChart = new Chart(ctx, { + type: "bar", + data: { + labels: labels, + datasets: [{ + label: "# Events", + data: data, + backgroundColor: [ + "rgba(255, 99, 132, 0.2)", + "rgba(54, 162, 235, 0.2)", + "rgba(255, 206, 86, 0.2)", + "rgba(75, 192, 192, 0.2)", + "rgba(153, 102, 255, 0.2)", + "rgba(255, 159, 64, 0.2)", + "rgba(200, 130, 40, 0.2)", + "rgba(255, 99, 132, 0.2)" + ], + borderColor: [ + "rgba(255, 99, 132, 1)", + "rgba(54, 162, 235, 1)", + "rgba(255, 206, 86, 1)", + "rgba(75, 192, 192, 1)", + "rgba(153, 102, 255, 1)", + "rgba(255, 159, 64, 1)", + "rgba(200, 130, 64, 1)", + "rgba(255, 99, 132, 1)" + ], + borderWidth: 1 + }] + } + }); +} + +function addEventsAfterPageLoaded() { + const element = document.getElementsByClassName("graph")[0]; + element.addEventListener("click", function() { + let eventsPerDateData = element.name; + busiestDayOfTheWeekGraph(eventsPerDateData); + }, false); +} + +document.addEventListener("DOMContentLoaded", addEventsAfterPageLoaded); \ No newline at end of file diff --git a/app/templates/agenda.html b/app/templates/agenda.html index f86e5a38..693c5eee 100644 --- a/app/templates/agenda.html +++ b/app/templates/agenda.html @@ -30,6 +30,15 @@ +
+ +
+
+ +
+ +
+
diff --git a/app/templates/base.html b/app/templates/base.html index fd313c61..5d211ad5 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -87,6 +87,7 @@ + diff --git a/tests/test_agenda_internal.py b/tests/test_agenda_internal.py index 277204dc..8f38153d 100644 --- a/tests/test_agenda_internal.py +++ b/tests/test_agenda_internal.py @@ -44,3 +44,26 @@ def test_get_events_per_dates_failure(self, yesterday_event, session): end=date.today(), ) assert list(events) == [] + + def test_make_dict_for_graph_data(self, today_event, session): + events_for_graph = agenda_events.make_dict_for_graph_data( + session, + user_id=today_event.owner_id, + ) + assert isinstance(events_for_graph, dict) + + def test_get_events_for_the_week_success(self, today_event, session): + events, events_for_graph = agenda_events.get_events_for_the_week( + session, + user_id=today_event.owner_id, + ) + assert isinstance(events_for_graph, dict) + assert list(events) == [today_event] + + def test_get_events_for_the_week_failure(self, yesterday_event, session): + events, events_for_graph = agenda_events.get_events_for_the_week( + session, + user_id=yesterday_event.owner_id, + ) + assert list(events) == [] + assert isinstance(events_for_graph, dict) diff --git a/tests/test_agenda_route.py b/tests/test_agenda_route.py index 1bd6682b..c3c633be 100644 --- a/tests/test_agenda_route.py +++ b/tests/test_agenda_route.py @@ -86,7 +86,7 @@ def test_agenda_between_two_dates( assert b"event 5" in resp.content assert b"event 6" not in resp.content - def test_agenda_start_bigger_than_end(self, agenda_test_client): + def test_agenda_start_bigger_than_end(self, agenda_test_client, session): start_date = self.today_date.date() end_date = (self.today_date - timedelta(days=2)).date() resp = agenda_test_client.get( From 2176d5f502f0723c1425748d49d24a6d1d082797 Mon Sep 17 00:00:00 2001 From: subscorp <33960879+subscorp@users.noreply.github.com> Date: Mon, 22 Feb 2021 11:29:39 +0200 Subject: [PATCH 16/46] fix: fixed a bug in index.html (#330) --- app/templates/index.html | 9 --------- 1 file changed, 9 deletions(-) diff --git a/app/templates/index.html b/app/templates/index.html index 15488dcc..5c926972 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -29,15 +29,6 @@

Start Audio Stop Audio

-
From b91708d299bde83bbd10ff93496b23af9d35dd8f Mon Sep 17 00:00:00 2001 From: ellenc345 Date: Mon, 22 Feb 2021 19:44:20 +0200 Subject: [PATCH 17/46] feat: add alembic configuration (#220) --- alembic.ini | 86 +++++++++++ app/database/alembic/README | 14 ++ app/database/alembic/__init__.py | 0 app/database/alembic/env.py | 79 ++++++++++ app/database/alembic/script.py.mako | 24 ++++ .../alembic/versions/91b42971b0df_.py | 135 ++++++++++++++++++ 6 files changed, 338 insertions(+) create mode 100644 alembic.ini create mode 100644 app/database/alembic/README create mode 100644 app/database/alembic/__init__.py create mode 100644 app/database/alembic/env.py create mode 100644 app/database/alembic/script.py.mako create mode 100644 app/database/alembic/versions/91b42971b0df_.py diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 00000000..16e9fab7 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,86 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +# script_location = alembic +script_location = app\\database\\alembic + +# template used to generate migration files +file_template = %%(year)d-%%(month).2d-%%(day).2d_%%(rev)s_%%(slug)s + +# timezone to use when rendering the date +# within the migration file as well as the filename. +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; this defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path +# version_locations = %(here)s/bar %(here)s/bat alembic/versions + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +# sqlalchemy.url = driver://user:pass@localhost/dbname + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks=black +# black.type=console_scripts +# black.entrypoint=black +# black.options=-l 79 + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/app/database/alembic/README b/app/database/alembic/README new file mode 100644 index 00000000..7239903d --- /dev/null +++ b/app/database/alembic/README @@ -0,0 +1,14 @@ +Generic single-database configuration. + +# following command will run all migration script and bring it to latest version +alembic upgrade head + +# If we like to incrementally upgrade and check for some errors +alembic upgrade +1 + +# To undo last migration +alembic downgrade -1 + +# To get more information +alembic current +alembic history - verbose \ No newline at end of file diff --git a/app/database/alembic/__init__.py b/app/database/alembic/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/database/alembic/env.py b/app/database/alembic/env.py new file mode 100644 index 00000000..bca6fab9 --- /dev/null +++ b/app/database/alembic/env.py @@ -0,0 +1,79 @@ +from logging.config import fileConfig +import os + +from alembic import context +from sqlalchemy import create_engine + +from app import config as app_config +from app.database.models import Base + + +SQLALCHEMY_DATABASE_URL = os.getenv( + "DATABASE_CONNECTION_STRING", app_config.DEVELOPMENT_DATABASE_STRING) + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name, disable_existing_loggers=False) + +# add your model's MetaData object here +# for 'autogenerate' support +target_metadata = Base.metadata + + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = SQLALCHEMY_DATABASE_URL + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + connectable = create_engine(SQLALCHEMY_DATABASE_URL) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata, + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/app/database/alembic/script.py.mako b/app/database/alembic/script.py.mako new file mode 100644 index 00000000..2c015630 --- /dev/null +++ b/app/database/alembic/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/app/database/alembic/versions/91b42971b0df_.py b/app/database/alembic/versions/91b42971b0df_.py new file mode 100644 index 00000000..18a5d836 --- /dev/null +++ b/app/database/alembic/versions/91b42971b0df_.py @@ -0,0 +1,135 @@ +"""empty message + +Revision ID: 91b42971b0df +Revises: +Create Date: 2021-02-06 16:15:07.861957 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import sqlite + +# revision identifiers, used by Alembic. +revision = '91b42971b0df' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index('ix_categories_id', table_name='categories') + op.drop_table('categories') + op.drop_index('ix_invitations_id', table_name='invitations') + op.drop_table('invitations') + op.drop_index('ix_users_id', table_name='users') + op.drop_table('users') + op.drop_index('ix_quotes_id', table_name='quotes') + op.drop_table('quotes') + op.drop_index('ix_wikipedia_events_id', table_name='wikipedia_events') + op.drop_table('wikipedia_events') + op.drop_index('ix_zodiac-signs_id', table_name='zodiac-signs') + op.drop_table('zodiac-signs') + op.drop_index('ix_events_id', table_name='events') + op.drop_table('events') + op.drop_index('ix_user_event_id', table_name='user_event') + op.drop_table('user_event') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('user_event', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('user_id', sa.INTEGER(), nullable=True), + sa.Column('event_id', sa.INTEGER(), nullable=True), + sa.ForeignKeyConstraint(['event_id'], ['events.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_user_event_id', 'user_event', ['id'], unique=False) + op.create_table('events', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('title', sa.VARCHAR(), nullable=False), + sa.Column('start', sa.DATETIME(), nullable=False), + sa.Column('end', sa.DATETIME(), nullable=False), + sa.Column('content', sa.VARCHAR(), nullable=True), + sa.Column('location', sa.VARCHAR(), nullable=True), + sa.Column('color', sa.VARCHAR(), nullable=True), + sa.Column('owner_id', sa.INTEGER(), nullable=True), + sa.Column('category_id', sa.INTEGER(), nullable=True), + sa.ForeignKeyConstraint( + ['category_id'], ['categories.id'], ), + sa.ForeignKeyConstraint(['owner_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_events_id', 'events', ['id'], unique=False) + op.create_table('zodiac-signs', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('name', sa.VARCHAR(), nullable=False), + sa.Column('start_month', sa.INTEGER(), nullable=False), + sa.Column('start_day_in_month', + sa.INTEGER(), nullable=False), + sa.Column('end_month', sa.INTEGER(), nullable=False), + sa.Column('end_day_in_month', + sa.INTEGER(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_zodiac-signs_id', 'zodiac-signs', ['id'], unique=False) + op.create_table('wikipedia_events', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('date_', sa.VARCHAR(), nullable=False), + sa.Column('wikipedia', sa.VARCHAR(), nullable=False), + sa.Column('events', sqlite.JSON(), nullable=True), + sa.Column('date_inserted', sa.DATETIME(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_wikipedia_events_id', + 'wikipedia_events', ['id'], unique=False) + op.create_table('quotes', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('text', sa.VARCHAR(), nullable=False), + sa.Column('author', sa.VARCHAR(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_quotes_id', 'quotes', ['id'], unique=False) + op.create_table('users', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('username', sa.VARCHAR(), nullable=False), + sa.Column('email', sa.VARCHAR(), nullable=False), + sa.Column('password', sa.VARCHAR(), nullable=False), + sa.Column('full_name', sa.VARCHAR(), nullable=True), + sa.Column('language', sa.VARCHAR(), nullable=True), + sa.Column('description', sa.VARCHAR(), nullable=True), + sa.Column('avatar', sa.VARCHAR(), nullable=True), + sa.Column('telegram_id', sa.VARCHAR(), nullable=True), + sa.Column('is_active', sa.BOOLEAN(), nullable=True), + sa.CheckConstraint('is_active IN (0, 1)'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('email'), + sa.UniqueConstraint('telegram_id'), + sa.UniqueConstraint('username') + ) + op.create_index('ix_users_id', 'users', ['id'], unique=False) + op.create_table('invitations', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('status', sa.VARCHAR(), nullable=False), + sa.Column('recipient_id', sa.INTEGER(), nullable=True), + sa.Column('event_id', sa.INTEGER(), nullable=True), + sa.Column('creation', sa.DATETIME(), nullable=True), + sa.ForeignKeyConstraint(['event_id'], ['events.id'], ), + sa.ForeignKeyConstraint(['recipient_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_invitations_id', 'invitations', ['id'], unique=False) + op.create_table('categories', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('name', sa.VARCHAR(), nullable=False), + sa.Column('color', sa.VARCHAR(), nullable=False), + sa.Column('user_id', sa.INTEGER(), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user_id', 'name', 'color') + ) + op.create_index('ix_categories_id', 'categories', ['id'], unique=False) + # ### end Alembic commands ### From a8c33bcb10127be08a0c43333d9411d45d0eef21 Mon Sep 17 00:00:00 2001 From: liaarbel <71096174+liaarbel@users.noreply.github.com> Date: Tue, 23 Feb 2021 13:51:20 +0200 Subject: [PATCH 18/46] feat: added dates calculator functions, templates and styles (#274) --- app/static/grid_style.css | 5 +++ app/static/js/dates_calculator.js | 21 +++++++++++++ app/templates/calendar_monthly_view.html | 31 +++++++++++++------ .../partials/calendar/calendar_base.html | 2 ++ 4 files changed, 50 insertions(+), 9 deletions(-) create mode 100644 app/static/js/dates_calculator.js diff --git a/app/static/grid_style.css b/app/static/grid_style.css index d824c1b0..63a4b598 100644 --- a/app/static/grid_style.css +++ b/app/static/grid_style.css @@ -371,4 +371,9 @@ main { .button:hover { font-weight: 700; +} + +.dates-calc { + background-color: #222831; + color: white; } \ No newline at end of file diff --git a/app/static/js/dates_calculator.js b/app/static/js/dates_calculator.js new file mode 100644 index 00000000..12754fd4 --- /dev/null +++ b/app/static/js/dates_calculator.js @@ -0,0 +1,21 @@ +window.addEventListener('DOMContentLoaded', (event) => { + document.getElementById("CalcBtn").addEventListener("click", hiddenDifference); +}); + +function hiddenDifference() { + if (document.getElementById("endDate").value == '') { + swal("Expected end date"); + return; + } + let date1 = document.getElementById("startDate").value; + const date2 = new Date(document.getElementById("endDate").value); + if (date1 != '') { + date1 = new Date(date1); + } + else { + date1 = Date.now(); + } + const diffDates = Math.abs(date2 - date1); + const diffInDays = Math.ceil(diffDates / (1000 * 60 * 60 * 24)); + document.getElementById("demo").innerText = "The difference: " + (diffInDays) + " days"; +} diff --git a/app/templates/calendar_monthly_view.html b/app/templates/calendar_monthly_view.html index 5a1d3cf2..d586ee88 100644 --- a/app/templates/calendar_monthly_view.html +++ b/app/templates/calendar_monthly_view.html @@ -1,17 +1,30 @@ {% extends "partials/calendar/calendar_base.html" %} {% block content %} -
+
-
{{ day.display() }}
-
Location 0oc 00:00
+
{{ day.display() }}
+
Location 0oc 00:00
+
+
+

Time calculator:

+ + + +
+ + + + +

-
-
+
+
{% include 'partials/calendar/monthly_view/monthly_grid.html' %} -
+ {% endblock content %} \ No newline at end of file diff --git a/app/templates/partials/calendar/calendar_base.html b/app/templates/partials/calendar/calendar_base.html index c70ce366..7f539c37 100644 --- a/app/templates/partials/calendar/calendar_base.html +++ b/app/templates/partials/calendar/calendar_base.html @@ -19,4 +19,6 @@ + + {% endblock body %} From 31b943277f941e3368949d1f24e07718c6ec871c Mon Sep 17 00:00:00 2001 From: Yuval Cagan <71037984+ShiZinDle@users.noreply.github.com> Date: Tue, 23 Feb 2021 14:44:02 +0200 Subject: [PATCH 19/46] Feature/meds reminder (#292) --- app/internal/calendar_privacy.py | 4 +- app/internal/meds.py | 399 ++++++++++++++++++ .../{dependancies.py => dependencies.py} | 0 app/internal/utils.py | 25 +- app/main.py | 2 + app/routers/meds.py | 59 +++ app/routers/salary/config.py | 2 - app/routers/salary/routes.py | 9 +- app/routers/salary/utils.py | 29 +- app/templates/agenda.html | 2 +- app/templates/celebrity.html | 2 +- app/templates/demo/home_email.html | 2 +- .../partials/edit_event_details_tab.html | 73 ++++ app/templates/eventview.html | 2 +- app/templates/four_o_four.j2 | 2 +- app/templates/hello.html | 2 +- app/templates/import_holidays.html | 2 +- app/templates/index.html | 2 +- app/templates/invitations.html | 2 +- app/templates/login.html | 2 +- app/templates/meds.j2 | 71 ++++ .../event/view_event_details_tab.html | 7 +- app/templates/partials/index/navigation.html | 8 +- app/templates/profile.html | 2 +- app/templates/register.html | 2 +- app/templates/salary/month.j2 | 2 +- app/templates/salary/pick.j2 | 2 +- app/templates/salary/settings.j2 | 2 +- app/templates/salary/view.j2 | 2 +- app/templates/search.html | 2 +- tests/client_fixture.py | 6 + tests/meds/test_internal.py | 311 ++++++++++++++ tests/meds/test_routers.py | 39 ++ tests/salary/test_utils.py | 12 +- tests/security_testing_routes.py | 2 +- tests/test_calendar_privacy.py | 2 +- tests/test_event.py | 2 +- tests/test_utils.py | 18 + 38 files changed, 1045 insertions(+), 69 deletions(-) create mode 100644 app/internal/meds.py rename app/internal/security/{dependancies.py => dependencies.py} (100%) create mode 100644 app/routers/meds.py create mode 100644 app/templates/event/partials/edit_event_details_tab.html create mode 100644 app/templates/meds.j2 create mode 100644 tests/meds/test_internal.py create mode 100644 tests/meds/test_routers.py diff --git a/app/internal/calendar_privacy.py b/app/internal/calendar_privacy.py index ad86ccd4..f38f0f4a 100644 --- a/app/internal/calendar_privacy.py +++ b/app/internal/calendar_privacy.py @@ -2,14 +2,14 @@ from app.database.models import User from app.internal.privacy import PrivacyKinds # TODO switch to using this when the user system is merged -# from app.internal.security.dependancies import ( +# from app.internal.security.dependencies import ( # current_user, CurrentUser) from fastapi import Depends # TODO add privacy as an attribute in current user -# in app.internal.security.dependancies +# in app.internal.security.dependencies # when user system is merged def can_show_calendar( requested_user_username: str, diff --git a/app/internal/meds.py b/app/internal/meds.py new file mode 100644 index 00000000..2edc817b --- /dev/null +++ b/app/internal/meds.py @@ -0,0 +1,399 @@ +from datetime import date, datetime, time, timedelta +from typing import Any, Dict, Iterator, List, Optional, Tuple + +from pydantic.main import BaseModel +from sqlalchemy.orm.session import Session + +from app.database.models import Event +from app.internal.utils import (create_model, get_time_from_string) + +MAX_EVENT_QUANTITY = 50 + +ERRORS = { + 'finish': 'Finish Date must must be later than or equal to Start Date', + 'max': 'Maximal Interval must must be larger than or equal to Minimal \ + Interval', + 'amount': 'Interval between Earliest Reminder and Latest Reminder not \ + long enough for Daily Amount with Minimal Interval', + 'quantity': 'Total number of reminders can\'t be larger than ' + + f'{MAX_EVENT_QUANTITY}. Please lower the daily amount, or ' + + 'choose a shorter time period.' +} + + +class Form(BaseModel): + """Represents a translated form object. + + name (str, optional) - Medication name. + first (time, optional) - First dose time, if given. + amount (int) - Daily dosage. + early (time) - Earliest reminder time. + late (time) - Latest reminder time. + min (time) - Minimal interval between reminders. + max (time) - Maximal interval between reminders. + start (datetime) - First reminder date and time. + end (datetime) - Last reminder date and time. + note (str, optional) - Additional description. + """ + name: Optional[str] + first: Optional[time] + amount: int + early: time + late: time + min: time + max: time + start: datetime + end: datetime + note: Optional[str] + + +def adjust_day(datetime_obj: datetime, early: time, late: time, + eq: bool = False) -> datetime: + """Returns datetime_obj as same or following day as needed. + + Args: + datetime_obj (datetime): Datetime object to adjust. + early (time): Earlir time object. + late (time): Later time object. + eq (bool): Apply time object comparison. + + Returns: + datetime: Datetime_obj with adjusted date. + """ + if late < early or eq and late == early: + datetime_obj += timedelta(days=1) + return datetime_obj + + +def trans_form(web_form: Dict[str, str]) -> Tuple[Form, Dict[str, Any]]: + """Converts all form data to useable types and return as a Tuple. + + Args: + form (dict(str, str)): Form to translate. + + Returns: + tuple(Form, dict(str, any)): Tuple consisting of: + - Form object with all converted form data. + - Dictionary version of Form object. + """ + form = {} + form['name'] = web_form['name'] + start_date = get_time_from_string(web_form['start']) + form['first'] = get_time_from_string(web_form['first']) + end_date = get_time_from_string(web_form['end']) + form['amount'] = int(web_form['amount']) + form['early'] = get_time_from_string(web_form['early']) + form['late'] = get_time_from_string(web_form['late']) + form['min'] = get_time_from_string(web_form['min']) + form['max'] = get_time_from_string(web_form['max']) + first_time = form['first'] if form['first'] else form['early'] + form['start'] = datetime.combine(start_date, first_time) + end_date = adjust_day( + end_date, web_form['early'], web_form['late'], eq=True) + form['end'] = datetime.combine(end_date, form['late']) + form['note'] = web_form['note'] + + form_obj = Form(**form) + form['start'] = form['start'].date() + form['end'] = form['end'].date() + return form_obj, form + + +def convert_time_to_minutes(time_obj: time) -> int: + """Returns time object as minutes. + + Args: + time_obj (time): Time object to convert to minutes. + + Returns: + int: Total minutes in time object. + """ + return round(time_obj.hour * 60 + time_obj.minute) + + +def get_interval_in_minutes(early: time, late: time) -> int: + """Returns interval between 2 time objects in minutes. + + Args: + early (time): Earlier time object. + late (time): Later time object. Interpreted as following day if earlier + than early. + + Returns: + int: Interval between time objects in minutes. + """ + if early == late: + return 0 + extra = int(early > late) + early_date = datetime.combine(datetime.min, early) + late_date = datetime.combine(datetime.min + timedelta(extra), late) + interval = late_date - early_date + return round(interval.seconds / 60) + + +def validate_amount(amount: int, min: time, early: time, + late: time) -> bool: + """Returns True if interval is sufficient for reminder amount with minimal + interval constraint, False otherwise + + Args: + amount (int): Reminder amount. + min (time): Minimal interval between reminders. + early (time) - Earliest reminder time. + late (time) - Latest reminder time. + + Returns: + bool: True if interval is sufficient for reminder amount with minimal + interval constraint, False otherwise + """ + min_time = (amount - 1) * convert_time_to_minutes(min) + interval = get_interval_in_minutes(early, late) + return min_time <= interval + + +def validate_events(datetimes: Iterator[datetime]) -> bool: + """Return True if total amount of reminders is less than max amount, False + otherwise. + + Args: + datetimes (list(datetime)): Reminder times. + + Returns: + bool: True if total amount of reminders is less than amx amount, False + otherwise. + """ + return len(list(datetimes)) <= MAX_EVENT_QUANTITY + + +def validate_form(form: Form) -> List[str]: + """Returns a list of error messages for given form. + + Args: + form (Form): Form object to validate. + + Returns: + list(str): Error messages for given form. + """ + errors = [] + if form.end < form.start: + errors.append(ERRORS['finish']) + if form.max < form.min: + errors.append(ERRORS['max']) + if not validate_amount(form.amount, form.min, form.early, form.late): + errors.append(ERRORS['amount']) + datetimes = get_reminder_datetimes(form) + if not validate_events(datetimes): + errors.append(ERRORS['quantity']) + + return errors + + +def calc_reminder_interval_in_seconds(form: Form) -> int: + """Returns interval between reminders in seconds. + + Args: + form (Form): Form object containing all relevant data. + + Returns: + int: Interval between reminders in seconds. + """ + if form.amount == 1: + return 0 + reminder_interval = get_interval_in_minutes(form.early, form.late) + max_med_interval = reminder_interval / (form.amount - 1) + min_minutes = convert_time_to_minutes(form.min) + max_minutes = convert_time_to_minutes(form.max) + avg_med_interval = (min_minutes + max_minutes) / 2 + return int(min(max_med_interval, avg_med_interval) * 60) + + +def get_reminder_times(form: Form) -> List[time]: + """Returns a list of time objects for reminders based on form data. + + Args: + form (Form): Form object containing all relevant data. + + Returns: + list(time): Time objects for reminders. + """ + interval = calc_reminder_interval_in_seconds(form) + times = [] + early_reminder = datetime.combine(datetime.min, form.early) + for i in range(form.amount): + reminder = early_reminder + timedelta(seconds=interval) * i + times.append(reminder.time()) + + wasted_time = get_interval_in_minutes(times[-1], form.late) / 2 + times = [(datetime.combine(datetime.min, time_obj) + + timedelta(minutes=wasted_time)).time() + for time_obj in times] + + return times + + +def validate_datetime(reminder: datetime, day: date, early: time, + late: time) -> bool: + """Returns True if reminder is between earlist and latest reminder times on + a given date or equal to any of them, False otherwise. + + Args: + reminder (datetime): Datetime object to validate. + day (date): Date for earlist reminder. + early (time): Earliest reminder time. + late (late): Latest reminder time. Interpreted as following day if + earlier than early. + + Returns: + bool: True if reminder is between earlist and latest reminder times on + a given date or equal to any of them, False otherwise. + """ + early_datetime = datetime.combine(day, early) + late_datetime = datetime.combine(day, late) + late_datetime = adjust_day(late_datetime, early, late) + return early_datetime <= reminder <= late_datetime + + +def validate_first_day_reminder(previous: datetime, reminder_time: time, + min: time, max: time) -> bool: + """Returns True if interval between reminders is valid, false otherwise. + + Args: + previous (datetime): Previous reminder. + reminder_time (time): New reminder time. + min (time) - Minimal interval between reminders. + max (time) - Maximal interval between reminders. + + Returns: + bool: True if interval between reminders is valid, false otherwise. + """ + interval = get_interval_in_minutes(previous.time(), reminder_time) + min_minutes = convert_time_to_minutes(min) + max_minutes = convert_time_to_minutes(max) + return max_minutes >= interval >= min_minutes + + +def get_different_time_reminder(previous: datetime, min: time, early: time, + late: time) -> Optional[datetime]: + """Returns datetime object for first day reminder with non-standard time. + + Args: + previous (datetime): Previous reminder. + min (time) - Minimal interval between reminders. + early (time): Earliest reminder time. + late (late): Latest reminder time. Interpreted as following day if + earlier than early. + + Returns: + datetime | None: First day reminder with non-standard time, if valid. + """ + reminder = previous + timedelta(minutes=convert_time_to_minutes(min)) + if validate_datetime(reminder, previous.date(), early, late): + return reminder + + +def create_first_day_reminder(form: Form, reminder_time: time, + previous: datetime) -> Optional[datetime]: + """Returns datetime object for reminder on first day. + + form (Form): Form object containing all relevant data. + reminder_time (time): Time object for new reminder. + previous (datetime): Previous reminder. + + Returns: + datetime | None: First day reminder. + """ + reminder = datetime.combine(form.start.date(), reminder_time) + reminder = adjust_day(reminder, form.early, reminder_time) + if reminder > form.start: + if not validate_first_day_reminder(previous, reminder_time, form.min, + form.max): + reminder = get_different_time_reminder(previous, form.min, + form.early, form.late) + return reminder + + +def get_first_day_reminders(form: Form, + times: List[time]) -> Iterator[datetime]: + """Generates datetime objects for reminders on the first day. + + Args: + form (Form): Form object containing all relevant data. + times (list(time)): Time objects for reminders. + + Yields: + datetime: First day reminder datetime object. + """ + yield form.start + previous = form.start + i = 1 + for reminder_time in times: + if i <= form.amount: + new = create_first_day_reminder(form, reminder_time, previous) + if new: + yield new + previous = new + i += 1 + + +def reminder_generator(times: List[time], early: time, start: datetime, + day: date, end: datetime) -> Iterator[datetime]: + """Generates datetime objects for reminders based on times and date. + + Args: + times (list(time)): Reminder times. + early (time): Earliest reminder time. + start (datetime): First reminder date and time. + day (date): Reminders date. + end (datetime) - Last reminder date and time. + + Yields: + datetime: Reminder datetime object. + """ + for time_obj in times: + extra = int(time_obj < early) + day_date = start.date() + timedelta(day + extra) + reminder = datetime.combine(day_date, time_obj) + if reminder <= end: + yield reminder + + +def get_reminder_datetimes(form: Form) -> Iterator[datetime]: + """Generates datetime object for reminders. + + Args: + form (Form): Form object containing all relevant data. + + Yields: + datetime: Reminder datetime object. + """ + times = get_reminder_times(form) + total_days = (form.end.date() - form.start.date()).days + 1 + for day in range(total_days): + if day == 0 and form.first: + yield from get_first_day_reminders(form, times) + else: + yield from reminder_generator(times, form.early, form.start, day, + form.end) + + +def create_events(session: Session, user_id: int, form: Form) -> None: + """Creates reminder events in the DB based on form data. + + Args: + session (Session): DB session. + user_id (int): ID of user to create events for. + form (Form): Form object containing all relevant data. + """ + title = 'It\'s time to take your meds' + if form.name: + title = f'{form.name.title()} - {title}' + datetimes = get_reminder_datetimes(form) + for event_time in datetimes: + event_data = { + 'title': title, + 'start': event_time, + 'end': event_time + timedelta(minutes=5), + 'content': form.note, + 'owner_id': user_id, + } + create_model(session, Event, **event_data) diff --git a/app/internal/security/dependancies.py b/app/internal/security/dependencies.py similarity index 100% rename from app/internal/security/dependancies.py rename to app/internal/security/dependencies.py diff --git a/app/internal/utils.py b/app/internal/utils.py index 6b96590f..2f669212 100644 --- a/app/internal/utils.py +++ b/app/internal/utils.py @@ -1,4 +1,5 @@ -from typing import Any, List, Optional +from datetime import date, datetime, time +from typing import Any, List, Optional, Union from sqlalchemy.orm import Session @@ -41,7 +42,7 @@ def get_current_user(session: Session) -> User: def get_available_users(session: Session) -> List[User]: - """this function return all availible users.""" + """this function return all available users.""" return session.query(User).filter(not User.disabled).all() @@ -58,6 +59,26 @@ def get_user(session: Session, user_id: int) -> Optional[User]: return session.query(User).filter_by(id=user_id).first() +def get_time_from_string(string: str) -> Optional[Union[date, time]]: + """Converts time string to a date or time object. + + Args: + string (str): Time string. + + Returns: + datetime.time | datetime.date | None: Date or Time object if valid, + None otherwise. + """ + formats = {'%Y-%m-%d': 'date', '%H:%M': 'time', '%H:%M:%S': 'time'} + for time_format, method in formats.items(): + try: + time_obj = getattr(datetime.strptime(string, time_format), method) + except ValueError: + pass + else: + return time_obj() + + def get_placeholder_user() -> User: """Creates a mock user. diff --git a/app/main.py b/app/main.py index aa95cc21..3560d142 100644 --- a/app/main.py +++ b/app/main.py @@ -67,6 +67,7 @@ def create_tables(engine, psql_environment): joke, login, logout, + meds, profile, register, search, @@ -116,6 +117,7 @@ async def swagger_ui_redirect(): joke.router, login.router, logout.router, + meds.router, profile.router, register.router, salary.router, diff --git a/app/routers/meds.py b/app/routers/meds.py new file mode 100644 index 00000000..53761eb6 --- /dev/null +++ b/app/routers/meds.py @@ -0,0 +1,59 @@ +from datetime import date, time, timedelta + +from fastapi import APIRouter +from fastapi.param_functions import Depends +from sqlalchemy.orm.session import Session +from starlette.requests import Request +from starlette.responses import RedirectResponse, Response +from starlette.status import HTTP_303_SEE_OTHER + +from app.dependencies import get_db, templates +from app.internal import meds +from app.internal.utils import get_current_user +from app.main import app + + +router = APIRouter( + prefix='/meds', + tags=['meds'], + dependencies=[Depends(get_db)], +) + + +@router.get('/') +@router.post('/') +async def medications(request: Request, + session: Session = Depends(get_db)) -> Response: + """Renders medication reminders creation form page. Creates reminders in DB + and redirects to home page upon submition if valid.""" + form = await request.form() + errors = [] + + form_data = { + 'name': '', + 'start': date.today(), + 'first': None, + 'end': date.today() + timedelta(days=7), + 'amount': 1, + 'early': time(8), + 'late': time(22), + 'min': time(0, 1), + 'max': time(23, 59), + 'note': '', + } + + if form: + form, form_data = meds.trans_form(form) + user = get_current_user(session) + errors = meds.validate_form(form) + if not errors: + meds.create_events(session, user.id, form) + return RedirectResponse(app.url_path_for('home'), + status_code=HTTP_303_SEE_OTHER) + + return templates.TemplateResponse('meds.j2', { + 'request': request, + 'errors': errors, + 'data': form_data, + 'quantity': meds.MAX_EVENT_QUANTITY, + }) diff --git a/app/routers/salary/config.py b/app/routers/salary/config.py index e7587207..490e9156 100644 --- a/app/routers/salary/config.py +++ b/app/routers/salary/config.py @@ -20,5 +20,3 @@ HOURS_SECONDS_RATIO = 3600 NUMERIC = Union[float, int] -HOUR_FORMAT = '%H:%M:%S' -ALT_HOUR_FORMAT = '%H:%M' diff --git a/app/routers/salary/routes.py b/app/routers/salary/routes.py index 72a8d262..bd5966cd 100644 --- a/app/routers/salary/routes.py +++ b/app/routers/salary/routes.py @@ -8,7 +8,8 @@ from app.database.models import SalarySettings from app.dependencies import get_db, templates -from app.internal.utils import create_model, get_current_user +from app.internal.utils import (create_model, get_current_user, + get_time_from_string) from app.routers.salary import utils router = APIRouter( @@ -111,9 +112,9 @@ async def create_settings(request: Request, 'holiday_category_id': form['holiday_category_id'], 'regular_hour_basis': form['regular_hour_basis'], 'night_hour_basis': form['night_hour_basis'], - 'night_start': utils.get_time_from_string(form['night_start']), - 'night_end': utils.get_time_from_string(form['night_end']), - 'night_min_len': utils.get_time_from_string(form['night_min_len']), + 'night_start': get_time_from_string(form['night_start']), + 'night_end': get_time_from_string(form['night_end']), + 'night_min_len': get_time_from_string(form['night_min_len']), 'first_overtime_amount': form['first_overtime_amount'], 'first_overtime_pay': form['first_overtime_pay'], 'second_overtime_pay': form['second_overtime_pay'], diff --git a/app/routers/salary/utils.py b/app/routers/salary/utils.py index aa922ed2..aadc1533 100644 --- a/app/routers/salary/utils.py +++ b/app/routers/salary/utils.py @@ -4,7 +4,7 @@ from sqlalchemy.orm.session import Session from app.database.models import Event, SalarySettings -from app.internal.utils import save +from app.internal.utils import get_time_from_string, save from app.routers.salary import config DEFAULT_SETTINGS = SalarySettings( @@ -58,7 +58,7 @@ def get_night_times(date: datetime, wage: SalarySettings, """ sub = timedelta(1 if prev_day else 0) return (datetime.combine(date - sub, wage.night_start), - datetime.combine(date + timedelta(1) - sub, wage.night_end)) + datetime.combine(date + timedelta(days=1) - sub, wage.night_end)) def is_night_shift(start: datetime, end: datetime, @@ -120,7 +120,7 @@ def get_relevant_holiday_times( date = end.date() try: return (datetime.combine(date, time(0)), - datetime.combine(date + timedelta(1), + datetime.combine(date + timedelta(days=1), time(0))) except NameError: return datetime.min, datetime.min @@ -358,11 +358,11 @@ def get_relevant_weeks(year: int, """ month_start, month_end = get_month_times(year, month) week_start = month_start - timedelta(month_start.weekday() + 1) - week_end = week_start + timedelta(7) + week_end = week_start + timedelta(days=7) while week_end <= month_end: yield week_start, week_end week_start = week_end - week_end += timedelta(7) + week_end += timedelta(days=7) def get_monthly_overtime( @@ -487,25 +487,6 @@ def get_settings(session: Session, user_id: int, return settings -def get_time_from_string(string: str) -> time: - """Converts time string to a time object. - - Args: - string (str): Time string. - - Returns: - datetime.time: Time object. - - raises: - ValueError: If string is not of format %H:%M:%S' or '%H:%M', - or if string is an invalid time. - """ - try: - return datetime.strptime(string, config.HOUR_FORMAT).time() - except ValueError: - return datetime.strptime(string, config.ALT_HOUR_FORMAT).time() - - def update_settings(session: Session, wage: SalarySettings, form: Dict[str, str]) -> bool: """Update salary settings instance according to info in `form`. diff --git a/app/templates/agenda.html b/app/templates/agenda.html index 693c5eee..2fb9bb60 100644 --- a/app/templates/agenda.html +++ b/app/templates/agenda.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "partials/index/index_base.html" %} {% block head %} {{ super() }} diff --git a/app/templates/celebrity.html b/app/templates/celebrity.html index e549f2ab..6e193e30 100644 --- a/app/templates/celebrity.html +++ b/app/templates/celebrity.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "partials/index/index_base.html" %} {% block content %} diff --git a/app/templates/demo/home_email.html b/app/templates/demo/home_email.html index e0dc5565..bbea32ad 100644 --- a/app/templates/demo/home_email.html +++ b/app/templates/demo/home_email.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "partials/index/index_base.html" %} {% block content %} diff --git a/app/templates/event/partials/edit_event_details_tab.html b/app/templates/event/partials/edit_event_details_tab.html new file mode 100644 index 00000000..75e48b41 --- /dev/null +++ b/app/templates/event/partials/edit_event_details_tab.html @@ -0,0 +1,73 @@ +
+ + +
+ +
+ + +
+
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+ + + + +
+
+ + + + +
+
+ + + +
+
+ +
+
+
+ + +
+
+ + + + +
+
diff --git a/app/templates/eventview.html b/app/templates/eventview.html index 602f290c..ef779db2 100644 --- a/app/templates/eventview.html +++ b/app/templates/eventview.html @@ -1,4 +1,4 @@ -{% extends 'base.html' %} +{% extends "partials/index/index_base.html" %} {% block head %} {{ super() }} diff --git a/app/templates/four_o_four.j2 b/app/templates/four_o_four.j2 index 78c5b2a4..cdaca95d 100644 --- a/app/templates/four_o_four.j2 +++ b/app/templates/four_o_four.j2 @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "partials/index/index_base.html" %} {% block title %} - 404{% endblock %} diff --git a/app/templates/hello.html b/app/templates/hello.html index ed668ab1..6140a52d 100644 --- a/app/templates/hello.html +++ b/app/templates/hello.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "partials/index/index_base.html" %} {% block content %} diff --git a/app/templates/import_holidays.html b/app/templates/import_holidays.html index cca08849..d48038de 100644 --- a/app/templates/import_holidays.html +++ b/app/templates/import_holidays.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "partials/index/index_base.html" %} {% block content %} diff --git a/app/templates/index.html b/app/templates/index.html index 5c926972..7e967f1f 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "partials/index/index_base.html" %} {% block content %} diff --git a/app/templates/invitations.html b/app/templates/invitations.html index a083b420..83d29418 100644 --- a/app/templates/invitations.html +++ b/app/templates/invitations.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "partials/index/index_base.html" %} {% block content %} diff --git a/app/templates/login.html b/app/templates/login.html index 739f3ef3..9b8b116e 100644 --- a/app/templates/login.html +++ b/app/templates/login.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "partials/index/index_base.html" %} {% block content %}

Login

diff --git a/app/templates/meds.j2 b/app/templates/meds.j2 new file mode 100644 index 00000000..6e117d17 --- /dev/null +++ b/app/templates/meds.j2 @@ -0,0 +1,71 @@ +{% extends "partials/index/index_base.html" %} + +{% block content %} +
+
+

Did You Remember to Take Your Meds?

+
+ + {% if errors %} +
+ {% for error in errors %} +

{{ error }}

+ {% endfor %} +
+ {% endif %} + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+{% endblock content %} \ No newline at end of file diff --git a/app/templates/partials/calendar/event/view_event_details_tab.html b/app/templates/partials/calendar/event/view_event_details_tab.html index 98478652..d2c235ed 100644 --- a/app/templates/partials/calendar/event/view_event_details_tab.html +++ b/app/templates/partials/calendar/event/view_event_details_tab.html @@ -11,10 +11,11 @@

{{ event.title }}

ICON {% if end_format != "" %} - - - + {% endif %} -
{{ 'Busy' if event.availability == True else 'Free' }}
+
+ {{ 'Free' if event.availability else 'Busy' }} +
diff --git a/app/templates/partials/index/navigation.html b/app/templates/partials/index/navigation.html index d9394182..b1fa3b3f 100644 --- a/app/templates/partials/index/navigation.html +++ b/app/templates/partials/index/navigation.html @@ -33,7 +33,7 @@ Search + +
diff --git a/app/templates/profile.html b/app/templates/profile.html index 9e3afc64..5ac4c16a 100644 --- a/app/templates/profile.html +++ b/app/templates/profile.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "partials/index/index_base.html" %} {% include "partials/calendar/event/text_editor_partial_head.html" %} {% block content %} diff --git a/app/templates/register.html b/app/templates/register.html index a660b33e..8c136120 100644 --- a/app/templates/register.html +++ b/app/templates/register.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "partials/index/index_base.html" %} {% block content %}

Please Register

diff --git a/app/templates/salary/month.j2 b/app/templates/salary/month.j2 index bc240968..c11e3984 100644 --- a/app/templates/salary/month.j2 +++ b/app/templates/salary/month.j2 @@ -1,4 +1,4 @@ -{% extends 'base.html' %} +{% extends "partials/index/index_base.html" %} {% block content %}
diff --git a/app/templates/salary/pick.j2 b/app/templates/salary/pick.j2 index 4c57f0cc..351431a4 100644 --- a/app/templates/salary/pick.j2 +++ b/app/templates/salary/pick.j2 @@ -1,4 +1,4 @@ -{% extends 'base.html' %} +{% extends "partials/index/index_base.html" %} {% block content %}
diff --git a/app/templates/salary/settings.j2 b/app/templates/salary/settings.j2 index 7c950148..6b266f9d 100644 --- a/app/templates/salary/settings.j2 +++ b/app/templates/salary/settings.j2 @@ -1,4 +1,4 @@ -{% extends 'base.html' %} +{% extends "partials/index/index_base.html" %} {% block content %}
diff --git a/app/templates/salary/view.j2 b/app/templates/salary/view.j2 index 95283348..3fa31b6a 100644 --- a/app/templates/salary/view.j2 +++ b/app/templates/salary/view.j2 @@ -1,4 +1,4 @@ -{% extends 'base.html' %} +{% extends "partials/index/index_base.html" %} {% block content %}
diff --git a/app/templates/search.html b/app/templates/search.html index 6db8bd60..36fcaa2d 100644 --- a/app/templates/search.html +++ b/app/templates/search.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "partials/index/index_base.html" %} {% block content %} diff --git a/tests/client_fixture.py b/tests/client_fixture.py index 465cfe8d..5f5f8971 100644 --- a/tests/client_fixture.py +++ b/tests/client_fixture.py @@ -14,6 +14,7 @@ friendview, google_connect, invitation, + meds, profile, weight, ) @@ -116,6 +117,11 @@ def salary_test_client() -> Iterator[TestClient]: yield from create_test_client(salary.get_db) +@pytest.fixture(scope="session") +def meds_test_client() -> Iterator[TestClient]: + yield from create_test_client(meds.get_db) + + @pytest.fixture(scope="session") def google_connect_test_client(): Base.metadata.create_all(bind=test_engine) diff --git a/tests/meds/test_internal.py b/tests/meds/test_internal.py new file mode 100644 index 00000000..66ca2165 --- /dev/null +++ b/tests/meds/test_internal.py @@ -0,0 +1,311 @@ +from datetime import date, datetime, time +from typing import Dict, List + +import pytest +from sqlalchemy.orm.session import Session + +from app.database.models import Event, User +from app.internal import meds + +NAME = 'Pasta' +QUOTE = 'I don\'t like sand. It\'s coarse and rough and irritating and it \ + gets everywhere.' +WEB_FORM = { + 'name': NAME, + 'start': '2015-10-21', + 'first': '', + 'end': '2015-10-22', + 'amount': '3', + 'early': '08:00', + 'late': '22:00', + 'min': '04:00', + 'max': '06:00', + 'note': QUOTE, +} +FORM = meds.trans_form(WEB_FORM)[0] + + +def create_test_form(form_dict: bool = False, + **kwargs: Dict[str, str]) -> meds.Form: + form = WEB_FORM.copy() + for k, v in kwargs.items(): + form[k] = v + if form_dict: + return form + translated_form, _ = meds.trans_form(form) + return translated_form + + +ADJUST = [ + (datetime(2015, 10, 21), time(8), time(22), False, datetime(2015, 10, 21)), + (datetime(2015, 10, 21), time(8), time(22), True, datetime(2015, 10, 21)), + (datetime(2015, 10, 21), time(8), time(8), False, datetime(2015, 10, 21)), + (datetime(2015, 10, 21), time(8), time(8), True, datetime(2015, 10, 22)), + (datetime(2015, 10, 21), time(8), time(2), False, datetime(2015, 10, 22)), + (datetime(2015, 10, 21), time(8), time(2), True, datetime(2015, 10, 22)), +] + +FORM_TRANS = [ + (WEB_FORM, meds.Form( + name=NAME, first=None, amount=3, early=time(8), late=time(22), + min=time(4), max=time(6), start=datetime(2015, 10, 21, 8), + end=datetime(2015, 10, 22, 22), note=QUOTE), + {'name': NAME, 'first': None, 'amount': 3, 'early': time(8), + 'late': time(22), 'min': time(4), 'max': time(6), + 'start': date(2015, 10, 21), 'end': date(2015, 10, 22), + 'note': QUOTE}), + (create_test_form(form_dict=True, first='13:30'), meds.Form( + name=NAME, first=time(13, 30), amount=3, early=time(8), late=time(22), + min=time(4), max=time(6), start=datetime(2015, 10, 21, 13, 30), + end=datetime(2015, 10, 22, 22), note=QUOTE), + {'name': NAME, 'first': time(13, 30), 'amount': 3, 'early': time(8), + 'late': time(22), 'min': time(4), 'max': time(6), + 'start': date(2015, 10, 21), 'end': date(2015, 10, 22), 'note': QUOTE}), +] + +TIMES = [ + (time(13), 780), + (time(17, 26), 1046), +] + +INTERVAL_MINUTE = [ + (time(4), time(4), 0), + (time(8), time(22), 840), + (time(12), time(2), 840), +] + +AMOUNTS = [ + (1, time(12), time(9), time(17), True), + (2, time(4), time(8), time(22), True), + (3, time(8), time(10), time(20), False), +] + +EVENTS = [ + (FORM, True), + (create_test_form(amount='60'), False), + (create_test_form(end='2015-11-22'), False), +] + +FORM_VALIDATE = [ + (FORM, [False, False, False, False]), + (create_test_form( + end=WEB_FORM['start'], max=WEB_FORM['min'], amount='1', late='10:00' + ), [False, False, False, False]), + (create_test_form(end='1985-10-26'), [True, False, False, False]), + (create_test_form(max='03:00'), [False, True, False, False]), + (create_test_form(late='10:00'), [False, False, True, False]), + (create_test_form(min='00:01', amount='60'), [False, False, False, True]), + (create_test_form( + end='1985-10-26', max='03:00', late='10:00', amount="60" + ), [True, True, True, False]), + (create_test_form(max='03:00', late='10:00', amount="60"), + [False, True, True, True]), +] + +CALC_INTERVAL = [ + (create_test_form(amount='1'), 0), + (FORM, 18000), + (create_test_form(min='00:01', max='23:59'), 25200), +] + +REMINDER_TIMES = [ + (FORM, [time(10), time(15), time(20)]), + (create_test_form(amount='1'), [time(15)]), + (create_test_form(min='00:01', max='23:59'), + [time(8), time(15), time(22)]), + (create_test_form(early='13:00', late='02:00'), + [time(14, 30), time(19, 30), time(0, 30)]), +] + +DATETIMES_VALIDATE = [ + (datetime(1605, 11, 5, 23), date(1605, 11, 5), time(8), time(22), False), + (datetime(1605, 11, 5, 21), date(1605, 11, 5), time(8), time(22), True), + (datetime(1605, 11, 5, 23), date(1605, 11, 5), time(12), time(2), True), +] + +VALIDATE_FIRST = [ + (datetime(2015, 10, 21, 10, 45), time(15), time(4), time(6), True), + (datetime(2015, 10, 21, 10, 45), time(12), time(4), time(6), False), + (datetime(2015, 10, 21, 10, 45), time(17), time(4), time(6), False), +] + +DIFFERENT = [ + (datetime(2015, 10, 21, 11, 45), time(4), time(8), time(22), + datetime(2015, 10, 21, 15, 45)), + (datetime(2015, 10, 21, 20, 45), time(4), time(8), time(22), None) +] + +CREATE_FIRST = [ + (create_test_form(first='10:45'), time(15), datetime(2015, 10, 21, 10, 45), + datetime(2015, 10, 21, 15)), + (create_test_form(first='10:45'), time(14), datetime(2015, 10, 21, 10, 45), + datetime(2015, 10, 21, 14, 45)), + (create_test_form(first='20:30', late='02:00'), time(1), + datetime(2015, 10, 21, 20, 30), datetime(2015, 10, 22, 1)), + (create_test_form(first='21:30', late='02:00'), time(1), + datetime(2015, 10, 21, 21, 30), datetime(2015, 10, 22, 1, 30)), + (create_test_form(first='16:30', late='02:00'), time(10), + datetime(2015, 10, 21, 16, 30), None), +] + +FIRST = [ + (create_test_form(first='10:45'), [time(10), time(15), time(20)], + [datetime(2015, 10, 21, 10, 45), datetime(2015, 10, 21, 15), + datetime(2015, 10, 21, 20)]), + (create_test_form(first='13:30'), [time(10), time(15), time(20)], + [datetime(2015, 10, 21, 13, 30), datetime(2015, 10, 21, 17, 30), + datetime(2015, 10, 21, 21, 30)]), + (create_test_form(first='17:20'), [time(10), time(15), time(20)], + [datetime(2015, 10, 21, 17, 20), datetime(2015, 10, 21, 21, 20)]), + (create_test_form(first='16:43', early='12:00', late='02:00'), + [time(14), time(19), time(0)], + [datetime(2015, 10, 21, 16, 43), datetime(2015, 10, 21, 20, 43), + datetime(2015, 10, 22, 0, 43)]), +] + +REMINDERS = [ + ([time(10), time(15), time(20)], time(8), datetime(2015, 10, 21, 8), + 0, datetime(2015, 10, 22, 22), + [datetime(2015, 10, 21, 10), datetime(2015, 10, 21, 15), + datetime(2015, 10, 21, 20)]), + ([time(10), time(15), time(20)], time(8), datetime(2015, 10, 21, 8), + 1, datetime(2015, 10, 22, 22), + [datetime(2015, 10, 22, 10), datetime(2015, 10, 22, 15), + datetime(2015, 10, 22, 20)]), + ([time(10), time(15), time(20)], time(8), datetime(2015, 10, 21, 8), + 2, datetime(2015, 10, 22, 22), []), +] + +DATETIMES = [ + (FORM, [datetime(2015, 10, 21, 10), datetime(2015, 10, 21, 15), + datetime(2015, 10, 21, 20), datetime(2015, 10, 22, 10), + datetime(2015, 10, 22, 15), datetime(2015, 10, 22, 20)]), + (create_test_form(first='13:30'), + [datetime(2015, 10, 21, 13, 30), datetime(2015, 10, 21, 17, 30), + datetime(2015, 10, 21, 21, 30), datetime(2015, 10, 22, 10), + datetime(2015, 10, 22, 15), datetime(2015, 10, 22, 20)]), +] + +CREATE = [ + (create_test_form(name=None), False), + (FORM, True), +] + + +@pytest.mark.parametrize('datetime_obj, early, late, eq, new_obj', ADJUST) +def test_adjust_day(datetime_obj: datetime, early: time, late: time, eq: bool, + new_obj: datetime) -> None: + assert meds.adjust_day(datetime_obj, early, late, eq) == new_obj + + +@pytest.mark.parametrize('form, form_obj ,form_dict', FORM_TRANS) +def test_trans_form(form: Dict[str, str], form_obj: meds.Form, + form_dict: Dict[str, str]) -> None: + translated_form_obj, translated_form_dict = meds.trans_form(form) + assert translated_form_obj == form_obj + assert translated_form_dict == form_dict + + +@pytest.mark.parametrize('time_obj, minutes', TIMES) +def test_convert_time_to_minutes(time_obj: time, minutes: int) -> None: + assert meds.convert_time_to_minutes(time_obj) == minutes + + +@pytest.mark.parametrize('early, late, interval', INTERVAL_MINUTE) +def test_get_interval_in_minutes(early: time, late: time, + interval: int) -> None: + assert meds.get_interval_in_minutes(early, late) == interval + + +@pytest.mark.parametrize('amount, minimum, early, late, boolean', AMOUNTS) +def test_validate_amount(amount: int, minimum: time, early: time, late: time, + boolean: bool) -> None: + assert meds.validate_amount(amount, minimum, early, late) == boolean + + +@pytest.mark.parametrize('form, boolean', EVENTS) +def test_validate_events(form: meds.Form, boolean: bool) -> None: + datetimes = meds.get_reminder_datetimes(form) + assert meds.validate_events(datetimes) is boolean + + +@pytest.mark.parametrize('form, booleans', FORM_VALIDATE) +def test_validate_form(form: meds.Form, booleans: List[bool]) -> None: + errors = meds.validate_form(form) + for i, error in enumerate(meds.ERRORS.values()): + message = error in errors + print(i, error, message) + assert message is booleans[i] + + +@pytest.mark.parametrize('form, interval', CALC_INTERVAL) +def test_calc_reminder_interval_in_seconds(form: meds.Form, + interval: int) -> None: + assert meds.calc_reminder_interval_in_seconds(form) == interval + + +@pytest.mark.parametrize('form, times', REMINDER_TIMES) +def test_get_reminder_times(form: meds.Form, times: List[time]) -> None: + assert meds.get_reminder_times(form) == times + + +@pytest.mark.parametrize('t, day, early, late, boolean', DATETIMES_VALIDATE) +def test_validate_datetime(t: datetime, day: date, early: time, late: time, + boolean: bool) -> None: + assert meds.validate_datetime(t, day, early, late) == boolean + + +@pytest.mark.parametrize('previous, reminder_time, minimum, maximum, boolean', + VALIDATE_FIRST) +def test_validate_first_day_reminder(previous: datetime, reminder_time: time, + minimum: time, maximum: time, + boolean: bool) -> None: + assert meds.validate_first_day_reminder( + previous, reminder_time, minimum, maximum) == boolean + + +@pytest.mark.parametrize('previous, minimum, early, late, reminder', DIFFERENT) +def test_get_different_time_reminder( + previous: datetime, minimum: time, early: time, late: time, + reminder: datetime) -> None: + new = meds.get_different_time_reminder(previous, minimum, early, late) + assert new == reminder + + +@pytest.mark.parametrize('form, time_obj, previous, reminder', CREATE_FIRST) +def test_create_first_day_reminder(form: meds.Form, time_obj: time, + previous: datetime, + reminder: datetime) -> None: + new = meds.create_first_day_reminder(form, time_obj, previous) + assert new == reminder + + +@pytest.mark.parametrize('form, times, datetimes', FIRST) +def test_get_first_day_reminders(form: meds.Form, times: List[time], + datetimes: List[datetime]) -> None: + assert list(meds.get_first_day_reminders(form, times)) == datetimes + + +@pytest.mark.parametrize('times, early, start, day, end, reminders', REMINDERS) +def test_reminder_generator( + times: List[time], early: time, start: datetime, day: date, end: datetime, + reminders: List[datetime]) -> None: + new = list(meds.reminder_generator(times, early, start, day, end)) + assert new == reminders + + +@pytest.mark.parametrize('form, datetimes', DATETIMES) +def test_get_reminder_datetimes(form: meds.Form, + datetimes: List[datetime]) -> None: + assert list(meds.get_reminder_datetimes(form)) == datetimes + + +@pytest.mark.parametrize('form, boolean', CREATE) +def test_create_events(session: Session, user: User, form: meds.Form, + boolean: bool) -> None: + assert session.query(Event).first() is None + meds.create_events(session, user.id, form) + event = session.query(Event).first() + assert event + title = '-' in event.title + assert title is boolean diff --git a/tests/meds/test_routers.py b/tests/meds/test_routers.py new file mode 100644 index 00000000..f14c3eb2 --- /dev/null +++ b/tests/meds/test_routers.py @@ -0,0 +1,39 @@ +from typing import Dict + +import pytest +from sqlalchemy.orm.session import Session +from starlette.testclient import TestClient + +from app.database.models import Event +from app.routers import meds +from tests.meds.test_internal import create_test_form, WEB_FORM + + +PYLENDAR = [ + (WEB_FORM, True), + (create_test_form(form_dict=True, end='1985-10-26'), False), +] + + +def test_meds_page_returns_ok(meds_test_client: TestClient) -> None: + path = meds.router.url_path_for('medications') + response = meds_test_client.get(path) + assert response.ok + + +@pytest.mark.parametrize('form, pylendar', PYLENDAR) +def test_meds_send_form_success(meds_test_client: TestClient, session: Session, + form: Dict[str, str], pylendar: bool) -> None: + assert session.query(Event).first() is None + path = meds.router.url_path_for('medications') + response = meds_test_client.post(path, data=form, allow_redirects=True) + assert response.ok + message = 'PyLendar' in response.text + assert message is pylendar + message = 'alert' in response.text + assert message is not pylendar + event = session.query(Event).first() + if pylendar: + assert event + else: + assert event is None diff --git a/tests/salary/test_utils.py b/tests/salary/test_utils.py index ed269d3f..b69bd793 100644 --- a/tests/salary/test_utils.py +++ b/tests/salary/test_utils.py @@ -1,4 +1,4 @@ -from datetime import datetime, time, timedelta +from datetime import datetime, timedelta from typing import Dict, List, Tuple from unittest import mock @@ -170,11 +170,6 @@ }), ] -TIMES = [ - ('13:30', time(13, 30)), - ('15:42:00', time(15, 42)) -] - UPDATES = [ ({ 'wage': '35', @@ -370,11 +365,6 @@ def test_get_settings(salary_session: Session, wage.category_id) -@pytest.mark.parametrize('string, formatted_time', TIMES) -def test_get_time_from_string(string: str, formatted_time: time) -> None: - assert utils.get_time_from_string(string) == formatted_time - - @pytest.mark.parametrize('form, boolean', UPDATES) def test_update_settings(salary_session: Session, wage: SalarySettings, form: Dict[str, str], boolean: bool) -> None: diff --git a/tests/security_testing_routes.py b/tests/security_testing_routes.py index 4df73e8b..6b9e7128 100644 --- a/tests/security_testing_routes.py +++ b/tests/security_testing_routes.py @@ -1,6 +1,6 @@ from fastapi import APIRouter, Depends, Request -from app.internal.security.dependancies import ( +from app.internal.security.dependencies import ( current_user, current_user_from_db, is_logged_in, is_manager, User ) diff --git a/tests/test_calendar_privacy.py b/tests/test_calendar_privacy.py index 641b8cf5..d94dfd56 100644 --- a/tests/test_calendar_privacy.py +++ b/tests/test_calendar_privacy.py @@ -1,6 +1,6 @@ from app.internal.calendar_privacy import can_show_calendar # TODO after user system is merged: -# from app.internal.security.dependancies import CurrentUser +# from app.internal.security.dependencies import CurrentUser from app.routers.user import create_user diff --git a/tests/test_event.py b/tests/test_event.py index 4c118669..87828e96 100644 --- a/tests/test_event.py +++ b/tests/test_event.py @@ -651,7 +651,7 @@ def test_delete_comment( class TestApp: client = TestClient(app) - date_test_data = [datetime.today() - timedelta(1), datetime.today()] + date_test_data = [datetime.today() - timedelta(days=1), datetime.today()] event_test_data = { "title": "Test Title", "location": "Fake City", diff --git a/tests/test_utils.py b/tests/test_utils.py index ab0a080f..34a88dfd 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,9 +1,22 @@ +from datetime import date, time + +import pytest from sqlalchemy.orm import Session from app.database.models import User from app.internal import utils +TIMES = [ + ('2021-01-14', date(2021, 1, 14)), + ('13:30', time(13, 30)), + ('15:42:00', time(15, 42)), + ('15', None), + ('2021-01', None), + ('15:42:00.5', None), +] + + class TestUtils: def test_save_success(self, user: User, session: Session) -> None: @@ -38,3 +51,8 @@ def test_get_current_user(self, session: Session) -> None: def test_get_user(self, user: User, session: Session) -> None: assert utils.get_user(session, user.id) == user assert utils.get_user(session, 2) is None + + @pytest.mark.parametrize('string, formatted_time', TIMES) + def test_get_time_from_string(self, string: str, + formatted_time: time) -> None: + assert utils.get_time_from_string(string) == formatted_time From f01371f5b54d120dd71f35222f0d5ac22b4c6168 Mon Sep 17 00:00:00 2001 From: ivarshav Date: Tue, 23 Feb 2021 15:35:57 +0200 Subject: [PATCH 20/46] Support Filter by Categories (#298) --- app/routers/agenda.py | 13 +++-- app/static/agenda_style.css | 33 +++++++++++++ app/static/js/categories_filter.js | 32 ++++++++++++ app/templates/agenda.html | 78 ++++++++++++++++++------------ 4 files changed, 121 insertions(+), 35 deletions(-) create mode 100644 app/static/js/categories_filter.js diff --git a/app/routers/agenda.py b/app/routers/agenda.py index 6cc5a7af..4563c99f 100644 --- a/app/routers/agenda.py +++ b/app/routers/agenda.py @@ -4,6 +4,7 @@ from typing import Optional, Tuple from fastapi import APIRouter, Depends, Request +from fastapi.encoders import jsonable_encoder from sqlalchemy.orm import Session from starlette.templating import _TemplateResponse @@ -50,10 +51,14 @@ def agenda( events = defaultdict(list) for event_obj in events_objects: - event_duration = agenda_events.get_time_delta_string( - event_obj.start, event_obj.end - ) - events[event_obj.start.date()].append((event_obj, event_duration)) + event_duration = agenda_events.get_time_delta_string(event_obj.start, + event_obj.end) + json_event_data = jsonable_encoder(event_obj) + json_event_data['duration'] = event_duration + json_event_data['start'] = event_obj.start.time().strftime("%H:%M") + event_key = event_obj.start.date().strftime("%d/%m/%Y") + events[event_key].append(json_event_data) + events_for_graph = json.dumps( agenda_events.make_dict_for_graph_data(db, user_id) ) diff --git a/app/static/agenda_style.css b/app/static/agenda_style.css index fb357f7f..950cb546 100644 --- a/app/static/agenda_style.css +++ b/app/static/agenda_style.css @@ -6,6 +6,23 @@ text-align: center; } +.agenda_filter_grid { + grid-area: header; +} + +.agenda_grid { + display: grid; + grid-template-areas: + "header filter" + "sidebar sidebar"; + grid-template-columns: 5fr 2fr; + grid-gap: 0 1.25em; +} + +.category_filter { + grid-area: filter; +} + .event_line { width: 80%; margin-left: 2em; @@ -18,6 +35,22 @@ grid-gap: 0.6em; } +.event_line[data-value="hidden"], +.wrapper[data-value="hidden"] +{ + display: none; +} + +.event_line[data-value="visible"] +{ + display: grid; +} + +.wrapper[data-value="visible"] +{ + display: block; +} + .duration { font-size: small; } diff --git a/app/static/js/categories_filter.js b/app/static/js/categories_filter.js new file mode 100644 index 00000000..14e940fa --- /dev/null +++ b/app/static/js/categories_filter.js @@ -0,0 +1,32 @@ +document.addEventListener("DOMContentLoaded", function () { + document.getElementById("category-button").addEventListener("click", function () { + filterByCategory(); + }); +}); + +function filterByCategory(){ + // TODO(issue#67): Allow filter by category name + const category = document.getElementById("category").value; + + const allEvents = document.getElementsByClassName("event_line"); + for (event of allEvents) { + if (event.dataset.name == category) + { + event.dataset.value = "visible"; + } + else { + event.dataset.value = "hidden"; + } + if (!Number.isInteger(+category) || !category || 0 === category.length) { + event.dataset.value = "visible"; + } + event.parentNode.dataset.value = "hidden"; + } + + // Set wrapper div to display "visible" if at least one child is visible. + for (event of allEvents) { + if (event.dataset.value === "visible") { + event.parentNode.dataset.value = "visible"; + } + } +} \ No newline at end of file diff --git a/app/templates/agenda.html b/app/templates/agenda.html index 2fb9bb60..54b65208 100644 --- a/app/templates/agenda.html +++ b/app/templates/agenda.html @@ -3,32 +3,47 @@ {% block head %} {{ super() }} + {% endblock %} {% block content %} -
-
- - +
+
+ +
+ + +
+
+ +
+
+
+ +
+
-
- -
-
-
- -
- -
-
- {{ gettext("Today") }} +
+ + +
- -
- {{ gettext("Next 30 days") }} + +
@@ -53,19 +68,20 @@

{{ start_date.strftime("%d/%m/%Y") }} - {{ end_date.strftime("%d/ {% endif %}

-
+
{% for events_date, events_list in events.items() %} - -
{{ events_date.strftime("%d/%m/%Y") }}
- {% for event in events_list %} -
- {% set availability = 'Busy' if event[0].availability == True else 'Free' %} -
-
{{ event[0].start.time().strftime("%H:%M") }} - {{ event[0].title | safe }}
- duration: {{ event[1] }} -
+
+
{{ events_date }}
+ {% for event in events_list %} +
+ {% set availability = 'Busy' if event.availability else 'Free' %} +
+
{{ event.start }} - {{ event.title | safe }}
+ duration: {{ event.duration }} +
+
+ {% endfor %}
- {% endfor %} {% endfor %}
From f85eb84341ac1a3d06ad3a1d1adef28eea926a45 Mon Sep 17 00:00:00 2001 From: liaarbel <71096174+liaarbel@users.noreply.github.com> Date: Tue, 23 Feb 2021 15:41:06 +0200 Subject: [PATCH 21/46] Feature/international days (#210) --- .gitignore | 1 + app/database/models.py | 9 + app/internal/international_days.py | 40 + app/internal/json_data_loader.py | 11 +- app/resources/international_days.json | 1832 +++++++++++++++++++++++++ app/routers/audio.py | 14 +- app/routers/dayview.py | 4 +- app/templates/calendar_day_view.html | 140 +- tests/test_international_days.py | 63 + 9 files changed, 2043 insertions(+), 71 deletions(-) create mode 100644 app/internal/international_days.py create mode 100644 app/resources/international_days.json create mode 100644 tests/test_international_days.py diff --git a/.gitignore b/.gitignore index fedb2e46..b8da1186 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ dev.db test.db +.idea config.py # Byte-compiled / optimized / DLL files diff --git a/app/database/models.py b/app/database/models.py index 048fa53f..4dec81d0 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -422,6 +422,15 @@ class Joke(Base): text = Column(String, nullable=False) +class InternationalDays(Base): + __tablename__ = "international_days" + + id = Column(Integer, primary_key=True, index=True) + day = Column(Integer, nullable=False) + month = Column(Integer, nullable=False) + international_day = Column(String, nullable=False) + + # insert language data # Credit to adrihanu https://stackoverflow.com/users/9127249/adrihanu diff --git a/app/internal/international_days.py b/app/internal/international_days.py new file mode 100644 index 00000000..6ded1189 --- /dev/null +++ b/app/internal/international_days.py @@ -0,0 +1,40 @@ +from datetime import datetime +from typing import Optional, Dict, Union + +from sqlalchemy.orm import Session +from sqlalchemy.sql.expression import func + +from app.database.models import InternationalDays + + +def get_international_day( + international_day: Dict[str, Union[str, int]] +) -> InternationalDays: + """Returns an international day object from the dictionary data. + + Args: + international_day: A dictionary international day + related information. + + Returns: + A new international day object. + """ + return InternationalDays( + day=international_day["day"], + month=international_day["month"], + international_day=international_day["international_day"], + ) + + +def get_international_day_per_day( + session: Session, date: datetime +) -> Optional[InternationalDays]: + day_num = date.day + month = date.month + international_day = (session.query(InternationalDays) + .filter(InternationalDays.day == day_num) + .filter(InternationalDays.month == month) + .order_by(func.random()) + .first() + ) + return international_day diff --git a/app/internal/json_data_loader.py b/app/internal/json_data_loader.py index 4e9d83e7..779bfa17 100644 --- a/app/internal/json_data_loader.py +++ b/app/internal/json_data_loader.py @@ -5,8 +5,8 @@ from loguru import logger from sqlalchemy.orm import Session -from app.database.models import Base, Joke, Quote, Zodiac -from app.internal import daily_quotes, jokes, zodiac +from app.database.models import Base, InternationalDays, Joke, Quote, Zodiac +from app.internal import daily_quotes, international_days, jokes, zodiac def load_to_database(session: Session) -> None: @@ -35,6 +35,13 @@ def load_to_database(session: Session) -> None: daily_quotes.get_quote, ) + _insert_into_database( + session, + 'app/resources/international_days.json', + InternationalDays, + international_days.get_international_day, + ) + _insert_into_database( session, 'app/resources/jokes.json', diff --git a/app/resources/international_days.json b/app/resources/international_days.json new file mode 100644 index 00000000..1390e56d --- /dev/null +++ b/app/resources/international_days.json @@ -0,0 +1,1832 @@ +[ + { + "day": 1, + "month": 1, + "international_day": "Ring a bell day and Copyright Law day" + }, + { + "day": 2, + "month": 1, + "international_day": "Science Ficyion Day and World introvert day" + }, + { + "day": 3, + "month": 1, + "international_day": "Drinking straw day and Festival of sleep day" + }, + { + "day": 4, + "month": 1, + "international_day": "Trivia Day and Weigh-in day" + }, + { + "day": 5, + "month": 1, + "international_day": "Whipped Cream day and Bird Day" + }, + { + "day": 6, + "month": 1, + "international_day": "Cuddle up day and Bean Day" + }, + { + "day": 7, + "month": 1, + "international_day": "Tempura day and Bobblehead day" + }, + { + "day": 8, + "month": 1, + "international_day": "Babble bath day and Joy Germ day" + }, + { + "day": 9, + "month": 1, + "international_day": "Apricot Day and Balloon Ascension day" + }, + { + "day": 10, + "month": 1, + "international_day": "Peculiar people day and Bittersweet Chocolate day" + }, + { + "day": 11, + "month": 1, + "international_day": "Step in a puddle and splash your friends day and Heritage treasures day" + }, + { + "day": 12, + "month": 1, + "international_day": "Kiss a ginger day and Marzipan day" + }, + { + "day": 13, + "month": 1, + "international_day": "Sticker day and Rubber duckie day" + }, + { + "day": 14, + "month": 1, + "international_day": "Dress up your pet day and International Kite day" + }, + { + "day": 15, + "month": 1, + "international_day": "Hat day and Bagel day" + }, + { + "day": 16, + "month": 1, + "international_day": "Nothong day and Religious freedom day" + }, + { + "day": 17, + "month": 1, + "international_day": "Ditch new year's resolutions day and Kid inventor's day" + }, + { + "day": 18, + "month": 1, + "international_day": "Thesaurus day and Martin luther king day" + }, + { + "day": 19, + "month": 1, + "international_day": "Popcorn day and Tin can day" + }, + { + "day": 20, + "month": 1, + "international_day": "Disc jockey day and Cheese lovers day" + }, + { + "day": 21, + "month": 1, + "international_day": "Hugging day and Playdate day" + }, + { + "day": 22, + "month": 1, + "international_day": "Answer your cat's Questions day and Hot sauce day" + }, + { + "day": 23, + "month": 1, + "international_day": "Pie day and Visit your local quilt shop day" + }, + { + "day": 24, + "month": 1, + "international_day": "Beer can appreciation day and Peanut Butter day" + }, + { + "day": 25, + "month": 1, + "international_day": "Bubble warp appreciation day and Opposite day" + }, + { + "day": 26, + "month": 1, + "international_day": "Australia day and Peanut brittle day" + }, + { + "day": 27, + "month": 1, + "international_day": "Chocolate cake day and World breast pumping day" + }, + { + "day": 28, + "month": 1, + "international_day": "International lego day and Global community engagement day" + }, + { + "day": 29, + "month": 1, + "international_day": "Fun at work day and Puzzle day" + }, + { + "day": 30, + "month": 1, + "international_day": "Inane answering message day and Seed swap day" + }, + { + "day": 31, + "month": 1, + "international_day": "Backward day and Gorilla suit day" + }, + { + "day": 1, + "month": 2, + "international_day": "Baked alaska day and World read aloud day" + }, + { + "day": 2, + "month": 2, + "international_day": "World play your ukulele day and Tater tot day" + }, + { + "day": 3, + "month": 2, + "international_day": "Carrot cake day and Golden retriver day" + }, + { + "day": 4, + "month": 2, + "international_day": "Thank a letter carrier day and World cancer day" + }, + { + "day": 5, + "month": 2, + "international_day": "World nutella day and Weatherperson's day" + }, + { + "day": 6, + "month": 2, + "international_day": "Take your child to the libray day and Frozen yogurt day" + }, + { + "day": 7, + "month": 2, + "international_day": "Yorkshire pudding day and Wava all your fingers at your neighbors day" + }, + { + "day": 8, + "month": 2, + "international_day": "Clean out your computer day and Molasses bar day" + }, + { + "day": 9, + "month": 2, + "international_day": "Pizza day and Safer internet day" + }, + { + "day": 10, + "month": 2, + "international_day": "Umbrella day and Plimsoll day" + }, + { + "day": 11, + "month": 2, + "international_day": "Fat food day and Peppermint patty day" + }, + { + "day": 12, + "month": 2, + "international_day": "Darwin day and No one eats alone day" + }, + { + "day": 13, + "month": 2, + "international_day": "Radio day and Tortellini day" + }, + { + "day": 14, + "month": 2, + "international_day": "Marriage day and Ferris Wheel day" + }, + { + "day": 15, + "month": 2, + "international_day": "Hippo day and Annoy squidward day" + }, + { + "day": 16, + "month": 2, + "international_day": "Innovation day and Tim Tam day" + }, + { + "day": 17, + "month": 2, + "international_day": "Random acts of kindness day and World human spirit day" + }, + { + "day": 18, + "month": 2, + "international_day": "Drink wine day and Pluto day" + }, + { + "day": 19, + "month": 2, + "international_day": "International Tug-of-War day and Chocolate mint day" + }, + { + "day": 20, + "month": 2, + "international_day": "Love your pet day and Pangolin day" + }, + { + "day": 21, + "month": 2, + "international_day": "Sticky bun day and World whale day" + }, + { + "day": 22, + "month": 2, + "international_day": "World thinking day and Single tasking day" + }, + { + "day": 23, + "month": 2, + "international_day": "Curling is cool day and Play tennis day" + }, + { + "day": 24, + "month": 2, + "international_day": "Pink day and Tortilla chip day" + }, + { + "day": 25, + "month": 2, + "international_day": "Toast day and Chilli day" + }, + { + "day": 26, + "month": 2, + "international_day": "Levi Strauss day and Personal chef day" + }, + { + "day": 27, + "month": 2, + "international_day": "Pokemon day and World NGO day" + }, + { + "day": 28, + "month": 2, + "international_day": "Tooth fairy day and Floral Design day" + }, + { + "day": 29, + "month": 2, + "international_day": "Extra day in leap year" + }, + { + "day": 1, + "month": 3, + "international_day": "Barista day and Fun facts about names day" + }, + { + "day": 2, + "month": 3, + "international_day": "Read across america day and Old stuff day" + }, + { + "day": 3, + "month": 3, + "international_day": "World wildlife day and What if cats and dogs had opposable Thumbs day" + }, + { + "day": 4, + "month": 3, + "international_day": "Grammar day and Marching band day" + }, + { + "day": 5, + "month": 3, + "international_day": "Day of unplugging and World book day" + }, + { + "day": 6, + "month": 3, + "international_day": "White chocolate cheesecake day and dentist's day" + }, + { + "day": 7, + "month": 3, + "international_day": "Be heard day and Plant power day" + }, + { + "day": 8, + "month": 3, + "international_day": "International women's day and Peanut cluster day" + }, + { + "day": 9, + "month": 3, + "international_day": "Meatball day and Barbie day" + }, + { + "day": 10, + "month": 3, + "international_day": "Pack your lunch day and International wig day" + }, + { + "day": 11, + "month": 3, + "international_day": "World plumbing day and Oatmeal nut waffles day" + }, + { + "day": 12, + "month": 3, + "international_day": "Girls scout day and International fanny pack day" + }, + { + "day": 13, + "month": 3, + "international_day": "Jewel day and Ken day" + }, + { + "day": 14, + "month": 3, + "international_day": "Learn about butterflies day and Pi day" + }, + { + "day": 15, + "month": 3, + "international_day": "World speech day and World consumer rights day" + }, + { + "day": 16, + "month": 3, + "international_day": "Lips appreciation day and St.urho's day" + }, + { + "day": 17, + "month": 3, + "international_day": "Saint Patrick's day" + }, + { + "day": 18, + "month": 3, + "international_day": "Awkward moments day and Companies that care day" + }, + { + "day": 19, + "month": 3, + "international_day": "World sleep day and Poultry day" + }, + { + "day": 20, + "month": 3, + "international_day": "International day of happines and Quilting day" + }, + { + "day": 21, + "month": 3, + "international_day": "World poetry day and Vermouth day" + }, + { + "day": 22, + "month": 3, + "international_day": "World water day and Gryffindor pride day" + }, + { + "day": 23, + "month": 3, + "international_day": "Melba toast day and Puppy day" + }, + { + "day": 24, + "month": 3, + "international_day": "Chocolate covered raisins day and Flatmates day" + }, + { + "day": 25, + "month": 3, + "international_day": "Waffle day and Tolkien Reading day" + }, + { + "day": 26, + "month": 3, + "international_day": "Good hair day and Purple day" + }, + { + "day": 27, + "month": 3, + "international_day": "Earth hour and International whiskey day" + }, + { + "day": 28, + "month": 3, + "international_day": "Neighbor day and Black forest cake day" + }, + { + "day": 29, + "month": 3, + "international_day": "Lemon chiffon cake day and Niagara falls runs dry day" + }, + { + "day": 30, + "month": 3, + "international_day": "Doctor's day and Take a walk in the park day" + }, + { + "day": 31, + "month": 3, + "international_day": "Crayola Crayon day and World backup day" + }, + { + "day": 1, + "month": 4, + "international_day": "Fun day and Tell a lie day" + }, + { + "day": 2, + "month": 4, + "international_day": "Ferret day and Walk to work day" + }, + { + "day": 3, + "month": 4, + "international_day": "DIY day and Chocolate mousse day" + }, + { + "day": 4, + "month": 4, + "international_day": "Vitamin C day and Geologist's day" + }, + { + "day": 5, + "month": 4, + "international_day": "Star trek first contact day and Read a road map day" + }, + { + "day": 6, + "month": 4, + "international_day": "World table tennis day and New beer's eve" + }, + { + "day": 7, + "month": 4, + "international_day": "Beer day and No housework day" + }, + { + "day": 8, + "month": 4, + "international_day": "Zoo lovers day and Pygmy hippo day" + }, + { + "day": 9, + "month": 4, + "international_day": "Unicorn day and ASMR day" + }, + { + "day": 10, + "month": 4, + "international_day": "Golfer's day and International safety pin day" + }, + { + "day": 11, + "month": 4, + "international_day": "Pet day and Cheese fondue day" + }, + { + "day": 12, + "month": 4, + "international_day": "Deskfast day and Hamster day" + }, + { + "day": 13, + "month": 4, + "international_day": "Scrabble day and Internatinal FND Awareness day" + }, + { + "day": 14, + "month": 4, + "international_day": "Dolphin day and Day of pink" + }, + { + "day": 15, + "month": 4, + "international_day": "Husband Appriciations day and High five day" + }, + { + "day": 16, + "month": 4, + "international_day": "Wear your pajamas to work day and Save the elephant day" + }, + { + "day": 17, + "month": 4, + "international_day": "Haiku poetry day and Blah blah blah day" + }, + { + "day": 18, + "month": 4, + "international_day": "Pinata day and Columnists day" + }, + { + "day": 19, + "month": 4, + "international_day": "Bicycle day and Hanging out day" + }, + { + "day": 20, + "month": 4, + "international_day": "Volunteer recognition day and Chinese language day" + }, + { + "day": 21, + "month": 4, + "international_day": "World creativity and innovation day and World stationery day" + }, + { + "day": 22, + "month": 4, + "international_day": "Teach your children to save day and Earth day" + }, + { + "day": 23, + "month": 4, + "international_day": "Talk like Shakespeare day and Asparagus day" + }, + { + "day": 24, + "month": 4, + "international_day": "Scream day and Pig in a blanket day" + }, + { + "day": 25, + "month": 4, + "international_day": "Pinhole photography day and Hug a plumber day" + }, + { + "day": 26, + "month": 4, + "international_day": "Hug an australian day and Burlesque day" + }, + { + "day": 27, + "month": 4, + "international_day": "Morse code day and Tell a story day" + }, + { + "day": 28, + "month": 4, + "international_day": "Superhero day and Stop food waste day" + }, + { + "day": 29, + "month": 4, + "international_day": "International dance day and We jump the world day" + }, + { + "day": 30, + "month": 4, + "international_day": "Hairball awareness day and honesty day" + }, + { + "day": 1, + "month": 5, + "international_day": "Tuba day and Therapeutic massage awareness day" + }, + { + "day": 2, + "month": 5, + "international_day": "World laughter day and Baby day" + }, + { + "day": 3, + "month": 5, + "international_day": "Lemonade day and Garden meditation day" + }, + { + "day": 4, + "month": 5, + "international_day": "Star wars day and 45 day" + }, + { + "day": 5, + "month": 5, + "international_day": "Nail day and Internatinal midwive's day" + }, + { + "day": 6, + "month": 5, + "international_day": "No diet day and Password day" + }, + { + "day": 7, + "month": 5, + "international_day": "Roast leg of lamb day and Public gardens day" + }, + { + "day": 8, + "month": 5, + "international_day": "Windmill day and No socks day" + }, + { + "day": 9, + "month": 5, + "international_day": "Moscato day and Lost sock memorial day" + }, + { + "day": 10, + "month": 5, + "international_day": "Mother ocean day and Stay up all night night" + }, + { + "day": 11, + "month": 5, + "international_day": "Eat what you want day and World ego awareness day" + }, + { + "day": 12, + "month": 5, + "international_day": "Receptionist's day and Limerick day" + }, + { + "day": 13, + "month": 5, + "international_day": "Numeracy day and Top gun day" + }, + { + "day": 14, + "month": 5, + "international_day": "Shades day and Chicken dance day" + }, + { + "day": 15, + "month": 5, + "international_day": "World whisky day and Chocolate chip day" + }, + { + "day": 16, + "month": 5, + "international_day": "Drawing day and Sea monkey day" + }, + { + "day": 17, + "month": 5, + "international_day": "Work from home day and International Day Against Homophobia and Transphobia and Biphobia" + }, + { + "day": 18, + "month": 5, + "international_day": "No dirty dishes day and Museum day" + }, + { + "day": 19, + "month": 5, + "international_day": "May ray day" + }, + { + "day": 20, + "month": 5, + "international_day": "Pick strawberries day and World bee day" + }, + { + "day": 21, + "month": 5, + "international_day": "World meditation day and I need a patch for that day" + }, + { + "day": 22, + "month": 5, + "international_day": "Sherlock Holmes day and Goth day" + }, + { + "day": 23, + "month": 5, + "international_day": "Turtle day and Lucky penny day" + }, + { + "day": 24, + "month": 5, + "international_day": "Tiara day and Escargot day" + }, + { + "day": 25, + "month": 5, + "international_day": "Tap dance day and Towel day" + }, + { + "day": 26, + "month": 5, + "international_day": "Senior health and fitness day and Paper airplane day" + }, + { + "day": 27, + "month": 5, + "international_day": "Sun screen day and World product day" + }, + { + "day": 28, + "month": 5, + "international_day": "Amnesty international day and Hamburger day" + }, + { + "day": 29, + "month": 5, + "international_day": "Biscuit day and Paper clip day" + }, + { + "day": 30, + "month": 5, + "international_day": "Mint julep day and Water a flower day" + }, + { + "day": 31, + "month": 5, + "international_day": "No tabbaco day and Save your hearing day" + }, + { + "day": 1, + "month": 6, + "international_day": "Say something nice day" + }, + { + "day": 2, + "month": 6, + "international_day": "Rocky road day and Running day" + }, + { + "day": 3, + "month": 6, + "international_day": "World bicycle day and Chocolate maccaroon day" + }, + { + "day": 4, + "month": 6, + "international_day": "Hug your cat day and Doughnut day" + }, + { + "day": 5, + "month": 6, + "international_day": "Sausage roll day and Coworking day" + }, + { + "day": 6, + "month": 6, + "international_day": "Gardening exercise day and Cancer survivors day" + }, + { + "day": 7, + "month": 6, + "international_day": "Chocolate ice cream day and VCR day" + }, + { + "day": 8, + "month": 6, + "international_day": "Best friends day and World oceans day" + }, + { + "day": 9, + "month": 6, + "international_day": "Rosé day and Donald duck day" + }, + { + "day": 10, + "month": 6, + "international_day": "Iced tea day and Jerky day" + }, + { + "day": 11, + "month": 6, + "international_day": "Yarn Bombing day and Corn of the cob day" + }, + { + "day": 12, + "month": 6, + "international_day": "Superman day and World gin day" + }, + { + "day": 13, + "month": 6, + "international_day": "Sewing machine day and World softball day" + }, + { + "day": 14, + "month": 6, + "international_day": "World blood donor day and Flag day" + }, + { + "day": 15, + "month": 6, + "international_day": "Nature photography day and Beer day Britain" + }, + { + "day": 16, + "month": 6, + "international_day": "World tapas day and Fresh Veggies day" + }, + { + "day": 17, + "month": 6, + "international_day": "Eat your vegetables day and Garbage man day" + }, + { + "day": 18, + "month": 6, + "international_day": "International picnic day and Go fishing day" + }, + { + "day": 19, + "month": 6, + "international_day": "Martini day and Juggling day" + }, + { + "day": 20, + "month": 6, + "international_day": "Ice cream soda day and World refugee day" + }, + { + "day": 21, + "month": 6, + "international_day": "World music day and International yoga day" + }, + { + "day": 22, + "month": 6, + "international_day": "World rainforest day and Onion rings day" + }, + { + "day": 23, + "month": 6, + "international_day": "Let it go day and International widows day" + }, + { + "day": 24, + "month": 6, + "international_day": "Bomb pop day and Swim in lap day" + }, + { + "day": 25, + "month": 6, + "international_day": "Global beatles day and Take your dog to work day" + }, + { + "day": 26, + "month": 6, + "international_day": "Blueberry cheesecake day and Beautician's day and World refrigeration day" + }, + { + "day": 27, + "month": 6, + "international_day": "Pineapple day and Sunglasses day" + }, + { + "day": 28, + "month": 6, + "international_day": "International body piercing day and Logistics day" + }, + { + "day": 29, + "month": 6, + "international_day": "Waffle iron day and Camera day" + }, + { + "day": 30, + "month": 6, + "international_day": "Seocial media day and Metheor watch day" + }, + { + "day": 1, + "month": 7, + "international_day": "Joke day and Gingersnap day" + }, + { + "day": 2, + "month": 7, + "international_day": "Anisette day and World UFO day" + }, + { + "day": 3, + "month": 7, + "international_day": "Air conditioning appreciation day and Eat beans day" + }, + { + "day": 4, + "month": 7, + "international_day": "Independence from meat day and Jackfruit day" + }, + { + "day": 5, + "month": 7, + "international_day": "Apple turnover day and Bikini day" + }, + { + "day": 6, + "month": 7, + "international_day": "International kissing day and Fried chicken day" + }, + { + "day": 7, + "month": 7, + "international_day": "Chocolate day and Macaroni day" + }, + { + "day": 8, + "month": 7, + "international_day": "Math 2.0 day and Chocolate with almonds day" + }, + { + "day": 9, + "month": 7, + "international_day": "Kebab day and Sugar cookie day" + }, + { + "day": 10, + "month": 7, + "international_day": "Pina colada day and Teddy bear picnic day" + }, + { + "day": 11, + "month": 7, + "international_day": "Blueberry muffin day and World population day" + }, + { + "day": 12, + "month": 7, + "international_day": "Etch a sketch day and New conversations day" + }, + { + "day": 13, + "month": 7, + "international_day": "French fries day and Cow appreciation day" + }, + { + "day": 14, + "month": 7, + "international_day": "Shark awareness day and Mac & cheese day" + }, + { + "day": 15, + "month": 7, + "international_day": "Gummi worm day and Hot dog day" + }, + { + "day": 16, + "month": 7, + "international_day": "Guinea pig appreciation day and Corn fritters day" + }, + { + "day": 17, + "month": 7, + "international_day": "World emoji day and Peach ice cream day" + }, + { + "day": 18, + "month": 7, + "international_day": "Caviar day and Insurance nerd day" + }, + { + "day": 19, + "month": 7, + "international_day": "Daiquiri day and Get out of the doghouse day" + }, + { + "day": 20, + "month": 7, + "international_day": "Moon day and International chess day" + }, + { + "day": 21, + "month": 7, + "international_day": "Junk food day and Lamington day" + }, + { + "day": 22, + "month": 7, + "international_day": "Hammock day and Crème brulee day" + }, + { + "day": 23, + "month": 7, + "international_day": "Peanut butter and chocolate day and Sprinkle day" + }, + { + "day": 24, + "month": 7, + "international_day": "Drive-thru day and Tequila day" + }, + { + "day": 25, + "month": 7, + "international_day": "Wine and cheese day and Parent's day" + }, + { + "day": 26, + "month": 7, + "international_day": "Aunt and uncle day and Coffee milk shake day" + }, + { + "day": 27, + "month": 7, + "international_day": "Walk on stilts day and Norfolk day" + }, + { + "day": 28, + "month": 7, + "international_day": "Milk chocolate day and World hepatitis day" + }, + { + "day": 29, + "month": 7, + "international_day": "Chili dog day and International tiger day" + }, + { + "day": 30, + "month": 7, + "international_day": "Cheesecake day and Friendship day" + }, + { + "day": 31, + "month": 7, + "international_day": "Raspberry cake day and Uncommon Instrument awareness day" + }, + { + "day": 1, + "month": 8, + "international_day": "Sisters day and Planner day" + }, + { + "day": 2, + "month": 8, + "international_day": "Ice cream sandwich day and Coloring book day" + }, + { + "day": 3, + "month": 8, + "international_day": "Watermelon day and White wine day" + }, + { + "day": 4, + "month": 8, + "international_day": "Coast guard day and International clouded leopard day" + }, + { + "day": 5, + "month": 8, + "international_day": "Work like a dog day and Blogger day" + }, + { + "day": 6, + "month": 8, + "international_day": "Fresh breath day and International beer day" + }, + { + "day": 7, + "month": 8, + "international_day": "Particularly preposterous packing day and Aged care employee day" + }, + { + "day": 8, + "month": 8, + "international_day": "International cat day and Happiness happens day" + }, + { + "day": 9, + "month": 8, + "international_day": "Melon day and Rice pudding day" + }, + { + "day": 10, + "month": 8, + "international_day": "Lazy day and World lion day" + }, + { + "day": 11, + "month": 8, + "international_day": "World calligraphy day and Son and daughter day" + }, + { + "day": 12, + "month": 8, + "international_day": "World Elephant day and Vinyl record day" + }, + { + "day": 13, + "month": 8, + "international_day": "Blame someone else day and International lefthanders day" + }, + { + "day": 14, + "month": 8, + "international_day": "Creamsicle day and Tattoo removal day" + }, + { + "day": 15, + "month": 8, + "international_day": "Check the chip day and Relaxation day" + }, + { + "day": 16, + "month": 8, + "international_day": "Rollercoaster day and Rum day" + }, + { + "day": 17, + "month": 8, + "international_day": "Thrift shop day and Vanilla custard day" + }, + { + "day": 18, + "month": 8, + "international_day": "Bad poetry day and Never give up day" + }, + { + "day": 19, + "month": 8, + "international_day": "International orangutan day and Photography day" + }, + { + "day": 20, + "month": 8, + "international_day": "Men's grooming day and International day of medical transporters" + }, + { + "day": 21, + "month": 8, + "international_day": "Senior citizen day and World honey bee day" + }, + { + "day": 22, + "month": 8, + "international_day": "Be an angel day and Eat a peach day" + }, + { + "day": 23, + "month": 8, + "international_day": "Cuban sandwich day and Ride the wind day" + }, + { + "day": 24, + "month": 8, + "international_day": "International strange music day and Knife day" + }, + { + "day": 25, + "month": 8, + "international_day": "Kiss and make up day and Banana split day" + }, + { + "day": 26, + "month": 8, + "international_day": "Burger day and Dog day" + }, + { + "day": 27, + "month": 8, + "international_day": "International bat night and Banana lovers day" + }, + { + "day": 28, + "month": 8, + "international_day": "Bow tie day and Franchise appreciation day" + }, + { + "day": 29, + "month": 8, + "international_day": "More herbs less salt day and Potteries bottle oven day" + }, + { + "day": 30, + "month": 8, + "international_day": "Slinky day and Amagwinya day" + }, + { + "day": 31, + "month": 8, + "international_day": "Trail mix day and Overdose awareness day" + }, + { + "day": 1, + "month": 9, + "international_day": "Building and code staff appreciation day and Tofu day" + }, + { + "day": 2, + "month": 9, + "international_day": "Calendar adjustment day and V-J day" + }, + { + "day": 3, + "month": 9, + "international_day": "Skyscraper day and Bring your manners to work day" + }, + { + "day": 4, + "month": 9, + "international_day": "Wildlife day and Macadamia nut day" + }, + { + "day": 5, + "month": 9, + "international_day": "Be late for something day and World samosa day" + }, + { + "day": 6, + "month": 9, + "international_day": "Read a book day and Mouthguard day" + }, + { + "day": 7, + "month": 9, + "international_day": "World duchenne awareness day and Beer lover's day" + }, + { + "day": 8, + "month": 9, + "international_day": "Star terk day and Pardon day" + }, + { + "day": 9, + "month": 9, + "international_day": "Teddy bear day and Wienerschnitzel day" + }, + { + "day": 10, + "month": 9, + "international_day": "World suicide prevention day and TV dinner day" + }, + { + "day": 11, + "month": 9, + "international_day": "Make your bed day and Drive your studebaker day" + }, + { + "day": 12, + "month": 9, + "international_day": "Video games day and Hug your hound day" + }, + { + "day": 13, + "month": 9, + "international_day": "Fortune cookie day and Boss/Employee exchange day" + }, + { + "day": 14, + "month": 9, + "international_day": "Eat a hoagie day and Cream filled doughnut day" + }, + { + "day": 15, + "month": 9, + "international_day": "World afro day and Cheese toast day" + }, + { + "day": 16, + "month": 9, + "international_day": "Guacamole day and Play doh day" + }, + { + "day": 17, + "month": 9, + "international_day": "Tradesmen day and International country music day" + }, + { + "day": 18, + "month": 9, + "international_day": "International red panda day and Cheeseburger day" + }, + { + "day": 19, + "month": 9, + "international_day": "Talk like a priate day and Butterscotch pudding day" + }, + { + "day": 20, + "month": 9, + "international_day": "Punch day and Pepperoni pizza day" + }, + { + "day": 21, + "month": 9, + "international_day": "World alzheimer's day and Escapology day" + }, + { + "day": 22, + "month": 9, + "international_day": "Business women's day and World car free day" + }, + { + "day": 23, + "month": 9, + "international_day": "Restless legs awareness day and Za'atar day and Fitness day" + }, + { + "day": 24, + "month": 9, + "international_day": "Hug a vegetarian day and Lash stylist's day" + }, + { + "day": 25, + "month": 9, + "international_day": "World dream day" + }, + { + "day": 26, + "month": 9, + "international_day": "Lumberjack day and Rivers day" + }, + { + "day": 27, + "month": 9, + "international_day": "Tourism day and Corned beef hash day" + }, + { + "day": 28, + "month": 9, + "international_day": "Drink beer day and International poke day" + }, + { + "day": 29, + "month": 9, + "international_day": "World heart day and Biscotti day" + }, + { + "day": 30, + "month": 9, + "international_day": "Ask a stupid question day and International podcast day" + }, + { + "day": 1, + "month": 10, + "international_day": "World smile day and International coffee day" + }, + { + "day": 2, + "month": 10, + "international_day": "Name your car day and World farm animals day" + }, + { + "day": 3, + "month": 10, + "international_day": "Techies day and Boyfriend's day" + }, + { + "day": 4, + "month": 10, + "international_day": "Vodka day and World habitat day" + }, + { + "day": 5, + "month": 10, + "international_day": "World teachers day and Chic spy day" + }, + { + "day": 6, + "month": 10, + "international_day": "Canadian beer day and Mad hatter day" + }, + { + "day": 7, + "month": 10, + "international_day": "Bathtub day and Frappe day" + }, + { + "day": 8, + "month": 10, + "international_day": "World Octopus day and Egg day" + }, + { + "day": 9, + "month": 10, + "international_day": "Scrubs day and Beer and pizza day" + }, + { + "day": 10, + "month": 10, + "international_day": "Hug a drummer day and SHIFT10 day" + }, + { + "day": 11, + "month": 10, + "international_day": "Coming out day and Canadian thanksgiving" + }, + { + "day": 12, + "month": 10, + "international_day": "Old farmers day and Own business day" + }, + { + "day": 13, + "month": 10, + "international_day": "No bra day and Train your brain day" + }, + { + "day": 14, + "month": 10, + "international_day": "Dessert day and International top spinning day" + }, + { + "day": 15, + "month": 10, + "international_day": "World student's day and Chicken cacciatore day" + }, + { + "day": 16, + "month": 10, + "international_day": "World food day and Dictionary day" + }, + { + "day": 17, + "month": 10, + "international_day": "Toy camera day and Spreadsheet day" + }, + { + "day": 18, + "month": 10, + "international_day": "Chocolate cupcake day and Developmental language disorder awareness day" + }, + { + "day": 19, + "month": 10, + "international_day": "Evaluate your life day and International gin and tonic day" + }, + { + "day": 20, + "month": 10, + "international_day": "International chef day and International sloth day" + }, + { + "day": 21, + "month": 10, + "international_day": "Apple day and Get smart about cerdit day" + }, + { + "day": 22, + "month": 10, + "international_day": "Caps lock day and International stuttering awareness day" + }, + { + "day": 23, + "month": 10, + "international_day": "Make a difference day and Ipod day" + }, + { + "day": 24, + "month": 10, + "international_day": "Unites nation day and Mother in law day" + }, + { + "day": 25, + "month": 10, + "international_day": "International artist day and Accounting day" + }, + { + "day": 26, + "month": 10, + "international_day": "Howl at the moon day and Pumpkin day" + }, + { + "day": 27, + "month": 10, + "international_day": "Black cat day and Cranky co-workers day" + }, + { + "day": 28, + "month": 10, + "international_day": "Plush animal lover's day" + }, + { + "day": 29, + "month": 10, + "international_day": "Animation day and Internet day and Cat day" + }, + { + "day": 30, + "month": 10, + "international_day": "Checklist day and Hug a sheep day" + }, + { + "day": 31, + "month": 10, + "international_day": "Magic day and Caramel apple day" + }, + { + "day": 1, + "month": 11, + "international_day": "World vegan day and Go cook for your pets day" + }, + { + "day": 2, + "month": 11, + "international_day": "Deviled egg day and Dynamic harmlessness day" + }, + { + "day": 3, + "month": 11, + "international_day": "Stress awareness day and Sandwich day" + }, + { + "day": 4, + "month": 11, + "international_day": "Use your common sense day and Men make dinner day" + }, + { + "day": 5, + "month": 11, + "international_day": "Love your red hair day and Love your lawyer day" + }, + { + "day": 6, + "month": 11, + "international_day": "Nachos day and Numbat day" + }, + { + "day": 7, + "month": 11, + "international_day": "Bittersweet chocolate with almonds day and Zero tasking day" + }, + { + "day": 8, + "month": 11, + "international_day": "World orphans day and World quality day" + }, + { + "day": 9, + "month": 11, + "international_day": "World freedom day and Chaos never dies day" + }, + { + "day": 10, + "month": 11, + "international_day": "Sesame street day and Top up day" + }, + { + "day": 11, + "month": 11, + "international_day": "Origami day and Sundae day" + }, + { + "day": 12, + "month": 11, + "international_day": "Happy hour day and Chicken soup for the soul day" + }, + { + "day": 13, + "month": 11, + "international_day": "World kindness day and Indian pudding day" + }, + { + "day": 14, + "month": 11, + "international_day": "Operating room nurse day and Tongue twister day" + }, + { + "day": 15, + "month": 11, + "international_day": "Clean out your refigerator day and Bundt cake day" + }, + { + "day": 16, + "month": 11, + "international_day": "Have a party with your bear day and Clarinet day" + }, + { + "day": 17, + "month": 11, + "international_day": "Homemade bread day and Unfriend day" + }, + { + "day": 18, + "month": 11, + "international_day": "Housing day and Social enterprise day" + }, + { + "day": 19, + "month": 11, + "international_day": "International men's day and World toilet day" + }, + { + "day": 20, + "month": 11, + "international_day": "Name your PC day and Universal children's day" + }, + { + "day": 21, + "month": 11, + "international_day": "World television day and Red mitten day" + }, + { + "day": 22, + "month": 11, + "international_day": "Go for a ride day and Cranberry relish day" + }, + { + "day": 23, + "month": 11, + "international_day": "Espresso day and Fibonacci day" + }, + { + "day": 24, + "month": 11, + "international_day": "Sardines day and Jukebox day" + }, + { + "day": 25, + "month": 11, + "international_day": "Shopping reminder day and Parfait day" + }, + { + "day": 26, + "month": 11, + "international_day": "Buy nothing day and Flossing day" + }, + { + "day": 27, + "month": 11, + "international_day": "Bavarian cream pie day and Pins and Needles day" + }, + { + "day": 28, + "month": 11, + "international_day": "French toast day and Aura awareness day" + }, + { + "day": 29, + "month": 11, + "international_day": "Chocolates day and Lemon cream pie day" + }, + { + "day": 30, + "month": 11, + "international_day": "Computer security day and Mousse day" + }, + { + "day": 1, + "month": 12, + "international_day": "Eat a red apple day and Day without art day" + }, + { + "day": 2, + "month": 12, + "international_day": "Fritters day" + }, + { + "day": 3, + "month": 12, + "international_day": "Bartender appreciation day and Make a gift day" + }, + { + "day": 4, + "month": 12, + "international_day": "Cookie day and International cheetah day" + }, + { + "day": 5, + "month": 12, + "international_day": "International ninja day and Repeal day" + }, + { + "day": 6, + "month": 12, + "international_day": "Miner's day and Walt disney day" + }, + { + "day": 7, + "month": 12, + "international_day": "Cotton candy day and Pearl harbor remembrance day" + }, + { + "day": 8, + "month": 12, + "international_day": "Brownie day and Lard day" + }, + { + "day": 9, + "month": 12, + "international_day": "Pastry day and Techno day" + }, + { + "day": 10, + "month": 12, + "international_day": "Human rights day and Lager day" + }, + { + "day": 11, + "month": 12, + "international_day": "Have a bagel day and Noodle ring day" + }, + { + "day": 12, + "month": 12, + "international_day": "Poinsettia day and Gingerbread house day" + }, + { + "day": 13, + "month": 12, + "international_day": "Violin day and Day of the horse" + }, + { + "day": 14, + "month": 12, + "international_day": "Monkey day and Roast chestnuts day" + }, + { + "day": 15, + "month": 12, + "international_day": "Cat herders day and Lemon cupcake day" + }, + { + "day": 16, + "month": 12, + "international_day": "Re-gifting day and Chocolate covered anything day" + }, + { + "day": 17, + "month": 12, + "international_day": "Ugly christmas sweater day and Maple syrup day" + }, + { + "day": 18, + "month": 12, + "international_day": "Bake cookies day and Roast suckling pig day" + }, + { + "day": 19, + "month": 12, + "international_day": "Look for an evergreen day and Oatmuffin day" + }, + { + "day": 20, + "month": 12, + "international_day": "Go caroling day and Games day" + }, + { + "day": 21, + "month": 12, + "international_day": "Humbug day and Flashlight day" + }, + { + "day": 22, + "month": 12, + "international_day": "Date nut bread day and Forefather's day" + }, + { + "day": 23, + "month": 12, + "international_day": "Roots day and Festivus day" + }, + { + "day": 24, + "month": 12, + "international_day": "Eggnog day" + }, + { + "day": 25, + "month": 12, + "international_day": "Pumpkin pie day" + }, + { + "day": 26, + "month": 12, + "international_day": "Thank you note day and Candy cane day" + }, + { + "day": 27, + "month": 12, + "international_day": "Make cut-out snowflakes day and Fruitcake day" + }, + { + "day": 28, + "month": 12, + "international_day": "Card playing day" + }, + { + "day": 29, + "month": 12, + "international_day": "Tick Tock day and Pepper pot day" + }, + { + "day": 30, + "month": 12, + "international_day": "Bacon day and Bicarbonate of soda day" + }, + { + "day": 31, + "month": 12, + "international_day": "Make up your mind day and Champagne day" + } +] \ No newline at end of file diff --git a/app/routers/audio.py b/app/routers/audio.py index 827f67ca..afd53b9c 100644 --- a/app/routers/audio.py +++ b/app/routers/audio.py @@ -2,7 +2,13 @@ from pathlib import Path from typing import List, Optional +from fastapi import APIRouter, Depends, Form, Request +from sqlalchemy.orm.session import Session +from starlette.responses import RedirectResponse +from starlette.status import HTTP_302_FOUND + from app.database.models import User +from app.dependencies import SOUNDS_PATH, get_db, templates from app.internal.audio import ( get_audio_settings, handle_vol, @@ -15,13 +21,7 @@ DEFAULT_SFX, DEFAULT_SFX_VOL, ) -from app.dependencies import SOUNDS_PATH, get_db, templates -from app.internal.security.dependancies import current_user -from fastapi import APIRouter, Depends, Form, Request -from sqlalchemy.orm.session import Session -from starlette.responses import RedirectResponse -from starlette.status import HTTP_302_FOUND - +from app.internal.security.dependencies import current_user router = APIRouter( prefix="/audio", diff --git a/app/routers/dayview.py b/app/routers/dayview.py index 6b4f887e..f9b1cefc 100644 --- a/app/routers/dayview.py +++ b/app/routers/dayview.py @@ -6,7 +6,7 @@ from app.database.models import Event, User from app.dependencies import get_db, templates -from app.internal import zodiac +from app.internal import international_days, zodiac from app.routers.user import get_all_user_events router = APIRouter() @@ -187,6 +187,7 @@ async def dayview( session=session, user_id=user.id, ) + inter_day = international_days.get_international_day_per_day(session, day) month = day.strftime("%B").upper() return templates.TemplateResponse( "calendar_day_view.html", @@ -196,6 +197,7 @@ async def dayview( "all_day_events": all_day_events, "month": month, "day": day.day, + "international_day": inter_day, "zodiac": zodiac_obj, "view": view, }, diff --git a/app/templates/calendar_day_view.html b/app/templates/calendar_day_view.html index ba681c9f..bb320981 100644 --- a/app/templates/calendar_day_view.html +++ b/app/templates/calendar_day_view.html @@ -1,77 +1,95 @@ {% extends "partials/calendar/calendar_base.html" %} {% block body %}
- -
- {% if view == 'day' %} - - {{month}} - {{day}} - {% if zodiac %} -
- zodiac sign + +
+ {% if view == 'day' %} + + {{month}} + {{day}} + {% if zodiac %} +
+ zodiac sign +
+ {% endif %} + {% else %} + {{day}} / {{month}} + {% endif %} +
+ {% if international_day %} +
+ The International days are: "{{ international_day.international_day }}"
{% endif %} - {% else %} - {{day}} / {{month}} - {% endif %} -
-
- {% for event in all_day_events %} -

{{ event.title }}

- {% endfor %} -
-
-
- {% for hour in range(25)%} -
-
- {% if view == 'day'%} - {% set hour = hour|string() %} - {{hour.zfill(2)}}:00 - {% endif %} -
-
-
- {% endfor %} +
+ {% for event in all_day_events %} +

{{ event.title }}

+ {% endfor %}
-
- {% for event, attr in events %} -
-
-

{{ event.title }}

- {% if attr.total_time_visible %} -

{{attr.total_time}}

- {% endif %} +
+
+ {% for hour in range(25)%} +
+
+ {% if view == 'day'%} + {% set hour = hour|string() %} + {{hour.zfill(2)}}:00 + {% endif %} +
+
+
+ {% endfor %}
-
- - - +
+ {% for event, attr in events %} +
+
+

{{ + event.title }}

+ {% if attr.total_time_visible %} +

+ {{attr.total_time}}

+ {% endif %} +
+
+ + + +
+
+ {% endfor %}
-
- {% endfor %}
-
- {% if view == 'day'%} - - {% endif %} + {% if view == 'day'%} + + {% endif %}
{% if view == 'day'%}
{% endif %} - + {% endblock body %} \ No newline at end of file diff --git a/tests/test_international_days.py b/tests/test_international_days.py new file mode 100644 index 00000000..c1b3f406 --- /dev/null +++ b/tests/test_international_days.py @@ -0,0 +1,63 @@ +from datetime import date, timedelta + +import pytest + +from app.database.models import InternationalDays +from app.internal import international_days +from app.internal.international_days import get_international_day_per_day +from app.internal.json_data_loader import _insert_into_database +from app.internal.utils import create_model, delete_instance + +DATE = date(2021, 6, 1) +DAY = "Hamburger day" + + +@pytest.fixture +def international_day(session): + inter_day = create_model( + session, + InternationalDays, + id=1, + day=1, + month=6, + international_day="Hamburger day", + ) + yield inter_day + delete_instance(session, inter_day) + + +@pytest.fixture +def all_international_days(session): + _insert_into_database( + session, + 'app/resources/international_days.json', + InternationalDays, + international_days.get_international_day, + ) + all_international_days = session.query(InternationalDays) + yield all_international_days + for day in all_international_days: + delete_instance(session, day) + + +def date_range(): + start = date(2024, 1, 1) + end = date(2024, 12, 31) + dates = (end + timedelta(days=1) - start).days + return [start + timedelta(days=i) for i in range(dates)] + + +def test_input_day_equal_output_day(session, international_day): + inter_day = international_days.get_international_day_per_day( + session, DATE).international_day + assert inter_day == DAY + + +def test_international_day_per_day_no_international_days(session): + result = international_days.get_international_day_per_day(session, DATE) + assert result is None + + +def test_all_international_days_per_day(session, all_international_days): + for day in date_range(): + assert get_international_day_per_day(session, day) From b94775196776137f564bef0658d3c228f2b3b7e5 Mon Sep 17 00:00:00 2001 From: Elior Digmi <71041101+Eliory09@users.noreply.github.com> Date: Wed, 24 Feb 2021 03:04:48 +0200 Subject: [PATCH 22/46] Feature/geolocation (#193) --- .DS_Store | Bin 0 -> 6148 bytes .gitignore | 4 + app/database/models.py | 2 + app/internal/event.py | 38 ++++++- app/routers/event.py | 19 +++- app/static/event/eventview.css | 10 +- .../event/view_event_details_tab.html | 53 +++++---- requirements.txt | 10 +- tests/asyncio_fixture.py | 2 + tests/conftest.py | 8 +- tests/event_fixture.py | 5 + tests/test_geolocation.py | 105 ++++++++++++++++++ 12 files changed, 225 insertions(+), 31 deletions(-) create mode 100644 .DS_Store create mode 100644 tests/test_geolocation.py diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 Union[Location, str]: + """Return location coordinates and accurate + address of the specified location.""" + try: + async with Nominatim( + user_agent="Pylendar", + adapter_factory=AioHTTPAdapter, + ) as geolocator: + geolocation = await geolocator.geocode(address) + except (GeocoderTimedOut, GeocoderUnavailable) as e: + logger.exception(str(e)) + else: + if geolocation is not None: + location = Location( + latitude=geolocation.latitude, + longitude=geolocation.longitude, + name=geolocation.raw["display_name"], + ) + return location + return address diff --git a/app/routers/event.py b/app/routers/event.py index d87e206e..a5673307 100644 --- a/app/routers/event.py +++ b/app/routers/event.py @@ -16,18 +16,20 @@ from app.database.models import Comment, Event, User, UserEvent from app.dependencies import get_db, logger, templates +from app.internal import comment as cmt +from app.internal.emotion import get_emotion from app.internal.event import ( get_invited_emails, + get_location_coordinates, get_messages, get_uninvited_regular_emails, raise_if_zoom_link_invalid, ) -from app.internal import comment as cmt -from app.internal.emotion import get_emotion from app.internal.privacy import PrivacyKinds from app.internal.utils import create_model, get_current_user from app.routers.categories import get_user_categories + EVENT_DATA = Tuple[Event, List[Dict[str, str]], str] TIME_FORMAT = "%Y-%m-%d %H:%M" START_FORMAT = "%A, %d/%m/%Y %H:%M" @@ -132,9 +134,16 @@ async def create_new_event( title, invited_emails, ) + latitude, longitude = None, None if vc_link: raise_if_zoom_link_invalid(vc_link) + else: + location_details = await get_location_coordinates(location) + if not isinstance(location_details, str): + location = location_details.name + latitude = location_details.latitude + longitude = location_details.longitude event = create_event( db=session, @@ -145,6 +154,8 @@ async def create_new_event( owner_id=owner_id, content=content, location=location, + latitude=latitude, + longitude=longitude, vc_link=vc_link, invitees=invited_emails, category_id=category_id, @@ -411,6 +422,8 @@ def create_event( content: Optional[str] = None, location: Optional[str] = None, vc_link: str = None, + latitude: Optional[str] = None, + longitude: Optional[str] = None, color: Optional[str] = None, invitees: List[str] = None, category_id: Optional[int] = None, @@ -432,6 +445,8 @@ def create_event( content=content, owner_id=owner_id, location=location, + latitude=latitude, + longitude=longitude, vc_link=vc_link, color=color, emotion=get_emotion(title, content), diff --git a/app/static/event/eventview.css b/app/static/event/eventview.css index 3a420e0e..0b768405 100644 --- a/app/static/event/eventview.css +++ b/app/static/event/eventview.css @@ -50,10 +50,6 @@ div.event_info_row, margin-block-end: 0.2em; } -.title { - border-bottom: 4px solid blue; -} - .title h1 { white-space: nowrap; margin-block-start: 0.2em; @@ -72,4 +68,8 @@ div.event_info_row, button { height: 100%; -} \ No newline at end of file +} + +.google_maps_object { + width: 100%; +} diff --git a/app/templates/partials/calendar/event/view_event_details_tab.html b/app/templates/partials/calendar/event/view_event_details_tab.html index d2c235ed..36b96d04 100644 --- a/app/templates/partials/calendar/event/view_event_details_tab.html +++ b/app/templates/partials/calendar/event/view_event_details_tab.html @@ -1,11 +1,11 @@
-
-

{{ event.title }}

-
-
- - -
+
+

{{ event.title }}

+
+
+ + +
ICON @@ -35,24 +35,39 @@

{{ event.title }}

VC linkVC URL
{% endif %} + +{% if event.latitude is not none and event.longitude is not none %} +
+ +
+{% endif %} + {% if event.invitees %}
-
- - - +
+ + +
{% endif %}

- {{event.owner.username}} + {{event.owner.username}}

- - - + + +
diff --git a/requirements.txt b/requirements.txt index 5bbdca17..5d07acea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ aiofiles==0.6.0 +aiohttp==3.7.3 aioredis==1.3.1 aiosmtpd==1.2.2 aiosmtplib==1.1.4 @@ -24,7 +25,7 @@ cachetools==4.2.0 certifi==2020.12.5 cffi==1.14.4 cfgv==3.2.0 -chardet==4.0.0 +chardet==3.0.4 click==7.1.2 colorama==0.4.4 coverage==5.3.1 @@ -44,6 +45,8 @@ fastapi-mail==0.3.3.1 filelock==3.0.12 flake8==3.8.4 frozendict==1.2 +geographiclib==1.50 +geopy==2.1.0 google-api-core==1.25.0 google-api-python-client==1.12.8 google-auth==1.24.0 @@ -77,6 +80,7 @@ mocker==1.1.1 multidict==5.1.0 mypy==0.790 mypy-extensions==0.4.3 +nest-asyncio==1.5.1 nltk==3.5 nodeenv==1.5.0 oauth2client==4.1.3 @@ -84,6 +88,7 @@ oauthlib==3.1.0 outcome==1.1.0 packaging==20.8 passlib==1.7.4 +pathspec==0.8.1 Pillow==8.1.0 pluggy==0.13.1 pre-commit==2.10.0 @@ -148,4 +153,5 @@ win32-setctime==1.0.3 word-forms==2.1.0 wsproto==1.0.0 yapf==0.30.0 -zipp==3.4.0 \ No newline at end of file +yarl==1.6.3 +zipp==3.4.0 diff --git a/tests/asyncio_fixture.py b/tests/asyncio_fixture.py index db6645c5..2506ab53 100644 --- a/tests/asyncio_fixture.py +++ b/tests/asyncio_fixture.py @@ -33,6 +33,7 @@ def fake_user_events(session): create_event( db=session, title='Cool today event', + color='red', start=today_date, end=today_date + timedelta(days=2), all_day=False, @@ -44,6 +45,7 @@ def fake_user_events(session): create_event( db=session, title='Cool (somewhen in two days) event', + color='blue', start=today_date + timedelta(days=1), end=today_date + timedelta(days=3), all_day=False, diff --git a/tests/conftest.py b/tests/conftest.py index 1d8a21d2..4c2d7f12 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,11 +1,12 @@ import calendar +import nest_asyncio import pytest +from app.config import PSQL_ENVIRONMENT +from app.database.models import Base from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker -from app.config import PSQL_ENVIRONMENT -from app.database.models import Base pytest_plugins = [ 'tests.user_fixture', @@ -80,3 +81,6 @@ def sqlite_engine(): @pytest.fixture def Calendar(): return calendar.Calendar(0) + + +nest_asyncio.apply() diff --git a/tests/event_fixture.py b/tests/event_fixture.py index 989c41fb..7c3d8a56 100644 --- a/tests/event_fixture.py +++ b/tests/event_fixture.py @@ -42,6 +42,7 @@ def today_event_2(sender: User, session: Session) -> Event: return create_event( db=session, title='event 2', + color='blue', start=today_date + timedelta(hours=3), end=today_date + timedelta(days=2, hours=3), all_day=False, @@ -55,6 +56,7 @@ def yesterday_event(sender: User, session: Session) -> Event: return create_event( db=session, title='event 3', + color='green', start=today_date - timedelta(hours=8), end=today_date, all_day=False, @@ -68,6 +70,7 @@ def next_week_event(sender: User, session: Session) -> Event: return create_event( db=session, title='event 4', + color='blue', start=today_date + timedelta(days=7, hours=2), end=today_date + timedelta(days=7, hours=4), all_day=False, @@ -81,6 +84,7 @@ def next_month_event(sender: User, session: Session) -> Event: return create_event( db=session, title='event 5', + color="green", start=today_date + timedelta(days=20, hours=4), end=today_date + timedelta(days=20, hours=6), all_day=False, @@ -94,6 +98,7 @@ def old_event(sender: User, session: Session) -> Event: return create_event( db=session, title='event 6', + color="red", start=today_date - timedelta(days=5), end=today_date - timedelta(days=1), all_day=False, diff --git a/tests/test_geolocation.py b/tests/test_geolocation.py new file mode 100644 index 00000000..9a9503fd --- /dev/null +++ b/tests/test_geolocation.py @@ -0,0 +1,105 @@ +import pytest + +from app.internal.event import get_location_coordinates +from app.database.models import Event +from sqlalchemy.sql import func + + +class TestGeolocation: + CORRECT_LOCATION_EVENT = { + "title": "test title", + "start_date": "2021-02-18", + "start_time": "18:00", + "end_date": "2021-02-18", + "end_time": "20:00", + "location_type": "address", + "location": "אדר 11, אשדוד", + "event_type": "on", + "description": "test1", + "color": "red", + "invited": "a@gmail.com", + "availability": "busy", + "privacy": "public", + } + + WRONG_LOCATION_EVENT = { + "title": "test title", + "start_date": "2021-02-18", + "start_time": "18:00", + "end_date": "2021-02-18", + "end_time": "20:00", + "location_type": "address", + "location": "not a real location with coords", + "event_type": "on", + "description": "test1", + "invited": "a@gmail.com", + "color": "red", + "availability": "busy", + "privacy": "public", + } + + CORRECT_LOCATIONS = [ + "Tamuz 13, Ashdod", + "Menachem Begin 21, Tel Aviv", + "רמב״ן 25, ירושלים", + ] + + WRONG_LOCATIONS = [ + "not a real location with coords", + "מיקום לא תקין", + "https://us02web.zoom.us/j/376584566", + ] + + @staticmethod + @pytest.mark.asyncio + @pytest.mark.parametrize("location", CORRECT_LOCATIONS) + async def test_get_location_coordinates_correct(location): + # Test geolocation search using valid locations. + location = await get_location_coordinates(location) + assert all(location) + + @staticmethod + @pytest.mark.asyncio + @pytest.mark.parametrize("location", WRONG_LOCATIONS) + async def test_get_location_coordinates_wrong(location): + # Test geolocation search using invalid locations. + location = await get_location_coordinates(location) + assert location == location + + @staticmethod + @pytest.mark.asyncio + async def test_event_location_correct(event_test_client, session): + # Test handling with location available on geopy servers. + response = event_test_client.post( + "event/edit", + data=TestGeolocation.CORRECT_LOCATION_EVENT, + ) + assert response.ok + event_id = session.query(func.count(Event.id)).scalar() + url = event_test_client.app.url_path_for( + "eventview", + event_id=event_id, + ) + response = event_test_client.get(url) + location = await get_location_coordinates( + TestGeolocation.CORRECT_LOCATION_EVENT["location"], + ) + address = location.name.split(" ")[0] + assert bytes(address, "utf-8") in response.content + + @staticmethod + def test_event_location_wrong(event_test_client, session): + # Test handling with location not available on geopy servers. + address = TestGeolocation.WRONG_LOCATION_EVENT["location"] + response = event_test_client.post( + "event/edit", + data=TestGeolocation.WRONG_LOCATION_EVENT, + ) + assert response.ok + event_id = session.query(func.count(Event.id)).scalar() + url = event_test_client.app.url_path_for( + "eventview", + event_id=event_id, + ) + response = event_test_client.get(url) + assert bytes(address, "utf-8") in response.content From b4b0ede54f98f5545f956a5661da4f1ab22ac5b4 Mon Sep 17 00:00:00 2001 From: Idan Pelled <71136218+IdanPelled@users.noreply.github.com> Date: Wed, 24 Feb 2021 16:54:51 +0200 Subject: [PATCH 23/46] Feature/notifications (#331) --- AUTHORS.md | 1 + app/database/models.py | 71 +++- app/database/schemas.py | 50 ++- app/internal/notification.py | 176 ++++++++++ app/internal/utils.py | 36 +- app/main.py | 4 +- app/routers/event.py | 1 + app/routers/export.py | 21 +- app/routers/invitation.py | 110 ------ app/routers/login.py | 42 ++- app/routers/notification.py | 192 +++++++++++ app/routers/register.py | 17 +- app/routers/share.py | 28 +- app/routers/user.py | 49 +-- app/static/notification.css | 70 ++++ app/templates/archive.html | 26 ++ app/templates/base.html | 72 ++-- app/templates/calendar/layout.html | 83 +++++ app/templates/invitations.html | 24 -- app/templates/notifications.html | 38 +++ .../partials/calendar/navigation.html | 5 + app/templates/partials/index/navigation.html | 14 +- app/templates/partials/notification/base.html | 56 +++ .../notification/generate_archive.html | 29 ++ .../notification/generate_notifications.html | 48 +++ tests/conftest.py | 55 +-- tests/fixtures/__init__.py | 0 tests/{ => fixtures}/association_fixture.py | 0 tests/{ => fixtures}/asyncio_fixture.py | 14 +- tests/{ => fixtures}/category_fixture.py | 0 tests/{ => fixtures}/client_fixture.py | 23 +- tests/{ => fixtures}/comment_fixture.py | 0 tests/{ => fixtures}/dayview_fixture.py | 0 tests/{ => fixtures}/event_fixture.py | 38 +-- tests/{ => fixtures}/invitation_fixture.py | 0 tests/{ => fixtures}/jokes_fixture.py | 2 +- tests/{ => fixtures}/logger_fixture.py | 0 tests/fixtures/message_fixture.py | 29 ++ tests/{ => fixtures}/quotes_fixture.py | 0 tests/{ => fixtures}/user_fixture.py | 12 +- tests/{ => fixtures}/zodiac_fixture.py | 0 tests/salary/test_routes.py | 1 - tests/test_a_telegram_asyncio.py | 285 ++++++++-------- tests/test_calendar_privacy.py | 32 +- tests/test_google_connect.py | 323 +++++++++--------- tests/test_invitation.py | 50 --- tests/test_notification.py | 177 ++++++++++ tests/test_share_event.py | 61 ++-- tests/test_statistics.py | 53 ++- tests/test_user.py | 120 +++---- tests/utils.py | 13 - 51 files changed, 1723 insertions(+), 828 deletions(-) create mode 100644 app/internal/notification.py delete mode 100644 app/routers/invitation.py create mode 100644 app/routers/notification.py create mode 100644 app/static/notification.css create mode 100644 app/templates/archive.html create mode 100644 app/templates/calendar/layout.html delete mode 100644 app/templates/invitations.html create mode 100644 app/templates/notifications.html create mode 100644 app/templates/partials/notification/base.html create mode 100644 app/templates/partials/notification/generate_archive.html create mode 100644 app/templates/partials/notification/generate_notifications.html create mode 100644 tests/fixtures/__init__.py rename tests/{ => fixtures}/association_fixture.py (100%) rename tests/{ => fixtures}/asyncio_fixture.py (83%) rename tests/{ => fixtures}/category_fixture.py (100%) rename tests/{ => fixtures}/client_fixture.py (91%) rename tests/{ => fixtures}/comment_fixture.py (100%) rename tests/{ => fixtures}/dayview_fixture.py (100%) rename tests/{ => fixtures}/event_fixture.py (83%) rename tests/{ => fixtures}/invitation_fixture.py (100%) rename tests/{ => fixtures}/jokes_fixture.py (89%) rename tests/{ => fixtures}/logger_fixture.py (100%) create mode 100644 tests/fixtures/message_fixture.py rename tests/{ => fixtures}/quotes_fixture.py (100%) rename tests/{ => fixtures}/user_fixture.py (68%) rename tests/{ => fixtures}/zodiac_fixture.py (100%) delete mode 100644 tests/test_invitation.py create mode 100644 tests/test_notification.py delete mode 100644 tests/utils.py diff --git a/AUTHORS.md b/AUTHORS.md index add6ca4b..f97e0934 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -33,6 +33,7 @@ * PureDreamer - Developer * ShiZinDle - Developer * YairEn - Developer + * IdanPelled - Developer # Special thanks to diff --git a/app/database/models.py b/app/database/models.py index 63d350ae..bdaa090d 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -1,6 +1,6 @@ from __future__ import annotations - from datetime import datetime +import enum from typing import Any, Dict from sqlalchemy import ( @@ -8,6 +8,7 @@ Column, DateTime, DDL, + Enum, event, Float, ForeignKey, @@ -203,20 +204,80 @@ class PSQLEnvironmentError(Exception): ) +class InvitationStatusEnum(enum.Enum): + UNREAD = 0 + ACCEPTED = 1 + DECLINED = 2 + + +class MessageStatusEnum(enum.Enum): + UNREAD = 0 + READ = 1 + + class Invitation(Base): __tablename__ = "invitations" id = Column(Integer, primary_key=True, index=True) - status = Column(String, nullable=False, default="unread") + creation = Column(DateTime, default=datetime.now, nullable=False) + status = Column( + Enum(InvitationStatusEnum), + default=InvitationStatusEnum.UNREAD, + nullable=False, + ) + recipient_id = Column(Integer, ForeignKey("users.id")) event_id = Column(Integer, ForeignKey("events.id")) - creation = Column(DateTime, default=datetime.now) - recipient = relationship("User") event = relationship("Event") + def decline(self, session: Session) -> None: + """declines the invitation.""" + self.status = InvitationStatusEnum.DECLINED + session.merge(self) + session.commit() + + def accept(self, session: Session) -> None: + """Accepts the invitation by creating an + UserEvent association that represents + participantship at the event.""" + + association = UserEvent( + user_id=self.recipient.id, + event_id=self.event.id, + ) + self.status = InvitationStatusEnum.ACCEPTED + session.merge(self) + session.add(association) + session.commit() + + def __repr__(self): + return f"" + + +class Message(Base): + __tablename__ = "messages" + + id = Column(Integer, primary_key=True, index=True) + body = Column(String, nullable=False) + link = Column(String) + creation = Column(DateTime, default=datetime.now, nullable=False) + status = Column( + Enum(MessageStatusEnum), + default=MessageStatusEnum.UNREAD, + nullable=False, + ) + + recipient_id = Column(Integer, ForeignKey("users.id")) + recipient = relationship("User") + + def mark_as_read(self, session): + self.status = MessageStatusEnum.READ + session.merge(self) + session.commit() + def __repr__(self): - return f"" + return f"" class UserSettings(Base): diff --git a/app/database/schemas.py b/app/database/schemas.py index 61d31a33..29748b6f 100644 --- a/app/database/schemas.py +++ b/app/database/schemas.py @@ -2,7 +2,7 @@ from pydantic import BaseModel, validator, EmailStr, EmailError -EMPTY_FIELD_STRING = 'field is required' +EMPTY_FIELD_STRING = "field is required" MIN_FIELD_LENGTH = 3 MAX_FIELD_LENGTH = 20 @@ -19,10 +19,14 @@ class UserBase(BaseModel): Validating fields types Returns a User object without sensitive information """ + username: str email: str full_name: str + + language_id: Optional[int] = 1 description: Optional[str] = None + target_weight: Optional[Union[int, float]] = None class Config: orm_mode = True @@ -30,6 +34,7 @@ class Config: class UserCreate(UserBase): """Validating fields types""" + password: str confirm_password: str @@ -37,41 +42,49 @@ class UserCreate(UserBase): Calling to field_not_empty validaion function, for each required field. """ - _fields_not_empty_username = validator( - 'username', allow_reuse=True)(fields_not_empty) - _fields_not_empty_full_name = validator( - 'full_name', allow_reuse=True)(fields_not_empty) - _fields_not_empty_password = validator( - 'password', allow_reuse=True)(fields_not_empty) + _fields_not_empty_username = validator("username", allow_reuse=True)( + fields_not_empty, + ) + _fields_not_empty_full_name = validator("full_name", allow_reuse=True)( + fields_not_empty, + ) + _fields_not_empty_password = validator("password", allow_reuse=True)( + fields_not_empty, + ) _fields_not_empty_confirm_password = validator( - 'confirm_password', allow_reuse=True)(fields_not_empty) - _fields_not_empty_email = validator( - 'email', allow_reuse=True)(fields_not_empty) - - @validator('confirm_password') + "confirm_password", + allow_reuse=True, + )(fields_not_empty) + _fields_not_empty_email = validator("email", allow_reuse=True)( + fields_not_empty, + ) + + @validator("confirm_password") def passwords_match( - cls, confirm_password: str, - values: UserBase) -> Union[ValueError, str]: + cls, + confirm_password: str, + values: UserBase, + ) -> Union[ValueError, str]: """Validating passwords fields identical.""" - if 'password' in values and confirm_password != values['password']: + if "password" in values and confirm_password != values["password"]: raise ValueError("doesn't match to password") return confirm_password - @validator('username') + @validator("username") def username_length(cls, username: str) -> Union[ValueError, str]: """Validating username length is legal""" if not (MIN_FIELD_LENGTH < len(username) < MAX_FIELD_LENGTH): raise ValueError("must contain between 3 to 20 charactars") return username - @validator('password') + @validator("password") def password_length(cls, password: str) -> Union[ValueError, str]: """Validating username length is legal""" if not (MIN_FIELD_LENGTH < len(password) < MAX_FIELD_LENGTH): raise ValueError("must contain between 3 to 20 charactars") return password - @validator('email') + @validator("email") def confirm_mail(cls, email: str) -> Union[ValueError, str]: """Validating email is valid mail address.""" try: @@ -86,5 +99,6 @@ class User(UserBase): Validating fields types Returns a User object without sensitive information """ + id: int is_active: bool diff --git a/app/internal/notification.py b/app/internal/notification.py new file mode 100644 index 00000000..af86c9bb --- /dev/null +++ b/app/internal/notification.py @@ -0,0 +1,176 @@ +from operator import attrgetter +from typing import Iterator, List, Union, Callable + +from fastapi import HTTPException +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm import Session +from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_406_NOT_ACCEPTABLE + +from app.database.models import ( + Invitation, + Message, + InvitationStatusEnum, + MessageStatusEnum, +) +from app.internal.utils import create_model + + +WRONG_NOTIFICATION_ID = ( + "The notification id you have entered is wrong\n." + "If you did not enter the notification id manually, report this exception." +) + +NOTIFICATION_TYPE = Union[Invitation, Message] + +UNREAD_STATUS = { + InvitationStatusEnum.UNREAD, + MessageStatusEnum.UNREAD, +} + +ARCHIVED = { + InvitationStatusEnum.DECLINED, + MessageStatusEnum.READ, +} + + +async def get_message_by_id( + message_id: int, + session: Session, +) -> Union[Message, None]: + """Returns an invitation by an id. + if id does not exist, returns None. + """ + return session.query(Message).filter_by(id=message_id).first() + + +def _is_unread(notification: NOTIFICATION_TYPE) -> bool: + """Returns True if notification is unread, False otherwise.""" + return notification.status in UNREAD_STATUS + + +def _is_archived(notification: NOTIFICATION_TYPE) -> bool: + """Returns True if notification should be + in archived page, False otherwise. + """ + return notification.status in ARCHIVED + + +def is_owner(user, notification: NOTIFICATION_TYPE) -> bool: + """Checks if user is owner of the notification. + + Args: + notification: a NOTIFICATION_TYPE object. + user: user schema object. + + Returns: + True or raises HTTPException. + """ + if notification.recipient_id == user.user_id: + return True + + msg = "The notification you are trying to access is not yours." + raise HTTPException( + status_code=HTTP_401_UNAUTHORIZED, + detail=msg, + ) + + +def raise_wrong_id_error() -> None: + """Raises HTTPException. + + Returns: + None + """ + raise HTTPException( + status_code=HTTP_406_NOT_ACCEPTABLE, + detail=WRONG_NOTIFICATION_ID, + ) + + +def filter_notifications( + session: Session, + user_id: int, + func: Callable[[NOTIFICATION_TYPE], bool], +) -> Iterator[NOTIFICATION_TYPE]: + """Filters notifications by "func".""" + yield from filter(func, get_all_notifications(session, user_id)) + + +def get_unread_notifications( + session: Session, + user_id: int, +) -> Iterator[NOTIFICATION_TYPE]: + """Returns all unread notifications.""" + yield from filter_notifications(session, user_id, _is_unread) + + +def get_archived_notifications( + session: Session, + user_id: int, +) -> List[NOTIFICATION_TYPE]: + """Returns all archived notifications.""" + yield from filter_notifications(session, user_id, _is_archived) + + +def get_all_notifications( + session: Session, + user_id: int, +) -> List[NOTIFICATION_TYPE]: + """Returns all notifications.""" + invitations: List[Invitation] = get_all_invitations( + session, + recipient_id=user_id, + ) + messages: List[Message] = get_all_messages(session, user_id) + + notifications = invitations + messages + return sort_notifications(notifications) + + +def sort_notifications( + notification: List[NOTIFICATION_TYPE], +) -> List[NOTIFICATION_TYPE]: + """Sorts the notifications by the creation date.""" + return sorted(notification, key=attrgetter("creation"), reverse=True) + + +def create_message( + session: Session, + msg: str, + recipient_id: int, + link=None, +) -> Message: + """Creates a new message.""" + return create_model( + session, + Message, + body=msg, + recipient_id=recipient_id, + link=link, + ) + + +def get_all_messages(session: Session, recipient_id: int) -> List[Message]: + """Returns all messages.""" + condition = Message.recipient_id == recipient_id + return session.query(Message).filter(condition).all() + + +def get_all_invitations(session: Session, **param) -> List[Invitation]: + """Returns all invitations filter by param.""" + try: + invitations = session.query(Invitation).filter_by(**param).all() + except SQLAlchemyError: + return [] + else: + return invitations + + +def get_invitation_by_id( + invitation_id: int, + session: Session, +) -> Union[Invitation, None]: + """Returns an invitation by an id. + if id does not exist, returns None. + """ + return session.query(Invitation).filter_by(id=invitation_id).first() diff --git a/app/internal/utils.py b/app/internal/utils.py index 2f669212..a7e208f5 100644 --- a/app/internal/utils.py +++ b/app/internal/utils.py @@ -2,6 +2,8 @@ from typing import Any, List, Optional, Union from sqlalchemy.orm import Session +from starlette.responses import RedirectResponse +from starlette.status import HTTP_302_FOUND from app.database.models import Base, User @@ -19,6 +21,7 @@ def save(session: Session, instance: Base) -> bool: def create_model(session: Session, model_class: Base, **kwargs: Any) -> Base: """Creates and saves a db model.""" instance = model_class(**kwargs) + save(session, instance) return instance @@ -69,7 +72,7 @@ def get_time_from_string(string: str) -> Optional[Union[date, time]]: datetime.time | datetime.date | None: Date or Time object if valid, None otherwise. """ - formats = {'%Y-%m-%d': 'date', '%H:%M': 'time', '%H:%M:%S': 'time'} + formats = {"%Y-%m-%d": "date", "%H:%M": "time", "%H:%M:%S": "time"} for time_format, method in formats.items(): try: time_obj = getattr(datetime.strptime(string, time_format), method) @@ -89,10 +92,31 @@ def get_placeholder_user() -> User: A User object. """ return User( - username='new_user', - email='my@email.po', - password='1a2s3d4f5g6', - full_name='My Name', + username="new_user", + email="my@email.po", + password="1a2s3d4f5g6", + full_name="My Name", language_id=1, - telegram_id='', + telegram_id="", ) + + +def safe_redirect_response( + url: str, + default: str = "/", + status_code: int = HTTP_302_FOUND, +): + """Returns a safe redirect response. + + Args: + url: the url to redirect to. + default: where to redirect if url isn't safe. + status_code: the response status code. + + Returns: + The Notifications HTML page. + """ + if not url.startswith("/"): + url = default + + return RedirectResponse(url=url, status_code=status_code) diff --git a/app/main.py b/app/main.py index 3560d142..3eb750a0 100644 --- a/app/main.py +++ b/app/main.py @@ -63,11 +63,11 @@ def create_tables(engine, psql_environment): four_o_four, friendview, google_connect, - invitation, joke, login, logout, meds, + notification, profile, register, search, @@ -113,11 +113,11 @@ async def swagger_ui_redirect(): four_o_four.router, friendview.router, google_connect.router, - invitation.router, joke.router, login.router, logout.router, meds.router, + notification.router, profile.router, register.router, salary.router, diff --git a/app/routers/event.py b/app/routers/event.py index a5673307..7858ebba 100644 --- a/app/routers/event.py +++ b/app/routers/event.py @@ -33,6 +33,7 @@ EVENT_DATA = Tuple[Event, List[Dict[str, str]], str] TIME_FORMAT = "%Y-%m-%d %H:%M" START_FORMAT = "%A, %d/%m/%Y %H:%M" + UPDATE_EVENTS_FIELDS = { "title": str, "start": dt, diff --git a/app/routers/export.py b/app/routers/export.py index a5fd4229..0fa5b279 100644 --- a/app/routers/export.py +++ b/app/routers/export.py @@ -9,7 +9,8 @@ from app.dependencies import get_db from app.internal.agenda_events import get_events_in_time_frame from app.internal.export import get_icalendar_with_multiple_events -from app.internal.utils import get_current_user +from app.internal.security.schema import CurrentUser +from tests.security_testing_routes import current_user router = APIRouter( prefix="/export", @@ -20,9 +21,10 @@ @router.get("/") def export( - start_date: Union[date, str], - end_date: Union[date, str], - db: Session = Depends(get_db), + start_date: Union[date, str], + end_date: Union[date, str], + db: Session = Depends(get_db), + user: CurrentUser = Depends(current_user), ) -> StreamingResponse: """Returns the Export page route. @@ -30,19 +32,18 @@ def export( start_date: A date or an empty string. end_date: A date or an empty string. db: Optional; The database connection. + user: user schema object. Returns: - # TODO add description + A StreamingResponse that contains an .ics file. """ - # TODO: connect to real user - user = get_current_user(db) - events = get_events_in_time_frame(start_date, end_date, user.id, db) + events = get_events_in_time_frame(start_date, end_date, user.user_id, db) file = BytesIO(get_icalendar_with_multiple_events(db, list(events))) return StreamingResponse( content=file, media_type="text/calendar", headers={ - # Change filename to "pylandar.ics". - "Content-Disposition": "attachment;filename=pylandar.ics", + # Change filename to "PyLendar.ics". + "Content-Disposition": "attachment;filename=PyLendar.ics", }, ) diff --git a/app/routers/invitation.py b/app/routers/invitation.py deleted file mode 100644 index da2ba209..00000000 --- a/app/routers/invitation.py +++ /dev/null @@ -1,110 +0,0 @@ -from typing import Any, List, Optional - -from fastapi import APIRouter, Depends, Request, status -from fastapi.responses import RedirectResponse, Response -from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy.orm import Session - -from app.database.models import Invitation -from app.dependencies import get_db, templates -from app.routers.share import accept - -router = APIRouter( - prefix="/invitations", - tags=["invitation"], - dependencies=[Depends(get_db)], -) - - -@router.get("/", include_in_schema=False) -def view_invitations( - request: Request, db: Session = Depends(get_db) -) -> Response: - """Returns the Invitations page route. - - Args: - request: The HTTP request. - db: Optional; The database connection. - - Returns: - The Invitations HTML page. - """ - return templates.TemplateResponse("invitations.html", { - "request": request, - # TODO: Connect to current user. - # recipient_id should be the current user - # but because we don't have one yet, - # "get_all_invitations" returns all invitations - "invitations": get_all_invitations(db), - }) - - -@router.post("/", include_in_schema=False) -async def accept_invitations( - request: Request, db: Session = Depends(get_db) -) -> RedirectResponse: - """Creates a new connection between the User and the Event in the database. - - See Also: - share.accept for more information. - - Args: - request: The HTTP request. - db: Optional; The database connection. - - Returns: - An updated Invitations HTML page. - """ - data = await request.form() - invite_id = list(data.values())[0] - - invitation = get_invitation_by_id(invite_id, db) - if invitation: - accept(invitation, db) - - url = router.url_path_for("view_invitations") - return RedirectResponse(url=url, status_code=status.HTTP_302_FOUND) - - -# TODO: should be a get request with the path of: -# @router.get("/all") -@router.get("/get_all_invitations") -def get_all_invitations( - db: Session = Depends(get_db), **param: Any -) -> List[Invitation]: - """Returns all Invitations filtered by the requested parameters. - - Args: - db: Optional; The database connection. - **param: A list of parameters to filter by. - - Returns: - A list of all Invitations. - """ - try: - invitations = list(db.query(Invitation).filter_by(**param)) - except SQLAlchemyError: - return [] - else: - return invitations - - -# TODO: should be a get request with the path of: -# @router.get("/{id}") -@router.post("/get_invitation_by_id") -def get_invitation_by_id( - invitation_id: int, db: Session = Depends(get_db) -) -> Optional[Invitation]: - """Returns an Invitation by an ID. - - Args: - invitation_id: The Invitation ID. - db: Optional; The database connection. - - Returns: - An Invitation object if found, otherwise returns None. - """ - return (db.query(Invitation) - .filter_by(id=invitation_id) - .first() - ) diff --git a/app/routers/login.py b/app/routers/login.py index 99fd5b5c..59645520 100644 --- a/app/routers/login.py +++ b/app/routers/login.py @@ -3,13 +3,11 @@ from fastapi import APIRouter, Depends, Request from sqlalchemy.orm import Session from starlette.responses import RedirectResponse -from starlette.status import HTTP_302_FOUND from app.dependencies import get_db, templates -from app.internal.security.ouath2 import ( - authenticate_user, create_jwt_token) +from app.internal.security.ouath2 import authenticate_user, create_jwt_token from app.internal.security import schema - +from app.internal.utils import safe_redirect_response router = APIRouter( prefix="", @@ -20,21 +18,23 @@ @router.get("/login") async def login_user_form( - request: Request, message: Optional[str] = "") -> templates: + request: Request, + message: Optional[str] = "", +) -> templates: """rendering login route get method""" - return templates.TemplateResponse("login.html", { - "request": request, - "message": message, - 'current_user': "logged in" - }) + return templates.TemplateResponse( + "login.html", + {"request": request, "message": message, "current_user": "logged in"}, + ) -@router.post('/login') +@router.post("/login") async def login( - request: Request, - next: Optional[str] = "/", - db: Session = Depends(get_db), - existing_jwt: Union[str, bool] = False) -> RedirectResponse: + request: Request, + next: Optional[str] = "/", + db: Session = Depends(get_db), + existing_jwt: Union[str, bool] = False, +) -> RedirectResponse: """rendering login route post method.""" form = await request.form() form_dict = dict(form) @@ -49,19 +49,17 @@ async def login( if user: user = await authenticate_user(db, user) if not user: - return templates.TemplateResponse("login.html", { - "request": request, - "message": 'Please check your credentials' - }) + return templates.TemplateResponse( + "login.html", + {"request": request, "message": "Please check your credentials"}, + ) # creating HTTPONLY cookie with jwt-token out of user unique data # for testing if not existing_jwt: jwt_token = create_jwt_token(user) else: jwt_token = existing_jwt - if not next.startswith("/"): - next = "/" - response = RedirectResponse(next, status_code=HTTP_302_FOUND) + response = safe_redirect_response(next) response.set_cookie( "Authorization", value=jwt_token, diff --git a/app/routers/notification.py b/app/routers/notification.py new file mode 100644 index 00000000..74b51102 --- /dev/null +++ b/app/routers/notification.py @@ -0,0 +1,192 @@ +from fastapi import APIRouter, Depends, Form, Request +from sqlalchemy.orm import Session + +from app.database.models import MessageStatusEnum +from app.dependencies import get_db, templates +from app.internal.notification import ( + get_all_messages, + get_archived_notifications, + get_invitation_by_id, + get_message_by_id, + get_unread_notifications, + is_owner, + raise_wrong_id_error, +) +from app.internal.security.dependencies import current_user, is_logged_in + +from app.internal.security.schema import CurrentUser +from app.internal.utils import safe_redirect_response + +router = APIRouter( + prefix="/notification", + tags=["notification"], + dependencies=[ + Depends(get_db), + Depends(is_logged_in), + ], +) + + +@router.get("/", include_in_schema=False) +async def view_notifications( + request: Request, + user: CurrentUser = Depends(current_user), + db: Session = Depends(get_db), +): + """Returns the Notifications page. + + Args: + request: The HTTP request. + db: Optional; The database connection. + user: user schema object. + + Returns: + The Notifications HTML page. + """ + return templates.TemplateResponse( + "notifications.html", + { + "request": request, + "new_messages": bool(get_all_messages), + "notifications": list( + get_unread_notifications( + session=db, + user_id=user.user_id, + ), + ), + }, + ) + + +@router.get("/archive", include_in_schema=False) +async def view_archive( + request: Request, + user: CurrentUser = Depends(current_user), + db: Session = Depends(get_db), +): + """Returns the Archived Notifications page. + + Args: + request: The HTTP request. + db: Optional; The database connection. + user: user schema object. + + Returns: + The Archived Notifications HTML page. + """ + return templates.TemplateResponse( + "archive.html", + { + "request": request, + "notifications": list( + get_archived_notifications( + session=db, + user_id=user.user_id, + ), + ), + }, + ) + + +@router.post("/invitation/accept") +async def accept_invitations( + invite_id: int = Form(...), + next_url: str = Form(...), + db: Session = Depends(get_db), + user: CurrentUser = Depends(current_user), +): + """Creates a new connection between the User and the Event in the database. + + See Also: + models.Invitation.accept for more information. + + Args: + invite_id: the id of the invitation. + next_url: url to redirect to. + db: Optional; The database connection. + user: user schema object. + + Returns: + A redirect to where the user called the route from. + """ + invitation = get_invitation_by_id(invite_id, session=db) + if invitation and is_owner(user, invitation): + invitation.accept(db) + return safe_redirect_response(next_url) + + raise_wrong_id_error() + + +@router.post("/invitation/decline") +async def decline_invitations( + invite_id: int = Form(...), + next_url: str = Form(...), + db: Session = Depends(get_db), + user: CurrentUser = Depends(current_user), +): + """Declines an invitations. + + Args: + invite_id: the id of the invitation. + db: Optional; The database connection. + next_url: url to redirect to. + user: user schema object. + + Returns: + A redirect to where the user called the route from. + """ + invitation = get_invitation_by_id(invite_id, session=db) + if invitation and is_owner(user, invitation): + invitation.decline(db) + return safe_redirect_response(next_url) + + raise_wrong_id_error() + + +@router.post("/message/read") +async def mark_message_as_read( + message_id: int = Form(...), + next_url: str = Form(...), + db: Session = Depends(get_db), + user: CurrentUser = Depends(current_user), +): + """Marks a message as read. + + Args: + message_id: the id of the message. + db: Optional; The database connection. + next_url: url to redirect to. + user: user schema object. + + Returns: + A redirect to where the user called the route from. + """ + message = await get_message_by_id(message_id, session=db) + if message and is_owner(user, message): + message.mark_as_read(db) + return safe_redirect_response(next_url) + + raise_wrong_id_error() + + +@router.post("/message/read/all") +async def mark_all_as_read( + next_url: str = Form(...), + db: Session = Depends(get_db), + user: CurrentUser = Depends(current_user), +): + """Marks all messages as read. + + Args: + next_url: url to redirect to. + user: user schema object. + db: Optional; The database connection. + + Returns: + A redirect to where the user called the route from. + """ + for message in get_all_messages(db, user.user_id): + if message.status == MessageStatusEnum.UNREAD: + message.mark_as_read(db) + + return safe_redirect_response(next_url) diff --git a/app/routers/register.py b/app/routers/register.py index 57f77165..2bd8c4bf 100644 --- a/app/routers/register.py +++ b/app/routers/register.py @@ -11,7 +11,7 @@ from app.database import schemas from app.database import models from app.dependencies import get_db, templates - +from app.internal.utils import save router = APIRouter( prefix="", @@ -20,6 +20,13 @@ ) +def _create_user(session, **kw) -> models.User: + """Creates and saves a new user.""" + user = models.User(**kw) + save(session, user) + return user + + async def create_user(db: Session, user: schemas.UserCreate) -> models.User: """ creating a new User object in the database, with hashed password @@ -32,12 +39,10 @@ async def create_user(db: Session, user: schemas.UserCreate) -> models.User: "email": user.email, "password": hashed_password, "description": user.description, + "language_id": user.language_id, + "target_weight": user.target_weight, } - db_user = models.User(**user_details) - db.add(db_user) - db.commit() - db.refresh(db_user) - return db_user + return _create_user(**user_details, session=db) async def check_unique_fields( diff --git a/app/routers/share.py b/app/routers/share.py index a33f44fd..fe5d449e 100644 --- a/app/routers/share.py +++ b/app/routers/share.py @@ -2,8 +2,7 @@ from sqlalchemy.orm import Session -from app.database.models import Event, Invitation, UserEvent -from app.internal.utils import save +from app.database.models import Event, Invitation from app.internal.export import get_icalendar from app.routers.user import does_user_exist, get_users @@ -32,11 +31,11 @@ def send_email_invitation( event: Event, ) -> bool: """Sends an email with an invitation.""" - - ical_invitation = get_icalendar(event, participants) # noqa: F841 - for _ in participants: - # TODO: send email - pass + if participants: + ical_invitation = get_icalendar(event, participants) # noqa: F841 + for _ in participants: + # TODO: send email + pass return True @@ -50,7 +49,6 @@ def send_in_app_invitation( for participant in participants: # email is unique recipient = get_users(email=participant, session=session)[0] - if recipient.id != event.owner.id: session.add(Invitation(recipient=recipient, event=event)) @@ -62,20 +60,6 @@ def send_in_app_invitation( return True -def accept(invitation: Invitation, session: Session) -> None: - """Accepts an invitation by creating an - UserEvent association that represents - participantship at the event.""" - - association = UserEvent( - user_id=invitation.recipient.id, - event_id=invitation.event.id - ) - invitation.status = 'accepted' - save(session, invitation) - save(session, association) - - def share(event: Event, participants: List[str], session: Session) -> bool: """Sends invitations to all event participants.""" diff --git a/app/routers/user.py b/app/routers/user.py index 05206c8f..8b8a0403 100644 --- a/app/routers/user.py +++ b/app/routers/user.py @@ -10,8 +10,7 @@ from app.database.models import Event, User, UserEvent from app.dependencies import get_db from app.internal.user.availability import disable, enable -from app.internal.utils import get_current_user, save - +from app.internal.utils import get_current_user router = APIRouter( prefix="/user", @@ -23,7 +22,7 @@ class UserModel(BaseModel): username: str password: str - email: str = Field(regex='^\\S+@\\S+\\.\\S+$') + email: str = Field(regex="^\\S+@\\S+\\.\\S+$") language: str language_id: int @@ -38,32 +37,8 @@ async def get_user(id: int, session=Depends(get_db)): return session.query(User).filter_by(id=id).first() -@router.post("/") -def manually_create_user(user: UserModel, session=Depends(get_db)): - create_user(**user.dict(), session=session) - return f'User {user.username} successfully created' - - -def create_user(username: str, - password: str, - email: str, - language_id: int, - session: Session) -> User: - """Creates and saves a new user.""" - - user = User( - username=username, - password=password, - email=email, - language_id=language_id - ) - save(session, user) - return user - - def get_users(session: Session, **param): """Returns all users filtered by param.""" - try: users = list(session.query(User).filter_by(**param)) except SQLAlchemyError: @@ -73,13 +48,10 @@ def get_users(session: Session, **param): def does_user_exist( - session: Session, - *, user_id=None, - username=None, email=None + session: Session, *, user_id=None, username=None, email=None ): """Returns True if user exists, False otherwise. - function can receive one of the there parameters""" - + function can receive one of the there parameters""" if user_id: return len(get_users(session=session, id=user_id)) == 1 if username: @@ -91,16 +63,16 @@ def does_user_exist( def get_all_user_events(session: Session, user_id: int) -> List[Event]: """Returns all events that the user participants in.""" - return ( - session.query(Event).join(UserEvent) - .filter(UserEvent.user_id == user_id).all() + session.query(Event) + .join(UserEvent) + .filter(UserEvent.user_id == user_id) + .all() ) @router.post("/disable") -def disable_logged_user( - request: Request, session: Session = Depends(get_db)): +def disable_logged_user(request: Request, session: Session = Depends(get_db)): """route that sends request to disable the user. after successful disable it will be directed to main page. if the disable fails user will stay at settings page @@ -113,8 +85,7 @@ def disable_logged_user( @router.post("/enable") -def enable_logged_user( - request: Request, session: Session = Depends(get_db)): +def enable_logged_user(request: Request, session: Session = Depends(get_db)): """router that sends a request to enable the user. if enable successful it will be directed to main page. if it fails user will stay at settings page diff --git a/app/static/notification.css b/app/static/notification.css new file mode 100644 index 00000000..7dbe0a7d --- /dev/null +++ b/app/static/notification.css @@ -0,0 +1,70 @@ +/* general */ +#main { + width: 90%; + margin: 0 25% 0 5%; +} + +#link { + font-size: 1.5rem; +} + + +/* notifications */ +#notifications-box { + margin-top: 1rem; + +} + +.notification { + padding: 0.5rem 1rem; + display: flex; + justify-content: space-between; +} + +.notification:hover { + background-color: var(--surface-variant); + border-radius: 0.2rem; +} + +.action, .description { + display: inline-block; +} + +.action { + width: 4rem; +} + + +/* buttons */ +.notification-btn { + background-color: transparent; + border: none; +} + +.notification-btn:focus { + outline: 0; +} + +.btn-accept { + color: green; +} + +.btn-decline { + color: red; +} + +#mark-all-as-read { + margin: 1rem; +} + + +/* form */ +.notification-form { + display: inline-block; +} + + +/* icons */ +.icon { + font-size: 1.5rem; +} diff --git a/app/templates/archive.html b/app/templates/archive.html new file mode 100644 index 00000000..a74da791 --- /dev/null +++ b/app/templates/archive.html @@ -0,0 +1,26 @@ +{% extends "./partials/notification/base.html" %} +{% block page_name %}Archive{% endblock page_name %} + +{% block description %} +
+
Archived Notifications
+

+ In this page you can view all of your archived notifications.
+ Any notification you have marked as read or declined, you will see here.
+ You can use the + button to accept an invitation that you already declined. +

+
+{% endblock description %} + +{% block link %} + +{% endblock link %} + +{% block notifications %} + {% include './partials/notification/generate_archive.html' %} +{% endblock notifications %} + +{% block no_notifications_msg %} + You don't have any archived notifications. +{% endblock no_notifications_msg %} diff --git a/app/templates/base.html b/app/templates/base.html index 5d211ad5..d6039fb4 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -39,40 +39,42 @@ - - - - - - - - Create Categories - - - - + + + + + + + + + Create Categories + + + + +
@@ -80,6 +82,7 @@ {% block content %}{% endblock %}
+ @@ -95,5 +98,4 @@ - diff --git a/app/templates/calendar/layout.html b/app/templates/calendar/layout.html new file mode 100644 index 00000000..5f98e980 --- /dev/null +++ b/app/templates/calendar/layout.html @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + Calendar + + + +
+ +
+
FEATURE NAME
+
+
+
+
+
{{day.display()}}
+
Location 0oc 00:00
+
+ +
+
+ {% block main %} {% endblock %} +
+
+
+ + + + + + + + diff --git a/app/templates/invitations.html b/app/templates/invitations.html deleted file mode 100644 index 83d29418..00000000 --- a/app/templates/invitations.html +++ /dev/null @@ -1,24 +0,0 @@ -{% extends "partials/index/index_base.html" %} - - -{% block content %} - -
-

{{ message }}

-
- - {% if invitations %} -
- {% for i in invitations %} -
- {{ i.event.owner.username }} - {{ i.event.title }} ({{ i.event.start }}) ({{ i.status }}) - - -
- {% endfor %} -
- {% else %} - You don't have any invitations. - {% endif %} - -{% endblock %} \ No newline at end of file diff --git a/app/templates/notifications.html b/app/templates/notifications.html new file mode 100644 index 00000000..7b33a17f --- /dev/null +++ b/app/templates/notifications.html @@ -0,0 +1,38 @@ +{% extends "./partials/notification/base.html" %} +{% block page_name %}Notifications{% endblock page_name %} + +{% block description %} +
+
New Notifications
+

+ In this page you can view all of your new notifications.
+ use the + button to mark as read + and the + and + to accept and decline. +

+
+{% endblock description %} + +{% block link %} + +{% endblock link %} + +{% block optional %} +
+ + +
+{% endblock optional %} + +{% block notifications %} + {% include './partials/notification/generate_notifications.html' %} +{% endblock notifications %} + +{% block no_notifications_msg %} + You don't have any new notifications. +{% endblock no_notifications_msg %} diff --git a/app/templates/partials/calendar/navigation.html b/app/templates/partials/calendar/navigation.html index ab65b8eb..ed73747d 100644 --- a/app/templates/partials/calendar/navigation.html +++ b/app/templates/partials/calendar/navigation.html @@ -7,6 +7,11 @@
+
+ + + +
diff --git a/app/templates/partials/index/navigation.html b/app/templates/partials/index/navigation.html index b1fa3b3f..60baf35d 100644 --- a/app/templates/partials/index/navigation.html +++ b/app/templates/partials/index/navigation.html @@ -20,17 +20,14 @@ - + diff --git a/app/templates/partials/notification/base.html b/app/templates/partials/notification/base.html new file mode 100644 index 00000000..0df8fcde --- /dev/null +++ b/app/templates/partials/notification/base.html @@ -0,0 +1,56 @@ +{% extends "partials/base.html" %} +{% block head %} + {{super()}} + + + + + +{% endblock head %} +{% block body %} +
+ {% include 'partials/calendar/navigation.html' %} +
+ {% include 'partials/calendar/feature_settings/example.html' %} +
+
+ {% block content %} +
+ {% block description %} + {% endblock description %} + +
+
+
+ {% block link %} + {% endblock link %} +
+ {% if notifications %} +
+ + {% block optional %} + {% endblock optional %} + +
+ {% block notifications %} + {% endblock notifications %} +
+ + {% else %} + {% block no_notifications_msg %} + {% endblock no_notifications_msg %} +
+ {% endif %} +
+ {% endblock content %} +
+
+ + + + +{% endblock body %} diff --git a/app/templates/partials/notification/generate_archive.html b/app/templates/partials/notification/generate_archive.html new file mode 100644 index 00000000..2d0e0ed3 --- /dev/null +++ b/app/templates/partials/notification/generate_archive.html @@ -0,0 +1,29 @@ +{% for n in notifications %} + {% set type = n.__table__.name %} + +
+ {% if type == "invitations" %} +
+ Invitation from {{ n.event.owner.username }} - + {{ n.event.title }} + (declined) +
+
+
+ + +
+
+ + {% elif type == "messages" %} +
+ {% if n.link %}{{ n.body }} + {% else %}{{ n.body }}{% endif %} +
+ {% endif %} + +
+{% endfor %} diff --git a/app/templates/partials/notification/generate_notifications.html b/app/templates/partials/notification/generate_notifications.html new file mode 100644 index 00000000..b779db22 --- /dev/null +++ b/app/templates/partials/notification/generate_notifications.html @@ -0,0 +1,48 @@ +{% for n in notifications %} + {% set type = n.__table__.name %} + +
+ {% if type == "invitations" %} +
+ Invitation from {{ n.event.owner.username }} - + {{ n.event.title }} + ({{ n.event.start.strftime('%H:%M %m/%d/%Y') }}) +
+
+
+ + +
+
+ + +
+
+ + {% elif type == "messages" %} +
+ {% if n.link %}{{ n.body }} + {% else %}{{ n.body }}{% endif %} +
+
+
+ + +
+
+ {% endif %} + +
+{% endfor %} diff --git a/tests/conftest.py b/tests/conftest.py index 4c2d7f12..ab9d02c6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,20 +9,24 @@ pytest_plugins = [ - 'tests.user_fixture', - 'tests.event_fixture', - 'tests.dayview_fixture', - 'tests.invitation_fixture', - 'tests.association_fixture', - 'tests.client_fixture', - 'tests.asyncio_fixture', - 'tests.logger_fixture', - 'tests.category_fixture', - 'smtpdfix', - 'tests.quotes_fixture', - 'tests.zodiac_fixture', - 'tests.jokes_fixture', - 'tests.comment_fixture', + "tests.fixtures.user_fixture", + "tests.fixtures.event_fixture", + "tests.fixtures.invitation_fixture", + "tests.fixtures.message_fixture", + "tests.fixtures.association_fixture", + "tests.fixtures.client_fixture", + "tests.fixtures.asyncio_fixture", + "tests.fixtures.logger_fixture", + "tests.fixtures.category_fixture", + "tests.fixtures.quotes_fixture", + "tests.fixtures.zodiac_fixture", + "tests.fixtures.dayview_fixture", + "tests.fixtures.comment_fixture", + "tests.fixtures.quotes_fixture", + "tests.fixtures.zodiac_fixture", + "tests.fixtures.jokes_fixture", + "tests.fixtures.comment_fixture", + "smtpdfix", ] # When testing in a PostgreSQL environment please make sure that: @@ -31,21 +35,22 @@ if PSQL_ENVIRONMENT: SQLALCHEMY_TEST_DATABASE_URL = ( - "postgresql://postgres:1234" - "@localhost/postgres" - ) - test_engine = create_engine( - SQLALCHEMY_TEST_DATABASE_URL + "postgresql://postgres:1234" "@localhost/postgres" ) + test_engine = create_engine(SQLALCHEMY_TEST_DATABASE_URL) else: SQLALCHEMY_TEST_DATABASE_URL = "sqlite:///./test.db" test_engine = create_engine( - SQLALCHEMY_TEST_DATABASE_URL, connect_args={"check_same_thread": False} + SQLALCHEMY_TEST_DATABASE_URL, + connect_args={"check_same_thread": False}, ) TestingSessionLocal = sessionmaker( - autocommit=False, autoflush=False, bind=test_engine) + autocommit=False, + autoflush=False, + bind=test_engine, +) def get_test_db(): @@ -66,11 +71,15 @@ def session(): def sqlite_engine(): SQLALCHEMY_TEST_DATABASE_URL = "sqlite:///./test.db" sqlite_test_engine = create_engine( - SQLALCHEMY_TEST_DATABASE_URL, connect_args={"check_same_thread": False} + SQLALCHEMY_TEST_DATABASE_URL, + connect_args={"check_same_thread": False}, ) TestingSession = sessionmaker( - autocommit=False, autoflush=False, bind=sqlite_test_engine) + autocommit=False, + autoflush=False, + bind=sqlite_test_engine, + ) yield sqlite_test_engine session = TestingSession() diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/association_fixture.py b/tests/fixtures/association_fixture.py similarity index 100% rename from tests/association_fixture.py rename to tests/fixtures/association_fixture.py diff --git a/tests/asyncio_fixture.py b/tests/fixtures/asyncio_fixture.py similarity index 83% rename from tests/asyncio_fixture.py rename to tests/fixtures/asyncio_fixture.py index 2506ab53..7f567b3b 100644 --- a/tests/asyncio_fixture.py +++ b/tests/fixtures/asyncio_fixture.py @@ -7,7 +7,7 @@ from app.main import app from app.routers import telegram from app.routers.event import create_event -from tests.client_fixture import get_test_placeholder_user +from tests.fixtures.client_fixture import get_test_placeholder_user from tests.conftest import get_test_db, test_engine @@ -32,24 +32,24 @@ def fake_user_events(session): session.commit() create_event( db=session, - title='Cool today event', - color='red', + title="Cool today event", + color="red", start=today_date, end=today_date + timedelta(days=2), all_day=False, - content='test event', + content="test event", owner_id=user.id, location="Here", is_google_event=False, ) create_event( db=session, - title='Cool (somewhen in two days) event', - color='blue', + title="Cool (somewhen in two days) event", + color="blue", start=today_date + timedelta(days=1), end=today_date + timedelta(days=3), all_day=False, - content='this week test event', + content="this week test event", owner_id=user.id, location="Here", is_google_event=False, diff --git a/tests/category_fixture.py b/tests/fixtures/category_fixture.py similarity index 100% rename from tests/category_fixture.py rename to tests/fixtures/category_fixture.py diff --git a/tests/client_fixture.py b/tests/fixtures/client_fixture.py similarity index 91% rename from tests/client_fixture.py rename to tests/fixtures/client_fixture.py index 5f5f8971..7a5d1e3c 100644 --- a/tests/client_fixture.py +++ b/tests/fixtures/client_fixture.py @@ -1,11 +1,12 @@ -from typing import Generator, Iterator +from typing import Generator, Iterator, Dict -from fastapi.testclient import TestClient import pytest +from fastapi.testclient import TestClient from sqlalchemy.orm import Session from app import main from app.database.models import Base, User + from app.routers import ( agenda, audio, @@ -13,8 +14,8 @@ event, friendview, google_connect, - invitation, meds, + notification, profile, weight, ) @@ -22,9 +23,15 @@ from tests import security_testing_routes from tests.conftest import get_test_db, test_engine +LOGIN_DATA_TYPE = Dict[str, str] + main.app.include_router(security_testing_routes.router) +def login_client(client: TestClient, data: LOGIN_DATA_TYPE) -> None: + client.post(client.app.url_path_for("login"), data=data) + + def get_test_placeholder_user() -> User: return User( username="fake_user", @@ -57,6 +64,11 @@ def agenda_test_client() -> Generator[TestClient, None, None]: yield from create_test_client(agenda.get_db) +@pytest.fixture(scope="session") +def notification_test_client(): + yield from create_test_client(notification.get_db) + + @pytest.fixture(scope="session") def friendview_test_client() -> Generator[TestClient, None, None]: yield from create_test_client(friendview.get_db) @@ -77,11 +89,6 @@ def home_test_client() -> Generator[TestClient, None, None]: yield from create_test_client(main.get_db) -@pytest.fixture(scope="session") -def invitation_test_client() -> Generator[TestClient, None, None]: - yield from create_test_client(invitation.get_db) - - @pytest.fixture(scope="session") def categories_test_client() -> Generator[TestClient, None, None]: yield from create_test_client(categories.get_db) diff --git a/tests/comment_fixture.py b/tests/fixtures/comment_fixture.py similarity index 100% rename from tests/comment_fixture.py rename to tests/fixtures/comment_fixture.py diff --git a/tests/dayview_fixture.py b/tests/fixtures/dayview_fixture.py similarity index 100% rename from tests/dayview_fixture.py rename to tests/fixtures/dayview_fixture.py diff --git a/tests/event_fixture.py b/tests/fixtures/event_fixture.py similarity index 83% rename from tests/event_fixture.py rename to tests/fixtures/event_fixture.py index 7c3d8a56..17213e6f 100644 --- a/tests/event_fixture.py +++ b/tests/fixtures/event_fixture.py @@ -13,10 +13,10 @@ def event(sender: User, category: Category, session: Session) -> Event: return create_event( db=session, - title='event', + title="event", start=today_date, end=today_date, - content='test event', + content="test event", owner_id=sender.id, location="Some random location", vc_link=None, @@ -28,11 +28,11 @@ def event(sender: User, category: Category, session: Session) -> Event: def today_event(sender: User, session: Session) -> Event: return create_event( db=session, - title='event 1', + title="event 1", start=today_date + timedelta(hours=7), end=today_date + timedelta(hours=9), all_day=False, - content='test event', + content="test event", owner_id=sender.id, ) @@ -41,12 +41,12 @@ def today_event(sender: User, session: Session) -> Event: def today_event_2(sender: User, session: Session) -> Event: return create_event( db=session, - title='event 2', - color='blue', + title="event 2", + color="blue", start=today_date + timedelta(hours=3), end=today_date + timedelta(days=2, hours=3), all_day=False, - content='test event', + content="test event", owner_id=sender.id, ) @@ -55,12 +55,12 @@ def today_event_2(sender: User, session: Session) -> Event: def yesterday_event(sender: User, session: Session) -> Event: return create_event( db=session, - title='event 3', - color='green', + title="event 3", + color="green", start=today_date - timedelta(hours=8), end=today_date, all_day=False, - content='test event', + content="test event", owner_id=sender.id, ) @@ -69,12 +69,12 @@ def yesterday_event(sender: User, session: Session) -> Event: def next_week_event(sender: User, session: Session) -> Event: return create_event( db=session, - title='event 4', - color='blue', + title="event 4", + color="blue", start=today_date + timedelta(days=7, hours=2), end=today_date + timedelta(days=7, hours=4), all_day=False, - content='test event', + content="test event", owner_id=sender.id, ) @@ -83,12 +83,12 @@ def next_week_event(sender: User, session: Session) -> Event: def next_month_event(sender: User, session: Session) -> Event: return create_event( db=session, - title='event 5', + title="event 5", color="green", start=today_date + timedelta(days=20, hours=4), end=today_date + timedelta(days=20, hours=6), all_day=False, - content='test event', + content="test event", owner_id=sender.id, ) @@ -97,12 +97,12 @@ def next_month_event(sender: User, session: Session) -> Event: def old_event(sender: User, session: Session) -> Event: return create_event( db=session, - title='event 6', + title="event 6", color="red", start=today_date - timedelta(days=5), end=today_date - timedelta(days=1), all_day=False, - content='test event', + content="test event", owner_id=sender.id, ) @@ -111,11 +111,11 @@ def old_event(sender: User, session: Session) -> Event: def all_day_event(sender: User, category: Category, session: Session) -> Event: return create_event( db=session, - title='event', + title="event", start=today_date, end=today_date, all_day=True, - content='test event', + content="test event", owner_id=sender.id, location="Some random location", category_id=category.id, diff --git a/tests/invitation_fixture.py b/tests/fixtures/invitation_fixture.py similarity index 100% rename from tests/invitation_fixture.py rename to tests/fixtures/invitation_fixture.py diff --git a/tests/jokes_fixture.py b/tests/fixtures/jokes_fixture.py similarity index 89% rename from tests/jokes_fixture.py rename to tests/fixtures/jokes_fixture.py index d7e3258c..062d5d45 100644 --- a/tests/jokes_fixture.py +++ b/tests/fixtures/jokes_fixture.py @@ -16,5 +16,5 @@ def joke(session: Session) -> Joke: yield from add_joke( session=session, id_joke=1, - text='Chuck Norris can slam a revolving door.', + text="Chuck Norris can slam a revolving door.", ) diff --git a/tests/logger_fixture.py b/tests/fixtures/logger_fixture.py similarity index 100% rename from tests/logger_fixture.py rename to tests/fixtures/logger_fixture.py diff --git a/tests/fixtures/message_fixture.py b/tests/fixtures/message_fixture.py new file mode 100644 index 00000000..839051ba --- /dev/null +++ b/tests/fixtures/message_fixture.py @@ -0,0 +1,29 @@ +import pytest +from sqlalchemy.orm import Session + +from app.database.models import Message, User +from app.internal.utils import create_model, delete_instance + + +@pytest.fixture +def message(user: User, session: Session) -> Message: + invitation = create_model( + session, Message, + body='A test message', + link='#', + recipient_id=user.id, + ) + yield invitation + delete_instance(session, invitation) + + +@pytest.fixture +def sec_message(user: User, session: Session) -> Message: + invitation = create_model( + session, Message, + body='A test message', + link='#', + recipient_id=user.id, + ) + yield invitation + delete_instance(session, invitation) diff --git a/tests/quotes_fixture.py b/tests/fixtures/quotes_fixture.py similarity index 100% rename from tests/quotes_fixture.py rename to tests/fixtures/quotes_fixture.py diff --git a/tests/user_fixture.py b/tests/fixtures/user_fixture.py similarity index 68% rename from tests/user_fixture.py rename to tests/fixtures/user_fixture.py index b50fb900..e2a7ad26 100644 --- a/tests/user_fixture.py +++ b/tests/fixtures/user_fixture.py @@ -4,20 +4,24 @@ from sqlalchemy.orm import Session from app.database.models import User +from app.database.schemas import UserCreate from app.internal.utils import create_model, delete_instance +from app.routers.register import create_user @pytest.fixture -def user(session: Session) -> Generator[User, None, None]: - mock_user = create_model( - session, - User, +async def user(session: Session) -> Generator[User, None, None]: + schema = UserCreate( username="test_username", password="test_password", + confirm_password="test_password", email="test.email@gmail.com", + full_name="test_full_name", + description="test_description", language_id=1, target_weight=60, ) + mock_user = await create_user(session, schema) yield mock_user delete_instance(session, mock_user) diff --git a/tests/zodiac_fixture.py b/tests/fixtures/zodiac_fixture.py similarity index 100% rename from tests/zodiac_fixture.py rename to tests/fixtures/zodiac_fixture.py diff --git a/tests/salary/test_routes.py b/tests/salary/test_routes.py index 13e22e05..830569ff 100644 --- a/tests/salary/test_routes.py +++ b/tests/salary/test_routes.py @@ -199,7 +199,6 @@ def test_invalid_category_redirect( response = salary_test_client.get(path) assert any(temp.status_code == status.HTTP_307_TEMPORARY_REDIRECT for temp in response.history) - print(response.text) assert message in response.text diff --git a/tests/test_a_telegram_asyncio.py b/tests/test_a_telegram_asyncio.py index faf99d98..ff1a7ddf 100644 --- a/tests/test_a_telegram_asyncio.py +++ b/tests/test_a_telegram_asyncio.py @@ -6,141 +6,145 @@ from app.telegram.handlers import MessageHandler, reply_unknown_user from app.telegram.keyboards import DATE_FORMAT from app.telegram.models import Bot, Chat -from tests.asyncio_fixture import today_date -from tests.client_fixture import get_test_placeholder_user +from tests.fixtures.asyncio_fixture import today_date +from tests.fixtures.client_fixture import get_test_placeholder_user def gen_message(text): return { - 'update_id': 10000000, - 'message': { - 'message_id': 2434, - 'from': { - 'id': 666666, - 'is_bot': False, - 'first_name': 'Moshe', - 'username': 'banana', - 'language_code': 'en' + "update_id": 10000000, + "message": { + "message_id": 2434, + "from": { + "id": 666666, + "is_bot": False, + "first_name": "Moshe", + "username": "banana", + "language_code": "en", }, - 'chat': { - 'id': 666666, - 'first_name': 'Moshe', - 'username': 'banana', - 'type': 'private' + "chat": { + "id": 666666, + "first_name": "Moshe", + "username": "banana", + "type": "private", }, - 'date': 1611240725, - 'text': f'{text}' - } + "date": 1611240725, + "text": f"{text}", + }, } def gen_callback(text): return { - 'update_id': 568265, - 'callback_query': { - 'id': '546565356486', - 'from': { - 'id': 666666, - 'is_bot': False, - 'first_name': 'Moshe', - 'username': 'banana', - 'language_code': 'en' - }, 'message': { - 'message_id': 838, - 'from': { - 'id': 2566252, - 'is_bot': True, - 'first_name': 'PyLandar', - 'username': 'pylander_bot' - }, 'chat': { - 'id': 666666, - 'first_name': 'Moshe', - 'username': 'banana', - 'type': 'private' + "update_id": 568265, + "callback_query": { + "id": "546565356486", + "from": { + "id": 666666, + "is_bot": False, + "first_name": "Moshe", + "username": "banana", + "language_code": "en", + }, + "message": { + "message_id": 838, + "from": { + "id": 2566252, + "is_bot": True, + "first_name": "PyLandar", + "username": "pylander_bot", + }, + "chat": { + "id": 666666, + "first_name": "Moshe", + "username": "banana", + "type": "private", }, - 'date': 161156, - 'text': 'Choose events day.', - 'reply_markup': { - 'inline_keyboard': [ + "date": 161156, + "text": "Choose events day.", + "reply_markup": { + "inline_keyboard": [ [ + {"text": "Today", "callback_data": "Today"}, { - 'text': 'Today', - 'callback_data': 'Today' + "text": "This week", + "callback_data": "This week", }, - { - 'text': 'This week', - 'callback_data': 'This week' - } - ] - ] - } + ], + ], + }, }, - 'chat_instance': '-154494', - 'data': f'{text}'}} + "chat_instance": "-154494", + "data": f"{text}", + }, + } class TestChatModel: - @staticmethod def test_private_message(): - chat = Chat(gen_message('Cool message')) - assert chat.message == 'Cool message' + chat = Chat(gen_message("Cool message")) + assert chat.message == "Cool message" assert chat.user_id == 666666 - assert chat.first_name == 'Moshe' + assert chat.first_name == "Moshe" @staticmethod def test_callback_message(): - chat = Chat(gen_callback('Callback Message')) - assert chat.message == 'Callback Message' + chat = Chat(gen_callback("Callback Message")) + assert chat.message == "Callback Message" assert chat.user_id == 666666 - assert chat.first_name == 'Moshe' + assert chat.first_name == "Moshe" @pytest.mark.asyncio async def test_bot_model(): bot = Bot("fake bot id", "https://google.com") - assert bot.base == 'https://api.telegram.org/botfake bot id/' - assert bot.webhook_setter_url == 'https://api.telegram.org/botfake \ -bot id/setWebhook?url=https://google.com/telegram/' + assert bot.base == "https://api.telegram.org/botfake bot id/" + assert ( + bot.webhook_setter_url + == "https://api.telegram.org/botfake \ +bot id/setWebhook?url=https://google.com/telegram/" + ) assert bot.base == bot._set_base_url("fake bot id") assert bot.webhook_setter_url == bot._set_webhook_setter_url( - "https://google.com") + "https://google.com", + ) set_request = await bot.set_webhook() assert set_request.json() == { - 'ok': False, - 'error_code': 404, - 'description': 'Not Found' + "ok": False, + "error_code": 404, + "description": "Not Found", } drop_request = await bot.drop_webhook() assert drop_request.json() == { - 'ok': False, - 'error_code': 404, - 'description': 'Not Found' + "ok": False, + "error_code": 404, + "description": "Not Found", } send_request = await bot.send_message("654654645", "hello") assert send_request.status_code == status.HTTP_404_NOT_FOUND assert send_request.json() == { - 'ok': False, - 'error_code': 404, - 'description': 'Not Found' + "ok": False, + "error_code": 404, + "description": "Not Found", } class TestBotClient: - @staticmethod @pytest.mark.asyncio async def test_user_not_registered(telegram_client): response = await telegram_client.post( - '/telegram/', json=gen_message('/start')) + "/telegram/", + json=gen_message("/start"), + ) assert response.status_code == status.HTTP_200_OK - assert b'Hello, Moshe!' in response.content - assert b'To use PyLendar Bot you have to register' \ - in response.content + assert b"Hello, Moshe!" in response.content + assert b"To use PyLendar Bot you have to register" in response.content @staticmethod @pytest.mark.asyncio @@ -148,9 +152,11 @@ async def test_user_registered(telegram_client, session): session.add(get_test_placeholder_user()) session.commit() response = await telegram_client.post( - '/telegram/', json=gen_message('/start')) + "/telegram/", + json=gen_message("/start"), + ) assert response.status_code == status.HTTP_200_OK - assert b'Welcome to PyLendar telegram client!' in response.content + assert b"Welcome to PyLendar telegram client!" in response.content class TestHandlers: @@ -158,21 +164,27 @@ class TestHandlers: @pytest.mark.asyncio async def test_start_handlers(self): - chat = Chat(gen_message('/start')) + chat = Chat(gen_message("/start")) message = MessageHandler(chat, self.TEST_USER) - assert '/start' in message.handlers - assert await message.process_callback() == '''Hello, Moshe! -Welcome to PyLendar telegram client!''' + assert "/start" in message.handlers + assert ( + await message.process_callback() + == """Hello, Moshe! +Welcome to PyLendar telegram client!""" + ) @pytest.mark.asyncio async def test_default_handlers(self): wrong_start = MessageHandler( - Chat(gen_message('start')), self.TEST_USER) + Chat(gen_message("start")), + self.TEST_USER, + ) wrong_show_events = MessageHandler( - Chat(gen_message('show_events')), self.TEST_USER) - message = MessageHandler( - Chat(gen_message('hello')), self.TEST_USER) + Chat(gen_message("show_events")), + self.TEST_USER, + ) + message = MessageHandler(Chat(gen_message("hello")), self.TEST_USER) assert await wrong_start.process_callback() == "Unknown command." assert await wrong_show_events.process_callback() == "Unknown command." @@ -180,34 +192,34 @@ async def test_default_handlers(self): @pytest.mark.asyncio async def test_show_events_handler(self): - chat = Chat(gen_message('/show_events')) + chat = Chat(gen_message("/show_events")) message = MessageHandler(chat, self.TEST_USER) - assert await message.process_callback() == 'Choose events day.' + assert await message.process_callback() == "Choose events day." @pytest.mark.asyncio async def test_no_today_events_handler(self): - chat = Chat(gen_callback('Today')) + chat = Chat(gen_callback("Today")) message = MessageHandler(chat, self.TEST_USER) assert await message.process_callback() == "There're no events today." @pytest.mark.asyncio async def test_today_handler(self, fake_user_events): - chat = Chat(gen_callback('Today')) + chat = Chat(gen_callback("Today")) message = MessageHandler(chat, fake_user_events) answer = f"{today_date.strftime('%A, %B %d')}:\n" assert await message.process_callback() == answer @pytest.mark.asyncio async def test_this_week_handler(self): - chat = Chat(gen_callback('This week')) + chat = Chat(gen_callback("This week")) message = MessageHandler(chat, self.TEST_USER) - assert await message.process_callback() == 'Choose a day.' + assert await message.process_callback() == "Choose a day." @pytest.mark.asyncio async def test_no_chosen_day_handler(self): - chat = Chat(gen_callback('10 Feb 2021')) + chat = Chat(gen_callback("10 Feb 2021")) message = MessageHandler(chat, self.TEST_USER) - message.handlers['10 Feb 2021'] = message.chosen_day_handler + message.handlers["10 Feb 2021"] = message.chosen_day_handler answer = "There're no events on February 10." assert await message.process_callback() == answer @@ -223,99 +235,101 @@ async def test_chosen_day_handler(self, fake_user_events): @pytest.mark.asyncio async def test_new_event_handler(self): - chat = Chat(gen_message('/new_event')) + chat = Chat(gen_message("/new_event")) message = MessageHandler(chat, self.TEST_USER) - answer = 'Please, give your event a title.' + answer = "Please, give your event a title." assert await message.process_callback() == answer @pytest.mark.asyncio async def test_process_new_event(self): - chat = Chat(gen_message('New Title')) + chat = Chat(gen_message("New Title")) message = MessageHandler(chat, self.TEST_USER) - answer = 'Title:\nNew Title\n\n' - answer += 'Add a description of the event.' + answer = "Title:\nNew Title\n\n" + answer += "Add a description of the event." assert await message.process_callback() == answer - chat = Chat(gen_message('New Content')) + chat = Chat(gen_message("New Content")) message = MessageHandler(chat, self.TEST_USER) - answer = 'Content:\nNew Content\n\n' - answer += 'Where the event will be held?' + answer = "Content:\nNew Content\n\n" + answer += "Where the event will be held?" assert await message.process_callback() == answer - chat = Chat(gen_message('Universe')) + chat = Chat(gen_message("Universe")) message = MessageHandler(chat, self.TEST_USER) - answer = 'Location:\nUniverse\n\n' - answer += 'When does it start?' + answer = "Location:\nUniverse\n\n" + answer += "When does it start?" assert await message.process_callback() == answer - chat = Chat(gen_message('Not valid start datetime input')) + chat = Chat(gen_message("Not valid start datetime input")) message = MessageHandler(chat, self.TEST_USER) - answer = '❗️ Please, enter a valid date/time.' + answer = "❗️ Please, enter a valid date/time." assert await message.process_callback() == answer - chat = Chat(gen_message('today')) + chat = Chat(gen_message("today")) message = MessageHandler(chat, self.TEST_USER) today = datetime.today() answer = f'Starts on:\n{today.strftime("%d %b %Y %H:%M")}\n\n' - answer += 'And when does it end?' + answer += "And when does it end?" assert await message.process_callback() == answer - chat = Chat(gen_message('Not valid end datetime input')) + chat = Chat(gen_message("Not valid end datetime input")) message = MessageHandler(chat, self.TEST_USER) - answer = '❗️ Please, enter a valid date/time.' + answer = "❗️ Please, enter a valid date/time." assert await message.process_callback() == answer - chat = Chat(gen_message('tomorrow')) + chat = Chat(gen_message("tomorrow")) message = MessageHandler(chat, self.TEST_USER) tomorrow = today + timedelta(days=1) - answer = 'Title:\nNew Title\n\n' - answer += 'Content:\nNew Content\n\n' - answer += 'Location:\nUniverse\n\n' + answer = "Title:\nNew Title\n\n" + answer += "Content:\nNew Content\n\n" + answer += "Location:\nUniverse\n\n" answer += f'Starts on:\n{today.strftime("%d %b %Y %H:%M")}\n\n' answer += f'Ends on:\n{tomorrow.strftime("%d %b %Y %H:%M")}' assert await message.process_callback() == answer - chat = Chat(gen_message('create')) + chat = Chat(gen_message("create")) message = MessageHandler(chat, self.TEST_USER) - answer = 'New event was successfully created 🎉' + answer = "New event was successfully created 🎉" assert await message.process_callback() == answer @pytest.mark.asyncio async def test_process_new_event_cancel(self): - chat = Chat(gen_message('/new_event')) + chat = Chat(gen_message("/new_event")) message = MessageHandler(chat, self.TEST_USER) - answer = 'Please, give your event a title.' + answer = "Please, give your event a title." assert await message.process_callback() == answer - chat = Chat(gen_message('cancel')) + chat = Chat(gen_message("cancel")) message = MessageHandler(chat, self.TEST_USER) - answer = '🚫 The process was canceled.' + answer = "🚫 The process was canceled." assert await message.process_callback() == answer @pytest.mark.asyncio async def test_process_new_event_restart(self): - chat = Chat(gen_message('/new_event')) + chat = Chat(gen_message("/new_event")) message = MessageHandler(chat, self.TEST_USER) - answer = 'Please, give your event a title.' + answer = "Please, give your event a title." assert await message.process_callback() == answer - chat = Chat(gen_message('New Title')) + chat = Chat(gen_message("New Title")) message = MessageHandler(chat, self.TEST_USER) - answer = 'Title:\nNew Title\n\n' - answer += 'Add a description of the event.' + answer = "Title:\nNew Title\n\n" + answer += "Add a description of the event." assert await message.process_callback() == answer - chat = Chat(gen_message('restart')) + chat = Chat(gen_message("restart")) message = MessageHandler(chat, self.TEST_USER) - answer = 'Please, give your event a title.' + answer = "Please, give your event a title." assert await message.process_callback() == answer @pytest.mark.asyncio async def test_reply_unknown_user(): - chat = Chat(gen_message('/show_events')) + chat = Chat(gen_message("/show_events")) answer = await reply_unknown_user(chat) - assert answer == ''' + assert ( + answer + == """ Hello, Moshe! To use PyLendar Bot you have to register @@ -325,4 +339,5 @@ async def test_reply_unknown_user(): Keep it secret! https://calendar.pythonic.guru/profile/ -''' +""" + ) diff --git a/tests/test_calendar_privacy.py b/tests/test_calendar_privacy.py index d94dfd56..0fde3d0a 100644 --- a/tests/test_calendar_privacy.py +++ b/tests/test_calendar_privacy.py @@ -1,7 +1,8 @@ from app.internal.calendar_privacy import can_show_calendar + # TODO after user system is merged: -# from app.internal.security.dependencies import CurrentUser -from app.routers.user import create_user +# from app.internal.security.dependancies import CurrentUser +from app.routers.register import _create_user def test_can_show_calendar_public(session, user): @@ -10,32 +11,37 @@ def test_can_show_calendar_public(session, user): # current_user = CurrentUser(**user.__dict__) current_user = user result = can_show_calendar( - requested_user_username='test_username', - db=session, current_user=current_user + requested_user_username="test_username", + db=session, + current_user=current_user, ) assert result is True session.commit() def test_can_show_calendar_private(session, user): - another_user = create_user( + another_user = _create_user( session=session, - username='new_test_username2', - email='new_test.email2@gmail.com', - password='passpar_2', - language_id=1 + username="new_test_username2", + email="new_test.email2@gmail.com", + password="passpar_2", + language_id=1, + full_name="test_full_name", + description="test_description", ) current_user = user # TODO to be replaced after user system is merged: # current_user = CurrentUser(**user.__dict__) result_a = can_show_calendar( - requested_user_username='new_test_username2', - db=session, current_user=current_user + requested_user_username="new_test_username2", + db=session, + current_user=current_user, ) result_b = can_show_calendar( - requested_user_username='test_username', - db=session, current_user=current_user + requested_user_username="test_username", + db=session, + current_user=current_user, ) assert result_a is False assert result_b is True diff --git a/tests/test_google_connect.py b/tests/test_google_connect.py index 02511266..58ac8aa3 100644 --- a/tests/test_google_connect.py +++ b/tests/test_google_connect.py @@ -5,12 +5,13 @@ import app.internal.google_connect as google_connect from app.routers.event import create_event from app.database.models import OAuthCredentials -from app.routers.user import create_user from google.oauth2.credentials import Credentials from googleapiclient.discovery import build from googleapiclient.http import HttpMock +from app.routers.register import _create_user + @pytest.fixture def google_events_mock(): @@ -24,25 +25,13 @@ def google_events_mock(): "created": "2021-01-13T09:10:02.000Z", "updated": "2021-01-13T09:10:02.388Z", "summary": "some title", - "creator": { - "email": "someemail", - "self": True - }, - "organizer": { - "email": "someemail", - "self": True - }, - "start": { - "dateTime": "2021-02-25T13:00:00+02:00" - }, - "end": { - "dateTime": "2021-02-25T14:00:00+02:00" - }, + "creator": {"email": "someemail", "self": True}, + "organizer": {"email": "someemail", "self": True}, + "start": {"dateTime": "2021-02-25T13:00:00+02:00"}, + "end": {"dateTime": "2021-02-25T14:00:00+02:00"}, "iCalUID": "somecode", "sequence": 0, - "reminders": { - "useDefault": True - } + "reminders": {"useDefault": True}, }, { "kind": "calendar#event", @@ -53,27 +42,15 @@ def google_events_mock(): "created": "2021-01-13T09:10:02.000Z", "updated": "2021-01-13T09:10:02.388Z", "summary": "some title to all day event", - "creator": { - "email": "someemail", - "self": True - }, - "organizer": { - "email": "someemail", - "self": True - }, - "start": { - "date": "2021-02-25" - }, - "end": { - "date": "2021-02-25" - }, + "creator": {"email": "someemail", "self": True}, + "organizer": {"email": "someemail", "self": True}, + "start": {"date": "2021-02-25"}, + "end": {"date": "2021-02-25"}, "iCalUID": "somecode", "sequence": 0, - "location": 'somelocation', - "reminders": { - "useDefault": True - } - } + "location": "somelocation", + "reminders": {"useDefault": True}, + }, ] @@ -85,7 +62,7 @@ def credentials(): token_uri="some_uri", client_id="somecode", client_secret="some_secret", - expiry=datetime(2021, 1, 28) + expiry=datetime(2021, 1, 28), ) return cred @@ -100,30 +77,30 @@ def test_push_events_to_db(google_events_mock, user, session): def test_db_cleanup(google_events_mock, user, session): for event in google_events_mock: location = None - title = event['summary'] + title = event["summary"] # support for all day events - if 'dateTime' in event['start'].keys(): + if "dateTime" in event["start"].keys(): # part time event - start = datetime.fromisoformat(event['start']['dateTime']) - end = datetime.fromisoformat(event['end']['dateTime']) + start = datetime.fromisoformat(event["start"]["dateTime"]) + end = datetime.fromisoformat(event["end"]["dateTime"]) else: # all day event - start = event['start']['date'].split('-') + start = event["start"]["date"].split("-") start = datetime( year=int(start[0]), month=int(start[1]), - day=int(start[2]) + day=int(start[2]), ) - end = event['end']['date'].split('-') + end = event["end"]["date"].split("-") end = datetime( year=int(end[0]), month=int(end[1]), - day=int(end[2]) + day=int(end[2]), ) - if 'location' in event.keys(): - location = event['location'] + if "location" in event.keys(): + location = event["location"] create_event( db=session, @@ -132,20 +109,26 @@ def test_db_cleanup(google_events_mock, user, session): end=end, owner_id=user.id, location=location, - is_google_event=True + is_google_event=True, ) assert google_connect.cleanup_user_google_calendar_events( - user, session) + user, + session, + ) @pytest.mark.usefixtures("session") def test_get_credentials_from_db(session): - user = create_user(session=session, - username='new_test_username', - password='new_test_password', - email='new_test.email@gmail.com', - language_id=1) + user = _create_user( + session=session, + username="new_test_username", + password="new_test_password", + email="new_test.email@gmail.com", + language_id=1, + full_name="test_full_name", + description="test_description", + ) credentials = OAuthCredentials( owner=user, @@ -154,7 +137,7 @@ def test_get_credentials_from_db(session): token_uri="some_uri", client_id="somecode", client_secret="some_secret", - expiry=datetime(2021, 2, 22) + expiry=datetime(2021, 2, 22), ) session.add(credentials) session.commit() @@ -166,17 +149,16 @@ def test_get_credentials_from_db(session): @pytest.mark.usefixtures("session", "user", "credentials") def test_refresh_token(mocker, session, user, credentials): - mocker.patch( - 'google.oauth2.credentials.Credentials.refresh', - return_value=logger.debug('refreshed') + "google.oauth2.credentials.Credentials.refresh", + return_value=logger.debug("refreshed"), ) assert google_connect.refresh_token(credentials, user, session) mocker.patch( - 'google.oauth2.credentials.Credentials.expired', - return_value=False + "google.oauth2.credentials.Credentials.expired", + return_value=False, ) assert google_connect.refresh_token(credentials, user, session) @@ -189,76 +171,75 @@ def __init__(self, service): self.service = service def list(self, *args): - request = self.service.events().list(calendarId='primary', - timeMin=datetime( - 2021, 1, 1).isoformat(), - timeMax=datetime( - 2022, 1, 1).isoformat(), - singleEvents=True, - orderBy='startTime' - ) - http = HttpMock( - 'calendar-linux.json', - {'status': '200'} + request = self.service.events().list( + calendarId="primary", + timeMin=datetime(2021, 1, 1).isoformat(), + timeMax=datetime(2022, 1, 1).isoformat(), + singleEvents=True, + orderBy="startTime", ) + http = HttpMock("calendar-linux.json", {"status": "200"}) response = request.execute(http=http) return response - http = HttpMock( - './tests/calendar-discovery.json', - {'status': '200'} - ) + http = HttpMock("./tests/calendar-discovery.json", {"status": "200"}) - service = build('calendar', 'v3', http=http) + service = build("calendar", "v3", http=http) mocker.patch( - 'googleapiclient.discovery.build', + "googleapiclient.discovery.build", return_value=service, - events=service + events=service, ) mocker.patch( - 'googleapiclient.discovery.Resource', - events=mock_events(service) + "googleapiclient.discovery.Resource", + events=mock_events(service), ) assert google_connect.get_current_year_events(credentials, user, session) -@pytest.mark.usefixtures("user", "session", - "google_connect_test_client", "credentials") +@pytest.mark.usefixtures( + "user", + "session", + "google_connect_test_client", + "credentials", +) def test_google_sync(mocker, google_connect_test_client, session, credentials): - create_user(session=session, - username='new_test_username', - password='new_test_password', - email='new_test.email@gmail.com', - language_id=1) + _create_user( + session=session, + username="new_test_username", + password="new_test_password", + email="new_test.email@gmail.com", + language_id=1, + full_name="test_full_name", + description="test_description", + ) mocker.patch( - 'app.routers.google_connect.get_credentials', - return_value=credentials + "app.routers.google_connect.get_credentials", + return_value=credentials, ) mocker.patch( - 'app.routers.google_connect.fetch_save_events', - return_value=None + "app.routers.google_connect.fetch_save_events", + return_value=None, ) connect = google_connect_test_client.get( - 'google/sync', - headers={ - "referer": 'http://testserver/' - }) + "google/sync", + headers={"referer": "http://testserver/"}, + ) assert connect.ok # second case mocker.patch( - 'app.routers.google_connect.get_credentials', - return_value=None + "app.routers.google_connect.get_credentials", + return_value=None, ) connect = google_connect_test_client.get( - 'google/sync', - headers={ - "referer": 'http://testserver/' - }) + "google/sync", + headers={"referer": "http://testserver/"}, + ) assert connect.ok @@ -270,97 +251,125 @@ def test_is_client_secret_none(): @pytest.mark.usefixtures("session") def test_clean_up_old_credentials_from_db(session): google_connect.clean_up_old_credentials_from_db(session) - assert len(session.query(OAuthCredentials) - .filter_by(user_id=None).all()) == 0 + assert ( + len(session.query(OAuthCredentials).filter_by(user_id=None).all()) == 0 + ) -@pytest.mark.usefixtures("session", 'user', 'credentials') -def test_get_credentials_from_consent_screen(mocker, session, - user, credentials): +@pytest.mark.usefixtures("session", "user", "credentials") +def test_get_credentials_from_consent_screen( + mocker, + session, + user, + credentials, +): mocker.patch( - 'google_auth_oauthlib.flow.InstalledAppFlow.from_client_secrets_file', - return_value=mocker.Mock(name='flow', **{ - "credentials": credentials, - "run_local_server": mocker.Mock(name='run_local_server', - return_value=logger.debug( - 'running server')) - }) + "google_auth_oauthlib.flow.InstalledAppFlow.from_client_secrets_file", + return_value=mocker.Mock( + name="flow", + **{ + "credentials": credentials, + "run_local_server": mocker.Mock( + name="run_local_server", + return_value=logger.debug("running server"), + ), + } + ), ) mocker.patch( - 'app.internal.google_connect.is_client_secret_none', - return_value=False + "app.internal.google_connect.is_client_secret_none", + return_value=False, ) - assert google_connect.get_credentials_from_consent_screen( - user, session) == credentials + assert ( + google_connect.get_credentials_from_consent_screen(user, session) + == credentials + ) @pytest.mark.usefixtures("session") def test_create_google_event(session): - user = create_user(session=session, - username='new_test_username', - password='new_test_password', - email='new_test.email@gmail.com', - language_id=1) + user = _create_user( + session=session, + username="new_test_username", + password="new_test_password", + email="new_test.email@gmail.com", + language_id=1, + full_name="test_full_name", + description="test_description", + ) event = google_connect.create_google_event( - 'title', - datetime(2021, 1, 1, 15, 15), - datetime(2021, 1, 1, 15, 30), - user, - 'location', - session - ) + "title", + datetime(2021, 1, 1, 15, 15), + datetime(2021, 1, 1, 15, 30), + user, + "location", + session, + ) - assert event.title == 'title' + assert event.title == "title" -@pytest.mark.usefixtures("session", "user", 'credentials') +@pytest.mark.usefixtures("session", "user", "credentials") def test_get_credentials(mocker, session, user, credentials): - user = create_user( + user = _create_user( session=session, - username='new_test_username', - password='new_test_password', - email='new_test.email@gmail.com', - language_id=1 + username="new_test_username", + password="new_test_password", + email="new_test.email@gmail.com", + language_id=1, + full_name="test_full_name", + description="test_description", ) mocker.patch( - 'app.internal.google_connect.get_credentials_from_consent_screen', - return_value=credentials + "app.internal.google_connect.get_credentials_from_consent_screen", + return_value=credentials, ) - assert google_connect.get_credentials(user=user, - session=session) == credentials + assert ( + google_connect.get_credentials(user=user, session=session) + == credentials + ) mocker.patch( - 'app.internal.google_connect.get_credentials', - return_value=credentials + "app.internal.google_connect.get_credentials", + return_value=credentials, ) mocker.patch( - 'app.internal.google_connect.refresh_token', - return_value=credentials + "app.internal.google_connect.refresh_token", + return_value=credentials, ) - assert google_connect.get_credentials(user=user, - session=session) == credentials - + assert ( + google_connect.get_credentials(user=user, session=session) + == credentials + ) -@pytest.mark.usefixtures("session", "user", - 'credentials', 'google_events_mock') -def test_fetch_save_events(mocker, session, user, credentials, - google_events_mock): +@pytest.mark.usefixtures( + "session", + "user", + "credentials", + "google_events_mock", +) +def test_fetch_save_events( + mocker, + session, + user, + credentials, + google_events_mock, +): mocker.patch( - 'app.internal.google_connect.get_current_year_events', - return_value=google_events_mock + "app.internal.google_connect.get_current_year_events", + return_value=google_events_mock, ) - assert google_connect.fetch_save_events(credentials, - user, session) is None + assert google_connect.fetch_save_events(credentials, user, session) is None -@pytest.mark.usefixtures("session", "user", 'credentials') +@pytest.mark.usefixtures("session", "user", "credentials") def test_push_credentials_to_db(session, user, credentials): assert google_connect.push_credentials_to_db(credentials, user, session) diff --git a/tests/test_invitation.py b/tests/test_invitation.py deleted file mode 100644 index c609a973..00000000 --- a/tests/test_invitation.py +++ /dev/null @@ -1,50 +0,0 @@ -from fastapi import status - -from app.routers.invitation import get_all_invitations, get_invitation_by_id - - -class TestInvitations: - NO_INVITATIONS = b"You don't have any invitations." - URL = "/invitations/" - - @staticmethod - def test_view_no_invitations(invitation_test_client): - response = invitation_test_client.get(TestInvitations.URL) - assert response.ok - assert TestInvitations.NO_INVITATIONS in response.content - - @staticmethod - def test_accept_invitations(user, invitation, invitation_test_client): - invitation = {"invite_id ": invitation.id} - resp = invitation_test_client.post( - TestInvitations.URL, data=invitation) - assert resp.status_code == status.HTTP_302_FOUND - - @staticmethod - def test_get_all_invitations_success(invitation, event, user, session): - invitations = get_all_invitations(event=event, db=session) - assert invitations == [invitation] - invitations = get_all_invitations(recipient=user, db=session) - assert invitations == [invitation] - - @staticmethod - def test_get_all_invitations_failure(user, session): - invitations = get_all_invitations(unknown_parameter=user, db=session) - assert invitations == [] - - invitations = get_all_invitations(recipient=None, db=session) - assert invitations == [] - - @staticmethod - def test_get_invitation_by_id(invitation, session): - get_invitation = get_invitation_by_id(invitation.id, db=session) - assert get_invitation == invitation - - @staticmethod - def test_repr(invitation): - invitation_repr = ( - f'' - ) - assert invitation.__repr__() == invitation_repr diff --git a/tests/test_notification.py b/tests/test_notification.py new file mode 100644 index 00000000..23eacfd2 --- /dev/null +++ b/tests/test_notification.py @@ -0,0 +1,177 @@ +from starlette.status import HTTP_406_NOT_ACCEPTABLE + +from app.database.models import InvitationStatusEnum, MessageStatusEnum +from app.internal.notification import get_all_invitations, get_invitation_by_id +from app.routers.notification import router +from tests.fixtures.client_fixture import login_client + + +class TestNotificationRoutes: + NO_NOTIFICATIONS = b"You don't have any new notifications." + NO_NOTIFICATION_IN_ARCHIVE = b"You don't have any archived notifications." + NEW_NOTIFICATIONS_URL = router.url_path_for("view_notifications") + LOGIN_DATA = {"username": "test_username", "password": "test_password"} + + def test_view_no_notifications( + self, + user, + notification_test_client, + ): + login_client(notification_test_client, self.LOGIN_DATA) + resp = notification_test_client.get(self.NEW_NOTIFICATIONS_URL) + assert resp.ok + assert self.NO_NOTIFICATIONS in resp.content + + def test_accept_invitations( + self, + user, + invitation, + notification_test_client, + ): + login_client(notification_test_client, self.LOGIN_DATA) + assert invitation.status == InvitationStatusEnum.UNREAD + data = { + "invite_id": invitation.id, + "next_url": self.NEW_NOTIFICATIONS_URL, + } + url = router.url_path_for("accept_invitations") + resp = notification_test_client.post(url, data=data) + assert resp.ok + assert InvitationStatusEnum.ACCEPTED + + def test_decline_invitations( + self, + user, + invitation, + notification_test_client, + session, + ): + login_client(notification_test_client, self.LOGIN_DATA) + assert invitation.status == InvitationStatusEnum.UNREAD + data = { + "invite_id": invitation.id, + "next_url": self.NEW_NOTIFICATIONS_URL, + } + url = router.url_path_for("decline_invitations") + resp = notification_test_client.post(url, data=data) + assert resp.ok + session.refresh(invitation) + assert invitation.status == InvitationStatusEnum.DECLINED + + def test_mark_message_as_read( + self, + user, + message, + notification_test_client, + session, + ): + login_client(notification_test_client, self.LOGIN_DATA) + assert message.status == MessageStatusEnum.UNREAD + data = { + "message_id": message.id, + "next_url": self.NEW_NOTIFICATIONS_URL, + } + url = router.url_path_for("mark_message_as_read") + resp = notification_test_client.post(url, data=data) + assert resp.ok + session.refresh(message) + assert message.status == MessageStatusEnum.READ + + def test_mark_all_as_read( + self, + user, + message, + sec_message, + notification_test_client, + session, + ): + login_client(notification_test_client, self.LOGIN_DATA) + url = router.url_path_for("mark_all_as_read") + assert message.status == MessageStatusEnum.UNREAD + assert sec_message.status == MessageStatusEnum.UNREAD + data = {"next_url": self.NEW_NOTIFICATIONS_URL} + resp = notification_test_client.post(url, data=data) + assert resp.ok + session.refresh(message) + session.refresh(sec_message) + assert message.status == MessageStatusEnum.READ + assert sec_message.status == MessageStatusEnum.READ + + def test_archive( + self, + user, + message, + notification_test_client, + session, + ): + login_client(notification_test_client, self.LOGIN_DATA) + archive_url = router.url_path_for("view_archive") + resp = notification_test_client.get(archive_url) + assert resp.ok + assert self.NO_NOTIFICATION_IN_ARCHIVE in resp.content + + # read message + data = { + "message_id": message.id, + "next_url": self.NEW_NOTIFICATIONS_URL, + } + url = router.url_path_for("mark_message_as_read") + notification_test_client.post(url, data=data) + + resp = notification_test_client.get(archive_url) + assert resp.ok + assert self.NO_NOTIFICATION_IN_ARCHIVE not in resp.content + + def test_wrong_id( + self, + user, + notification_test_client, + session, + ): + login_client(notification_test_client, self.LOGIN_DATA) + data = { + "message_id": 1, + "next_url": "/", + } + url = router.url_path_for("mark_message_as_read") + resp = notification_test_client.post(url, data=data) + assert resp.status_code == HTTP_406_NOT_ACCEPTABLE + + +class TestNotification: + def test_get_all_invitations_success( + self, + invitation, + event, + user, + session, + ): + invitations = get_all_invitations(event=event, session=session) + assert invitations == [invitation] + invitations = get_all_invitations(recipient=user, session=session) + assert invitations == [invitation] + + def test_get_all_invitations_failure(self, user, session): + invitations = get_all_invitations( + unknown_parameter=user, + session=session, + ) + assert invitations == [] + + invitations = get_all_invitations(recipient=None, session=session) + assert invitations == [] + + def test_get_invitation_by_id(self, invitation, session): + get_invitation = get_invitation_by_id(invitation.id, session=session) + assert get_invitation == invitation + + def test_invitation_repr(self, invitation): + invitation_repr = ( + f"" + ) + assert invitation.__repr__() == invitation_repr + + def test_message_repr(self, message): + message_repr = f"" + assert message.__repr__() == message_repr diff --git a/tests/test_share_event.py b/tests/test_share_event.py index 67679b2c..e202597a 100644 --- a/tests/test_share_event.py +++ b/tests/test_share_event.py @@ -1,49 +1,68 @@ -from app.routers.invitation import get_all_invitations -from app.routers.share import (accept, send_email_invitation, - send_in_app_invitation, share, sort_emails) +from app.database.models import InvitationStatusEnum +from app.internal.notification import get_all_invitations +from app.routers.share import ( + send_email_invitation, + send_in_app_invitation, + share, + sort_emails, +) class TestShareEvent: - def test_share_success(self, user, event, session): - participants = [user.email] - share(event, participants, session) - invitations = get_all_invitations(db=session, recipient_id=user.id) + share(event, [user.email], session) + invitations = get_all_invitations( + session=session, + recipient_id=user.id, + ) assert invitations != [] def test_share_failure(self, event, session): participants = [event.owner.email] share(event, participants, session) invitations = get_all_invitations( - db=session, recipient_id=event.owner.id) + session=session, + recipient_id=event.owner.id, + ) assert invitations == [] def test_sort_emails(self, user, session): # the user is being imported # so he will be created data = [ - 'test.email@gmail.com', # registered user - 'not_logged_in@gmail.com', # unregistered user + "test.email@gmail.com", # registered user + "not_logged_in@gmail.com", # unregistered user ] sorted_data = sort_emails(data, session=session) assert sorted_data == { - 'registered': ['test.email@gmail.com'], - 'unregistered': ['not_logged_in@gmail.com'] + "registered": ["test.email@gmail.com"], + "unregistered": ["not_logged_in@gmail.com"], } def test_send_in_app_invitation_success( - self, user, sender, event, session + self, + user, + sender, + event, + session, ): assert send_in_app_invitation([user.email], event, session=session) - invitation = get_all_invitations(db=session, recipient=user)[0] + invitation = get_all_invitations(session=session, recipient=user)[0] assert invitation.event.owner == sender assert invitation.recipient == user session.delete(invitation) def test_send_in_app_invitation_failure( - self, user, sender, event, session): - assert (send_in_app_invitation( - [sender.email], event, session=session) is False) + self, + user, + sender, + event, + session, + ): + assert ( + send_in_app_invitation([sender.email], event, session=session) + is False + ) def test_send_email_invitation(self, user, event): send_email_invitation([user.email], event) @@ -51,5 +70,9 @@ def test_send_email_invitation(self, user, event): assert True def test_accept(self, invitation, session): - accept(invitation, session=session) - assert invitation.status == 'accepted' + invitation.accept(session=session) + assert invitation.status == InvitationStatusEnum.ACCEPTED + + def test_decline(self, invitation, session): + invitation.decline(session=session) + assert invitation.status == InvitationStatusEnum.DECLINED diff --git a/tests/test_statistics.py b/tests/test_statistics.py index 7ef52afb..10cc8457 100644 --- a/tests/test_statistics.py +++ b/tests/test_statistics.py @@ -1,40 +1,63 @@ import datetime +from app.internal.notification import get_all_invitations from app.internal.statistics import get_statistics from app.internal.statistics import INVALID_DATE_RANGE, INVALID_USER from app.internal.statistics import SUCCESS_STATUS from app.routers.event import create_event -from app.routers.user import create_user -from app.routers.share import send_in_app_invitation, accept -from app.routers.invitation import get_all_invitations +from app.routers.register import _create_user +from app.routers.share import send_in_app_invitation def create_events_and_user_events(session, start, end, owner, invitations): for _ in range(1, 3): event = create_event( - db=session, title="title" + str(_), start=start, end=end, - owner_id=owner, location="location" + str(_)) + db=session, + title="title" + str(_), + start=start, + end=end, + owner_id=owner, + location="location" + str(_), + ) send_in_app_invitation(invitations, event, session) def create_data(session): - _ = [create_user("user" + str(_), "password" + str(_), - "email" + str(_) + '@' + 'gmail.com', "Hebrew", - session) for _ in range(1, 4)] + _ = [ + _create_user( + username="user" + str(_), + password="password" + str(_), + email="email" + str(_) + "@" + "gmail.com", + language_id="Hebrew", + session=session, + description="", + full_name="", + ) + for _ in range(1, 4) + ] start = datetime.datetime.now() + datetime.timedelta(hours=-1) end = datetime.datetime.now() + datetime.timedelta(hours=1) - create_events_and_user_events(session, start, end, 1, - ["email2@gmail.com", "email3@gmail.com"]) + create_events_and_user_events( + session, + start, + end, + 1, + ["email2@gmail.com", "email3@gmail.com"], + ) start = datetime.datetime.now() + datetime.timedelta(days=-1) end = datetime.datetime.now() + datetime.timedelta(days=-1, hours=2) - create_events_and_user_events(session, start, end, 1, - ["email2@gmail.com", "email3@gmail.com"]) + create_events_and_user_events( + session, + start, + end, + 1, + ["email2@gmail.com", "email3@gmail.com"], + ) start = datetime.datetime.now() + datetime.timedelta(hours=1) end = datetime.datetime.now() + datetime.timedelta(hours=1.5) - create_events_and_user_events(session, start, end, 2, - ["email3@gmail.com"]) + create_events_and_user_events(session, start, end, 2, ["email3@gmail.com"]) for invitation in get_all_invitations(session): - accept(invitation, session) + invitation.accept(session) def test_statistics_invalid_date_range(session): diff --git a/tests/test_user.py b/tests/test_user.py index 213e7589..1a1ed0a7 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -1,9 +1,8 @@ from datetime import datetime import pytest -from app.routers.user import ( - create_user, does_user_exist, get_users -) +from app.routers.register import _create_user +from app.routers.user import does_user_exist, get_users from app.internal.user.availability import disable, enable from app.internal.utils import save from app.database.models import UserEvent, Event @@ -13,12 +12,14 @@ @pytest.fixture def user1(session): # a user made for testing who doesn't own any event. - user = create_user( + user = _create_user( session=session, - username='new_test_username', - password='new_test_password', - email='new2_test.email@gmail.com', - language_id='english' + username="new_test_username", + full_name="test_user", + password="new_test_password", + email="new2_test.email@gmail.com", + language_id="english", + description="", ) return user @@ -27,21 +28,23 @@ def user1(session): @pytest.fixture def user2(session): # a user made for testing who already owns an event. - user = create_user( + user = _create_user( session=session, - username='new_test_username2', - password='new_test_password2', - email='new_test_love231.email@gmail.com', - language_id='english' + username="new_test_username2", + full_name="test_user", + password="new_test_password2", + email="new_test_love231.email@gmail.com", + language_id="english", + description="", ) data = { - 'title': 'user2 event', - 'start': datetime.strptime('2021-05-05 14:59', '%Y-%m-%d %H:%M'), - 'end': datetime.strptime('2021-05-05 15:01', '%Y-%m-%d %H:%M'), - 'location': 'https://us02web.zoom.us/j/875384596', - 'content': 'content', - 'owner_id': user.id, + "title": "user2 event", + "start": datetime.strptime("2021-05-05 14:59", "%Y-%m-%d %H:%M"), + "end": datetime.strptime("2021-05-05 15:01", "%Y-%m-%d %H:%M"), + "location": "https://us02web.zoom.us/j/875384596", + "content": "content", + "owner_id": user.id, } create_event(session, **data) @@ -52,12 +55,12 @@ def user2(session): @pytest.fixture def event1(session, user2): data = { - 'title': 'test event title', - 'start': datetime.strptime('2021-05-05 14:59', '%Y-%m-%d %H:%M'), - 'end': datetime.strptime('2021-05-05 15:01', '%Y-%m-%d %H:%M'), - 'location': 'https://us02web.zoom.us/j/87538459r6', - 'content': 'content', - 'owner_id': user2.id, + "title": "test event title", + "start": datetime.strptime("2021-05-05 14:59", "%Y-%m-%d %H:%M"), + "end": datetime.strptime("2021-05-05 15:01", "%Y-%m-%d %H:%M"), + "location": "https://us02web.zoom.us/j/87538459r6", + "content": "content", + "owner_id": user2.id, } event = create_event(session, **data) @@ -68,41 +71,41 @@ def test_disabling_no_event_user(session, user1): # users without any future event can disable themselves disable(session, user1.id) assert user1.disabled - future_events = list(session.query(Event.id) - .join(UserEvent) - .filter( - UserEvent.user_id == user1.id, - Event.start > datetime - .now())) + future_events = list( + session.query(Event.id) + .join(UserEvent) + .filter(UserEvent.user_id == user1.id, Event.start > datetime.now()), + ) assert not future_events # making sure that after disabling the user he can be easily enabled. enable(session, user1.id) assert not user1.disabled -def test_disabling_user_participating_event( - session, user1, event1): +def test_disabling_user_participating_event(session, user1, event1): """making sure only users who only participate in events can disable and enable themselves.""" - association = UserEvent( - user_id=user1.id, - event_id=event1.id - ) + association = UserEvent(user_id=user1.id, event_id=event1.id) save(session, association) disable(session, user1.id) assert user1.disabled - future_events = list(session.query(Event.id) - .join(UserEvent) - .filter( - UserEvent.user_id == user1.id, - Event.start > datetime.now(), - Event.owner_id == user1.id)) + future_events = list( + session.query(Event.id) + .join(UserEvent) + .filter( + UserEvent.user_id == user1.id, + Event.start > datetime.now(), + Event.owner_id == user1.id, + ), + ) assert not future_events enable(session, user1.id) assert not user1.disabled - deleted_user_event_connection = session.query(UserEvent).filter( - UserEvent.user_id == user1.id, - UserEvent.event_id == event1.id).first() + deleted_user_event_connection = ( + session.query(UserEvent) + .filter(UserEvent.user_id == user1.id, UserEvent.event_id == event1.id) + .first() + ) session.delete(deleted_user_event_connection) @@ -113,18 +116,19 @@ def test_disabling_event_owning_user(session, user2): class TestUser: - def test_create_user(self, session): - user = create_user( + user = _create_user( session=session, - username='new_test_username', - password='new_test_password', - email='new_test.email@gmail.com', - language_id=1 + username="new_test_username", + password="new_test_password", + email="new_test.email@gmail.com", + language_id=1, + description="", + full_name="test_user", ) - assert user.username == 'new_test_username' - assert user.password == 'new_test_password' - assert user.email == 'new_test.email@gmail.com' + assert user.username == "new_test_username" + assert user.password == "new_test_password" + assert user.email == "new_test.email@gmail.com" assert user.language_id == 1 session.delete(user) session.commit() @@ -135,7 +139,7 @@ def test_get_users_success(self, user, session): assert get_users(email=user.email, session=session) == [user] def test_get_users_failure(self, session, user): - assert get_users(username='wrong username', session=session) == [] + assert get_users(username="wrong username", session=session) == [] assert get_users(wrong_param=user.username, session=session) == [] def test_does_user_exist_success(self, user, session): @@ -144,8 +148,8 @@ def test_does_user_exist_success(self, user, session): assert does_user_exist(email=user.email, session=session) def test_does_user_exist_failure(self, session): - assert not does_user_exist(username='wrong username', session=session) + assert not does_user_exist(username="wrong username", session=session) assert not does_user_exist(session=session) def test_repr(self, user): - assert user.__repr__() == f'' + assert user.__repr__() == f"" diff --git a/tests/utils.py b/tests/utils.py deleted file mode 100644 index 58ffdbd0..00000000 --- a/tests/utils.py +++ /dev/null @@ -1,13 +0,0 @@ -from sqlalchemy.orm import Session - - -def create_model(session: Session, model_class, **kw): - instance = model_class(**kw) - session.add(instance) - session.commit() - return instance - - -def delete_instance(session: Session, instance): - session.delete(instance) - session.commit() From 70ceaf01417917d29f1db06746e9614d0622c177 Mon Sep 17 00:00:00 2001 From: Yam Mesicka Date: Wed, 24 Feb 2021 19:33:41 +0200 Subject: [PATCH 24/46] fix: Make isort coop nicely with black (#338) --- .pre-commit-config.yaml | 42 ++- app/database/alembic/env.py | 10 +- .../alembic/versions/91b42971b0df_.py | 252 ++++++++------- app/database/models.py | 15 +- app/database/schemas.py | 2 +- app/internal/astronomy.py | 24 +- app/internal/audio.py | 7 +- app/internal/calendar_privacy.py | 15 +- app/internal/emotion.py | 74 +++-- app/internal/event.py | 1 - app/internal/export.py | 47 +-- app/internal/google_connect.py | 152 +++++---- app/internal/import_file.py | 156 +++++---- app/internal/import_holidays.py | 34 +- app/internal/international_days.py | 30 +- app/internal/logger_customizer.py | 45 +-- app/internal/meds.py | 169 ++++++---- app/internal/notification.py | 5 +- app/internal/on_this_day_events.py | 39 ++- app/internal/security/dependencies.py | 34 +- app/internal/security/ouath2.py | 17 +- app/internal/translation.py | 22 +- app/internal/user/availability.py | 8 +- app/main.py | 4 +- app/routers/about_us.py | 10 +- app/routers/agenda.py | 56 ++-- app/routers/audio.py | 12 +- app/routers/calendar_grid.py | 128 +++----- app/routers/categories.py | 89 +++--- app/routers/credits.py | 15 +- app/routers/event.py | 5 +- app/routers/event_images.py | 266 ++++++++-------- app/routers/four_o_four.py | 6 +- app/routers/friendview.py | 23 +- app/routers/google_connect.py | 20 +- app/routers/joke.py | 4 +- app/routers/login.py | 2 +- app/routers/logout.py | 3 +- app/routers/meds.py | 58 ++-- app/routers/notification.py | 1 - app/routers/profile.py | 110 ++++--- app/routers/register.py | 5 +- app/routers/weekview.py | 53 ++-- app/routers/weight.py | 54 ++-- app/routers/whatsapp.py | 6 +- app/telegram/bot.py | 1 + app/telegram/handlers.py | 165 +++++----- tests/conftest.py | 4 +- tests/fixtures/asyncio_fixture.py | 4 +- tests/fixtures/client_fixture.py | 3 +- tests/fixtures/logger_fixture.py | 18 +- tests/meds/test_routers.py | 23 +- tests/salary/conftest.py | 46 +-- tests/salary/test_routes.py | 298 ++++++++++-------- tests/security_testing_routes.py | 36 +-- tests/test_a_telegram_asyncio.py | 2 +- tests/test_astronomy.py | 22 +- tests/test_categories.py | 177 +++++++---- tests/test_dayview.py | 3 +- tests/test_email.py | 254 +++++++++------ tests/test_emotion.py | 4 +- tests/test_event.py | 9 +- tests/test_geolocation.py | 4 +- tests/test_google_connect.py | 11 +- tests/test_holidays.py | 16 +- tests/test_login.py | 259 +++++++++------ tests/test_profile.py | 101 +++--- tests/test_statistics.py | 9 +- tests/test_translation.py | 102 ++++-- tests/test_user.py | 7 +- tests/test_utils.py | 33 +- tests/test_weekview.py | 33 +- 72 files changed, 2149 insertions(+), 1625 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 459e779a..a29abf81 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,19 +1,4 @@ repos: - # Flake8 to check style is OK - - repo: https://gitlab.com/pycqa/flake8 - rev: 3.8.4 - hooks: - - id: flake8 - # yapf to fix many style mistakes - - repo: https://github.com/ambv/black - rev: 20.8b1 - hooks: - - id: black - entry: black - language: python - language_version: python3 - require_serial: true - types_or: [python, pyi] # More built in style checks and fixes - repo: https://github.com/pre-commit/pre-commit-hooks rev: v3.4.0 @@ -28,6 +13,28 @@ repos: - id: check-merge-conflict - id: end-of-file-fixer - id: sort-simple-yaml + - repo: https://github.com/pycqa/isort + rev: 5.7.0 + hooks: + - id: isort + name: isort (python) + args: ["--profile", "black", "--line-length", "79"] + - id: isort + name: isort (cython) + types: [cython] + - id: isort + name: isort (pyi) + types: [pyi] + # Black: to fix many style mistakes + - repo: https://github.com/ambv/black + rev: 20.8b1 + hooks: + - id: black + entry: black + language: python + language_version: python3 + require_serial: true + types_or: [python, pyi] - repo: meta hooks: - id: check-useless-excludes @@ -35,3 +42,8 @@ repos: rev: v2.1.0 hooks: - id: add-trailing-comma + # Flake8 to check style is OK + - repo: https://gitlab.com/pycqa/flake8 + rev: 3.8.4 + hooks: + - id: flake8 diff --git a/app/database/alembic/env.py b/app/database/alembic/env.py index bca6fab9..d1f1431a 100644 --- a/app/database/alembic/env.py +++ b/app/database/alembic/env.py @@ -1,5 +1,5 @@ -from logging.config import fileConfig import os +from logging.config import fileConfig from alembic import context from sqlalchemy import create_engine @@ -7,9 +7,10 @@ from app import config as app_config from app.database.models import Base - SQLALCHEMY_DATABASE_URL = os.getenv( - "DATABASE_CONNECTION_STRING", app_config.DEVELOPMENT_DATABASE_STRING) + "DATABASE_CONNECTION_STRING", + app_config.DEVELOPMENT_DATABASE_STRING, +) # this is the Alembic Config object, which provides # access to the values within the .ini file in use. @@ -66,7 +67,8 @@ def run_migrations_online(): with connectable.connect() as connection: context.configure( - connection=connection, target_metadata=target_metadata, + connection=connection, + target_metadata=target_metadata, ) with context.begin_transaction(): diff --git a/app/database/alembic/versions/91b42971b0df_.py b/app/database/alembic/versions/91b42971b0df_.py index 18a5d836..c380e48d 100644 --- a/app/database/alembic/versions/91b42971b0df_.py +++ b/app/database/alembic/versions/91b42971b0df_.py @@ -5,12 +5,12 @@ Create Date: 2021-02-06 16:15:07.861957 """ -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy.dialects import sqlite # revision identifiers, used by Alembic. -revision = '91b42971b0df' +revision = "91b42971b0df" down_revision = None branch_labels = None depends_on = None @@ -18,118 +18,148 @@ def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_index('ix_categories_id', table_name='categories') - op.drop_table('categories') - op.drop_index('ix_invitations_id', table_name='invitations') - op.drop_table('invitations') - op.drop_index('ix_users_id', table_name='users') - op.drop_table('users') - op.drop_index('ix_quotes_id', table_name='quotes') - op.drop_table('quotes') - op.drop_index('ix_wikipedia_events_id', table_name='wikipedia_events') - op.drop_table('wikipedia_events') - op.drop_index('ix_zodiac-signs_id', table_name='zodiac-signs') - op.drop_table('zodiac-signs') - op.drop_index('ix_events_id', table_name='events') - op.drop_table('events') - op.drop_index('ix_user_event_id', table_name='user_event') - op.drop_table('user_event') + op.drop_index("ix_categories_id", table_name="categories") + op.drop_table("categories") + op.drop_index("ix_invitations_id", table_name="invitations") + op.drop_table("invitations") + op.drop_index("ix_users_id", table_name="users") + op.drop_table("users") + op.drop_index("ix_quotes_id", table_name="quotes") + op.drop_table("quotes") + op.drop_index("ix_wikipedia_events_id", table_name="wikipedia_events") + op.drop_table("wikipedia_events") + op.drop_index("ix_zodiac-signs_id", table_name="zodiac-signs") + op.drop_table("zodiac-signs") + op.drop_index("ix_events_id", table_name="events") + op.drop_table("events") + op.drop_index("ix_user_event_id", table_name="user_event") + op.drop_table("user_event") # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('user_event', - sa.Column('id', sa.INTEGER(), nullable=False), - sa.Column('user_id', sa.INTEGER(), nullable=True), - sa.Column('event_id', sa.INTEGER(), nullable=True), - sa.ForeignKeyConstraint(['event_id'], ['events.id'], ), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), - sa.PrimaryKeyConstraint('id') - ) - op.create_index('ix_user_event_id', 'user_event', ['id'], unique=False) - op.create_table('events', - sa.Column('id', sa.INTEGER(), nullable=False), - sa.Column('title', sa.VARCHAR(), nullable=False), - sa.Column('start', sa.DATETIME(), nullable=False), - sa.Column('end', sa.DATETIME(), nullable=False), - sa.Column('content', sa.VARCHAR(), nullable=True), - sa.Column('location', sa.VARCHAR(), nullable=True), - sa.Column('color', sa.VARCHAR(), nullable=True), - sa.Column('owner_id', sa.INTEGER(), nullable=True), - sa.Column('category_id', sa.INTEGER(), nullable=True), - sa.ForeignKeyConstraint( - ['category_id'], ['categories.id'], ), - sa.ForeignKeyConstraint(['owner_id'], ['users.id'], ), - sa.PrimaryKeyConstraint('id') - ) - op.create_index('ix_events_id', 'events', ['id'], unique=False) - op.create_table('zodiac-signs', - sa.Column('id', sa.INTEGER(), nullable=False), - sa.Column('name', sa.VARCHAR(), nullable=False), - sa.Column('start_month', sa.INTEGER(), nullable=False), - sa.Column('start_day_in_month', - sa.INTEGER(), nullable=False), - sa.Column('end_month', sa.INTEGER(), nullable=False), - sa.Column('end_day_in_month', - sa.INTEGER(), nullable=False), - sa.PrimaryKeyConstraint('id') - ) - op.create_index('ix_zodiac-signs_id', 'zodiac-signs', ['id'], unique=False) - op.create_table('wikipedia_events', - sa.Column('id', sa.INTEGER(), nullable=False), - sa.Column('date_', sa.VARCHAR(), nullable=False), - sa.Column('wikipedia', sa.VARCHAR(), nullable=False), - sa.Column('events', sqlite.JSON(), nullable=True), - sa.Column('date_inserted', sa.DATETIME(), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - op.create_index('ix_wikipedia_events_id', - 'wikipedia_events', ['id'], unique=False) - op.create_table('quotes', - sa.Column('id', sa.INTEGER(), nullable=False), - sa.Column('text', sa.VARCHAR(), nullable=False), - sa.Column('author', sa.VARCHAR(), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - op.create_index('ix_quotes_id', 'quotes', ['id'], unique=False) - op.create_table('users', - sa.Column('id', sa.INTEGER(), nullable=False), - sa.Column('username', sa.VARCHAR(), nullable=False), - sa.Column('email', sa.VARCHAR(), nullable=False), - sa.Column('password', sa.VARCHAR(), nullable=False), - sa.Column('full_name', sa.VARCHAR(), nullable=True), - sa.Column('language', sa.VARCHAR(), nullable=True), - sa.Column('description', sa.VARCHAR(), nullable=True), - sa.Column('avatar', sa.VARCHAR(), nullable=True), - sa.Column('telegram_id', sa.VARCHAR(), nullable=True), - sa.Column('is_active', sa.BOOLEAN(), nullable=True), - sa.CheckConstraint('is_active IN (0, 1)'), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('email'), - sa.UniqueConstraint('telegram_id'), - sa.UniqueConstraint('username') - ) - op.create_index('ix_users_id', 'users', ['id'], unique=False) - op.create_table('invitations', - sa.Column('id', sa.INTEGER(), nullable=False), - sa.Column('status', sa.VARCHAR(), nullable=False), - sa.Column('recipient_id', sa.INTEGER(), nullable=True), - sa.Column('event_id', sa.INTEGER(), nullable=True), - sa.Column('creation', sa.DATETIME(), nullable=True), - sa.ForeignKeyConstraint(['event_id'], ['events.id'], ), - sa.ForeignKeyConstraint(['recipient_id'], ['users.id'], ), - sa.PrimaryKeyConstraint('id') - ) - op.create_index('ix_invitations_id', 'invitations', ['id'], unique=False) - op.create_table('categories', - sa.Column('id', sa.INTEGER(), nullable=False), - sa.Column('name', sa.VARCHAR(), nullable=False), - sa.Column('color', sa.VARCHAR(), nullable=False), - sa.Column('user_id', sa.INTEGER(), nullable=False), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('user_id', 'name', 'color') - ) - op.create_index('ix_categories_id', 'categories', ['id'], unique=False) + op.create_table( + "user_event", + sa.Column("id", sa.INTEGER(), nullable=False), + sa.Column("user_id", sa.INTEGER(), nullable=True), + sa.Column("event_id", sa.INTEGER(), nullable=True), + sa.ForeignKeyConstraint( + ["event_id"], + ["events.id"], + ), + sa.ForeignKeyConstraint( + ["user_id"], + ["users.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index("ix_user_event_id", "user_event", ["id"], unique=False) + op.create_table( + "events", + sa.Column("id", sa.INTEGER(), nullable=False), + sa.Column("title", sa.VARCHAR(), nullable=False), + sa.Column("start", sa.DATETIME(), nullable=False), + sa.Column("end", sa.DATETIME(), nullable=False), + sa.Column("content", sa.VARCHAR(), nullable=True), + sa.Column("location", sa.VARCHAR(), nullable=True), + sa.Column("color", sa.VARCHAR(), nullable=True), + sa.Column("owner_id", sa.INTEGER(), nullable=True), + sa.Column("category_id", sa.INTEGER(), nullable=True), + sa.ForeignKeyConstraint( + ["category_id"], + ["categories.id"], + ), + sa.ForeignKeyConstraint( + ["owner_id"], + ["users.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index("ix_events_id", "events", ["id"], unique=False) + op.create_table( + "zodiac-signs", + sa.Column("id", sa.INTEGER(), nullable=False), + sa.Column("name", sa.VARCHAR(), nullable=False), + sa.Column("start_month", sa.INTEGER(), nullable=False), + sa.Column("start_day_in_month", sa.INTEGER(), nullable=False), + sa.Column("end_month", sa.INTEGER(), nullable=False), + sa.Column("end_day_in_month", sa.INTEGER(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index("ix_zodiac-signs_id", "zodiac-signs", ["id"], unique=False) + op.create_table( + "wikipedia_events", + sa.Column("id", sa.INTEGER(), nullable=False), + sa.Column("date_", sa.VARCHAR(), nullable=False), + sa.Column("wikipedia", sa.VARCHAR(), nullable=False), + sa.Column("events", sqlite.JSON(), nullable=True), + sa.Column("date_inserted", sa.DATETIME(), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + "ix_wikipedia_events_id", + "wikipedia_events", + ["id"], + unique=False, + ) + op.create_table( + "quotes", + sa.Column("id", sa.INTEGER(), nullable=False), + sa.Column("text", sa.VARCHAR(), nullable=False), + sa.Column("author", sa.VARCHAR(), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index("ix_quotes_id", "quotes", ["id"], unique=False) + op.create_table( + "users", + sa.Column("id", sa.INTEGER(), nullable=False), + sa.Column("username", sa.VARCHAR(), nullable=False), + sa.Column("email", sa.VARCHAR(), nullable=False), + sa.Column("password", sa.VARCHAR(), nullable=False), + sa.Column("full_name", sa.VARCHAR(), nullable=True), + sa.Column("language", sa.VARCHAR(), nullable=True), + sa.Column("description", sa.VARCHAR(), nullable=True), + sa.Column("avatar", sa.VARCHAR(), nullable=True), + sa.Column("telegram_id", sa.VARCHAR(), nullable=True), + sa.Column("is_active", sa.BOOLEAN(), nullable=True), + sa.CheckConstraint("is_active IN (0, 1)"), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("email"), + sa.UniqueConstraint("telegram_id"), + sa.UniqueConstraint("username"), + ) + op.create_index("ix_users_id", "users", ["id"], unique=False) + op.create_table( + "invitations", + sa.Column("id", sa.INTEGER(), nullable=False), + sa.Column("status", sa.VARCHAR(), nullable=False), + sa.Column("recipient_id", sa.INTEGER(), nullable=True), + sa.Column("event_id", sa.INTEGER(), nullable=True), + sa.Column("creation", sa.DATETIME(), nullable=True), + sa.ForeignKeyConstraint( + ["event_id"], + ["events.id"], + ), + sa.ForeignKeyConstraint( + ["recipient_id"], + ["users.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index("ix_invitations_id", "invitations", ["id"], unique=False) + op.create_table( + "categories", + sa.Column("id", sa.INTEGER(), nullable=False), + sa.Column("name", sa.VARCHAR(), nullable=False), + sa.Column("color", sa.VARCHAR(), nullable=False), + sa.Column("user_id", sa.INTEGER(), nullable=False), + sa.ForeignKeyConstraint( + ["user_id"], + ["users.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("user_id", "name", "color"), + ) + op.create_index("ix_categories_id", "categories", ["id"], unique=False) # ### end Alembic commands ### diff --git a/app/database/models.py b/app/database/models.py index bdaa090d..af19c004 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -1,34 +1,35 @@ from __future__ import annotations -from datetime import datetime + import enum +from datetime import datetime from typing import Any, Dict from sqlalchemy import ( + DDL, + JSON, Boolean, Column, DateTime, - DDL, Enum, - event, Float, ForeignKey, Index, Integer, - JSON, String, Time, UniqueConstraint, + event, ) from sqlalchemy.dialects.postgresql import TSVECTOR from sqlalchemy.exc import IntegrityError, SQLAlchemyError -from sqlalchemy.ext.declarative.api import declarative_base, DeclarativeMeta -from sqlalchemy.orm import relationship, Session +from sqlalchemy.ext.declarative.api import DeclarativeMeta, declarative_base +from sqlalchemy.orm import Session, relationship from sqlalchemy.sql.schema import CheckConstraint +import app.routers.salary.config as SalaryConfig from app.config import PSQL_ENVIRONMENT from app.dependencies import logger from app.internal.privacy import PrivacyKinds -import app.routers.salary.config as SalaryConfig Base: DeclarativeMeta = declarative_base() diff --git a/app/database/schemas.py b/app/database/schemas.py index 29748b6f..9bad241c 100644 --- a/app/database/schemas.py +++ b/app/database/schemas.py @@ -1,6 +1,6 @@ from typing import Optional, Union -from pydantic import BaseModel, validator, EmailStr, EmailError +from pydantic import BaseModel, EmailError, EmailStr, validator EMPTY_FIELD_STRING = "field is required" MIN_FIELD_LENGTH = 3 diff --git a/app/internal/astronomy.py b/app/internal/astronomy.py index 4c150c52..435ce935 100644 --- a/app/internal/astronomy.py +++ b/app/internal/astronomy.py @@ -1,5 +1,5 @@ -from datetime import datetime import functools +from datetime import datetime from typing import Any, Dict import httpx @@ -12,7 +12,8 @@ async def get_astronomical_data( - date: datetime, location: str + date: datetime, + location: str, ) -> Dict[str, Any]: """Returns astronomical data (sun and moon) for date and location. @@ -30,13 +31,14 @@ async def get_astronomical_data( sunrise, sunset, moonrise, moonset, moon_phase, and moon_illumination. """ - formatted_date = date.strftime('%Y-%m-%d') + formatted_date = date.strftime("%Y-%m-%d") return await _get_astronomical_data_from_api(formatted_date, location) @functools.lru_cache(maxsize=128) async def _get_astronomical_data_from_api( - date: str, location: str + date: str, + location: str, ) -> Dict[str, Any]: """Returns astronomical_data from a Weather API call. @@ -48,16 +50,18 @@ async def _get_astronomical_data_from_api( A dictionary with the results from the API call. """ input_query_string = { - 'key': config.ASTRONOMY_API_KEY, - 'q': location, - 'dt': date, + "key": config.ASTRONOMY_API_KEY, + "q": location, + "dt": date, } output: Dict[str, Any] = {} try: async with httpx.AsyncClient() as client: response = await client.get( - ASTRONOMY_URL, params=input_query_string) + ASTRONOMY_URL, + params=input_query_string, + ) except httpx.HTTPError: output["success"] = False output["error"] = NO_API_RESPONSE @@ -70,9 +74,9 @@ async def _get_astronomical_data_from_api( output["success"] = True try: - output.update(response.json()['location']) + output.update(response.json()["location"]) return output except KeyError: output["success"] = False - output["error"] = response.json()['error']['message'] + output["error"] = response.json()["error"]["message"] return output diff --git a/app/internal/audio.py b/app/internal/audio.py index 1dbd68e7..48449c33 100644 --- a/app/internal/audio.py +++ b/app/internal/audio.py @@ -1,6 +1,8 @@ -from sqlalchemy.orm.session import Session -from typing import Dict, List, Optional, Tuple, Union from enum import Enum +from typing import Dict, List, Optional, Tuple, Union + +from sqlalchemy.orm.session import Session + from app.database.models import ( AudioTracks, User, @@ -8,7 +10,6 @@ UserSettings, ) - DEFAULT_MUSIC = ["GASTRONOMICA.mp3"] DEFAULT_MUSIC_VOL = 0.5 DEFAULT_SFX = "click_1.wav" diff --git a/app/internal/calendar_privacy.py b/app/internal/calendar_privacy.py index f38f0f4a..250a8620 100644 --- a/app/internal/calendar_privacy.py +++ b/app/internal/calendar_privacy.py @@ -1,12 +1,13 @@ -from app.dependencies import get_db +from fastapi import Depends + from app.database.models import User +from app.dependencies import get_db from app.internal.privacy import PrivacyKinds + # TODO switch to using this when the user system is merged # from app.internal.security.dependencies import ( # current_user, CurrentUser) -from fastapi import Depends - # TODO add privacy as an attribute in current user # in app.internal.security.dependencies @@ -14,14 +15,14 @@ def can_show_calendar( requested_user_username: str, db: Depends(get_db), - current_user: User + current_user: User, # TODO to be added after user system is merged: # CurrentUser = Depends(current_user) ) -> bool: """Check whether current user can show the requested calendar""" - requested_user = db.query(User).filter( - User.username == requested_user_username - ).first() + requested_user = ( + db.query(User).filter(User.username == requested_user_username).first() + ) privacy = current_user.privacy is_current_user = current_user.username == requested_user.username if privacy == PrivacyKinds.Private.name and is_current_user: diff --git a/app/internal/emotion.py b/app/internal/emotion.py index 950b9c31..32dc0a35 100644 --- a/app/internal/emotion.py +++ b/app/internal/emotion.py @@ -1,41 +1,55 @@ -import text2emotion as te from typing import Dict, NamedTuple, Union -from app.config import ( - CONTENT_WEIGHTS, - LEVEL_OF_SIGNIFICANCE, - TITLE_WEIGHTS) +import text2emotion as te +from app.config import CONTENT_WEIGHTS, LEVEL_OF_SIGNIFICANCE, TITLE_WEIGHTS -EMOTIONS = {"Happy": "😃", - "Sad": "🙁", - "Angry": "😠", - "Fear": "😱", - "Surprise": "😮"} +EMOTIONS = { + "Happy": "😃", + "Sad": "🙁", + "Angry": "😠", + "Fear": "😱", + "Surprise": "😮", +} -Emoticon = NamedTuple("Emoticon", [("dominant", str), ("score", float), - ("code", str)]) +Emoticon = NamedTuple( + "Emoticon", + [("dominant", str), ("score", float), ("code", str)], +) DupEmotion = NamedTuple("DupEmotion", [("dominant", str), ("flag", bool)]) -def get_weight(emotion: str, title_emotion: Dict[str, float], - content_emotion: Dict[str, float] = None) -> float: +def get_weight( + emotion: str, + title_emotion: Dict[str, float], + content_emotion: Dict[str, float] = None, +) -> float: if not content_emotion: return title_emotion[emotion] - return (title_emotion[emotion] * TITLE_WEIGHTS + - content_emotion[emotion] * CONTENT_WEIGHTS) - - -def score_comp(emotion_score: float, dominant_emotion: Emoticon, emotion: str, - code: str, flag: bool) -> DupEmotion: + return ( + title_emotion[emotion] * TITLE_WEIGHTS + + content_emotion[emotion] * CONTENT_WEIGHTS + ) + + +def score_comp( + emotion_score: float, + dominant_emotion: Emoticon, + emotion: str, + code: str, + flag: bool, +) -> DupEmotion: """ score comparison between emotions. returns the dominant and if equals we flag it """ if emotion_score > dominant_emotion.score: flag = False - dominant_emotion = Emoticon(dominant=emotion, score=emotion_score, - code=code) + dominant_emotion = Emoticon( + dominant=emotion, + score=emotion_score, + code=code, + ) elif emotion_score == dominant_emotion.score: flag = True return DupEmotion(dominant=dominant_emotion, flag=flag) @@ -58,17 +72,23 @@ def get_dominant_emotion(title: str, content: str) -> Emoticon: if has_content: weight_parameters.append(content_emotion) emotion_score = get_weight(*weight_parameters) - score_comparison = score_comp(emotion_score, dominant_emotion, emotion, - code, duplicate_dominant_flag) + score_comparison = score_comp( + emotion_score, + dominant_emotion, + emotion, + code, + duplicate_dominant_flag, + ) dominant_emotion, duplicate_dominant_flag = [*score_comparison] if duplicate_dominant_flag: return Emoticon(dominant=None, score=0, code=None) return dominant_emotion -def is_emotion_above_significance(dominant_emotion: Emoticon, - significance: float = - LEVEL_OF_SIGNIFICANCE) -> bool: +def is_emotion_above_significance( + dominant_emotion: Emoticon, + significance: float = LEVEL_OF_SIGNIFICANCE, +) -> bool: """ get the dominant emotion, emotion score and emoticon code and check if the emotion score above the constrain we set diff --git a/app/internal/event.py b/app/internal/event.py index a771149c..962f48fb 100644 --- a/app/internal/event.py +++ b/app/internal/event.py @@ -13,7 +13,6 @@ from app.database.models import Event - ZOOM_REGEX = re.compile(r"https://.*?\.zoom.us/[a-z]/.[^.,\b\s]+") diff --git a/app/internal/export.py b/app/internal/export.py index 4736a721..99c64af7 100644 --- a/app/internal/export.py +++ b/app/internal/export.py @@ -1,8 +1,10 @@ from datetime import datetime from typing import List -from icalendar import Calendar, Event as IEvent, vCalAddress, vText import pytz +from icalendar import Calendar +from icalendar import Event as IEvent +from icalendar import vCalAddress, vText from sqlalchemy.orm import Session from app.config import DOMAIN, ICAL_VERSION, PRODUCT_ID @@ -31,7 +33,8 @@ def get_icalendar(event: Event, emails: List[str]) -> bytes: def get_icalendar_with_multiple_events( - session: Session, events: List[Event] + session: Session, + events: List[Event], ) -> bytes: """Returns an iCalendar event in bytes. @@ -58,8 +61,8 @@ def get_icalendar_with_multiple_events( def _create_icalendar() -> Calendar: """Returns an iCalendar.""" calendar = Calendar() - calendar.add('version', ICAL_VERSION) - calendar.add('prodid', PRODUCT_ID) + calendar.add("version", ICAL_VERSION) + calendar.add("prodid", PRODUCT_ID) return calendar @@ -92,19 +95,19 @@ def _create_icalendar_event(event: Event) -> IEvent: An iCalendar event. """ data = [ - ('organizer', _get_v_cal_address(event.owner.email, organizer=True)), - ('uid', _generate_id(event)), - ('dtstart', event.start), - ('dtstamp', datetime.now(tz=pytz.utc)), - ('dtend', event.end), - ('summary', event.title), + ("organizer", _get_v_cal_address(event.owner.email, organizer=True)), + ("uid", _generate_id(event)), + ("dtstart", event.start), + ("dtstamp", datetime.now(tz=pytz.utc)), + ("dtend", event.end), + ("summary", event.title), ] if event.location: - data.append(('location', event.location)) + data.append(("location", event.location)) if event.content: - data.append(('description', event.content)) + data.append(("description", event.content)) ievent = IEvent() for param in data: @@ -123,13 +126,13 @@ def _get_v_cal_address(email: str, organizer: bool = False) -> vCalAddress: Returns: A vCalAddress object. """ - attendee = vCalAddress(f'MAILTO:{email}') + attendee = vCalAddress(f"MAILTO:{email}") if organizer: - attendee.params['partstat'] = vText('ACCEPTED') - attendee.params['role'] = vText('CHAIR') + attendee.params["partstat"] = vText("ACCEPTED") + attendee.params["role"] = vText("CHAIR") else: - attendee.params['partstat'] = vText('NEEDS-ACTION') - attendee.params['role'] = vText('PARTICIPANT') + attendee.params["partstat"] = vText("NEEDS-ACTION") + attendee.params["role"] = vText("PARTICIPANT") return attendee @@ -147,10 +150,10 @@ def _generate_id(event: Event) -> bytes: A unique encoded ID in bytes. """ return ( - str(event.id) - + event.start.strftime('%Y%m%d') - + event.end.strftime('%Y%m%d') - + f'@{DOMAIN}' + str(event.id) + + event.start.strftime("%Y%m%d") + + event.end.strftime("%Y%m%d") + + f"@{DOMAIN}" ).encode() @@ -163,4 +166,4 @@ def _add_attendees(ievent: IEvent, emails: List[str]): """ for email in emails: if verify_email_pattern(email): - ievent.add('attendee', _get_v_cal_address(email), encode=0) + ievent.add("attendee", _get_v_cal_address(email), encode=0) diff --git a/app/internal/google_connect.py b/app/internal/google_connect.py index b04ef5e9..1a7e826f 100644 --- a/app/internal/google_connect.py +++ b/app/internal/google_connect.py @@ -1,63 +1,72 @@ from datetime import datetime -from fastapi import Depends +from fastapi import Depends from google.auth.transport.requests import Request as google_request from google.oauth2.credentials import Credentials from google_auth_oauthlib.flow import InstalledAppFlow from googleapiclient.discovery import build -from app.database.models import Event, User, OAuthCredentials, UserEvent -from app.dependencies import get_db, SessionLocal from app.config import CLIENT_SECRET_FILE +from app.database.models import Event, OAuthCredentials, User, UserEvent +from app.dependencies import SessionLocal, get_db from app.routers.event import create_event - -SCOPES = ['https://www.googleapis.com/auth/calendar'] +SCOPES = ["https://www.googleapis.com/auth/calendar"] -def get_credentials(user: User, - session: SessionLocal = Depends(get_db)) -> Credentials: +def get_credentials( + user: User, + session: SessionLocal = Depends(get_db), +) -> Credentials: credentials = get_credentials_from_db(user) if credentials is not None: credentials = refresh_token(credentials, session, user) else: credentials = get_credentials_from_consent_screen( - user=user, session=session) + user=user, + session=session, + ) return credentials -def fetch_save_events(credentials: Credentials, user: User, - session: SessionLocal = Depends(get_db)) -> None: +def fetch_save_events( + credentials: Credentials, + user: User, + session: SessionLocal = Depends(get_db), +) -> None: if credentials is not None: events = get_current_year_events(credentials, user, session) push_events_to_db(events, user, session) def clean_up_old_credentials_from_db( - session: SessionLocal = Depends(get_db) + session: SessionLocal = Depends(get_db), ) -> None: session.query(OAuthCredentials).filter_by(user_id=None).delete() session.commit() -def get_credentials_from_consent_screen(user: User, - session: SessionLocal = Depends(get_db) - ) -> Credentials: +def get_credentials_from_consent_screen( + user: User, + session: SessionLocal = Depends(get_db), +) -> Credentials: credentials = None if not is_client_secret_none(): # if there is no client_secrets.json flow = InstalledAppFlow.from_client_secrets_file( client_secrets_file=CLIENT_SECRET_FILE, - scopes=SCOPES + scopes=SCOPES, ) - flow.run_local_server(prompt='consent') + flow.run_local_server(prompt="consent") credentials = flow.credentials push_credentials_to_db( - credentials=credentials, user=user, session=session + credentials=credentials, + user=user, + session=session, ) clean_up_old_credentials_from_db(session=session) @@ -65,9 +74,11 @@ def get_credentials_from_consent_screen(user: User, return credentials -def push_credentials_to_db(credentials: Credentials, user: User, - session: SessionLocal = Depends(get_db) - ) -> OAuthCredentials: +def push_credentials_to_db( + credentials: Credentials, + user: User, + session: SessionLocal = Depends(get_db), +) -> OAuthCredentials: oauth_credentials = OAuthCredentials( owner=user, @@ -76,7 +87,7 @@ def push_credentials_to_db(credentials: Credentials, user: User, token_uri=credentials.token_uri, client_id=credentials.client_id, client_secret=credentials.client_secret, - expiry=credentials.expiry + expiry=credentials.expiry, ) session.add(oauth_credentials) @@ -89,59 +100,73 @@ def is_client_secret_none() -> bool: def get_current_year_events( - credentials: Credentials, user: User, - session: SessionLocal = Depends(get_db)) -> list: - '''Getting user events from google calendar''' + credentials: Credentials, + user: User, + session: SessionLocal = Depends(get_db), +) -> list: + """Getting user events from google calendar""" current_year = datetime.now().year - start = datetime(current_year, 1, 1).isoformat() + 'Z' - end = datetime(current_year + 1, 1, 1).isoformat() + 'Z' - - service = build('calendar', 'v3', credentials=credentials) - events_result = service.events().list( - calendarId='primary', - timeMin=start, - timeMax=end, - singleEvents=True, - orderBy='startTime' - ).execute() - - events = events_result.get('items', []) + start = datetime(current_year, 1, 1).isoformat() + "Z" + end = datetime(current_year + 1, 1, 1).isoformat() + "Z" + + service = build("calendar", "v3", credentials=credentials) + events_result = ( + service.events() + .list( + calendarId="primary", + timeMin=start, + timeMax=end, + singleEvents=True, + orderBy="startTime", + ) + .execute() + ) + + events = events_result.get("items", []) return events -def push_events_to_db(events: list, user: User, - session: SessionLocal = Depends(get_db)) -> bool: - '''Adding google events to db''' +def push_events_to_db( + events: list, + user: User, + session: SessionLocal = Depends(get_db), +) -> bool: + """Adding google events to db""" cleanup_user_google_calendar_events(user, session) for event in events: # Running over the events that have come from the API - title = event.get('summary') # The Google event title + title = event.get("summary") # The Google event title # support for all day events - if 'dateTime' in event['start']: + if "dateTime" in event["start"]: # This case handles part time events (not all day events) - start = datetime.fromisoformat(event['start']['dateTime']) - end = datetime.fromisoformat(event['end']['dateTime']) + start = datetime.fromisoformat(event["start"]["dateTime"]) + end = datetime.fromisoformat(event["end"]["dateTime"]) else: # This case handles all day events - start_in_str = event['start']['date'] - start = datetime.strptime(start_in_str, '%Y-%m-%d') + start_in_str = event["start"]["date"] + start = datetime.strptime(start_in_str, "%Y-%m-%d") - end_in_str = event['end']['date'] - end = datetime.strptime(end_in_str, '%Y-%m-%d') + end_in_str = event["end"]["date"] + end = datetime.strptime(end_in_str, "%Y-%m-%d") # if Google Event has a location attached - location = event.get('location') + location = event.get("location") create_google_event(title, start, end, user, location, session) return True -def create_google_event(title: str, start: datetime, - end: datetime, user: User, location: str, - session: SessionLocal = Depends(get_db)) -> Event: +def create_google_event( + title: str, + start: datetime, + end: datetime, + user: User, + location: str, + session: SessionLocal = Depends(get_db), +) -> Event: return create_event( # creating an event obj and pushing it into the db db=session, @@ -150,14 +175,15 @@ def create_google_event(title: str, start: datetime, end=end, owner_id=user.id, location=location, - is_google_event=True + is_google_event=True, ) def cleanup_user_google_calendar_events( - user: User, session: SessionLocal = Depends(get_db) + user: User, + session: SessionLocal = Depends(get_db), ) -> bool: - '''removing all user google events so the next time will be syncronized''' + """removing all user google events so the next time will be syncronized""" for user_event in user.events: user_event_id = user_event.id @@ -171,8 +197,8 @@ def cleanup_user_google_calendar_events( def get_credentials_from_db(user: User) -> Credentials: - '''bring user credential to use with google calendar api - and save the credential in the db''' + """bring user credential to use with google calendar api + and save the credential in the db""" credentials = None @@ -184,15 +210,17 @@ def get_credentials_from_db(user: User) -> Credentials: token_uri=db_credentials.token_uri, client_id=db_credentials.client_id, client_secret=db_credentials.client_secret, - expiry=db_credentials.expiry + expiry=db_credentials.expiry, ) return credentials -def refresh_token(credentials: Credentials, - user: User, session: SessionLocal = Depends(get_db) - ) -> Credentials: +def refresh_token( + credentials: Credentials, + user: User, + session: SessionLocal = Depends(get_db), +) -> Credentials: refreshed_credentials = credentials if credentials.expired: @@ -204,7 +232,7 @@ def refresh_token(credentials: Credentials, token_uri=credentials.token_uri, client_id=credentials.client_id, client_secret=credentials.client_secret, - expiry=credentials.expiry + expiry=credentials.expiry, ) session.add(refreshed_credentials) diff --git a/app/internal/import_file.py b/app/internal/import_file.py index 38b83307..fa6b2b48 100644 --- a/app/internal/import_file.py +++ b/app/internal/import_file.py @@ -1,12 +1,19 @@ +import re from collections import defaultdict from datetime import datetime from pathlib import Path -import re from typing import ( - Any, DefaultDict, Dict, Iterator, List, Optional, Tuple, Union + Any, + DefaultDict, + Dict, + Iterator, + List, + Optional, + Tuple, + Union, ) -from icalendar import cal, Calendar +from icalendar import Calendar, cal from loguru import logger from sqlalchemy.orm.session import Session @@ -27,21 +34,32 @@ DATE_FORMAT2 = "%m-%d-%Y %H:%M" DESC_EVENT = "VEVENT" -EVENT_PATTERN = re.compile(r"^(\w{" + str(int(EVENT_HEADER_NOT_EMPTY)) + "," + - str(EVENT_HEADER_LIMIT) + r"}),\s(\w{0," + - str(EVENT_CONTENT_LIMIT) + - r"}),\s(\d{2}-\d{2}-\d{4})," + - r"\s(\d{2}-\d{2}-\d{4})(?:,\s([\w\s-]{0," + - str(LOCATION_LIMIT) + - r"}))?$") +EVENT_PATTERN = re.compile( + r"^(\w{" + + str(int(EVENT_HEADER_NOT_EMPTY)) + + "," + + str(EVENT_HEADER_LIMIT) + + r"}),\s(\w{0," + + str(EVENT_CONTENT_LIMIT) + + r"}),\s(\d{2}-\d{2}-\d{4})," + + r"\s(\d{2}-\d{2}-\d{4})(?:,\s([\w\s-]{0," + + str(LOCATION_LIMIT) + + r"}))?$", +) -EVENT_PATTERN2 = re.compile(r"^(\w{" + str(int(EVENT_HEADER_NOT_EMPTY)) + "," + - str(EVENT_HEADER_LIMIT) + r"}),\s(\w{0," + - str(EVENT_CONTENT_LIMIT) + - r"}),\s(\d{2}-\d{2}-\d{4}\s\d{2}:\d{2})," + - r"\s(\d{2}-\d{2}-\d{4}\s\d{2}:\d{2})" + - r"(?:,\s([\w\s-]{0," + str(LOCATION_LIMIT) + - r"}))?$") +EVENT_PATTERN2 = re.compile( + r"^(\w{" + + str(int(EVENT_HEADER_NOT_EMPTY)) + + "," + + str(EVENT_HEADER_LIMIT) + + r"}),\s(\w{0," + + str(EVENT_CONTENT_LIMIT) + + r"}),\s(\d{2}-\d{2}-\d{4}\s\d{2}:\d{2})," + + r"\s(\d{2}-\d{2}-\d{4}\s\d{2}:\d{2})" + + r"(?:,\s([\w\s-]{0," + + str(LOCATION_LIMIT) + + r"}))?$", +) def import_events(path: str, user_id: int, session: Session) -> bool: @@ -79,8 +97,11 @@ def _is_file_valid_to_import(path: str) -> bool: Returns: True if the file is a valid to be imported, otherwise returns False. """ - return (_is_file_exists(path) and _is_file_extension_valid(path) - and _is_file_size_valid(path)) + return ( + _is_file_exists(path) + and _is_file_extension_valid(path) + and _is_file_size_valid(path) + ) def _is_file_exists(path: str) -> bool: @@ -96,8 +117,8 @@ def _is_file_exists(path: str) -> bool: def _is_file_extension_valid( - path: str, - extension: Union[str, Tuple[str, ...]] = VALID_FILE_EXTENSION, + path: str, + extension: Union[str, Tuple[str, ...]] = VALID_FILE_EXTENSION, ) -> bool: """Whether the path is a valid file extension. @@ -176,29 +197,34 @@ def _is_valid_data_event_ics(component: cal.Event) -> bool: Returns: True if valid, otherwise returns False. """ - return not (str(component.get('summary')) is None - or component.get('dtstart') is None - or component.get('dtend') is None - or not _is_date_in_range(component.get('dtstart').dt) - or not _is_date_in_range(component.get('dtend').dt) - ) + return not ( + str(component.get("summary")) is None + or component.get("dtstart") is None + or component.get("dtend") is None + or not _is_date_in_range(component.get("dtstart").dt) + or not _is_date_in_range(component.get("dtend").dt) + ) def _add_event_component_ics( - component: cal.Event, calendar_content: List[Dict[str, Any]]) -> None: + component: cal.Event, + calendar_content: List[Dict[str, Any]], +) -> None: """Appends event data from an *.ics file. Args: component: An event component. calendar_content: A list of event data. """ - calendar_content.append({ - "Head": str(component.get('summary')), - "Content": str(component.get('description')), - "S_Date": component.get('dtstart').dt.replace(tzinfo=None), - "E_Date": component.get('dtend').dt.replace(tzinfo=None), - "Location": str(component.get('location')), - }) + calendar_content.append( + { + "Head": str(component.get("summary")), + "Content": str(component.get("description")), + "S_Date": component.get("dtstart").dt.replace(tzinfo=None), + "E_Date": component.get("dtend").dt.replace(tzinfo=None), + "Location": str(component.get("location")), + }, + ) def _get_data_from_txt_file(txt_file_path: str) -> List[Dict[str, Any]]: @@ -219,7 +245,9 @@ def _get_data_from_txt_file(txt_file_path: str) -> List[Dict[str, Any]]: event_data = _get_event_data_from_text(event) if not _is_event_dates_valid( - event_data["start_date"], event_data["end_date"]): + event_data["start_date"], + event_data["end_date"], + ): return [] _add_event_component_txt(event_data, calendar_content) @@ -298,15 +326,17 @@ def _is_event_dates_valid(start: str, end: str) -> bool: assert start_date is not None and end_date is not None - is_date_in_range = (_is_date_in_range(start_date) - and _is_date_in_range(end_date)) + is_date_in_range = _is_date_in_range(start_date) and _is_date_in_range( + end_date, + ) is_end_after_start = _is_start_date_before_end_date(start_date, end_date) is_duration_valid = _is_event_duration_valid(start_date, end_date) return is_date_in_range and is_end_after_start and is_duration_valid def _add_event_component_txt( - event: Dict[str, Any], calendar_content: List[Dict[str, Any]] + event: Dict[str, Any], + calendar_content: List[Dict[str, Any]], ) -> None: """Appends event data from a txt file. @@ -321,13 +351,15 @@ def _add_event_component_txt( start_date = datetime.strptime(event["start_date"], DATE_FORMAT) end_date = datetime.strptime(event["end_date"], DATE_FORMAT) - calendar_content.append({ - "Head": event["head"], - "Content": event["content"], - "S_Date": start_date, - "E_Date": end_date, - "Location": event["location"], - }) + calendar_content.append( + { + "Head": event["head"], + "Content": event["content"], + "S_Date": start_date, + "E_Date": end_date, + "Location": event["location"], + }, + ) def _convert_string_to_date(string_date: str) -> Optional[datetime]: @@ -350,7 +382,8 @@ def _convert_string_to_date(string_date: str) -> Optional[datetime]: def _is_date_in_range( - date: datetime, year_range: int = EVENT_VALID_YEARS + date: datetime, + year_range: int = EVENT_VALID_YEARS, ) -> bool: """Whether the date is in range. @@ -385,7 +418,9 @@ def _is_start_date_before_end_date(start: datetime, end: datetime) -> bool: def _is_event_duration_valid( - start: datetime, end: datetime, max_days: int = EVENT_DURATION_LIMIT + start: datetime, + end: datetime, + max_days: int = EVENT_DURATION_LIMIT, ) -> bool: """Whether an event duration is valid. @@ -402,8 +437,8 @@ def _is_event_duration_valid( def _is_file_valid_to_save_to_database( - events: List[Dict[str, Any]], - max_event_start_date: int = MAX_EVENTS_START_DATE, + events: List[Dict[str, Any]], + max_event_start_date: int = MAX_EVENTS_START_DATE, ) -> bool: """Whether the number of events starting on the same date is valid. @@ -430,7 +465,9 @@ def _is_file_valid_to_save_to_database( def _save_events_to_database( - events: List[Dict[str, Any]], user_id: int, session: Session + events: List[Dict[str, Any]], + user_id: int, + session: Session, ) -> None: """Inserts the events into the Event table. @@ -446,11 +483,12 @@ def _save_events_to_database( end = event["E_Date"] location = event["Location"] owner_id = user_id - create_event(db=session, - title=title, - content=content, - start=start, - end=end, - location=location, - owner_id=owner_id, - ) + create_event( + db=session, + title=title, + content=content, + start=start, + end=end, + location=location, + owner_id=owner_id, + ) diff --git a/app/internal/import_holidays.py b/app/internal/import_holidays.py index 6f5f6f0c..66266f30 100644 --- a/app/internal/import_holidays.py +++ b/app/internal/import_holidays.py @@ -1,13 +1,15 @@ import re from datetime import datetime, timedelta +from typing import List, Match -from app.database.models import User, Event, UserEvent from sqlalchemy.orm import Session -from typing import List, Match + +from app.database.models import Event, User, UserEvent REGEX_EXTRACT_HOLIDAYS = re.compile( - r'SUMMARY:(?P.*)(\n.*){1,8}DTSTAMP:(?P<date>\w{8})', - re.MULTILINE) + r"SUMMARY:(?P<title>.*)(\n.*){1,8}DTSTAMP:(?P<date>\w{8})", + re.MULTILINE, +) def get_holidays_from_file(file: List[Event], session: Session) -> List[Event]: @@ -22,24 +24,27 @@ def get_holidays_from_file(file: List[Event], session: Session) -> List[Event]: holidays = [] for holiday in parsed_holidays: holiday_event = create_holiday_event( - holiday, session.query(User).filter_by(id=1).first().id) + holiday, + session.query(User).filter_by(id=1).first().id, + ) holidays.append(holiday_event) return holidays def create_holiday_event(holiday: Match[str], owner_id: int) -> Event: valid_ascii_chars_range = 128 - title = holiday.groupdict()['title'].strip() - title_to_save = ''.join(i if ord(i) < valid_ascii_chars_range - else '' for i in title) - date = holiday.groupdict()['date'].strip() - format_string = '%Y%m%d' + title = holiday.groupdict()["title"].strip() + title_to_save = "".join( + i if ord(i) < valid_ascii_chars_range else "" for i in title + ) + date = holiday.groupdict()["date"].strip() + format_string = "%Y%m%d" holiday = Event( title=title_to_save, start=datetime.strptime(date, format_string), end=datetime.strptime(date, format_string) + timedelta(days=1), - content='holiday', - owner_id=owner_id + content="holiday", + owner_id=owner_id, ) return holiday @@ -55,10 +60,7 @@ def save_holidays_to_db(holidays: List[Event], session: Session): session.flush(holidays) userevents = [] for holiday in holidays: - userevent = UserEvent( - user_id=holiday.owner_id, - event_id=holiday.id - ) + userevent = UserEvent(user_id=holiday.owner_id, event_id=holiday.id) userevents.append(userevent) session.add_all(userevents) session.commit() diff --git a/app/internal/international_days.py b/app/internal/international_days.py index 6ded1189..618ea008 100644 --- a/app/internal/international_days.py +++ b/app/internal/international_days.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import Optional, Dict, Union +from typing import Dict, Optional, Union from sqlalchemy.orm import Session from sqlalchemy.sql.expression import func @@ -8,16 +8,16 @@ def get_international_day( - international_day: Dict[str, Union[str, int]] + international_day: Dict[str, Union[str, int]], ) -> InternationalDays: """Returns an international day object from the dictionary data. - Args: - international_day: A dictionary international day - related information. + Args: + international_day: A dictionary international day + related information. - Returns: - A new international day object. + Returns: + A new international day object. """ return InternationalDays( day=international_day["day"], @@ -27,14 +27,16 @@ def get_international_day( def get_international_day_per_day( - session: Session, date: datetime + session: Session, + date: datetime, ) -> Optional[InternationalDays]: day_num = date.day month = date.month - international_day = (session.query(InternationalDays) - .filter(InternationalDays.day == day_num) - .filter(InternationalDays.month == month) - .order_by(func.random()) - .first() - ) + international_day = ( + session.query(InternationalDays) + .filter(InternationalDays.day == day_num) + .filter(InternationalDays.month == month) + .order_by(func.random()) + .first() + ) return international_day diff --git a/app/internal/logger_customizer.py b/app/internal/logger_customizer.py index ddc06c0c..01026326 100644 --- a/app/internal/logger_customizer.py +++ b/app/internal/logger_customizer.py @@ -1,7 +1,8 @@ -from pathlib import Path import sys +from pathlib import Path -from loguru import _Logger as Logger, logger +from loguru import _Logger as Logger +from loguru import logger class LoggerConfigError(Exception): @@ -9,14 +10,16 @@ class LoggerConfigError(Exception): class LoggerCustomizer: - @classmethod - def make_logger(cls, log_path: Path, - log_filename: str, - log_level: str, - log_rotation_interval: str, - log_retention_interval: str, - log_format: str) -> Logger: + def make_logger( + cls, + log_path: Path, + log_filename: str, + log_level: str, + log_rotation_interval: str, + log_retention_interval: str, + log_format: str, + ) -> Logger: """Creates a logger from given configurations Args: @@ -42,23 +45,25 @@ def make_logger(cls, log_path: Path, level=log_level, retention=log_retention_interval, rotation=log_rotation_interval, - format=log_format + format=log_format, ) except (TypeError, ValueError) as err: raise LoggerConfigError( f"You have an issue with the logger configuration: {err!r}, " - "fix it please") + "fix it please", + ) return logger @classmethod - def customize_logging(cls, - file_path: Path, - level: str, - rotation: str, - retention: str, - format: str - ) -> Logger: + def customize_logging( + cls, + file_path: Path, + level: str, + rotation: str, + retention: str, + format: str, + ) -> Logger: """Used to customize the logger instance Args: @@ -79,7 +84,7 @@ def customize_logging(cls, enqueue=True, backtrace=True, level=level.upper(), - format=format + format=format, ) logger.add( str(file_path), @@ -88,7 +93,7 @@ def customize_logging(cls, enqueue=True, backtrace=True, level=level.upper(), - format=format + format=format, ) return logger diff --git a/app/internal/meds.py b/app/internal/meds.py index 2edc817b..b4461b58 100644 --- a/app/internal/meds.py +++ b/app/internal/meds.py @@ -5,19 +5,19 @@ from sqlalchemy.orm.session import Session from app.database.models import Event -from app.internal.utils import (create_model, get_time_from_string) +from app.internal.utils import create_model, get_time_from_string MAX_EVENT_QUANTITY = 50 ERRORS = { - 'finish': 'Finish Date must must be later than or equal to Start Date', - 'max': 'Maximal Interval must must be larger than or equal to Minimal \ - Interval', - 'amount': 'Interval between Earliest Reminder and Latest Reminder not \ - long enough for Daily Amount with Minimal Interval', - 'quantity': 'Total number of reminders can\'t be larger than ' - + f'{MAX_EVENT_QUANTITY}. Please lower the daily amount, or ' - + 'choose a shorter time period.' + "finish": "Finish Date must must be later than or equal to Start Date", + "max": "Maximal Interval must must be larger than or equal to Minimal \ + Interval", + "amount": "Interval between Earliest Reminder and Latest Reminder not \ + long enough for Daily Amount with Minimal Interval", + "quantity": "Total number of reminders can't be larger than " + + f"{MAX_EVENT_QUANTITY}. Please lower the daily amount, or " + + "choose a shorter time period.", } @@ -35,6 +35,7 @@ class Form(BaseModel): end (datetime) - Last reminder date and time. note (str, optional) - Additional description. """ + name: Optional[str] first: Optional[time] amount: int @@ -47,8 +48,12 @@ class Form(BaseModel): note: Optional[str] -def adjust_day(datetime_obj: datetime, early: time, late: time, - eq: bool = False) -> datetime: +def adjust_day( + datetime_obj: datetime, + early: time, + late: time, + eq: bool = False, +) -> datetime: """Returns datetime_obj as same or following day as needed. Args: @@ -77,25 +82,29 @@ def trans_form(web_form: Dict[str, str]) -> Tuple[Form, Dict[str, Any]]: - Dictionary version of Form object. """ form = {} - form['name'] = web_form['name'] - start_date = get_time_from_string(web_form['start']) - form['first'] = get_time_from_string(web_form['first']) - end_date = get_time_from_string(web_form['end']) - form['amount'] = int(web_form['amount']) - form['early'] = get_time_from_string(web_form['early']) - form['late'] = get_time_from_string(web_form['late']) - form['min'] = get_time_from_string(web_form['min']) - form['max'] = get_time_from_string(web_form['max']) - first_time = form['first'] if form['first'] else form['early'] - form['start'] = datetime.combine(start_date, first_time) + form["name"] = web_form["name"] + start_date = get_time_from_string(web_form["start"]) + form["first"] = get_time_from_string(web_form["first"]) + end_date = get_time_from_string(web_form["end"]) + form["amount"] = int(web_form["amount"]) + form["early"] = get_time_from_string(web_form["early"]) + form["late"] = get_time_from_string(web_form["late"]) + form["min"] = get_time_from_string(web_form["min"]) + form["max"] = get_time_from_string(web_form["max"]) + first_time = form["first"] if form["first"] else form["early"] + form["start"] = datetime.combine(start_date, first_time) end_date = adjust_day( - end_date, web_form['early'], web_form['late'], eq=True) - form['end'] = datetime.combine(end_date, form['late']) - form['note'] = web_form['note'] + end_date, + web_form["early"], + web_form["late"], + eq=True, + ) + form["end"] = datetime.combine(end_date, form["late"]) + form["note"] = web_form["note"] form_obj = Form(**form) - form['start'] = form['start'].date() - form['end'] = form['end'].date() + form["start"] = form["start"].date() + form["end"] = form["end"].date() return form_obj, form @@ -131,8 +140,7 @@ def get_interval_in_minutes(early: time, late: time) -> int: return round(interval.seconds / 60) -def validate_amount(amount: int, min: time, early: time, - late: time) -> bool: +def validate_amount(amount: int, min: time, early: time, late: time) -> bool: """Returns True if interval is sufficient for reminder amount with minimal interval constraint, False otherwise @@ -176,14 +184,14 @@ def validate_form(form: Form) -> List[str]: """ errors = [] if form.end < form.start: - errors.append(ERRORS['finish']) + errors.append(ERRORS["finish"]) if form.max < form.min: - errors.append(ERRORS['max']) + errors.append(ERRORS["max"]) if not validate_amount(form.amount, form.min, form.early, form.late): - errors.append(ERRORS['amount']) + errors.append(ERRORS["amount"]) datetimes = get_reminder_datetimes(form) if not validate_events(datetimes): - errors.append(ERRORS['quantity']) + errors.append(ERRORS["quantity"]) return errors @@ -224,15 +232,23 @@ def get_reminder_times(form: Form) -> List[time]: times.append(reminder.time()) wasted_time = get_interval_in_minutes(times[-1], form.late) / 2 - times = [(datetime.combine(datetime.min, time_obj) - + timedelta(minutes=wasted_time)).time() - for time_obj in times] + times = [ + ( + datetime.combine(datetime.min, time_obj) + + timedelta(minutes=wasted_time) + ).time() + for time_obj in times + ] return times -def validate_datetime(reminder: datetime, day: date, early: time, - late: time) -> bool: +def validate_datetime( + reminder: datetime, + day: date, + early: time, + late: time, +) -> bool: """Returns True if reminder is between earlist and latest reminder times on a given date or equal to any of them, False otherwise. @@ -253,8 +269,12 @@ def validate_datetime(reminder: datetime, day: date, early: time, return early_datetime <= reminder <= late_datetime -def validate_first_day_reminder(previous: datetime, reminder_time: time, - min: time, max: time) -> bool: +def validate_first_day_reminder( + previous: datetime, + reminder_time: time, + min: time, + max: time, +) -> bool: """Returns True if interval between reminders is valid, false otherwise. Args: @@ -272,8 +292,12 @@ def validate_first_day_reminder(previous: datetime, reminder_time: time, return max_minutes >= interval >= min_minutes -def get_different_time_reminder(previous: datetime, min: time, early: time, - late: time) -> Optional[datetime]: +def get_different_time_reminder( + previous: datetime, + min: time, + early: time, + late: time, +) -> Optional[datetime]: """Returns datetime object for first day reminder with non-standard time. Args: @@ -291,8 +315,11 @@ def get_different_time_reminder(previous: datetime, min: time, early: time, return reminder -def create_first_day_reminder(form: Form, reminder_time: time, - previous: datetime) -> Optional[datetime]: +def create_first_day_reminder( + form: Form, + reminder_time: time, + previous: datetime, +) -> Optional[datetime]: """Returns datetime object for reminder on first day. form (Form): Form object containing all relevant data. @@ -305,15 +332,25 @@ def create_first_day_reminder(form: Form, reminder_time: time, reminder = datetime.combine(form.start.date(), reminder_time) reminder = adjust_day(reminder, form.early, reminder_time) if reminder > form.start: - if not validate_first_day_reminder(previous, reminder_time, form.min, - form.max): - reminder = get_different_time_reminder(previous, form.min, - form.early, form.late) + if not validate_first_day_reminder( + previous, + reminder_time, + form.min, + form.max, + ): + reminder = get_different_time_reminder( + previous, + form.min, + form.early, + form.late, + ) return reminder -def get_first_day_reminders(form: Form, - times: List[time]) -> Iterator[datetime]: +def get_first_day_reminders( + form: Form, + times: List[time], +) -> Iterator[datetime]: """Generates datetime objects for reminders on the first day. Args: @@ -335,8 +372,13 @@ def get_first_day_reminders(form: Form, i += 1 -def reminder_generator(times: List[time], early: time, start: datetime, - day: date, end: datetime) -> Iterator[datetime]: +def reminder_generator( + times: List[time], + early: time, + start: datetime, + day: date, + end: datetime, +) -> Iterator[datetime]: """Generates datetime objects for reminders based on times and date. Args: @@ -372,8 +414,13 @@ def get_reminder_datetimes(form: Form) -> Iterator[datetime]: if day == 0 and form.first: yield from get_first_day_reminders(form, times) else: - yield from reminder_generator(times, form.early, form.start, day, - form.end) + yield from reminder_generator( + times, + form.early, + form.start, + day, + form.end, + ) def create_events(session: Session, user_id: int, form: Form) -> None: @@ -384,16 +431,16 @@ def create_events(session: Session, user_id: int, form: Form) -> None: user_id (int): ID of user to create events for. form (Form): Form object containing all relevant data. """ - title = 'It\'s time to take your meds' + title = "It's time to take your meds" if form.name: - title = f'{form.name.title()} - {title}' + title = f"{form.name.title()} - {title}" datetimes = get_reminder_datetimes(form) for event_time in datetimes: event_data = { - 'title': title, - 'start': event_time, - 'end': event_time + timedelta(minutes=5), - 'content': form.note, - 'owner_id': user_id, + "title": title, + "start": event_time, + "end": event_time + timedelta(minutes=5), + "content": form.note, + "owner_id": user_id, } create_model(session, Event, **event_data) diff --git a/app/internal/notification.py b/app/internal/notification.py index af86c9bb..bc638e85 100644 --- a/app/internal/notification.py +++ b/app/internal/notification.py @@ -1,5 +1,5 @@ from operator import attrgetter -from typing import Iterator, List, Union, Callable +from typing import Callable, Iterator, List, Union from fastapi import HTTPException from sqlalchemy.exc import SQLAlchemyError @@ -8,13 +8,12 @@ from app.database.models import ( Invitation, - Message, InvitationStatusEnum, + Message, MessageStatusEnum, ) from app.internal.utils import create_model - WRONG_NOTIFICATION_ID = ( "The notification id you have entered is wrong\n." "If you did not enter the notification id manually, report this exception." diff --git a/app/internal/on_this_day_events.py b/app/internal/on_this_day_events.py index 3a058df1..faa47c0c 100644 --- a/app/internal/on_this_day_events.py +++ b/app/internal/on_this_day_events.py @@ -1,10 +1,10 @@ -from datetime import date, datetime import json +from datetime import date, datetime from typing import Any, Dict +import requests from fastapi import Depends from loguru import logger -import requests from sqlalchemy import func from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import Session @@ -14,36 +14,35 @@ from app.dependencies import get_db -def insert_on_this_day_data( - db: Session = Depends(get_db) -) -> Dict[str, Any]: +def insert_on_this_day_data(db: Session = Depends(get_db)) -> Dict[str, Any]: now = datetime.now() day, month = now.day, now.month res = requests.get( - f'https://byabbe.se/on-this-day/{month}/{day}/events.json') + f"https://byabbe.se/on-this-day/{month}/{day}/events.json", + ) text = json.loads(res.text) - res_events = text.get('events') - res_date = text.get('date') - res_wiki = text.get('wikipedia') - db.add(WikipediaEvents(events=res_events, - date_=res_date, wikipedia=res_wiki)) + res_events = text.get("events") + res_date = text.get("date") + res_wiki = text.get("wikipedia") + db.add( + WikipediaEvents(events=res_events, date_=res_date, wikipedia=res_wiki), + ) db.commit() return text -def get_on_this_day_events( - db: Session = Depends(get_db) -) -> Dict[str, Any]: +def get_on_this_day_events(db: Session = Depends(get_db)) -> Dict[str, Any]: try: - data = (db.query(WikipediaEvents). - filter( - func.date(WikipediaEvents.date_inserted) == date.today()). - one()) + data = ( + db.query(WikipediaEvents) + .filter(func.date(WikipediaEvents.date_inserted) == date.today()) + .one() + ) except NoResultFound: data = insert_on_this_day_data(db) except (SQLAlchemyError, AttributeError) as e: - logger.error(f'on this day failed with error: {e}') - data = {'events': [], 'wikipedia': 'https://en.wikipedia.org/'} + logger.error(f"on this day failed with error: {e}") + data = {"events": [], "wikipedia": "https://en.wikipedia.org/"} return data diff --git a/app/internal/security/dependencies.py b/app/internal/security/dependencies.py index 584235dd..e42e4c0e 100644 --- a/app/internal/security/dependencies.py +++ b/app/internal/security/dependencies.py @@ -1,18 +1,22 @@ from fastapi import Depends, HTTPException -from starlette.status import HTTP_401_UNAUTHORIZED from starlette.requests import Request +from starlette.status import HTTP_401_UNAUTHORIZED from app.database.models import User from app.dependencies import get_db +from app.internal.security import schema from app.internal.security.ouath2 import ( - Session, get_jwt_token, get_authorization_cookie + Session, + get_authorization_cookie, + get_jwt_token, ) -from app.internal.security import schema async def is_logged_in( - request: Request, db: Session = Depends(get_db), - jwt: str = Depends(get_authorization_cookie)) -> bool: + request: Request, + db: Session = Depends(get_db), + jwt: str = Depends(get_authorization_cookie), +) -> bool: """ A dependency function protecting routes for only logged in user """ @@ -21,8 +25,10 @@ async def is_logged_in( async def is_manager( - request: Request, db: Session = Depends(get_db), - jwt: str = Depends(get_authorization_cookie)) -> bool: + request: Request, + db: Session = Depends(get_db), + jwt: str = Depends(get_authorization_cookie), +) -> bool: """ A dependency function protecting routes for only logged in manager """ @@ -30,9 +36,10 @@ async def is_manager( if jwt_payload.get("is_manager"): return True raise HTTPException( - status_code=HTTP_401_UNAUTHORIZED, - headers=request.url.path, - detail="You don't have a permition to enter this page") + status_code=HTTP_401_UNAUTHORIZED, + headers=request.url.path, + detail="You don't have a permition to enter this page", + ) async def current_user_from_db( @@ -52,9 +59,10 @@ async def current_user_from_db( return db_user else: raise HTTPException( - status_code=HTTP_401_UNAUTHORIZED, - headers=request.url.path, - detail="Your token is incorrect. Please log in again") + status_code=HTTP_401_UNAUTHORIZED, + headers=request.url.path, + detail="Your token is incorrect. Please log in again", + ) async def current_user( diff --git a/app/internal/security/ouath2.py b/app/internal/security/ouath2.py index d520c4cc..6f99ae48 100644 --- a/app/internal/security/ouath2.py +++ b/app/internal/security/ouath2.py @@ -1,20 +1,20 @@ from datetime import datetime, timedelta from typing import Union -from passlib.context import CryptContext +import jwt from fastapi import Depends, HTTPException from fastapi.security import OAuth2PasswordBearer -import jwt from jwt.exceptions import InvalidSignatureError +from passlib.context import CryptContext from sqlalchemy.orm import Session from starlette.requests import Request from starlette.responses import RedirectResponse from starlette.status import HTTP_401_UNAUTHORIZED -from . import schema from app.config import JWT_ALGORITHM, JWT_KEY, JWT_MIN_EXP from app.database.models import User +from . import schema pwd_context = CryptContext(schemes=["bcrypt"]) oauth_schema = OAuth2PasswordBearer(tokenUrl="/login") @@ -65,16 +65,16 @@ def create_jwt_token( async def get_jwt_token( db: Session, - token: str = Depends(oauth_schema), - path: Union[bool, str] = None) -> User: + token: str = Depends(oauth_schema), + path: Union[bool, str] = None, +) -> User: """ Check whether JWT token is correct. Returns jwt payloads if correct. Raises HTTPException if fails to decode. """ try: - jwt_payload = jwt.decode( - token, JWT_KEY, algorithms=JWT_ALGORITHM) + jwt_payload = jwt.decode(token, JWT_KEY, algorithms=JWT_ALGORITHM) except InvalidSignatureError: raise HTTPException( status_code=HTTP_401_UNAUTHORIZED, @@ -91,7 +91,8 @@ async def get_jwt_token( raise HTTPException( status_code=HTTP_401_UNAUTHORIZED, headers=path, - detail="Your token is incorrect. Please log in again") + detail="Your token is incorrect. Please log in again", + ) return jwt_payload diff --git a/app/internal/translation.py b/app/internal/translation.py index e033781f..4fd0510e 100644 --- a/app/internal/translation.py +++ b/app/internal/translation.py @@ -5,7 +5,7 @@ from loguru import logger from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound from sqlalchemy.orm.session import Session -from textblob import download_corpora, TextBlob +from textblob import TextBlob, download_corpora from textblob.exceptions import NotTranslated from app.database.models import Language @@ -31,10 +31,11 @@ def translate_text_for_user(text: str, session: Session, user_id: int) -> str: return translate_text(text, target_lang) -def translate_text(text: str, - target_lang: str, - original_lang: Optional[str] = None, - ) -> str: +def translate_text( + text: str, + target_lang: str, + original_lang: Optional[str] = None, +) -> str: """Translates text to the target language. Args: @@ -56,9 +57,12 @@ def translate_text(text: str, return text try: - return str(TextBlob(text).translate( - from_lang=language_code, - to=_get_language_code(target_lang))) + return str( + TextBlob(text).translate( + from_lang=language_code, + to=_get_language_code(target_lang), + ), + ) except NotTranslated: return text @@ -88,7 +92,7 @@ def _get_user_language(user_id: int, session: Session) -> str: logger.critical(e) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail='Error raised', + detail="Error raised", ) diff --git a/app/internal/user/availability.py b/app/internal/user/availability.py index a94d5a8b..f855ea75 100644 --- a/app/internal/user/availability.py +++ b/app/internal/user/availability.py @@ -3,6 +3,7 @@ from sqlalchemy.orm import Session from app.database.models import Event, User + # from app.internal.utils import get_current_user @@ -11,8 +12,11 @@ def disable(session: Session, user_id: int) -> bool: returns: True if function worked properly False if it didn't.""" - future_events_user_owns = session.query(Event).filter( - Event.start > datetime.now(), Event.owner_id == user_id).all() + future_events_user_owns = ( + session.query(Event) + .filter(Event.start > datetime.now(), Event.owner_id == user_id) + .all() + ) if future_events_user_owns: return False diff --git a/app/main.py b/app/main.py index 3eb750a0..751c35ba 100644 --- a/app/main.py +++ b/app/main.py @@ -9,11 +9,11 @@ from app import config from app.database import engine, models from app.dependencies import ( - get_db, - logger, MEDIA_PATH, SOUNDS_PATH, STATIC_PATH, + get_db, + logger, templates, ) from app.internal import daily_quotes, json_data_loader diff --git a/app/routers/about_us.py b/app/routers/about_us.py index e7aa7f98..e9d03681 100644 --- a/app/routers/about_us.py +++ b/app/routers/about_us.py @@ -2,12 +2,14 @@ from app.dependencies import templates - router = APIRouter() @router.get("/about") def about(request: Request): - return templates.TemplateResponse("about_us.html", { - "request": request, - }) + return templates.TemplateResponse( + "about_us.html", + { + "request": request, + }, + ) diff --git a/app/routers/agenda.py b/app/routers/agenda.py index 4563c99f..51d304ce 100644 --- a/app/routers/agenda.py +++ b/app/routers/agenda.py @@ -1,6 +1,6 @@ +import json from collections import defaultdict from datetime import date, timedelta -import json from typing import Optional, Tuple from fastapi import APIRouter, Depends, Request @@ -15,9 +15,9 @@ def calc_dates_range_for_agenda( - start: Optional[date], - end: Optional[date], - days: Optional[int], + start: Optional[date], + end: Optional[date], + days: Optional[int], ) -> Tuple[date, date]: """Create start and end dates according to the parameters in the page.""" if days is not None: @@ -31,41 +31,51 @@ def calc_dates_range_for_agenda( @router.get("/agenda", include_in_schema=False) def agenda( - request: Request, - db: Session = Depends(get_db), - start_date: Optional[date] = None, - end_date: Optional[date] = None, - days: Optional[int] = None, + request: Request, + db: Session = Depends(get_db), + start_date: Optional[date] = None, + end_date: Optional[date] = None, + days: Optional[int] = None, ) -> _TemplateResponse: """Route for the agenda page, using dates range or exact amount of days.""" user_id = 1 # there is no user session yet, so I use user id- 1. start_date, end_date = calc_dates_range_for_agenda( - start_date, end_date, days + start_date, + end_date, + days, ) events_objects = agenda_events.get_events_per_dates( - db, user_id, start_date, end_date + db, + user_id, + start_date, + end_date, ) events = defaultdict(list) for event_obj in events_objects: - event_duration = agenda_events.get_time_delta_string(event_obj.start, - event_obj.end) + event_duration = agenda_events.get_time_delta_string( + event_obj.start, + event_obj.end, + ) json_event_data = jsonable_encoder(event_obj) - json_event_data['duration'] = event_duration - json_event_data['start'] = event_obj.start.time().strftime("%H:%M") + json_event_data["duration"] = event_duration + json_event_data["start"] = event_obj.start.time().strftime("%H:%M") event_key = event_obj.start.date().strftime("%d/%m/%Y") events[event_key].append(json_event_data) events_for_graph = json.dumps( - agenda_events.make_dict_for_graph_data(db, user_id) + agenda_events.make_dict_for_graph_data(db, user_id), + ) + return templates.TemplateResponse( + "agenda.html", + { + "request": request, + "events": events, + "start_date": start_date, + "end_date": end_date, + "events_for_graph": events_for_graph, + }, ) - return templates.TemplateResponse("agenda.html", { - "request": request, - "events": events, - "start_date": start_date, - "end_date": end_date, - "events_for_graph": events_for_graph, - }) diff --git a/app/routers/audio.py b/app/routers/audio.py index afd53b9c..f85c75b7 100644 --- a/app/routers/audio.py +++ b/app/routers/audio.py @@ -10,16 +10,16 @@ from app.database.models import User from app.dependencies import SOUNDS_PATH, get_db, templates from app.internal.audio import ( - get_audio_settings, - handle_vol, - SoundKind, - Sound, - init_audio_tracks, - save_audio_settings, DEFAULT_MUSIC, DEFAULT_MUSIC_VOL, DEFAULT_SFX, DEFAULT_SFX_VOL, + Sound, + SoundKind, + get_audio_settings, + handle_vol, + init_audio_tracks, + save_audio_settings, ) from app.internal.security.dependencies import current_user diff --git a/app/routers/calendar_grid.py b/app/routers/calendar_grid.py index b8b0878f..98e94016 100644 --- a/app/routers/calendar_grid.py +++ b/app/routers/calendar_grid.py @@ -1,7 +1,7 @@ import calendar -from datetime import date, datetime, timedelta import itertools import locale +from datetime import date, datetime, timedelta from typing import Dict, Iterator, List, Tuple import pytz @@ -32,21 +32,16 @@ def __init__(self, date: datetime): self.dailyevents: List[Tuple] = [] self.events: List[Tuple] = [] self.css: Dict[str, str] = { - 'day_container': 'day', - 'date': 'day-number', - 'daily_event': 'month-event', - 'daily_event_front': ' '.join([ - 'daily', - 'front', - 'background-warmyellow' - ]), - 'daily_event_back': ' '.join([ - 'daily', - 'back', - 'text-darkblue', - 'background-lightgray' - ]), - 'event': 'event', + "day_container": "day", + "date": "day-number", + "daily_event": "month-event", + "daily_event_front": " ".join( + ["daily", "front", "background-warmyellow"], + ), + "daily_event_back": " ".join( + ["daily", "back", "text-darkblue", "background-lightgray"], + ), + "event": "event", } def __str__(self) -> str: @@ -62,12 +57,12 @@ def set_id(self) -> str: @classmethod def get_user_local_time(cls) -> datetime: - greenwich = pytz.timezone('GB') + greenwich = pytz.timezone("GB") return greenwich.localize(datetime.now()) @classmethod def convert_str_to_date(cls, date_string: str) -> datetime: - return datetime.strptime(date_string, '%d-%B-%Y') + return datetime.strptime(date_string, "%d-%B-%Y") @classmethod def is_weekend(cls, date: date) -> bool: @@ -79,21 +74,16 @@ class DayWeekend(Day): def __init__(self, date: datetime): super().__init__(date) self.css = { - 'day_container': 'day ', - 'date': ' '.join(['day-number', 'text-gray']), - 'daily_event': 'month-event', - 'daily_event_front': ' '.join([ - 'daily', - 'front', - 'background-warmyellow' - ]), - 'daily_event_back': ' '.join([ - 'daily', - 'back', - 'text-darkblue', - 'background-lightgray' - ]), - 'event': 'event', + "day_container": "day ", + "date": " ".join(["day-number", "text-gray"]), + "daily_event": "month-event", + "daily_event_front": " ".join( + ["daily", "front", "background-warmyellow"], + ), + "daily_event_back": " ".join( + ["daily", "back", "text-darkblue", "background-lightgray"], + ), + "event": "event", } @@ -101,26 +91,18 @@ class Today(Day): def __init__(self, date: datetime): super().__init__(date) self.css = { - 'day_container': ' '.join([ - 'day', - 'text-darkblue', - 'background-yellow' - ]), - 'date': 'day-number', - 'daily_event': 'month-event', - 'daily_event_front': ' '.join([ - 'daily', - 'front', - 'text-lightgray', - 'background-darkblue' - ]), - 'daily_event_back': ' '.join([ - 'daily', - 'back', - 'text-darkblue', - 'background-lightgray' - ]), - 'event': 'event', + "day_container": " ".join( + ["day", "text-darkblue", "background-yellow"], + ), + "date": "day-number", + "daily_event": "month-event", + "daily_event_front": " ".join( + ["daily", "front", "text-lightgray", "background-darkblue"], + ), + "daily_event_back": " ".join( + ["daily", "back", "text-darkblue", "background-lightgray"], + ), + "event": "event", } @@ -128,25 +110,18 @@ class FirstDayMonth(Day): def __init__(self, date: datetime): super().__init__(date) self.css = { - 'day_container': ' '.join([ - 'day', - 'text-darkblue', - 'background-lightgray' - ]), - 'date': 'day-number', - 'daily_event': 'month-event', - 'daily_event_front': ' '.join([ - 'daily front', - 'text-lightgray', - 'background-red' - ]), - 'daily_event_back': ' '.join([ - 'daily', - 'back', - 'text-darkblue', - 'background-lightgray' - ]), - 'event': 'event', + "day_container": " ".join( + ["day", "text-darkblue", "background-lightgray"], + ), + "date": "day-number", + "daily_event": "month-event", + "daily_event_front": " ".join( + ["daily front", "text-lightgray", "background-red"], + ), + "daily_event_back": " ".join( + ["daily", "back", "text-darkblue", "background-lightgray"], + ), + "event": "event", } def __str__(self) -> str: @@ -175,8 +150,7 @@ def create_day(day: datetime) -> Day: def get_next_date(date: datetime) -> Iterator[Day]: """Generate date objects from a starting given date.""" yield from ( - create_day(date + timedelta(days=i)) - for i in itertools.count(start=1) + create_day(date + timedelta(days=i)) for i in itertools.count(start=1) ) @@ -197,13 +171,13 @@ def get_n_days(date: datetime, n: int) -> Iterator[Day]: def create_weeks( - days: Iterator[Day], - length: int = Week.WEEK_DAYS + days: Iterator[Day], + length: int = Week.WEEK_DAYS, ) -> List[Week]: """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)] + return [Week(ndays[i : i + length]) for i in range(0, num_days, length)] def get_month_block(day: Day, n: int = MONTH_BLOCK) -> List[Week]: diff --git a/app/routers/categories.py b/app/routers/categories.py index 525350ea..c48334d3 100644 --- a/app/routers/categories.py +++ b/app/routers/categories.py @@ -9,10 +9,8 @@ from starlette.datastructures import ImmutableMultiDict from starlette.templating import _TemplateResponse - from app.database.models import Category -from app.dependencies import get_db -from app.dependencies import templates +from app.dependencies import get_db, templates HEX_COLOR_FORMAT = r"^(?:[0-9a-fA-F]{3}){1,2}$" @@ -33,55 +31,62 @@ class Config: "name": "Guitar lessons", "color": "aabbcc", "user_id": 1, - } + }, } # TODO(issue#29): get current user_id from session @router.get("/user", include_in_schema=False) -def get_categories(request: Request, - db_session: Session = Depends(get_db)) -> List[Category]: +def get_categories( + request: Request, + db_session: Session = Depends(get_db), +) -> List[Category]: if validate_request_params(request.query_params): return get_user_categories(db_session, **request.query_params) else: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Request {request.query_params} contains " - f"unallowed params.") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Request {request.query_params} contains " + f"unallowed params.", + ) @router.get("/") def category_color_insert(request: Request) -> _TemplateResponse: - return templates.TemplateResponse("categories.html", { - "request": request - }) + return templates.TemplateResponse("categories.html", {"request": request}) # TODO(issue#29): get current user_id from session @router.post("/") -async def set_category(request: Request, - name: str = Form(None), - color: str = Form(None), - db_sess: Session = Depends(get_db)): +async def set_category( + request: Request, + name: str = Form(None), + color: str = Form(None), + db_sess: Session = Depends(get_db), +): message = "" - user_id = 1 # until issue#29 will get current user_id from session - color = color.replace('#', '') + user_id = 1 # until issue#29 will get current user_id from session + color = color.replace("#", "") if not validate_color_format(color): - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Color {color} if not from " - f"expected format.") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Color {color} if not from " f"expected format.", + ) try: Category.create(db_sess, name=name, color=color, user_id=user_id) except IntegrityError: db_sess.rollback() message = "Category already exists" - return templates.TemplateResponse("categories.html", - dictionary_req(request, message, - name, color)) + return templates.TemplateResponse( + "categories.html", + dictionary_req(request, message, name, color), + ) message = f"Congratulation! You have created a new category: {name}" - return templates.TemplateResponse("categories.html", - dictionary_req(request, message, - name, color)) + return templates.TemplateResponse( + "categories.html", + dictionary_req(request, message, name, color), + ) def validate_request_params(query_params: ImmutableMultiDict) -> bool: @@ -98,8 +103,11 @@ def validate_request_params(query_params: ImmutableMultiDict) -> bool: intersection_set = request_params.intersection(all_fields) if "color" in intersection_set: is_valid_color = validate_color_format(query_params["color"]) - return union_set == all_fields and "user_id" in intersection_set and ( - is_valid_color) + return ( + union_set == all_fields + and "user_id" in intersection_set + and (is_valid_color) + ) def validate_color_format(color: str) -> bool: @@ -111,14 +119,19 @@ def validate_color_format(color: str) -> bool: return False -def get_user_categories(db_session: Session, - user_id: int, **params) -> List[Category]: +def get_user_categories( + db_session: Session, user_id: int, **params +) -> List[Category]: """ Returns user's categories, filtered by params. """ try: - categories = db_session.query(Category).filter_by( - user_id=user_id).filter_by(**params).all() + categories = ( + db_session.query(Category) + .filter_by(user_id=user_id) + .filter_by(**params) + .all() + ) except SQLAlchemyError: return [] else: @@ -127,9 +140,9 @@ def get_user_categories(db_session: Session, def dictionary_req(request, message, name, color) -> Dict: dictionary_tamplates = { - "request": request, - "message": message, - "name": name, - "color": color, - } + "request": request, + "message": message, + "name": name, + "color": color, + } return dictionary_tamplates diff --git a/app/routers/credits.py b/app/routers/credits.py index 1a35fd4b..59f8d7f0 100644 --- a/app/routers/credits.py +++ b/app/routers/credits.py @@ -1,7 +1,7 @@ -from fastapi import APIRouter, Request import json from typing import List +from fastapi import APIRouter, Request from loguru import logger from starlette.templating import _TemplateResponse @@ -14,11 +14,10 @@ def credits_from_json() -> List: path = RESOURCES_DIR / "credits.json" try: - with open(path, 'r') as json_file: + with open(path, "r") as json_file: json_list = json.load(json_file) except (IOError, ValueError): - logger.exception( - "An error occurred during reading of json file") + logger.exception("An error occurred during reading of json file") return [] return json_list @@ -26,7 +25,7 @@ def credits_from_json() -> List: @router.get("/credits") def credits(request: Request) -> _TemplateResponse: credit_list = credits_from_json() - return templates.TemplateResponse("credits.html", { - "request": request, - "credit_list": credit_list - }) + return templates.TemplateResponse( + "credits.html", + {"request": request, "credit_list": credit_list}, + ) diff --git a/app/routers/event.py b/app/routers/event.py index 7858ebba..918f050a 100644 --- a/app/routers/event.py +++ b/app/routers/event.py @@ -1,8 +1,8 @@ -from datetime import datetime as dt import json +import urllib +from datetime import datetime as dt from operator import attrgetter from typing import Any, Dict, List, Optional, Tuple -import urllib from fastapi import APIRouter, Depends, HTTPException, Request from pydantic import BaseModel @@ -29,7 +29,6 @@ from app.internal.utils import create_model, get_current_user from app.routers.categories import get_user_categories - EVENT_DATA = Tuple[Event, List[Dict[str, str]], str] TIME_FORMAT = "%Y-%m-%d %H:%M" START_FORMAT = "%A, %d/%m/%Y %H:%M" diff --git a/app/routers/event_images.py b/app/routers/event_images.py index 9a1ae495..bfe56cd3 100644 --- a/app/routers/event_images.py +++ b/app/routers/event_images.py @@ -1,5 +1,5 @@ -from functools import lru_cache import re +from functools import lru_cache from typing import Optional from nltk.tokenize import word_tokenize @@ -7,135 +7,135 @@ from app import config -FLAIRS_EXTENSION = '.jpg' -FLAIRS_REL_PATH = f'{config.STATIC_ABS_PATH}\\event_flairs' +FLAIRS_EXTENSION = ".jpg" +FLAIRS_REL_PATH = f"{config.STATIC_ABS_PATH}\\event_flairs" IMAGES_RELATED_WORDS_MAP = { - 'birthday': 'birthday', - 'coffee': 'coffee', - 'coffees': 'coffee', - 'concert': 'concert', - 'gig': 'concert', - 'concerts': 'concert', - 'gigs': 'concert', - 'bicycle': 'cycle', - 'cycling': 'cycle', - 'bike': 'cycle', - 'bicycles': 'cycle', - 'bikes': 'cycle', - 'biking': 'cycle', - 'dentist': 'dentist', - 'dentistry': 'dentist', - 'dental': 'dentist', - 'dinner': 'food', - 'dinners': 'food', - 'restaurant': 'food', - 'restaurants': 'food', - 'family meal': 'food', - 'lunch': 'food', - 'lunches': 'food', - 'luncheon': 'food', - 'cocktail': 'drank', - 'drinks': 'drank', - 'cocktails': 'drank', - 'golf': 'golf', - 'graduation': 'graduate', - 'gym': 'gym', - 'workout': 'gym', - 'workouts': 'gym', - 'haircut': 'haircut', - 'hair': 'haircut', - 'halloween': 'halloween', - 'helloween': 'halloween', - "hallowe'en": 'halloween', - 'allhalloween': 'halloween', - "all hallows' eve": 'halloween', - "all saints' Eve": 'halloween', - 'hiking': 'hike', - 'hike': 'hike', - 'hikes': 'hike', - 'kayaking': 'kayak', - 'piano': 'music', - 'singing': 'music', - 'music class': 'music', - 'choir practice': 'music', - 'flute': 'music', - 'orchestra': 'music', - 'oboe': 'music', - 'clarinet': 'music', - 'saxophone': 'music', - 'cornett': 'music', - 'trumpet': 'music', - 'contrabass': 'music', - 'cello': 'music', - 'trombone': 'music', - 'tuba': 'music', - 'music ensemble': 'music', - 'string quartett': 'music', - 'guitar lesson': 'music', - 'classical music': 'music', - 'choir': 'music', - 'manicure': 'manicure', - 'pedicure': 'manicure', - 'manicures': 'manicure', - 'pedicures': 'manicure', - 'massage': 'massage', - 'back rub': 'massage', - 'backrub': 'massage', - 'massages': 'massage', - 'pills': 'pill', - 'medicines': 'pill', - 'medicine': 'pill', - 'drug': 'pill', - 'drugs': 'pill', - 'ping pong': 'pingpong', - 'table tennis': 'pingpong', - 'ping-pong': 'pingpong', - 'pingpong': 'pingpong', - 'plan week': 'plan', - 'plan quarter': 'plan', - 'plan day': 'plan', - 'plan vacation': 'plan', - 'week planning': 'plan', - 'vacation planning': 'plan', - 'pokemon': 'pokemon', - 'reading': 'read', - 'newspaper': 'read', - 'fridge repair': 'repair', - 'handyman': 'repair', - 'electrician': 'repair', - 'diy': 'repair', - 'jog': 'ran', - 'jogging': 'ran', - 'running': 'ran', - 'jogs': 'ran', - 'runs': 'ran', - 'sail': 'sail', - 'sailing': 'sail', - 'boat cruise': 'sail', - 'sailboat': 'sail', - 'santa claus': 'santa', - 'father christmas': 'santa', - 'skiing': 'ski', - 'ski': 'ski', - 'skis': 'ski', - 'snowboarding': 'ski', - 'snowshoeing': 'ski', - 'snow shoe': 'ski', - 'snow boarding': 'ski', - 'soccer': 'soccer', - 'swim': 'swam', - 'swimming': 'swam', - 'swims': 'swam', - 'tennis': 'tennis', - 'thanksgiving': 'thanksgiving', - 'wedding': 'wed', - 'wedding eve': 'wed', - 'wedding-eve party': 'wed', - 'weddings': 'wed', - 'christmas': 'christmas', - 'xmas': 'christmas', - 'x-mas': 'christmas', - 'yoga': 'yoga', + "birthday": "birthday", + "coffee": "coffee", + "coffees": "coffee", + "concert": "concert", + "gig": "concert", + "concerts": "concert", + "gigs": "concert", + "bicycle": "cycle", + "cycling": "cycle", + "bike": "cycle", + "bicycles": "cycle", + "bikes": "cycle", + "biking": "cycle", + "dentist": "dentist", + "dentistry": "dentist", + "dental": "dentist", + "dinner": "food", + "dinners": "food", + "restaurant": "food", + "restaurants": "food", + "family meal": "food", + "lunch": "food", + "lunches": "food", + "luncheon": "food", + "cocktail": "drank", + "drinks": "drank", + "cocktails": "drank", + "golf": "golf", + "graduation": "graduate", + "gym": "gym", + "workout": "gym", + "workouts": "gym", + "haircut": "haircut", + "hair": "haircut", + "halloween": "halloween", + "helloween": "halloween", + "hallowe'en": "halloween", + "allhalloween": "halloween", + "all hallows' eve": "halloween", + "all saints' Eve": "halloween", + "hiking": "hike", + "hike": "hike", + "hikes": "hike", + "kayaking": "kayak", + "piano": "music", + "singing": "music", + "music class": "music", + "choir practice": "music", + "flute": "music", + "orchestra": "music", + "oboe": "music", + "clarinet": "music", + "saxophone": "music", + "cornett": "music", + "trumpet": "music", + "contrabass": "music", + "cello": "music", + "trombone": "music", + "tuba": "music", + "music ensemble": "music", + "string quartett": "music", + "guitar lesson": "music", + "classical music": "music", + "choir": "music", + "manicure": "manicure", + "pedicure": "manicure", + "manicures": "manicure", + "pedicures": "manicure", + "massage": "massage", + "back rub": "massage", + "backrub": "massage", + "massages": "massage", + "pills": "pill", + "medicines": "pill", + "medicine": "pill", + "drug": "pill", + "drugs": "pill", + "ping pong": "pingpong", + "table tennis": "pingpong", + "ping-pong": "pingpong", + "pingpong": "pingpong", + "plan week": "plan", + "plan quarter": "plan", + "plan day": "plan", + "plan vacation": "plan", + "week planning": "plan", + "vacation planning": "plan", + "pokemon": "pokemon", + "reading": "read", + "newspaper": "read", + "fridge repair": "repair", + "handyman": "repair", + "electrician": "repair", + "diy": "repair", + "jog": "ran", + "jogging": "ran", + "running": "ran", + "jogs": "ran", + "runs": "ran", + "sail": "sail", + "sailing": "sail", + "boat cruise": "sail", + "sailboat": "sail", + "santa claus": "santa", + "father christmas": "santa", + "skiing": "ski", + "ski": "ski", + "skis": "ski", + "snowboarding": "ski", + "snowshoeing": "ski", + "snow shoe": "ski", + "snow boarding": "ski", + "soccer": "soccer", + "swim": "swam", + "swimming": "swam", + "swims": "swam", + "tennis": "tennis", + "thanksgiving": "thanksgiving", + "wedding": "wed", + "wedding eve": "wed", + "wedding-eve party": "wed", + "weddings": "wed", + "christmas": "christmas", + "xmas": "christmas", + "x-mas": "christmas", + "yoga": "yoga", } @@ -148,7 +148,7 @@ def generate_flare_link_from_lemmatized_word(lemmatized_word: str) -> str: Returns: str: The suitable link. """ - return f'{FLAIRS_REL_PATH}\\{lemmatized_word}{FLAIRS_EXTENSION}' + return f"{FLAIRS_REL_PATH}\\{lemmatized_word}{FLAIRS_EXTENSION}" def remove_non_alphabet_chars(text: str) -> str: @@ -160,8 +160,8 @@ def remove_non_alphabet_chars(text: str) -> str: Returns: str: The string after the removal. """ - regex = re.compile('[^a-zA-Z]') - return regex.sub('', text) + regex = re.compile("[^a-zA-Z]") + return regex.sub("", text) def get_image_name(related_word: str) -> Optional[str]: @@ -213,5 +213,5 @@ def attach_image_to_event(event_content: str) -> str: link = search_token_in_related_words(token) if link: return link - link = '#' + link = "#" return link diff --git a/app/routers/four_o_four.py b/app/routers/four_o_four.py index 0e989677..5dd2fe47 100644 --- a/app/routers/four_o_four.py +++ b/app/routers/four_o_four.py @@ -1,7 +1,8 @@ -from app.dependencies import templates from fastapi import APIRouter from starlette.requests import Request +from app.dependencies import templates + router = APIRouter( prefix="/404", tags=["404"], @@ -11,5 +12,4 @@ @router.get("/") async def not_implemented(request: Request): - return templates.TemplateResponse("four_o_four.j2", - {"request": request}) + return templates.TemplateResponse("four_o_four.j2", {"request": request}) diff --git a/app/routers/friendview.py b/app/routers/friendview.py index 1ed86f8c..40c9fcfd 100644 --- a/app/routers/friendview.py +++ b/app/routers/friendview.py @@ -1,28 +1,31 @@ +from typing import Union + from fastapi import APIRouter, Depends, Request from sqlalchemy.orm import Session from starlette.templating import _TemplateResponse -from typing import Union from app.dependencies import get_db, templates from app.internal import friend_view - router = APIRouter(tags=["friendview"]) @router.get("/friendview") def friendview( - request: Request, - db: Session = Depends(get_db), - my_friend: Union[str, None] = None, + request: Request, + db: Session = Depends(get_db), + my_friend: Union[str, None] = None, ) -> _TemplateResponse: # TODO: Waiting for user registration user_id = 1 events_list = friend_view.get_events_per_friend(db, user_id, my_friend) - return templates.TemplateResponse("friendview.html", { - "request": request, - "events": events_list, - "my_friend": my_friend, - }) + return templates.TemplateResponse( + "friendview.html", + { + "request": request, + "events": events_list, + "my_friend": my_friend, + }, + ) diff --git a/app/routers/google_connect.py b/app/routers/google_connect.py index cbf79e18..ccbd8d3f 100644 --- a/app/routers/google_connect.py +++ b/app/routers/google_connect.py @@ -1,10 +1,10 @@ -from fastapi import Depends, APIRouter, Request -from starlette.responses import RedirectResponse +from fastapi import APIRouter, Depends, Request from loguru import logger +from starlette.responses import RedirectResponse -from app.internal.utils import get_current_user from app.dependencies import get_db -from app.internal.google_connect import get_credentials, fetch_save_events +from app.internal.google_connect import fetch_save_events, get_credentials +from app.internal.utils import get_current_user from app.routers.profile import router as profile router = APIRouter( @@ -15,11 +15,13 @@ @router.get("/sync") -async def google_sync(request: Request, - session=Depends(get_db)) -> RedirectResponse: - '''Sync with Google - if user never synced with google this funcion will take +async def google_sync( + request: Request, + session=Depends(get_db), +) -> RedirectResponse: + """Sync with Google - if user never synced with google this funcion will take the user to a consent screen to use his google calendar data with the app. - ''' + """ user = get_current_user(session) # getting active user @@ -33,5 +35,5 @@ async def google_sync(request: Request, # fetch and save the events com from Google Calendar fetch_save_events(credentials=credentials, user=user, session=session) - url = profile.url_path_for('profile') + url = profile.url_path_for("profile") return RedirectResponse(url=url) diff --git a/app/routers/joke.py b/app/routers/joke.py index 07b7b453..f35dfae9 100644 --- a/app/routers/joke.py +++ b/app/routers/joke.py @@ -1,8 +1,8 @@ from fastapi import APIRouter, Depends, Request -from app.internal import jokes from sqlalchemy.orm import Session -from app.dependencies import get_db +from app.dependencies import get_db +from app.internal import jokes router = APIRouter() diff --git a/app/routers/login.py b/app/routers/login.py index 59645520..d4d11400 100644 --- a/app/routers/login.py +++ b/app/routers/login.py @@ -5,8 +5,8 @@ from starlette.responses import RedirectResponse from app.dependencies import get_db, templates -from app.internal.security.ouath2 import authenticate_user, create_jwt_token from app.internal.security import schema +from app.internal.security.ouath2 import authenticate_user, create_jwt_token from app.internal.utils import safe_redirect_response router = APIRouter( diff --git a/app/routers/logout.py b/app/routers/logout.py index 009de760..4f52825f 100644 --- a/app/routers/logout.py +++ b/app/routers/logout.py @@ -2,7 +2,6 @@ from starlette.responses import RedirectResponse from starlette.status import HTTP_302_FOUND - router = APIRouter( prefix="", tags=["/logout"], @@ -10,7 +9,7 @@ ) -@router.get('/logout') +@router.get("/logout") async def logout(request: Request): response = RedirectResponse(url="/login", status_code=HTTP_302_FOUND) response.delete_cookie("Authorization") diff --git a/app/routers/meds.py b/app/routers/meds.py index 53761eb6..d9449706 100644 --- a/app/routers/meds.py +++ b/app/routers/meds.py @@ -12,34 +12,35 @@ from app.internal.utils import get_current_user from app.main import app - router = APIRouter( - prefix='/meds', - tags=['meds'], + prefix="/meds", + tags=["meds"], dependencies=[Depends(get_db)], ) -@router.get('/') -@router.post('/') -async def medications(request: Request, - session: Session = Depends(get_db)) -> Response: +@router.get("/") +@router.post("/") +async def medications( + request: Request, + session: Session = Depends(get_db), +) -> Response: """Renders medication reminders creation form page. Creates reminders in DB and redirects to home page upon submition if valid.""" form = await request.form() errors = [] form_data = { - 'name': '', - 'start': date.today(), - 'first': None, - 'end': date.today() + timedelta(days=7), - 'amount': 1, - 'early': time(8), - 'late': time(22), - 'min': time(0, 1), - 'max': time(23, 59), - 'note': '', + "name": "", + "start": date.today(), + "first": None, + "end": date.today() + timedelta(days=7), + "amount": 1, + "early": time(8), + "late": time(22), + "min": time(0, 1), + "max": time(23, 59), + "note": "", } if form: @@ -48,12 +49,17 @@ async def medications(request: Request, errors = meds.validate_form(form) if not errors: meds.create_events(session, user.id, form) - return RedirectResponse(app.url_path_for('home'), - status_code=HTTP_303_SEE_OTHER) - - return templates.TemplateResponse('meds.j2', { - 'request': request, - 'errors': errors, - 'data': form_data, - 'quantity': meds.MAX_EVENT_QUANTITY, - }) + return RedirectResponse( + app.url_path_for("home"), + status_code=HTTP_303_SEE_OTHER, + ) + + return templates.TemplateResponse( + "meds.j2", + { + "request": request, + "errors": errors, + "data": form_data, + "quantity": meds.MAX_EVENT_QUANTITY, + }, + ) diff --git a/app/routers/notification.py b/app/routers/notification.py index 74b51102..540016a5 100644 --- a/app/routers/notification.py +++ b/app/routers/notification.py @@ -13,7 +13,6 @@ raise_wrong_id_error, ) from app.internal.security.dependencies import current_user, is_logged_in - from app.internal.security.schema import CurrentUser from app.internal.utils import safe_redirect_response diff --git a/app/routers/profile.py b/app/routers/profile.py index e1473048..5a29d8ca 100644 --- a/app/routers/profile.py +++ b/app/routers/profile.py @@ -1,18 +1,20 @@ import io -from loguru import logger from fastapi import APIRouter, Depends, File, Request, UploadFile +from loguru import logger from PIL import Image +from sqlalchemy.exc import SQLAlchemyError from starlette.responses import RedirectResponse from starlette.status import HTTP_302_FOUND -from sqlalchemy.exc import SQLAlchemyError from app import config from app.database.models import User -from app.dependencies import get_db, MEDIA_PATH, templates, GOOGLE_ERROR +from app.dependencies import GOOGLE_ERROR, MEDIA_PATH, get_db, templates +from app.internal.import_holidays import ( + get_holidays_from_file, + save_holidays_to_db, +) from app.internal.on_this_day_events import get_on_this_day_events -from app.internal.import_holidays import (get_holidays_from_file, - save_holidays_to_db) from app.internal.privacy import PrivacyKinds PICTURE_EXTENSION = config.PICTURE_EXTENSION @@ -27,20 +29,21 @@ def get_placeholder_user(): return User( - username='new_user', - email='my@email.po', - password='1a2s3d4f5g6', - full_name='My Name', + username="new_user", + email="my@email.po", + password="1a2s3d4f5g6", + full_name="My Name", language_id=1, - telegram_id='', + telegram_id="", ) @router.get("/") async def profile( - request: Request, - session=Depends(get_db), - new_user=Depends(get_placeholder_user)): + request: Request, + session=Depends(get_db), + new_user=Depends(get_placeholder_user), +): # Get relevant data from database upcoming_events = range(5) user = session.query(User).filter_by(id=1).first() @@ -49,28 +52,41 @@ async def profile( session.commit() user = session.query(User).filter_by(id=1).first() - signs = ['Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', - 'Virgo', 'Libra', 'Scorpio', 'Sagittarius', - 'Capricorn', 'Aquarius', 'Pisces'] + signs = [ + "Aries", + "Taurus", + "Gemini", + "Cancer", + "Leo", + "Virgo", + "Libra", + "Scorpio", + "Sagittarius", + "Capricorn", + "Aquarius", + "Pisces", + ] on_this_day_data = get_on_this_day_events(session) - return templates.TemplateResponse("profile.html", { - "request": request, - "user": user, - "events": upcoming_events, - "signs": signs, - "google_error": GOOGLE_ERROR, - "on_this_day_data": on_this_day_data, - "privacy": PrivacyKinds - }) + return templates.TemplateResponse( + "profile.html", + { + "request": request, + "user": user, + "events": upcoming_events, + "signs": signs, + "google_error": GOOGLE_ERROR, + "on_this_day_data": on_this_day_data, + "privacy": PrivacyKinds, + }, + ) @router.post("/update_user_fullname") -async def update_user_fullname( - request: Request, session=Depends(get_db)): +async def update_user_fullname(request: Request, session=Depends(get_db)): user = session.query(User).filter_by(id=1).first() data = await request.form() - new_fullname = data['fullname'] + new_fullname = data["fullname"] # Update database user.full_name = new_fullname @@ -81,11 +97,10 @@ async def update_user_fullname( @router.post("/update_user_email") -async def update_user_email( - request: Request, session=Depends(get_db)): +async def update_user_email(request: Request, session=Depends(get_db)): user = session.query(User).filter_by(id=1).first() data = await request.form() - new_email = data['email'] + new_email = data["email"] # Update database user.email = new_email @@ -96,11 +111,10 @@ async def update_user_email( @router.post("/update_user_description") -async def update_profile( - request: Request, session=Depends(get_db)): +async def update_profile(request: Request, session=Depends(get_db)): user = session.query(User).filter_by(id=1).first() data = await request.form() - new_description = data['description'] + new_description = data["description"] # Update database user.description = new_description @@ -112,7 +126,9 @@ async def update_profile( @router.post("/upload_user_photo") async def upload_user_photo( - file: UploadFile = File(...), session=Depends(get_db)): + file: UploadFile = File(...), + session=Depends(get_db), +): user = session.query(User).filter_by(id=1).first() pic = await file.read() @@ -127,11 +143,10 @@ async def upload_user_photo( @router.post("/update_telegram_id") -async def update_telegram_id( - request: Request, session=Depends(get_db)): +async def update_telegram_id(request: Request, session=Depends(get_db)): user = session.query(User).filter_by(id=1).first() data = await request.form() - new_telegram_id = data['telegram_id'] + new_telegram_id = data["telegram_id"] # Update database user.telegram_id = new_telegram_id @@ -142,13 +157,10 @@ async def update_telegram_id( @router.post("/privacy") -async def update_calendar_privacy( - request: Request, - session=Depends(get_db) -): +async def update_calendar_privacy(request: Request, session=Depends(get_db)): user = session.query(User).filter_by(id=1).first() data = await request.form() - new_privacy = data['privacy'] + new_privacy = data["privacy"] # Update database user.privacy = new_privacy @@ -160,9 +172,12 @@ async def update_calendar_privacy( @router.get("/holidays/import") def import_holidays(request: Request): - return templates.TemplateResponse("import_holidays.html", { - "request": request, - }) + return templates.TemplateResponse( + "import_holidays.html", + { + "request": request, + }, + ) async def process_image(image, user): @@ -185,8 +200,7 @@ def get_image_crop_area(width, height): @router.post("/holidays/update") -async def update( - file: UploadFile = File(...), session=Depends(get_db)): +async def update(file: UploadFile = File(...), session=Depends(get_db)): icsfile = await file.read() holidays = get_holidays_from_file(icsfile.decode(), session) try: diff --git a/app/routers/register.py b/app/routers/register.py index 2bd8c4bf..11927359 100644 --- a/app/routers/register.py +++ b/app/routers/register.py @@ -7,10 +7,9 @@ from starlette.status import HTTP_302_FOUND from starlette.templating import _TemplateResponse -from app.internal.security.ouath2 import get_hashed_password -from app.database import schemas -from app.database import models +from app.database import models, schemas from app.dependencies import get_db, templates +from app.internal.security.ouath2 import get_hashed_password from app.internal.utils import save router = APIRouter( diff --git a/app/routers/weekview.py b/app/routers/weekview.py index efac161a..9b4f2bb9 100644 --- a/app/routers/weekview.py +++ b/app/routers/weekview.py @@ -7,12 +7,13 @@ from sqlalchemy.orm.session import Session from app.database.models import Event, User -from app.dependencies import get_db, TEMPLATES_PATH +from app.dependencies import TEMPLATES_PATH, get_db from app.routers.dayview import ( - DivAttributes, dayview, get_events_and_attributes + DivAttributes, + dayview, + get_events_and_attributes, ) - templates = Jinja2Templates(directory=TEMPLATES_PATH) @@ -32,30 +33,38 @@ def get_week_dates(firstday: datetime) -> Iterator[datetime]: async def get_day_events_and_attributes( - request: Request, day: datetime, session: Session, user: User, - ) -> DayEventsAndAttrs: + request: Request, + day: datetime, + session: Session, + user: User, +) -> DayEventsAndAttrs: template = await dayview( request=request, - date=day.strftime('%Y-%m-%d'), - view='week', - session=session + date=day.strftime("%Y-%m-%d"), + view="week", + session=session, ) events_and_attrs = get_events_and_attributes( - day=day, session=session, user_id=user.id) + day=day, + session=session, + user_id=user.id, + ) return DayEventsAndAttrs(day, template, events_and_attrs) -@router.get('/week/{firstday}') -async def weekview( - request: Request, firstday: str, session=Depends(get_db) - ): - user = session.query(User).filter_by(username='test_username').first() - firstday = datetime.strptime(firstday, '%Y-%m-%d') +@router.get("/week/{firstday}") +async def weekview(request: Request, firstday: str, session=Depends(get_db)): + user = session.query(User).filter_by(username="test_username").first() + firstday = datetime.strptime(firstday, "%Y-%m-%d") week_days = get_week_dates(firstday) - week = [await get_day_events_and_attributes( - request, day, session, user - ) for day in week_days] - return templates.TemplateResponse("weekview.html", { - "request": request, - "week": week, - }) + week = [ + await get_day_events_and_attributes(request, day, session, user) + for day in week_days + ] + return templates.TemplateResponse( + "weekview.html", + { + "request": request, + "week": week, + }, + ) diff --git a/app/routers/weight.py b/app/routers/weight.py index 058f72d6..9f35a5dc 100644 --- a/app/routers/weight.py +++ b/app/routers/weight.py @@ -5,44 +5,44 @@ from starlette.responses import RedirectResponse from app.database.models import User -from app.dependencies import get_db -from app.dependencies import templates +from app.dependencies import get_db, templates - -router = APIRouter(tags=["weight"],) +router = APIRouter( + tags=["weight"], +) @router.get("/weight") async def get_weight( - request: Request, - session: Session = Depends(get_db), - target: Union[float, None] = None, - current_weight: Union[float, None] = None, - ): + request: Request, + session: Session = Depends(get_db), + target: Union[float, None] = None, + current_weight: Union[float, None] = None, +): # TODO Waiting for user registration user_id = 1 user = session.query(User).filter_by(id=user_id).first() target = user.target_weight if current_weight: - return RedirectResponse(url='/') - return templates.TemplateResponse("weight.html", { - "request": request, - "target": target, - "current_weight": current_weight, - } + return RedirectResponse(url="/") + return templates.TemplateResponse( + "weight.html", + { + "request": request, + "target": target, + "current_weight": current_weight, + }, ) @router.post("/weight") -async def weight( - request: Request, - session: Session = Depends(get_db)): +async def weight(request: Request, session: Session = Depends(get_db)): user_id = 1 user = session.query(User).filter_by(id=user_id).first() data = await request.form() - target = data['target'] - current_weight = data['current_weight'] + target = data["target"] + current_weight = data["current_weight"] if target: user.target_weight = target session.commit() @@ -60,10 +60,12 @@ async def weight( else: way_message = f"Great! You have reached your goal: {target} Kg" - return templates.TemplateResponse("weight.html", { - "request": request, - "target": target, - "current_weight": current_weight, - "way_message": way_message - } + return templates.TemplateResponse( + "weight.html", + { + "request": request, + "target": target, + "current_weight": current_weight, + "way_message": way_message, + }, ) diff --git a/app/routers/whatsapp.py b/app/routers/whatsapp.py index cbd1e254..1d7a625c 100644 --- a/app/routers/whatsapp.py +++ b/app/routers/whatsapp.py @@ -1,7 +1,7 @@ from typing import Optional +from urllib.parse import urlencode from fastapi import APIRouter -from urllib.parse import urlencode router = APIRouter(tags=["utils"]) @@ -19,7 +19,7 @@ def make_link(phone_number: Optional[str], message: Optional[str]) -> str: Returns: A WhatsApp message URL. """ - api = 'https://api.whatsapp.com/send?' - url_query = {'phone': phone_number, 'text': message} + api = "https://api.whatsapp.com/send?" + url_query = {"phone": phone_number, "text": message} link = api + urlencode(url_query) return link diff --git a/app/telegram/bot.py b/app/telegram/bot.py index 3bfe15ef..83fc0517 100644 --- a/app/telegram/bot.py +++ b/app/telegram/bot.py @@ -2,6 +2,7 @@ from app import config from app.dependencies import get_settings + from .models import Bot settings: config.Settings = get_settings() diff --git a/app/telegram/handlers.py b/app/telegram/handlers.py index 8b2a6719..d6619c93 100644 --- a/app/telegram/handlers.py +++ b/app/telegram/handlers.py @@ -6,10 +6,16 @@ from app.database.models import User from app.dependencies import get_db from app.routers.event import create_event + from .bot import telegram_bot from .keyboards import ( - DATE_FORMAT, field_kb, gen_inline_keyboard, - get_this_week_buttons, new_event_kb, show_events_kb) + DATE_FORMAT, + field_kb, + gen_inline_keyboard, + get_this_week_buttons, + new_event_kb, + show_events_kb, +) from .models import Chat @@ -18,21 +24,22 @@ def __init__(self, chat: Chat, user: User): self.chat = chat self.user = user self.handlers = {} - self.handlers['/start'] = self.start_handler - self.handlers['/show_events'] = self.show_events_handler - self.handlers['/new_event'] = self.new_event_handler - self.handlers['Today'] = self.today_handler - self.handlers['This week'] = self.this_week_handler + self.handlers["/start"] = self.start_handler + self.handlers["/show_events"] = self.show_events_handler + self.handlers["/new_event"] = self.new_event_handler + self.handlers["Today"] = self.today_handler + self.handlers["This week"] = self.this_week_handler # Add next 6 days to handlers dict for row in get_this_week_buttons(): for button in row: - self.handlers[button['text']] = self.chosen_day_handler + self.handlers[button["text"]] = self.chosen_day_handler async def process_callback(self): if self.chat.user_id in telegram_bot.MEMORY: return await self.process_new_event( - telegram_bot.MEMORY[self.chat.user_id]) + telegram_bot.MEMORY[self.chat.user_id], + ) elif self.chat.message in self.handlers: return await self.handlers[self.chat.message]() return await self.default_handler() @@ -43,118 +50,121 @@ async def default_handler(self): return answer async def start_handler(self): - answer = f'''Hello, {self.chat.first_name}! -Welcome to PyLendar telegram client!''' + answer = f"""Hello, {self.chat.first_name}! +Welcome to PyLendar telegram client!""" await telegram_bot.send_message(chat_id=self.chat.user_id, text=answer) return answer async def show_events_handler(self): - answer = 'Choose events day.' + answer = "Choose events day." await telegram_bot.send_message( chat_id=self.chat.user_id, text=answer, - reply_markup=show_events_kb) + reply_markup=show_events_kb, + ) return answer async def today_handler(self): today = datetime.datetime.today() events = [ - _.events for _ in self.user.events - if _.events.start <= today <= _.events.end] + _.events + for _ in self.user.events + if _.events.start <= today <= _.events.end + ] if not events: return await self._process_no_events_today() answer = f"{today.strftime('%A, %B %d')}:\n" - await telegram_bot.send_message( - chat_id=self.chat.user_id, text=answer) + await telegram_bot.send_message(chat_id=self.chat.user_id, text=answer) for event in events: await self._send_event(event) return answer async def _process_no_events_today(self): answer = "There're no events today." - await telegram_bot.send_message( - chat_id=self.chat.user_id, text=answer) + await telegram_bot.send_message(chat_id=self.chat.user_id, text=answer) return answer async def this_week_handler(self): - answer = 'Choose a day.' + answer = "Choose a day." this_week_kb = gen_inline_keyboard(get_this_week_buttons()) await telegram_bot.send_message( chat_id=self.chat.user_id, text=answer, - reply_markup=this_week_kb) + reply_markup=this_week_kb, + ) return answer async def chosen_day_handler(self): chosen_date = datetime.datetime.strptime( - self.chat.message, DATE_FORMAT) + self.chat.message, + DATE_FORMAT, + ) events = [ - _.events for _ in self.user.events - if _.events.start <= chosen_date <= _.events.end] + _.events + for _ in self.user.events + if _.events.start <= chosen_date <= _.events.end + ] if not events: return await self._process_no_events_on_date(chosen_date) answer = f"{chosen_date.strftime('%A, %B %d')}:\n" - await telegram_bot.send_message( - chat_id=self.chat.user_id, text=answer) + await telegram_bot.send_message(chat_id=self.chat.user_id, text=answer) for event in events: await self._send_event(event) return answer async def _process_no_events_on_date(self, date): answer = f"There're no events on {date.strftime('%B %d')}." - await telegram_bot.send_message( - chat_id=self.chat.user_id, text=answer) + await telegram_bot.send_message(chat_id=self.chat.user_id, text=answer) return answer async def _send_event(self, event): start = event.start.strftime("%d %b %Y %H:%M") end = event.end.strftime("%d %b %Y %H:%M") - text = f'Title:\n{event.title}\n\n' - text += f'Content:\n{event.content}\n\n' - text += f'Location:\n{event.location}\n\n' - text += f'Starts on:\n{start}\n\n' - text += f'Ends on:\n{end}' - await telegram_bot.send_message( - chat_id=self.chat.user_id, text=text) + text = f"Title:\n{event.title}\n\n" + text += f"Content:\n{event.content}\n\n" + text += f"Location:\n{event.location}\n\n" + text += f"Starts on:\n{start}\n\n" + text += f"Ends on:\n{end}" + await telegram_bot.send_message(chat_id=self.chat.user_id, text=text) await asyncio.sleep(1) async def process_new_event(self, memo_dict): - if self.chat.message == 'cancel': + if self.chat.message == "cancel": return await self._cancel_new_event_processing() - elif self.chat.message == 'restart': + elif self.chat.message == "restart": return await self._restart_new_event_processing() - elif 'title' not in memo_dict: + elif "title" not in memo_dict: return await self._process_title(memo_dict) - elif 'content' not in memo_dict: + elif "content" not in memo_dict: return await self._process_content(memo_dict) - elif 'location' not in memo_dict: + elif "location" not in memo_dict: return await self._process_location(memo_dict) - elif 'start' not in memo_dict: + elif "start" not in memo_dict: return await self._process_start_date(memo_dict) - elif 'end' not in memo_dict: + elif "end" not in memo_dict: return await self._process_end_date(memo_dict) - elif self.chat.message == 'create': + elif self.chat.message == "create": return await self._submit_new_event(memo_dict) async def new_event_handler(self): telegram_bot.MEMORY[self.chat.user_id] = {} - answer = 'Please, give your event a title.' + answer = "Please, give your event a title." await telegram_bot.send_message( chat_id=self.chat.user_id, text=answer, - reply_markup=field_kb) + reply_markup=field_kb, + ) return answer async def _cancel_new_event_processing(self): del telegram_bot.MEMORY[self.chat.user_id] - answer = '🚫 The process was canceled.' - await telegram_bot.send_message( - chat_id=self.chat.user_id, text=answer) + answer = "🚫 The process was canceled." + await telegram_bot.send_message(chat_id=self.chat.user_id, text=answer) return answer async def _restart_new_event_processing(self): @@ -162,33 +172,36 @@ async def _restart_new_event_processing(self): return answer async def _process_title(self, memo_dict): - memo_dict['title'] = self.chat.message + memo_dict["title"] = self.chat.message answer = f'Title:\n{memo_dict["title"]}\n\n' - answer += 'Add a description of the event.' + answer += "Add a description of the event." await telegram_bot.send_message( chat_id=self.chat.user_id, text=answer, - reply_markup=field_kb) + reply_markup=field_kb, + ) return answer async def _process_content(self, memo_dict): - memo_dict['content'] = self.chat.message + memo_dict["content"] = self.chat.message answer = f'Content:\n{memo_dict["content"]}\n\n' - answer += 'Where the event will be held?' + answer += "Where the event will be held?" await telegram_bot.send_message( chat_id=self.chat.user_id, text=answer, - reply_markup=field_kb) + reply_markup=field_kb, + ) return answer async def _process_location(self, memo_dict): - memo_dict['location'] = self.chat.message + memo_dict["location"] = self.chat.message answer = f'Location:\n{memo_dict["location"]}\n\n' - answer += 'When does it start?' + answer += "When does it start?" await telegram_bot.send_message( chat_id=self.chat.user_id, text=answer, - reply_markup=field_kb) + reply_markup=field_kb, + ) return answer async def _process_start_date(self, memo_dict): @@ -198,21 +211,23 @@ async def _process_start_date(self, memo_dict): return await self._process_bad_date_input() async def _add_start_date(self, memo_dict, date): - memo_dict['start'] = date + memo_dict["start"] = date answer = f'Starts on:\n{date.strftime("%d %b %Y %H:%M")}\n\n' - answer += 'And when does it end?' + answer += "And when does it end?" await telegram_bot.send_message( chat_id=self.chat.user_id, text=answer, - reply_markup=field_kb) + reply_markup=field_kb, + ) return answer async def _process_bad_date_input(self): - answer = '❗️ Please, enter a valid date/time.' + answer = "❗️ Please, enter a valid date/time." await telegram_bot.send_message( chat_id=self.chat.user_id, text=answer, - reply_markup=field_kb) + reply_markup=field_kb, + ) return answer async def _process_end_date(self, memo_dict): @@ -222,32 +237,32 @@ async def _process_end_date(self, memo_dict): return await self._process_bad_date_input() async def _add_end_date(self, memo_dict, date): - memo_dict['end'] = date + memo_dict["end"] = date start_time = memo_dict["start"].strftime("%d %b %Y %H:%M") answer = f'Title:\n{memo_dict["title"]}\n\n' answer += f'Content:\n{memo_dict["content"]}\n\n' answer += f'Location:\n{memo_dict["location"]}\n\n' - answer += f'Starts on:\n{start_time}\n\n' + answer += f"Starts on:\n{start_time}\n\n" answer += f'Ends on:\n{date.strftime("%d %b %Y %H:%M")}' await telegram_bot.send_message( chat_id=self.chat.user_id, text=answer, - reply_markup=new_event_kb) + reply_markup=new_event_kb, + ) return answer async def _submit_new_event(self, memo_dict): - answer = 'New event was successfully created 🎉' - await telegram_bot.send_message( - chat_id=self.chat.user_id, text=answer) + answer = "New event was successfully created 🎉" + await telegram_bot.send_message(chat_id=self.chat.user_id, text=answer) # Save to database create_event( db=next(get_db()), - title=memo_dict['title'], - start=memo_dict['start'], - end=memo_dict['end'], - content=memo_dict['content'], + title=memo_dict["title"], + start=memo_dict["start"], + end=memo_dict["end"], + content=memo_dict["content"], owner_id=self.user.id, - location=memo_dict['location'], + location=memo_dict["location"], ) # Delete current session del telegram_bot.MEMORY[self.chat.user_id] @@ -255,7 +270,7 @@ async def _submit_new_event(self, memo_dict): async def reply_unknown_user(chat): - answer = f''' + answer = f""" Hello, {chat.first_name}! To use PyLendar Bot you have to register @@ -265,6 +280,6 @@ async def reply_unknown_user(chat): Keep it secret! https://calendar.pythonic.guru/profile/ -''' +""" await telegram_bot.send_message(chat_id=chat.user_id, text=answer) return answer diff --git a/tests/conftest.py b/tests/conftest.py index ab9d02c6..9cc2cf02 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,11 +2,11 @@ import nest_asyncio import pytest -from app.config import PSQL_ENVIRONMENT -from app.database.models import Base from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker +from app.config import PSQL_ENVIRONMENT +from app.database.models import Base pytest_plugins = [ "tests.fixtures.user_fixture", diff --git a/tests/fixtures/asyncio_fixture.py b/tests/fixtures/asyncio_fixture.py index 7f567b3b..e1553250 100644 --- a/tests/fixtures/asyncio_fixture.py +++ b/tests/fixtures/asyncio_fixture.py @@ -1,14 +1,14 @@ from datetime import datetime, timedelta -from httpx import AsyncClient import pytest +from httpx import AsyncClient from app.database.models import Base from app.main import app from app.routers import telegram from app.routers.event import create_event -from tests.fixtures.client_fixture import get_test_placeholder_user from tests.conftest import get_test_db, test_engine +from tests.fixtures.client_fixture import get_test_placeholder_user @pytest.fixture diff --git a/tests/fixtures/client_fixture.py b/tests/fixtures/client_fixture.py index 7a5d1e3c..a42a0af6 100644 --- a/tests/fixtures/client_fixture.py +++ b/tests/fixtures/client_fixture.py @@ -1,4 +1,4 @@ -from typing import Generator, Iterator, Dict +from typing import Dict, Generator, Iterator import pytest from fastapi.testclient import TestClient @@ -6,7 +6,6 @@ from app import main from app.database.models import Base, User - from app.routers import ( agenda, audio, diff --git a/tests/fixtures/logger_fixture.py b/tests/fixtures/logger_fixture.py index f6102f80..e3f488f7 100644 --- a/tests/fixtures/logger_fixture.py +++ b/tests/fixtures/logger_fixture.py @@ -1,21 +1,23 @@ import logging +import pytest from _pytest.logging import caplog as _caplog # noqa: F401 from loguru import logger -import pytest from app import config from app.internal.logger_customizer import LoggerCustomizer -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def logger_instance(): - _logger = LoggerCustomizer.make_logger(config.LOG_PATH, - config.LOG_FILENAME, - config.LOG_LEVEL, - config.LOG_ROTATION_INTERVAL, - config.LOG_RETENTION_INTERVAL, - config.LOG_FORMAT) + _logger = LoggerCustomizer.make_logger( + config.LOG_PATH, + config.LOG_FILENAME, + config.LOG_LEVEL, + config.LOG_ROTATION_INTERVAL, + config.LOG_RETENTION_INTERVAL, + config.LOG_FORMAT, + ) return _logger diff --git a/tests/meds/test_routers.py b/tests/meds/test_routers.py index f14c3eb2..caa34785 100644 --- a/tests/meds/test_routers.py +++ b/tests/meds/test_routers.py @@ -6,31 +6,34 @@ from app.database.models import Event from app.routers import meds -from tests.meds.test_internal import create_test_form, WEB_FORM - +from tests.meds.test_internal import WEB_FORM, create_test_form PYLENDAR = [ (WEB_FORM, True), - (create_test_form(form_dict=True, end='1985-10-26'), False), + (create_test_form(form_dict=True, end="1985-10-26"), False), ] def test_meds_page_returns_ok(meds_test_client: TestClient) -> None: - path = meds.router.url_path_for('medications') + path = meds.router.url_path_for("medications") response = meds_test_client.get(path) assert response.ok -@pytest.mark.parametrize('form, pylendar', PYLENDAR) -def test_meds_send_form_success(meds_test_client: TestClient, session: Session, - form: Dict[str, str], pylendar: bool) -> None: +@pytest.mark.parametrize("form, pylendar", PYLENDAR) +def test_meds_send_form_success( + meds_test_client: TestClient, + session: Session, + form: Dict[str, str], + pylendar: bool, +) -> None: assert session.query(Event).first() is None - path = meds.router.url_path_for('medications') + path = meds.router.url_path_for("medications") response = meds_test_client.post(path, data=form, allow_redirects=True) assert response.ok - message = 'PyLendar' in response.text + message = "PyLendar" in response.text assert message is pylendar - message = 'alert' in response.text + message = "alert" in response.text assert message is not pylendar event = session.query(Event).first() if pylendar: diff --git a/tests/salary/conftest.py b/tests/salary/conftest.py index 0cffee6d..51c77288 100644 --- a/tests/salary/conftest.py +++ b/tests/salary/conftest.py @@ -7,34 +7,33 @@ from app.internal.utils import create_model, delete_instance from app.routers.salary import config from app.routers.salary.routes import router -from tests.conftest import get_test_db -from tests.conftest import test_engine +from tests.conftest import get_test_db, test_engine MESSAGES = { - 'create_settings': 'Already created your settings?', - 'pick_settings': 'Edit Settings', - 'edit_settings': 'Settings don\'t need editing?', - 'pick_category': 'View Salary', - 'view_salary': 'Need to alter your settings?', - 'salary_calc': 'Total Salary:', + "create_settings": "Already created your settings?", + "pick_settings": "Edit Settings", + "edit_settings": "Settings don't need editing?", + "pick_category": "View Salary", + "view_salary": "Need to alter your settings?", + "salary_calc": "Total Salary:", } ROUTES = { - 'home': router.url_path_for('salary_home'), - 'new': router.url_path_for('create_settings'), - 'edit_pick': router.url_path_for('pick_settings'), - 'edit': lambda x: router.url_path_for('edit_settings', category_id=x), - 'view_pick': router.url_path_for('pick_category'), - 'view': lambda x: router.url_path_for('view_salary', category_id=x), + "home": router.url_path_for("salary_home"), + "new": router.url_path_for("create_settings"), + "edit_pick": router.url_path_for("pick_settings"), + "edit": lambda x: router.url_path_for("edit_settings", category_id=x), + "view_pick": router.url_path_for("pick_category"), + "view": lambda x: router.url_path_for("view_salary", category_id=x), } CATEGORY_ID = 1 INVALID_CATEGORY_ID = 2 ALT_CATEGORY_ID = 42 -MONTH = '2021-01' +MONTH = "2021-01" -@pytest.fixture(scope='package') +@pytest.fixture(scope="package") def salary_session() -> Iterator[Session]: Base.metadata.create_all(bind=test_engine) session = get_test_db() @@ -46,18 +45,21 @@ def salary_session() -> Iterator[Session]: @pytest.fixture def salary_user(salary_session: Session): test_user = create_model( - salary_session, User, - username='test_username', - password='test_password', - email='test.email@gmail.com', + salary_session, + User, + username="test_username", + password="test_password", + email="test.email@gmail.com", ) yield test_user delete_instance(salary_session, test_user) @pytest.fixture -def wage(salary_session: Session, - salary_user: User) -> Iterator[SalarySettings]: +def wage( + salary_session: Session, + salary_user: User, +) -> Iterator[SalarySettings]: wage = create_model( salary_session, SalarySettings, diff --git a/tests/salary/test_routes.py b/tests/salary/test_routes.py index 830569ff..c788071a 100644 --- a/tests/salary/test_routes.py +++ b/tests/salary/test_routes.py @@ -1,7 +1,7 @@ from unittest import mock -from fastapi import status import pytest +from fastapi import status from requests.sessions import Session from starlette.testclient import TestClient @@ -12,32 +12,40 @@ from tests.salary.test_utils import get_event_by_category PATHS = [ - (conftest.ROUTES['new']), - (conftest.ROUTES['edit_pick']), - (conftest.ROUTES['edit'](conftest.CATEGORY_ID)), - (conftest.ROUTES['view_pick']), - (conftest.ROUTES['view'](conftest.CATEGORY_ID)), + (conftest.ROUTES["new"]), + (conftest.ROUTES["edit_pick"]), + (conftest.ROUTES["edit"](conftest.CATEGORY_ID)), + (conftest.ROUTES["view_pick"]), + (conftest.ROUTES["view"](conftest.CATEGORY_ID)), ] EMPTY_PICKS = [ - (conftest.ROUTES['edit_pick']), - (conftest.ROUTES['view_pick']), + (conftest.ROUTES["edit_pick"]), + (conftest.ROUTES["view_pick"]), ] CATEGORY_PICK = [ - (conftest.ROUTES['edit_pick'], conftest.MESSAGES['edit_settings']), - (conftest.ROUTES['view_pick'], conftest.MESSAGES['view_salary']), + (conftest.ROUTES["edit_pick"], conftest.MESSAGES["edit_settings"]), + (conftest.ROUTES["view_pick"], conftest.MESSAGES["view_salary"]), ] INVALID = [ - (conftest.ROUTES['edit'](conftest.ALT_CATEGORY_ID), - conftest.MESSAGES['pick_settings']), - (conftest.ROUTES['view'](conftest.ALT_CATEGORY_ID), - conftest.MESSAGES['pick_category']), - (conftest.ROUTES['edit'](conftest.INVALID_CATEGORY_ID), - conftest.MESSAGES['pick_settings']), - (conftest.ROUTES['view'](conftest.INVALID_CATEGORY_ID), - conftest.MESSAGES['pick_category']), + ( + conftest.ROUTES["edit"](conftest.ALT_CATEGORY_ID), + conftest.MESSAGES["pick_settings"], + ), + ( + conftest.ROUTES["view"](conftest.ALT_CATEGORY_ID), + conftest.MESSAGES["pick_category"], + ), + ( + conftest.ROUTES["edit"](conftest.INVALID_CATEGORY_ID), + conftest.MESSAGES["pick_settings"], + ), + ( + conftest.ROUTES["view"](conftest.INVALID_CATEGORY_ID), + conftest.MESSAGES["pick_category"], + ), ] @@ -49,10 +57,10 @@ def get_current_user(salary_session: Session) -> User: def test_get_user_categories() -> None: # Code revision required after categories feature is added categories = { - 1: 'Workout', - 17: 'Flight', - 42: 'Going to the Movies', - 666: 'Lucy\'s Inferno', + 1: "Workout", + 17: "Flight", + 42: "Going to the Movies", + 666: "Lucy's Inferno", } assert routes.get_user_categories() == categories @@ -60,161 +68,201 @@ def test_get_user_categories() -> None: def test_get_holiday_categories() -> None: # Code revision required after holiday times feature is added holidays = { - 1: 'Israel - Jewish', - 3: 'Iraq - Muslim', - 17: 'Cuba - Santeria', - 666: 'Hell - Satanist', + 1: "Israel - Jewish", + 3: "Iraq - Muslim", + 17: "Cuba - Santeria", + 666: "Hell - Satanist", } assert routes.get_holiday_categories() == holidays -def test_get_salary_categories_empty(salary_session: Session, - salary_user: User) -> None: +def test_get_salary_categories_empty( + salary_session: Session, + salary_user: User, +) -> None: # Code revision required after categories feature is added assert routes.get_salary_categories(salary_session, salary_user.id) == {} -def test_get_salary_categories(salary_session: Session, - wage: SalarySettings) -> None: +def test_get_salary_categories( + salary_session: Session, + wage: SalarySettings, +) -> None: # Code revision required after categories feature is added - assert wage.category_id in routes.get_salary_categories(salary_session, - wage.user_id, True) + assert wage.category_id in routes.get_salary_categories( + salary_session, + wage.user_id, + True, + ) -def test_get_salary_categories_new(salary_session: Session, - wage: SalarySettings) -> None: +def test_get_salary_categories_new( + salary_session: Session, + wage: SalarySettings, +) -> None: # Code revision required after categories feature is added assert wage.category_id not in routes.get_salary_categories( - salary_session, wage.user_id, False) - - -@pytest.mark.parametrize('path', PATHS) -def test_pages_respond_ok(salary_test_client: TestClient, - wage: SalarySettings, path: str) -> None: + salary_session, + wage.user_id, + False, + ) + + +@pytest.mark.parametrize("path", PATHS) +def test_pages_respond_ok( + salary_test_client: TestClient, + wage: SalarySettings, + path: str, +) -> None: response = salary_test_client.get(path) assert response.ok -def test_home_page_redirects_to_new( - salary_test_client: TestClient) -> None: - response = salary_test_client.get(conftest.ROUTES['home']) +def test_home_page_redirects_to_new(salary_test_client: TestClient) -> None: + response = salary_test_client.get(conftest.ROUTES["home"]) assert response.ok - assert conftest.MESSAGES['create_settings'] in response.text + assert conftest.MESSAGES["create_settings"] in response.text -@mock.patch('app.routers.salary.routes.get_current_user', - new=get_current_user) -def test_home_page_redirects_to_view(salary_test_client: TestClient, - wage: SalarySettings) -> None: - response = salary_test_client.get(conftest.ROUTES['home']) +@mock.patch("app.routers.salary.routes.get_current_user", new=get_current_user) +def test_home_page_redirects_to_view( + salary_test_client: TestClient, + wage: SalarySettings, +) -> None: + response = salary_test_client.get(conftest.ROUTES["home"]) assert response.ok - assert conftest.MESSAGES['pick_category'] in response.text + assert conftest.MESSAGES["pick_category"] in response.text -@mock.patch('app.routers.salary.routes.get_current_user', - new=get_current_user) -def test_create_settings(salary_test_client: TestClient, - salary_session: Session, salary_user: User) -> None: +@mock.patch("app.routers.salary.routes.get_current_user", new=get_current_user) +def test_create_settings( + salary_test_client: TestClient, + salary_session: Session, + salary_user: User, +) -> None: category_id = conftest.CATEGORY_ID - assert utils.get_settings(salary_session, salary_user.id, - category_id) is None + assert ( + utils.get_settings(salary_session, salary_user.id, category_id) is None + ) data = { - 'category_id': category_id, - 'wage': utils.DEFAULT_SETTINGS.wage, - 'off_day': utils.DEFAULT_SETTINGS.off_day, - 'holiday_category_id': utils.DEFAULT_SETTINGS.holiday_category_id, - 'regular_hour_basis': utils.DEFAULT_SETTINGS.regular_hour_basis, - 'night_hour_basis': utils.DEFAULT_SETTINGS.night_hour_basis, - 'night_start': utils.DEFAULT_SETTINGS.night_start, - 'night_end': utils.DEFAULT_SETTINGS.night_end, - 'night_min_len': utils.DEFAULT_SETTINGS.night_min_len, - 'first_overtime_amount': utils.DEFAULT_SETTINGS.first_overtime_amount, - 'first_overtime_pay': utils.DEFAULT_SETTINGS.first_overtime_pay, - 'second_overtime_pay': utils.DEFAULT_SETTINGS.second_overtime_pay, - 'week_working_hours': utils.DEFAULT_SETTINGS.week_working_hours, - 'daily_transport': utils.DEFAULT_SETTINGS.daily_transport, + "category_id": category_id, + "wage": utils.DEFAULT_SETTINGS.wage, + "off_day": utils.DEFAULT_SETTINGS.off_day, + "holiday_category_id": utils.DEFAULT_SETTINGS.holiday_category_id, + "regular_hour_basis": utils.DEFAULT_SETTINGS.regular_hour_basis, + "night_hour_basis": utils.DEFAULT_SETTINGS.night_hour_basis, + "night_start": utils.DEFAULT_SETTINGS.night_start, + "night_end": utils.DEFAULT_SETTINGS.night_end, + "night_min_len": utils.DEFAULT_SETTINGS.night_min_len, + "first_overtime_amount": utils.DEFAULT_SETTINGS.first_overtime_amount, + "first_overtime_pay": utils.DEFAULT_SETTINGS.first_overtime_pay, + "second_overtime_pay": utils.DEFAULT_SETTINGS.second_overtime_pay, + "week_working_hours": utils.DEFAULT_SETTINGS.week_working_hours, + "daily_transport": utils.DEFAULT_SETTINGS.daily_transport, } response = salary_test_client.post( - conftest.ROUTES['new'], data=data, allow_redirects=True) + conftest.ROUTES["new"], + data=data, + allow_redirects=True, + ) assert response.ok - assert conftest.MESSAGES['view_salary'] in response.text + assert conftest.MESSAGES["view_salary"] in response.text settings = utils.get_settings(salary_session, salary_user.id, category_id) assert settings delete_instance(salary_session, settings) -@pytest.mark.parametrize('path', EMPTY_PICKS) -def test_empty_category_pick_redirects_to_new(salary_test_client: TestClient, - path: str) -> None: +@pytest.mark.parametrize("path", EMPTY_PICKS) +def test_empty_category_pick_redirects_to_new( + salary_test_client: TestClient, + path: str, +) -> None: response = salary_test_client.get(path) - assert any(temp.status_code == status.HTTP_307_TEMPORARY_REDIRECT - for temp in response.history) - - -@pytest.mark.parametrize('path, message', CATEGORY_PICK) -@mock.patch('app.routers.salary.routes.get_current_user', - new=get_current_user) -def test_pick_category(salary_test_client: TestClient, wage: SalarySettings, - path: str, message: str) -> None: - data = {'category_id': wage.category_id} + assert any( + temp.status_code == status.HTTP_307_TEMPORARY_REDIRECT + for temp in response.history + ) + + +@pytest.mark.parametrize("path, message", CATEGORY_PICK) +@mock.patch("app.routers.salary.routes.get_current_user", new=get_current_user) +def test_pick_category( + salary_test_client: TestClient, + wage: SalarySettings, + path: str, + message: str, +) -> None: + data = {"category_id": wage.category_id} response = salary_test_client.post(path, data=data, allow_redirects=True) assert message in response.text -@mock.patch('app.routers.salary.routes.get_current_user', - new=get_current_user) -def test_edit_settings(salary_test_client: TestClient, salary_session: Session, - wage: SalarySettings) -> None: +@mock.patch("app.routers.salary.routes.get_current_user", new=get_current_user) +def test_edit_settings( + salary_test_client: TestClient, + salary_session: Session, + wage: SalarySettings, +) -> None: category_id = wage.category_id settings = utils.get_settings(salary_session, wage.user_id, category_id) - route = conftest.ROUTES['edit'](category_id) + route = conftest.ROUTES["edit"](category_id) data = { - 'wage': wage.wage + 1, - 'off_day': wage.off_day, - 'holiday_category_id': wage.holiday_category_id, - 'regular_hour_basis': wage.regular_hour_basis, - 'night_hour_basis': wage.night_hour_basis, - 'night_start': wage.night_start, - 'night_end': wage.night_end, - 'night_min_len': wage.night_min_len, - 'first_overtime_amount': wage.first_overtime_amount, - 'first_overtime_pay': wage.first_overtime_pay, - 'second_overtime_pay': wage.second_overtime_pay, - 'week_working_hours': wage.week_working_hours, - 'daily_transport': wage.daily_transport, + "wage": wage.wage + 1, + "off_day": wage.off_day, + "holiday_category_id": wage.holiday_category_id, + "regular_hour_basis": wage.regular_hour_basis, + "night_hour_basis": wage.night_hour_basis, + "night_start": wage.night_start, + "night_end": wage.night_end, + "night_min_len": wage.night_min_len, + "first_overtime_amount": wage.first_overtime_amount, + "first_overtime_pay": wage.first_overtime_pay, + "second_overtime_pay": wage.second_overtime_pay, + "week_working_hours": wage.week_working_hours, + "daily_transport": wage.daily_transport, } response = salary_test_client.post(route, data=data, allow_redirects=True) assert response.ok - assert conftest.MESSAGES['view_salary'] in response.text - assert settings != utils.get_settings(salary_session, wage.user_id, - wage.category_id) + assert conftest.MESSAGES["view_salary"] in response.text + assert settings != utils.get_settings( + salary_session, + wage.user_id, + wage.category_id, + ) -@pytest.mark.parametrize('path, message', INVALID) -@mock.patch('app.routers.salary.routes.get_current_user', - new=get_current_user) +@pytest.mark.parametrize("path, message", INVALID) +@mock.patch("app.routers.salary.routes.get_current_user", new=get_current_user) def test_invalid_category_redirect( - salary_test_client: TestClient, wage: SalarySettings, path: str, - message: str) -> None: + salary_test_client: TestClient, + wage: SalarySettings, + path: str, + message: str, +) -> None: response = salary_test_client.get(path) - assert any(temp.status_code == status.HTTP_307_TEMPORARY_REDIRECT - for temp in response.history) + assert any( + temp.status_code == status.HTTP_307_TEMPORARY_REDIRECT + for temp in response.history + ) assert message in response.text -@mock.patch('app.routers.salary.routes.get_current_user', - new=get_current_user) -@mock.patch('app.routers.salary.utils.get_event_by_category', - new=get_event_by_category) -def test_view_salary(salary_test_client: TestClient, - wage: SalarySettings) -> None: - route = (conftest.ROUTES['view'](wage.category_id)) +@mock.patch("app.routers.salary.routes.get_current_user", new=get_current_user) +@mock.patch( + "app.routers.salary.utils.get_event_by_category", + new=get_event_by_category, +) +def test_view_salary( + salary_test_client: TestClient, + wage: SalarySettings, +) -> None: + route = conftest.ROUTES["view"](wage.category_id) data = { - 'month': conftest.MONTH, - 'bonus': 1000, - 'deduction': 1000, - 'overtime': True + "month": conftest.MONTH, + "bonus": 1000, + "deduction": 1000, + "overtime": True, } response = salary_test_client.post(route, data=data) assert response.ok - assert conftest.MESSAGES['salary_calc'] in response.text + assert conftest.MESSAGES["salary_calc"] in response.text diff --git a/tests/security_testing_routes.py b/tests/security_testing_routes.py index 6b9e7128..36f95732 100644 --- a/tests/security_testing_routes.py +++ b/tests/security_testing_routes.py @@ -1,16 +1,17 @@ from fastapi import APIRouter, Depends, Request from app.internal.security.dependencies import ( - current_user, current_user_from_db, - is_logged_in, is_manager, User + User, + current_user, + current_user_from_db, + is_logged_in, + is_manager, ) +# These routes are for security testing. +# They represent an example for how to use +# security dependencies in other routes. -""" -These routes are for security testing. -They represent an example for how to use -security dependencies in other routes. -""" router = APIRouter( prefix="", tags=["/security"], @@ -18,9 +19,8 @@ ) -@router.get('/is_logged_in') -async def is_logged_in( - request: Request, user: bool = Depends(is_logged_in)): +@router.get("/is_logged_in") +async def is_logged_in(request: Request, user: bool = Depends(is_logged_in)): """This is how to protect route for logged in user only. Dependency will return True. if user not looged-in, will be redirected to login route. @@ -28,9 +28,8 @@ async def is_logged_in( return {"user": user} -@router.get('/is_manager') -async def is_manager( - request: Request, user: bool = Depends(is_manager)): +@router.get("/is_manager") +async def is_manager(request: Request, user: bool = Depends(is_manager)): """This is how to protect route for logged in manager only. Dependency will return True. if user not looged-in, or have no manager permission, @@ -39,9 +38,11 @@ async def is_manager( return {"manager": user} -@router.get('/current_user_from_db') +@router.get("/current_user_from_db") async def current_user_from_db( - request: Request, user: User = Depends(current_user_from_db)): + request: Request, + user: User = Depends(current_user_from_db), +): """This is how to protect route for logged in user only. Dependency will return User object. if user not looged-in, will be redirected to login route. @@ -49,9 +50,8 @@ async def current_user_from_db( return {"user": user.username} -@router.get('/current_user') -async def current_user( - request: Request, user: User = Depends(current_user)): +@router.get("/current_user") +async def current_user(request: Request, user: User = Depends(current_user)): """This is how to protect route for logged in user only. Dependency will return schema.CurrentUser object, contains user_id and username. diff --git a/tests/test_a_telegram_asyncio.py b/tests/test_a_telegram_asyncio.py index ff1a7ddf..e880ac1d 100644 --- a/tests/test_a_telegram_asyncio.py +++ b/tests/test_a_telegram_asyncio.py @@ -1,7 +1,7 @@ from datetime import datetime, timedelta -from fastapi import status import pytest +from fastapi import status from app.telegram.handlers import MessageHandler, reply_unknown_user from app.telegram.keyboards import DATE_FORMAT diff --git a/tests/test_astronomy.py b/tests/test_astronomy.py index 58924c58..78954f40 100644 --- a/tests/test_astronomy.py +++ b/tests/test_astronomy.py @@ -1,14 +1,13 @@ import datetime -from fastapi import status import httpx import pytest import requests import responses import respx +from fastapi import status -from app.internal.astronomy import ASTRONOMY_URL -from app.internal.astronomy import get_astronomical_data +from app.internal.astronomy import ASTRONOMY_URL, get_astronomical_data RESPONSE_FROM_MOCK = { "location": { @@ -29,14 +28,14 @@ "moonset": "03:04 AM", "moon_phase": "Waxing Gibbous", "moon_illumination": "79", - } - } + }, + }, } ERROR_RESPONSE_FROM_MOCK = { "error": { "message": "Error Text", - } + }, } @@ -45,7 +44,7 @@ async def test_get_astronomical_data(httpx_mock): requested_date = datetime.datetime(day=4, month=4, year=2020) httpx_mock.add_response(method="GET", json=RESPONSE_FROM_MOCK) output = await get_astronomical_data(requested_date, "tel aviv") - assert output['success'] + assert output["success"] @respx.mock @@ -58,7 +57,7 @@ async def test_astronomical_data_error_from_api(): json=ERROR_RESPONSE_FROM_MOCK, ) output = await get_astronomical_data(requested_date, "123") - assert not output['success'] + assert not output["success"] @respx.mock @@ -66,9 +65,10 @@ async def test_astronomical_data_error_from_api(): async def test_astronomical_exception_from_api(httpx_mock): requested_date = datetime.datetime.now() + datetime.timedelta(days=3) respx.get(ASTRONOMY_URL).mock( - return_value=httpx.Response(status.HTTP_500_INTERNAL_SERVER_ERROR)) + return_value=httpx.Response(status.HTTP_500_INTERNAL_SERVER_ERROR), + ) output = await get_astronomical_data(requested_date, "456") - assert not output['success'] + assert not output["success"] @responses.activate @@ -82,4 +82,4 @@ async def test_astronomical_no_response_from_api(): ) requests.get(ASTRONOMY_URL) output = await get_astronomical_data(requested_date, "789") - assert not output['success'] + assert not output["success"] diff --git a/tests/test_categories.py b/tests/test_categories.py index bf670d10..7b9e3490 100644 --- a/tests/test_categories.py +++ b/tests/test_categories.py @@ -1,14 +1,15 @@ import pytest from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.testing import mock - from starlette import status from starlette.datastructures import ImmutableMultiDict from app.database.models import Event -from app.routers.categories import (get_user_categories, - validate_request_params, - validate_color_format) +from app.routers.categories import ( + get_user_categories, + validate_color_format, + validate_request_params, +) class TestCategories: @@ -23,37 +24,53 @@ def test_get_categories_logic_succeeded(session, user, category): @staticmethod def test_creating_new_category(categories_test_client, session, user): - CORRECT_ADD_CATEGORY_DATA = {"user_id": user.id, - "name": "Foo", - "color": "eecc11"} - response = categories_test_client.post("/categories/", - data=CORRECT_ADD_CATEGORY_DATA) + CORRECT_ADD_CATEGORY_DATA = { + "user_id": user.id, + "name": "Foo", + "color": "eecc11", + } + response = categories_test_client.post( + "/categories/", + data=CORRECT_ADD_CATEGORY_DATA, + ) assert response.ok assert TestCategories.CREATE_CATEGORY in response.content @staticmethod - def test_create_not_unique_category_failed(categories_test_client, sender, - category): - CATEGORY_ALREADY_EXISTS = {"name": "Guitar Lesson", - "color": "121212", - "user_id": sender.id} - response = categories_test_client.post("/categories/", - data=CATEGORY_ALREADY_EXISTS) + def test_create_not_unique_category_failed( + categories_test_client, + sender, + category, + ): + CATEGORY_ALREADY_EXISTS = { + "name": "Guitar Lesson", + "color": "121212", + "user_id": sender.id, + } + response = categories_test_client.post( + "/categories/", + data=CATEGORY_ALREADY_EXISTS, + ) assert response.ok assert TestCategories.CATEGORY_ALREADY_EXISTS_MSG in response.content @staticmethod def test_creating_new_category_bad_color_format(client, user): - response = client.post("/categories/", - data={"user_id": user.id, "name": "Foo", - "color": "bad format"}) + response = client.post( + "/categories/", + data={"user_id": user.id, "name": "Foo", "color": "bad format"}, + ) assert response.status_code == status.HTTP_400_BAD_REQUEST assert TestCategories.BAD_COLOR_FORMAT in response.json()["detail"] @staticmethod def test_create_event_with_category(category): - event = Event(title="OOO", content="Guitar rocks!!", - owner_id=category.user_id, category_id=category.id) + event = Event( + title="OOO", + content="Guitar rocks!!", + owner_id=category.user_id, + category_id=category.id, + ) assert event.category_id is not None assert event.category_id == category.id @@ -66,36 +83,51 @@ def test_update_event_with_category(today_event, category): @staticmethod def test_get_user_categories(client, category): - response = client.get(f"/categories/user/?" - f"user_id={category.user_id}" - f"&name={category.name}&color={category.color}") + response = client.get( + f"/categories/user/?" + f"user_id={category.user_id}" + f"&name={category.name}&color={category.color}", + ) assert response.ok assert len(response.json()) == 1 assert set(response.json()[0].items()) == { - ("user_id", category.user_id), ("color", "121212"), - ("name", "Guitar Lesson"), ("id", category.id)} + ("user_id", category.user_id), + ("color", "121212"), + ("name", "Guitar Lesson"), + ("id", category.id), + } @staticmethod def test_get_category_by_name(client, sender, category): - response = client.get(f"/categories/user/?" - f"user_id={category.user_id}" - f"&name={category.name}") + response = client.get( + f"/categories/user/?" + f"user_id={category.user_id}" + f"&name={category.name}", + ) assert response.ok assert len(response.json()) == 1 assert set(response.json()[0].items()) == { - ("user_id", category.user_id), ("color", "121212"), - ("name", "Guitar Lesson"), ("id", category.id)} + ("user_id", category.user_id), + ("color", "121212"), + ("name", "Guitar Lesson"), + ("id", category.id), + } @staticmethod def test_get_category_by_color(client, sender, category): - response = client.get(f"/categories/user/?" - f"user_id={category.user_id}&" - f"color={category.color}") + response = client.get( + f"/categories/user/?" + f"user_id={category.user_id}&" + f"color={category.color}", + ) assert response.ok assert len(response.json()) == 1 assert set(response.json()[0].items()) == { - ("user_id", category.user_id), ("color", "121212"), - ("name", "Guitar Lesson"), ("id", category.id)} + ("user_id", category.user_id), + ("color", "121212"), + ("name", "Guitar Lesson"), + ("id", category.id), + } @staticmethod def test_get_category_bad_request(client): @@ -110,38 +142,61 @@ def test_get_category_ok_request(client): @staticmethod def test_repr(category): - assert category.__repr__() == \ - f'<Category {category.id} {category.name} {category.color}>' + assert ( + category.__repr__() + == f"<Category {category.id} {category.name} {category.color}>" + ) @staticmethod def test_to_dict(category): - assert {c.name: getattr(category, c.name) for c in - category.__table__.columns} == category.to_dict() - - @staticmethod - @pytest.mark.parametrize('params, expected_result', [ - (ImmutableMultiDict([('user_id', ''), ('name', ''), - ('color', 'aabbcc')]), True), - (ImmutableMultiDict([('user_id', ''), ('name', '')]), True), - (ImmutableMultiDict([('user_id', ''), ('color', 'aabbcc')]), True), - (ImmutableMultiDict([('user_id', '')]), True), - (ImmutableMultiDict([('name', ''), ('color', 'aabbcc')]), False), - (ImmutableMultiDict([]), False), - (ImmutableMultiDict([('user_id', ''), ('name', ''), - ('color', 'aabbcc'), ('bad_param', '')]), False), - ]) + assert { + c.name: getattr(category, c.name) + for c in category.__table__.columns + } == category.to_dict() + + @staticmethod + @pytest.mark.parametrize( + "params, expected_result", + [ + ( + ImmutableMultiDict( + [("user_id", ""), ("name", ""), ("color", "aabbcc")], + ), + True, + ), + (ImmutableMultiDict([("user_id", ""), ("name", "")]), True), + (ImmutableMultiDict([("user_id", ""), ("color", "aabbcc")]), True), + (ImmutableMultiDict([("user_id", "")]), True), + (ImmutableMultiDict([("name", ""), ("color", "aabbcc")]), False), + (ImmutableMultiDict([]), False), + ( + ImmutableMultiDict( + [ + ("user_id", ""), + ("name", ""), + ("color", "aabbcc"), + ("bad_param", ""), + ], + ), + False, + ), + ], + ) def test_validate_request_params(params, expected_result): assert validate_request_params(params) == expected_result @staticmethod - @pytest.mark.parametrize('color, expected_result', [ - ("aabbcc", True), - ("110033", True), - ("114b33", True), - ("", False), - ("aabbcg", False), - ("aabbc", False), - ]) + @pytest.mark.parametrize( + "color, expected_result", + [ + ("aabbcc", True), + ("110033", True), + ("114b33", True), + ("", False), + ("aabbcg", False), + ("aabbc", False), + ], + ) def test_validate_color_format(color, expected_result): assert validate_color_format(color) == expected_result diff --git a/tests/test_dayview.py b/tests/test_dayview.py index 48414553..ef77eb05 100644 --- a/tests/test_dayview.py +++ b/tests/test_dayview.py @@ -1,7 +1,7 @@ from datetime import datetime, timedelta -from bs4 import BeautifulSoup import pytest +from bs4 import BeautifulSoup from app.database.models import Event from app.routers.dayview import ( @@ -9,7 +9,6 @@ is_all_day_event_in_day, is_specific_time_event_in_day, ) - from app.routers.event import create_event diff --git a/tests/test_email.py b/tests/test_email.py index 37138239..10022beb 100644 --- a/tests/test_email.py +++ b/tests/test_email.py @@ -1,10 +1,15 @@ -from fastapi import BackgroundTasks, status import pytest +from fastapi import BackgroundTasks, status from sqlalchemy.orm import Session from app.database.models import User -from app.internal.email import (mail, send, send_email_file, - send_email_invitation, verify_email_pattern) +from app.internal.email import ( + mail, + send, + send_email_file, + send_email_invitation, + verify_email_pattern, +) from app.internal.utils import create_model, delete_instance @@ -16,10 +21,14 @@ def test_email_send(client, user, event, smtpd): mail.config.MAIL_TLS = False with mail.record_messages() as outbox: response = client.post( - "/email/send", data={ - "event_used": event.id, "user_to_send": user.id, + "/email/send", + data={ + "event_used": event.id, + "user_to_send": user.id, "title": "Testing", - "background_tasks": BackgroundTasks}) + "background_tasks": BackgroundTasks, + }, + ) assert len(outbox) == 1 assert response.ok @@ -30,10 +39,14 @@ def test_failed_email_send(client, user, event, smtpd): mail.config.MAIL_PORT = smtpd.port with mail.record_messages() as outbox: response = client.post( - "/email/send", data={ - "event_used": event.id + 1, "user_to_send": user.id, + "/email/send", + data={ + "event_used": event.id + 1, + "user_to_send": user.id, "title": "Testing", - "background_tasks": BackgroundTasks}) + "background_tasks": BackgroundTasks, + }, + ) assert len(outbox) == 0 assert not response.ok @@ -59,29 +72,40 @@ def test_send_mail_no_body(client, configured_smtpd): response = client.post("/email/invitation/") assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY - assert response.json() == {'detail': [{ - 'loc': ['body'], - 'msg': 'field required', - 'type': 'value_error.missing'}]} + assert response.json() == { + "detail": [ + { + "loc": ["body"], + "msg": "field required", + "type": "value_error.missing", + }, + ], + } assert not outbox def test_send_mail_invalid_email(client, configured_smtpd): with mail.record_messages() as outbox: - response = client.post("/email/invitation/", json={ - "sender_name": "string", - "recipient_name": "string", - "recipient_mail": "test#mail.com" - }) + response = client.post( + "/email/invitation/", + json={ + "sender_name": "string", + "recipient_name": "string", + "recipient_mail": "test#mail.com", + }, + ) assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY assert response.json() == { - "detail": "Please enter valid email address"} + "detail": "Please enter valid email address", + } assert not outbox -def assert_validation_error_missing_body_fields(validation_msg, - missing_fields): +def assert_validation_error_missing_body_fields( + validation_msg, + missing_fields, +): """ helper function for asserting with open api validation errors look at https://fastapi.tiangolo.com/tutorial/path-params/#data-validation @@ -108,102 +132,130 @@ def assert_validation_error_missing_body_fields(validation_msg, assert loc[1] in missing_fields -@pytest.mark.parametrize("body, missing_fields", [ - ( +@pytest.mark.parametrize( + "body, missing_fields", + [ + ( {"sender_name": "string", "recipient_name": "string"}, ["recipient_mail"], - ), - - ( + ), + ( {"sender_name": "string", "recipient_mail": "test@mail.com"}, ["recipient_name"], - ), - ( + ), + ( {"recipient_name": "string", "recipient_mail": "test@mail.com"}, ["sender_name"], - ), - ( + ), + ( {"sender_name": "string"}, ["recipient_name", "recipient_mail"], - ), - ( + ), + ( {"recipient_name": "string"}, ["sender_name", "recipient_mail"], - ), - ( + ), + ( {"recipient_mail": "test@mail.com"}, ["sender_name", "recipient_name"], - ), -]) -def test_send_mail_partial_body(body, missing_fields, - client, configured_smtpd): + ), + ], +) +def test_send_mail_partial_body( + body, + missing_fields, + client, + configured_smtpd, +): with mail.record_messages() as outbox: response = client.post("/email/invitation/", json=body) assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY - assert_validation_error_missing_body_fields(response.json(), - missing_fields) + assert_validation_error_missing_body_fields( + response.json(), + missing_fields, + ) assert not outbox def test_send_mail_valid_email(client, configured_smtpd): with mail.record_messages() as outbox: - response = client.post("/email/invitation/", json={ - "sender_name": "string", - "recipient_name": "string", - "recipient_mail": "test@mail.com" - } - ) + response = client.post( + "/email/invitation/", + json={ + "sender_name": "string", + "recipient_name": "string", + "recipient_mail": "test@mail.com", + }, + ) assert response.ok assert outbox -@pytest.mark.parametrize("sender_name,recipient_name,recipient_mail", [ - ("", "other_person", "other@mail.com"), - ("us_person", "", "other@mail.com"), -]) -def test_send_mail_bad_invitation(client, - configured_smtpd, - sender_name, - recipient_name, - recipient_mail): +@pytest.mark.parametrize( + "sender_name,recipient_name,recipient_mail", + [ + ("", "other_person", "other@mail.com"), + ("us_person", "", "other@mail.com"), + ], +) +def test_send_mail_bad_invitation( + client, + configured_smtpd, + sender_name, + recipient_name, + recipient_mail, +): with mail.record_messages() as outbox: - response = client.post("/email/invitation/", json={ - "sender_name": sender_name, - "recipient_name": recipient_name, - "recipient_mail": recipient_mail - } - ) + response = client.post( + "/email/invitation/", + json={ + "sender_name": sender_name, + "recipient_name": recipient_name, + "recipient_mail": recipient_mail, + }, + ) assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY - assert response.json() == { - "detail": "Couldn't send the email!"} + assert response.json() == {"detail": "Couldn't send the email!"} assert not outbox -@pytest.mark.parametrize("sender_name,recipient_name,recipient_mail", [ - ("", "other_person", "other@mail.com"), - ("us_person", "", "other@mail.com"), - ("us_person", "other_person", "other#mail.com"), -]) -def test_send_mail_bad_invitation_internal(client, - configured_smtpd, - sender_name, - recipient_name, - recipient_mail): +@pytest.mark.parametrize( + "sender_name,recipient_name,recipient_mail", + [ + ("", "other_person", "other@mail.com"), + ("us_person", "", "other@mail.com"), + ("us_person", "other_person", "other#mail.com"), + ], +) +def test_send_mail_bad_invitation_internal( + client, + configured_smtpd, + sender_name, + recipient_name, + recipient_mail, +): background_task = BackgroundTasks() - assert not send_email_invitation(sender_name, - recipient_name, - recipient_mail, - background_task) - - -@pytest.mark.parametrize("recipient_mail,file_path", [ - ("other@mail.com", "non_existing_file"), - ("other#mail.com", __file__), -]) -def test_send_mail_bad_file_internal(client, - configured_smtpd, - recipient_mail, - file_path): + assert not send_email_invitation( + sender_name, + recipient_name, + recipient_mail, + background_task, + ) + + +@pytest.mark.parametrize( + "recipient_mail,file_path", + [ + ("other@mail.com", "non_existing_file"), + ("other#mail.com", __file__), + ], +) +def test_send_mail_bad_file_internal( + client, + configured_smtpd, + recipient_mail, + file_path, +): background_task = BackgroundTasks() assert not send_email_file(file_path, recipient_mail, background_task) @@ -216,10 +268,11 @@ def test_send_mail_good_file_internal(client, configured_smtpd): @pytest.fixture def bad_user(session: Session) -> User: test_user = create_model( - session, User, - username='test_username', - password='test_password', - email='test.email#gmail.com', + session, + User, + username="test_username", + password="test_password", + email="test.email#gmail.com", language_id=1, ) yield test_user @@ -228,15 +281,18 @@ def bad_user(session: Session) -> User: def test_send(session, bad_user, event): background_task = BackgroundTasks() - assert not send(session=session, - event_used=1, - user_to_send=1, - title="Test", - background_tasks=background_task) + assert not send( + session=session, + event_used=1, + user_to_send=1, + title="Test", + background_tasks=background_task, + ) -@pytest.mark.parametrize("email", ["test#mail.com", - "test_mail.com", - "test@mail-com"]) +@pytest.mark.parametrize( + "email", + ["test#mail.com", "test_mail.com", "test@mail-com"], +) def test_verify_email_pattern(email): assert not verify_email_pattern(email) diff --git a/tests/test_emotion.py b/tests/test_emotion.py index 644dfe14..a47dca3a 100644 --- a/tests/test_emotion.py +++ b/tests/test_emotion.py @@ -4,15 +4,13 @@ from app.internal.emotion import ( Emoticon, - is_emotion_above_significance, get_dominant_emotion, get_emotion, get_html_emoticon, + is_emotion_above_significance, ) - from app.routers.event import create_event - HAPPY_MESSAGE = "This is great" # 100% happy SAD_MESSAGE = "I'm so lonely and feel bad" # 100% sad ANGRY_MESSAGE = "I'm so mad, stop it" # 100% angry diff --git a/tests/test_event.py b/tests/test_event.py index 87828e96..dac6d817 100644 --- a/tests/test_event.py +++ b/tests/test_event.py @@ -1,14 +1,13 @@ -from datetime import datetime, timedelta import json -import pytest +from datetime import datetime, timedelta +import pytest from fastapi import HTTPException, Request from fastapi.testclient import TestClient -from sqlalchemy.sql.elements import Null from sqlalchemy.orm.session import Session +from sqlalchemy.sql.elements import Null from starlette import status - from app.database.models import Comment, Event from app.dependencies import get_db from app.internal.privacy import PrivacyKinds @@ -168,7 +167,7 @@ def test_eventview_with_id(event_test_client, session, event): assert b"Some random location" in response.content waze_link = b"https://waze.com/ul?q=Some%20random%20location" assert waze_link in response.content - assert b'VC link' not in response.content + assert b"VC link" not in response.content def test_eventview_without_location(event_test_client, session, event): diff --git a/tests/test_geolocation.py b/tests/test_geolocation.py index 9a9503fd..0a634586 100644 --- a/tests/test_geolocation.py +++ b/tests/test_geolocation.py @@ -1,8 +1,8 @@ import pytest +from sqlalchemy.sql import func -from app.internal.event import get_location_coordinates from app.database.models import Event -from sqlalchemy.sql import func +from app.internal.event import get_location_coordinates class TestGeolocation: diff --git a/tests/test_google_connect.py b/tests/test_google_connect.py index 58ac8aa3..cfb1f466 100644 --- a/tests/test_google_connect.py +++ b/tests/test_google_connect.py @@ -1,15 +1,14 @@ from datetime import datetime -import pytest -from loguru import logger - -import app.internal.google_connect as google_connect -from app.routers.event import create_event -from app.database.models import OAuthCredentials +import pytest from google.oauth2.credentials import Credentials from googleapiclient.discovery import build from googleapiclient.http import HttpMock +from loguru import logger +import app.internal.google_connect as google_connect +from app.database.models import OAuthCredentials +from app.routers.event import create_event from app.routers.register import _create_user diff --git a/tests/test_holidays.py b/tests/test_holidays.py index 7dfab593..79723be0 100644 --- a/tests/test_holidays.py +++ b/tests/test_holidays.py @@ -1,22 +1,24 @@ import os + +from sqlalchemy.orm import Session + from app.database.models import Event, User from app.routers import profile -from sqlalchemy.orm import Session class TestHolidaysImport: - HOLIDAYS = '/profile/holidays/import' + HOLIDAYS = "/profile/holidays/import" @staticmethod def test_import_holidays_page_exists(client): resp = client.get(TestHolidaysImport.HOLIDAYS) assert resp.ok - assert b'Import holidays using ics file' in resp.content + assert b"Import holidays using ics file" in resp.content def test_get_holidays(self, session: Session, user: User): current_folder = os.path.dirname(os.path.realpath(__file__)) - resource_folder = os.path.join(current_folder, 'resources') - test_file = os.path.join(resource_folder, 'ics_example.txt') + resource_folder = os.path.join(current_folder, "resources") + test_file = os.path.join(resource_folder, "ics_example.txt") with open(test_file) as file: ics_content = file.read() holidays = profile.get_holidays_from_file(ics_content, session) @@ -25,8 +27,8 @@ def test_get_holidays(self, session: Session, user: User): def test_wrong_file_get_holidays(self, session: Session, user: User): current_folder = os.path.dirname(os.path.realpath(__file__)) - resource_folder = os.path.join(current_folder, 'resources') - test_file = os.path.join(resource_folder, 'wrong_ics_example.txt') + resource_folder = os.path.join(current_folder, "resources") + test_file = os.path.join(resource_folder, "wrong_ics_example.txt") with open(test_file) as file: ics_content = file.read() holidays = profile.get_holidays_from_file(ics_content, session) diff --git a/tests/test_login.py b/tests/test_login.py index 11432738..f11eb0f6 100644 --- a/tests/test_login.py +++ b/tests/test_login.py @@ -1,5 +1,4 @@ import pytest - from starlette.status import HTTP_302_FOUND from app.database.models import User @@ -13,205 +12,265 @@ def test_login_route_ok(security_test_client): REGISTER_DETAIL = { - 'username': 'correct_user', 'full_name': 'full_name', - 'password': 'correct_password', 'confirm_password': 'correct_password', - 'email': 'example@email.com', 'description': ""} + "username": "correct_user", + "full_name": "full_name", + "password": "correct_password", + "confirm_password": "correct_password", + "email": "example@email.com", + "description": "", +} LOGIN_WRONG_DETAILS = [ - ('wrong_user', 'wrong_password', b'Please check your credentials'), - ('correct_user', 'wrong_password', b'Please check your credentials'), - ('wrong_user', 'correct_password', b'Please check your credentials'), - ('', 'correct_password', b'Please check your credentials'), - ('correct_user', '', b'Please check your credentials'), - ('', '', b'Please check your credentials'), - ] - -LOGIN_DATA = {'username': 'correct_user', 'password': 'correct_password'} + ("wrong_user", "wrong_password", b"Please check your credentials"), + ("correct_user", "wrong_password", b"Please check your credentials"), + ("wrong_user", "correct_password", b"Please check your credentials"), + ("", "correct_password", b"Please check your credentials"), + ("correct_user", "", b"Please check your credentials"), + ("", "", b"Please check your credentials"), +] + +LOGIN_DATA = {"username": "correct_user", "password": "correct_password"} WRONG_LOGIN_DATA = { - 'username': 'incorrect_user', 'password': 'correct_password'} + "username": "incorrect_user", + "password": "correct_password", +} @pytest.mark.parametrize( - "username, password, expected_response", LOGIN_WRONG_DETAILS) + "username, password, expected_response", + LOGIN_WRONG_DETAILS, +) def test_login_fails( - session, security_test_client, username, password, expected_response): + session, + security_test_client, + username, + password, + expected_response, +): security_test_client.post( - security_test_client.app.url_path_for('register'), - data=REGISTER_DETAIL) - data = {'username': username, 'password': password} + security_test_client.app.url_path_for("register"), + data=REGISTER_DETAIL, + ) + data = {"username": username, "password": password} data = security_test_client.post( - security_test_client.app.url_path_for('login'), - data=data).content + security_test_client.app.url_path_for("login"), + data=data, + ).content assert expected_response in data def test_login_successfull(session, security_test_client): security_test_client.post( - security_test_client.app.url_path_for('register'), - data=REGISTER_DETAIL) + security_test_client.app.url_path_for("register"), + data=REGISTER_DETAIL, + ) res = security_test_client.post( - security_test_client.app.url_path_for('login'), - data=LOGIN_DATA) + security_test_client.app.url_path_for("login"), + data=LOGIN_DATA, + ) assert res.status_code == HTTP_302_FOUND def test_is_logged_in_dependency_with_logged_in_user( - session, security_test_client): + session, + security_test_client, +): security_test_client.post( - security_test_client.app.url_path_for('register'), - data=REGISTER_DETAIL) + security_test_client.app.url_path_for("register"), + data=REGISTER_DETAIL, + ) security_test_client.post( - security_test_client.app.url_path_for('login'), - data=LOGIN_DATA) + security_test_client.app.url_path_for("login"), + data=LOGIN_DATA, + ) res = security_test_client.get( - security_test_client.app.url_path_for('is_logged_in')) + security_test_client.app.url_path_for("is_logged_in"), + ) assert res.json() == {"user": True} def test_is_logged_in_dependency_without_logged_in_user( - session, security_test_client): + session, + security_test_client, +): res = security_test_client.get( - security_test_client.app.url_path_for('logout')) + security_test_client.app.url_path_for("logout"), + ) res = security_test_client.get( - security_test_client.app.url_path_for('is_logged_in')) - assert b'Please log in' in res.content + security_test_client.app.url_path_for("is_logged_in"), + ) + assert b"Please log in" in res.content def test_is_manager_in_dependency_with_logged_in_regular_user( - session, security_test_client): + session, + security_test_client, +): security_test_client.post( - security_test_client.app.url_path_for('register'), - data=REGISTER_DETAIL) + security_test_client.app.url_path_for("register"), + data=REGISTER_DETAIL, + ) security_test_client.post( - security_test_client.app.url_path_for('login'), - data=LOGIN_DATA) + security_test_client.app.url_path_for("login"), + data=LOGIN_DATA, + ) res = security_test_client.get( - security_test_client.app.url_path_for('is_manager')) + security_test_client.app.url_path_for("is_manager"), + ) assert b"have a permition" in res.content def test_is_manager_in_dependency_with_logged_in_manager( - session, security_test_client): + session, + security_test_client, +): security_test_client.post( - security_test_client.app.url_path_for('register'), - data=REGISTER_DETAIL) - manager = session.query(User).filter( - User.username == 'correct_user').first() + security_test_client.app.url_path_for("register"), + data=REGISTER_DETAIL, + ) + manager = ( + session.query(User).filter(User.username == "correct_user").first() + ) manager.is_manager = True session.commit() security_test_client.post( - security_test_client.app.url_path_for('login'), data=LOGIN_DATA) + security_test_client.app.url_path_for("login"), + data=LOGIN_DATA, + ) res = security_test_client.get( - security_test_client.app.url_path_for('is_manager')) + security_test_client.app.url_path_for("is_manager"), + ) assert res.json() == {"manager": True} def test_logout(session, security_test_client): res = security_test_client.get( - security_test_client.app.url_path_for('logout')) - assert b'Login' in res.content + security_test_client.app.url_path_for("logout"), + ) + assert b"Login" in res.content def test_incorrect_secret_key_in_token(session, security_test_client): user = LoginUser(**LOGIN_DATA) incorrect_token = create_jwt_token(user, jwt_key="wrong secret key") security_test_client.post( - security_test_client.app.url_path_for('register'), - data=REGISTER_DETAIL) + security_test_client.app.url_path_for("register"), + data=REGISTER_DETAIL, + ) params = f"?existing_jwt={incorrect_token}" security_test_client.post( - security_test_client.app.url_path_for('login') + f'{params}', - data=LOGIN_DATA) + security_test_client.app.url_path_for("login") + f"{params}", + data=LOGIN_DATA, + ) res = security_test_client.get( - security_test_client.app.url_path_for('is_logged_in')) - assert b'Your token is incorrect' in res.content + security_test_client.app.url_path_for("is_logged_in"), + ) + assert b"Your token is incorrect" in res.content def test_expired_token(session, security_test_client): - security_test_client.get( - security_test_client.app.url_path_for('logout')) + security_test_client.get(security_test_client.app.url_path_for("logout")) user = LoginUser(**LOGIN_DATA) incorrect_token = create_jwt_token(user, jwt_min_exp=-1) security_test_client.post( - security_test_client.app.url_path_for('register'), - data=REGISTER_DETAIL) + security_test_client.app.url_path_for("register"), + data=REGISTER_DETAIL, + ) params = f"?existing_jwt={incorrect_token}" security_test_client.post( - security_test_client.app.url_path_for('login') + f'{params}', - data=LOGIN_DATA) + security_test_client.app.url_path_for("login") + f"{params}", + data=LOGIN_DATA, + ) res = security_test_client.get( - security_test_client.app.url_path_for('is_logged_in')) - assert b'expired' in res.content + security_test_client.app.url_path_for("is_logged_in"), + ) + assert b"expired" in res.content def test_corrupted_token(session, security_test_client): user = LoginUser(**LOGIN_DATA) incorrect_token = create_jwt_token(user) + "s" security_test_client.post( - security_test_client.app.url_path_for('register'), - data=REGISTER_DETAIL) + security_test_client.app.url_path_for("register"), + data=REGISTER_DETAIL, + ) params = f"?existing_jwt={incorrect_token}" security_test_client.post( - security_test_client.app.url_path_for('login') + f'{params}', - data=LOGIN_DATA) + security_test_client.app.url_path_for("login") + f"{params}", + data=LOGIN_DATA, + ) res = security_test_client.get( - security_test_client.app.url_path_for('is_logged_in')) - assert b'Your token is incorrect' in res.content + security_test_client.app.url_path_for("is_logged_in"), + ) + assert b"Your token is incorrect" in res.content def test_current_user_from_db_dependency_ok(session, security_test_client): security_test_client.post( - security_test_client.app.url_path_for('register'), - data=REGISTER_DETAIL) + security_test_client.app.url_path_for("register"), + data=REGISTER_DETAIL, + ) security_test_client.post( - security_test_client.app.url_path_for('login'), data=LOGIN_DATA) + security_test_client.app.url_path_for("login"), + data=LOGIN_DATA, + ) res = security_test_client.get( - security_test_client.app.url_path_for('current_user_from_db')) - assert res.json() == {"user": 'correct_user'} + security_test_client.app.url_path_for("current_user_from_db"), + ) + assert res.json() == {"user": "correct_user"} def test_current_user_from_db_dependency_not_logged_in( - session, security_test_client): - security_test_client.get( - security_test_client.app.url_path_for('logout')) + session, + security_test_client, +): + security_test_client.get(security_test_client.app.url_path_for("logout")) res = security_test_client.get( - security_test_client.app.url_path_for('current_user_from_db')) - assert b'Please log in' in res.content + security_test_client.app.url_path_for("current_user_from_db"), + ) + assert b"Please log in" in res.content def test_current_user_from_db_dependency_wrong_details( - session, security_test_client): - security_test_client.get( - security_test_client.app.url_path_for('logout')) + session, + security_test_client, +): + security_test_client.get(security_test_client.app.url_path_for("logout")) security_test_client.post( - security_test_client.app.url_path_for('register'), - data=REGISTER_DETAIL) + security_test_client.app.url_path_for("register"), + data=REGISTER_DETAIL, + ) user = LoginUser(**WRONG_LOGIN_DATA) incorrect_token = create_jwt_token(user) params = f"?existing_jwt={incorrect_token}" security_test_client.post( - security_test_client.app.url_path_for('login') + f'{params}', - data=LOGIN_DATA) + security_test_client.app.url_path_for("login") + f"{params}", + data=LOGIN_DATA, + ) res = security_test_client.get( - security_test_client.app.url_path_for('current_user_from_db')) - assert b'Your token is incorrect' in res.content + security_test_client.app.url_path_for("current_user_from_db"), + ) + assert b"Your token is incorrect" in res.content def test_current_user_dependency_ok(session, security_test_client): security_test_client.post( - security_test_client.app.url_path_for('register'), - data=REGISTER_DETAIL) + security_test_client.app.url_path_for("register"), + data=REGISTER_DETAIL, + ) security_test_client.post( - security_test_client.app.url_path_for('login'), data=LOGIN_DATA) + security_test_client.app.url_path_for("login"), + data=LOGIN_DATA, + ) res = security_test_client.get( - security_test_client.app.url_path_for('current_user')) - assert res.json() == {"user": 'correct_user'} + security_test_client.app.url_path_for("current_user"), + ) + assert res.json() == {"user": "correct_user"} -def test_current_user_dependency_not_logged_in( - session, security_test_client): - security_test_client.get( - security_test_client.app.url_path_for('logout')) +def test_current_user_dependency_not_logged_in(session, security_test_client): + security_test_client.get(security_test_client.app.url_path_for("logout")) res = security_test_client.get( - security_test_client.app.url_path_for('current_user')) - assert b'Please log in' in res.content + security_test_client.app.url_path_for("current_user"), + ) + assert b"Please log in" in res.content diff --git a/tests/test_profile.py b/tests/test_profile.py index 880c6ebc..faa1e80b 100644 --- a/tests/test_profile.py +++ b/tests/test_profile.py @@ -1,8 +1,8 @@ import os +import pytest from fastapi import status from PIL import Image -import pytest from app import config from app.dependencies import MEDIA_PATH @@ -11,98 +11,98 @@ CROP_RESULTS = [ (20, 10, (5, 0, 15, 10)), (10, 20, (0, 5, 10, 15)), - (10, 10, (0, 0, 10, 10)) + (10, 10, (0, 0, 10, 10)), ] def test_get_placeholder_user(): user = get_placeholder_user() - assert user.username == 'new_user' - assert user.email == 'my@email.po' - assert user.password == '1a2s3d4f5g6' - assert user.full_name == 'My Name' + assert user.username == "new_user" + assert user.email == "my@email.po" + assert user.password == "1a2s3d4f5g6" + assert user.full_name == "My Name" -@pytest.mark.parametrize('width, height, result', CROP_RESULTS) +@pytest.mark.parametrize("width, height, result", CROP_RESULTS) def test_get_image_crop_area(width, height, result): assert get_image_crop_area(width, height) == result def test_profile_page(profile_test_client): - profile = profile_test_client.get('/profile') + profile = profile_test_client.get("/profile") data = profile.content assert profile.ok - assert b'profile.png' in data - assert b'FakeName' in data - assert b'Happy new user!' in data - assert b'On This Day' in data + assert b"profile.png" in data + assert b"FakeName" in data + assert b"Happy new user!" in data + assert b"On This Day" in data def test_update_user_fullname(profile_test_client): - new_name_data = { - 'fullname': 'Peter' - } + new_name_data = {"fullname": "Peter"} # Get profile page and initialize database - profile = profile_test_client.get('/profile') + profile = profile_test_client.get("/profile") # Post new data profile = profile_test_client.post( - '/profile/update_user_fullname', data=new_name_data) + "/profile/update_user_fullname", + data=new_name_data, + ) assert profile.status_code == status.HTTP_302_FOUND # Get updated data - data = profile_test_client.get('/profile').content - assert b'Peter' in data + data = profile_test_client.get("/profile").content + assert b"Peter" in data def test_update_user_email(profile_test_client): - new_email = { - 'email': 'very@new.email' - } + new_email = {"email": "very@new.email"} # Get profile page and initialize database - profile = profile_test_client.get('/profile') + profile = profile_test_client.get("/profile") # Post new data profile = profile_test_client.post( - '/profile/update_user_email', data=new_email) + "/profile/update_user_email", + data=new_email, + ) assert profile.status_code == status.HTTP_302_FOUND # Get updated data - data = profile_test_client.get('/profile').content - assert b'very@new.email' in data + data = profile_test_client.get("/profile").content + assert b"very@new.email" in data def test_update_user_description(profile_test_client): - new_description = { - 'description': "FastAPI Developer" - } + new_description = {"description": "FastAPI Developer"} # Get profile page and initialize database - profile = profile_test_client.get('/profile') + profile = profile_test_client.get("/profile") # Post new data profile = profile_test_client.post( - '/profile/update_user_description', data=new_description) + "/profile/update_user_description", + data=new_description, + ) assert profile.status_code == status.HTTP_302_FOUND # Get updated data - data = profile_test_client.get('/profile').content + data = profile_test_client.get("/profile").content assert b"FastAPI Developer" in data def test_update_telegram_id(profile_test_client): - new_telegram_id = { - 'telegram_id': "12345" - } + new_telegram_id = {"telegram_id": "12345"} # Get profile page and initialize database - profile = profile_test_client.get('/profile') + profile = profile_test_client.get("/profile") # Post new data profile = profile_test_client.post( - '/profile/update_telegram_id', data=new_telegram_id) + "/profile/update_telegram_id", + data=new_telegram_id, + ) assert profile.status_code == status.HTTP_302_FOUND # Get updated data - data = profile_test_client.get('/profile').content + data = profile_test_client.get("/profile").content assert b"12345" in data @@ -110,36 +110,35 @@ def test_upload_user_photo(profile_test_client): example_new_photo = f"{MEDIA_PATH}/example.png" # Get profile page and initialize database - profile = profile_test_client.get('/profile') + profile = profile_test_client.get("/profile") # Post new data profile = profile_test_client.post( - '/profile/upload_user_photo', - files={'file': ( - "filename", open(example_new_photo, "rb"), "image/png")}) + "/profile/upload_user_photo", + files={ + "file": ("filename", open(example_new_photo, "rb"), "image/png"), + }, + ) assert profile.status_code == status.HTTP_302_FOUND # Validate new picture saved in media directory - assert 'fake_user.png' in os.listdir(MEDIA_PATH) + assert "fake_user.png" in os.listdir(MEDIA_PATH) # Validate new picture size - new_avatar_path = os.path.join(MEDIA_PATH, 'fake_user.png') + new_avatar_path = os.path.join(MEDIA_PATH, "fake_user.png") assert Image.open(new_avatar_path).size == config.AVATAR_SIZE os.remove(new_avatar_path) def test_update_calendar_privacy(profile_test_client): - new_privacy = { - 'privacy': "Public" - } + new_privacy = {"privacy": "Public"} # Get profile page and initialize database - profile = profile_test_client.get('/profile') + profile = profile_test_client.get("/profile") # Post new data - profile = profile_test_client.post( - '/profile/privacy', data=new_privacy) + profile = profile_test_client.post("/profile/privacy", data=new_privacy) assert profile.status_code == status.HTTP_302_FOUND # Get updated data - data = profile_test_client.get('/profile').content + data = profile_test_client.get("/profile").content assert b"Public" in data diff --git a/tests/test_statistics.py b/tests/test_statistics.py index 10cc8457..707d571a 100644 --- a/tests/test_statistics.py +++ b/tests/test_statistics.py @@ -1,9 +1,12 @@ import datetime from app.internal.notification import get_all_invitations -from app.internal.statistics import get_statistics -from app.internal.statistics import INVALID_DATE_RANGE, INVALID_USER -from app.internal.statistics import SUCCESS_STATUS +from app.internal.statistics import ( + INVALID_DATE_RANGE, + INVALID_USER, + SUCCESS_STATUS, + get_statistics, +) from app.routers.event import create_event from app.routers.register import _create_user from app.routers.share import send_in_app_invitation diff --git a/tests/test_translation.py b/tests/test_translation.py index c9e5720c..fe3abd85 100644 --- a/tests/test_translation.py +++ b/tests/test_translation.py @@ -1,11 +1,14 @@ +import pytest from fastapi import HTTPException from iso639 import languages -import pytest from textblob import TextBlob from app.internal.translation import ( - _detect_text_language, _get_language_code, _get_user_language, - translate_text, translate_text_for_user + _detect_text_language, + _get_language_code, + _get_user_language, + translate_text, + translate_text_for_user, ) TEXT = [ @@ -20,31 +23,46 @@ def test_translate_text_with_original_lang(text, target_lang, original_lang): answer = translate_text(text, target_lang, original_lang) assert "Hello my friend" == answer - assert TextBlob(text).detect_language() == languages.get( - name=original_lang.capitalize()).alpha2 - assert TextBlob(answer).detect_language() == languages.get( - name=target_lang.capitalize()).alpha2 + assert ( + TextBlob(text).detect_language() + == languages.get(name=original_lang.capitalize()).alpha2 + ) + assert ( + TextBlob(answer).detect_language() + == languages.get(name=target_lang.capitalize()).alpha2 + ) @pytest.mark.parametrize("text, target_lang, original_lang", TEXT) def test_translate_text_without_original_lang( - text, target_lang, original_lang): + text, + target_lang, + original_lang, +): answer = translate_text(text, target_lang) assert "Hello my friend" == answer - assert TextBlob(answer).detect_language() == languages.get( - name=target_lang.capitalize()).alpha2 + assert ( + TextBlob(answer).detect_language() + == languages.get(name=target_lang.capitalize()).alpha2 + ) @pytest.mark.parametrize("text, target_lang, original_lang", TEXT) def test_translate_text_with_identical_original_and_target_lang( - text, target_lang, original_lang): + text, + target_lang, + original_lang, +): answer = translate_text(text, original_lang, original_lang) assert answer == text @pytest.mark.parametrize("text, target_lang, original_lang", TEXT) def test_translate_text_with_same_original_target_lang_without_original_lang( - text, target_lang, original_lang): + text, + target_lang, + original_lang, +): answer = translate_text(text, original_lang) assert answer == text @@ -73,7 +91,12 @@ def test_get_user_language(user, session): @pytest.mark.parametrize("text, target_lang, original_lang", TEXT) def test_translate_text_for_valid_user( - text, target_lang, original_lang, session, user): + text, + target_lang, + original_lang, + session, + user, +): user_id = user.id answer = translate_text_for_user(text, session, user_id) assert answer == "Hello my friend" @@ -90,36 +113,51 @@ def test_detect_text_language(): assert answer == "en" -@pytest.mark.parametrize("text, target_lang, original_lang", - [("Hoghhflaff", "english", "spanish"), - ("Bdonfdjourr", "english", "french"), - ("Hafdllnnc", "english", "german"), - ]) +@pytest.mark.parametrize( + "text, target_lang, original_lang", + [ + ("Hoghhflaff", "english", "spanish"), + ("Bdonfdjourr", "english", "french"), + ("Hafdllnnc", "english", "german"), + ], +) def test_translate_text_with_text_impossible_to_translate( - text, target_lang, original_lang): + text, + target_lang, + original_lang, +): answer = translate_text(text, target_lang, original_lang) assert answer == text -@pytest.mark.parametrize("text, target_lang, original_lang", - [("@Здравствуй#мой$друг!", "english", "russian"), - ("@Hola#mi$amigo!", "english", "spanish"), - ("@Bonjour#mon$ami!", "english", "french"), - ("@Hallo#mein$Freund!", "english", "german"), - ]) +@pytest.mark.parametrize( + "text, target_lang, original_lang", + [ + ("@Здравствуй#мой$друг!", "english", "russian"), + ("@Hola#mi$amigo!", "english", "spanish"), + ("@Bonjour#mon$ami!", "english", "french"), + ("@Hallo#mein$Freund!", "english", "german"), + ], +) def test_translate_text_with_symbols(text, target_lang, original_lang): answer = translate_text(text, target_lang, original_lang) assert "@ Hello # my $ friend!" == answer -@pytest.mark.parametrize("text, target_lang, original_lang", - [("Привет мой друг", "italian", "spanish"), - ("Hola mi amigo", "english", "russian"), - ("Bonjour, mon ami", "russian", "german"), - ("Ciao amico", "french", "german") - ]) +@pytest.mark.parametrize( + "text, target_lang, original_lang", + [ + ("Привет мой друг", "italian", "spanish"), + ("Hola mi amigo", "english", "russian"), + ("Bonjour, mon ami", "russian", "german"), + ("Ciao amico", "french", "german"), + ], +) def test_translate_text_with_with_incorrect_lang( - text, target_lang, original_lang): + text, + target_lang, + original_lang, +): answer = translate_text(text, target_lang, original_lang) assert answer == text diff --git a/tests/test_user.py b/tests/test_user.py index 1a1ed0a7..b2dc545c 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -1,12 +1,13 @@ from datetime import datetime + import pytest -from app.routers.register import _create_user -from app.routers.user import does_user_exist, get_users +from app.database.models import Event, UserEvent from app.internal.user.availability import disable, enable from app.internal.utils import save -from app.database.models import UserEvent, Event from app.routers.event import create_event +from app.routers.register import _create_user +from app.routers.user import does_user_exist, get_users @pytest.fixture diff --git a/tests/test_utils.py b/tests/test_utils.py index 34a88dfd..79d07714 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -6,33 +6,31 @@ from app.database.models import User from app.internal import utils - TIMES = [ - ('2021-01-14', date(2021, 1, 14)), - ('13:30', time(13, 30)), - ('15:42:00', time(15, 42)), - ('15', None), - ('2021-01', None), - ('15:42:00.5', None), + ("2021-01-14", date(2021, 1, 14)), + ("13:30", time(13, 30)), + ("15:42:00", time(15, 42)), + ("15", None), + ("2021-01", None), + ("15:42:00.5", None), ] class TestUtils: - def test_save_success(self, user: User, session: Session) -> None: - user.username = 'edit_username' + user.username = "edit_username" assert utils.save(session, user) def test_save_failure(self, session: Session) -> None: - user = 'not a user instance' + user = "not a user instance" assert not utils.save(session, user) def test_create_model(self, session: Session) -> None: assert session.query(User).first() is None info = { - 'username': 'test', - 'email': 'test@test.com', - 'password': 'test1234' + "username": "test", + "email": "test@test.com", + "password": "test1234", } utils.create_model(session, User, **info) assert session.query(User).first() @@ -52,7 +50,10 @@ def test_get_user(self, user: User, session: Session) -> None: assert utils.get_user(session, user.id) == user assert utils.get_user(session, 2) is None - @pytest.mark.parametrize('string, formatted_time', TIMES) - def test_get_time_from_string(self, string: str, - formatted_time: time) -> None: + @pytest.mark.parametrize("string, formatted_time", TIMES) + def test_get_time_from_string( + self, + string: str, + formatted_time: time, + ) -> None: assert utils.get_time_from_string(string) == formatted_time diff --git a/tests/test_weekview.py b/tests/test_weekview.py index 7980b38b..a1d40a04 100644 --- a/tests/test_weekview.py +++ b/tests/test_weekview.py @@ -1,5 +1,5 @@ -from bs4 import BeautifulSoup import pytest +from bs4 import BeautifulSoup from app.routers.event import create_event from app.routers.weekview import get_week_dates @@ -9,48 +9,53 @@ def create_weekview_event(events, session, user): for event in events: create_event( db=session, - title='test', + title="test", start=event.start, end=event.end, owner_id=user.id, - color=event.color + color=event.color, ) def test_get_week_dates(weekdays, sunday): week_dates = list(get_week_dates(sunday)) for i in range(6): - assert week_dates[i].strftime('%A') == weekdays[i] + assert week_dates[i].strftime("%A") == weekdays[i] def test_weekview_day_names(session, user, client, weekdays): response = client.get("/week/2021-1-3") - soup = BeautifulSoup(response.content, 'html.parser') - day_divs = soup.find_all("div", {"class": 'day-name'}) + soup = BeautifulSoup(response.content, "html.parser") + day_divs = soup.find_all("div", {"class": "day-name"}) for i in range(6): assert weekdays[i][:3].upper() in str(day_divs[i]) def test_weekview_day_dates(session, user, client, sunday): response = client.get("/week/2021-1-3") - soup = BeautifulSoup(response.content, 'html.parser') - day_divs = soup.find_all("span", {"class": 'date-nums'}) + soup = BeautifulSoup(response.content, "html.parser") + day_divs = soup.find_all("span", {"class": "date-nums"}) week_dates = list(get_week_dates(sunday)) for i in range(6): - time_str = f'{week_dates[i].day} / {week_dates[i].month}' + time_str = f"{week_dates[i].day} / {week_dates[i].month}" assert time_str in day_divs[i] @pytest.mark.parametrize( "date,event", - [("2021-1-31", 'event1'), - ("2021-1-31", 'event2'), - ("2021-2-3", 'event3')] + [("2021-1-31", "event1"), ("2021-1-31", "event2"), ("2021-2-3", "event3")], ) def test_weekview_html_events( - event1, event2, event3, session, user, client, date, event + event1, + event2, + event3, + session, + user, + client, + date, + event, ): create_weekview_event([event1, event2, event3], session=session, user=user) response = client.get(f"/week/{date}") - soup = BeautifulSoup(response.content, 'html.parser') + soup = BeautifulSoup(response.content, "html.parser") assert event in str(soup.find("div", {"id": event})) From 9998fd85b7ae509a16d6950b5fc9be7ff9df1aa2 Mon Sep 17 00:00:00 2001 From: Yam Mesicka <yammesicka@gmail.com> Date: Wed, 24 Feb 2021 21:59:44 +0200 Subject: [PATCH 25/46] Delete .DS_Store --- .DS_Store | Bin 6148 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**<q8>++&mCkOWA81W14cNZ<zv;LbK1Poaz?KmsK2CSc!( z0ynLxE!0092;Krf2c+FF_Fe*7ECH>lEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0<F0fCPF1$Cyrb|F7^5{eNG?83~ZUUlGt@xh*qZDeu<Z%US-OSsOPv j)R!Z4KLME7ReXlK;d!wEw5GODWMKRea10D2@KpjYNUI8I From 095ad21919d4f54e705abc9be7aea9b65f4c31be Mon Sep 17 00:00:00 2001 From: Yam Mesicka <yammesicka@gmail.com> Date: Wed, 24 Feb 2021 21:59:59 +0200 Subject: [PATCH 26/46] Delete git --- git | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 git diff --git a/git b/git deleted file mode 100644 index e69de29b..00000000 From 2199bf9e81ce01c4690718511e02cf06c0f52c1b Mon Sep 17 00:00:00 2001 From: Yam Mesicka <yammesicka@gmail.com> Date: Wed, 24 Feb 2021 22:01:11 +0200 Subject: [PATCH 27/46] Update requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 5d07acea..694f6209 100644 --- a/requirements.txt +++ b/requirements.txt @@ -69,6 +69,7 @@ importlib-metadata==3.3.0 inflect==4.1.0 iniconfig==1.1.1 iso-639==0.4.5 +isort==5.6.4 Jinja2==2.11.2 joblib==1.0.0 lazy-object-proxy==1.5.2 From 32e3a74ed3bc760be56a3b7e7d2c57b4f0aaa3f6 Mon Sep 17 00:00:00 2001 From: Elior Digmi <71041101+Eliory09@users.noreply.github.com> Date: Wed, 24 Feb 2021 22:47:33 +0200 Subject: [PATCH 28/46] Feature/shared list (#278) --- app/database/models.py | 46 +++++++- app/routers/event.py | 76 ++++++++++++- app/static/event/eventedit.css | 10 +- app/static/event/eventview.css | 26 ++--- app/static/js/shared_list.js | 31 ++++++ app/templates/eventedit.html | 73 +++++++----- app/templates/eventview.html | 6 +- .../event/edit_event_details_tab.html | 24 ++++ .../event/view_event_details_tab.html | 56 ++++++---- tests/test_shared_list.py | 104 ++++++++++++++++++ 10 files changed, 383 insertions(+), 69 deletions(-) create mode 100644 app/static/js/shared_list.js create mode 100644 tests/test_shared_list.py diff --git a/app/database/models.py b/app/database/models.py index af19c004..9eb42fba 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -114,6 +114,11 @@ class Event(Base): cascade="all, delete", back_populates="events", ) + shared_list = relationship( + "SharedList", + uselist=False, + back_populates="event", + ) comments = relationship("Comment", back_populates="event") # PostgreSQL @@ -479,6 +484,41 @@ def __repr__(self): ) +class SharedListItem(Base): + __tablename__ = "shared_list_item" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String, nullable=False) + amount = Column(Float, nullable=False) + participant = Column(String, nullable=True) + notes = Column(String, nullable=True) + shared_list_id = Column(Integer, ForeignKey("shared_list.id")) + + shared_list = relationship("SharedList", back_populates="items") + + def __repr__(self): + return ( + f"<Item {self.id}: {self.name} " + f"Amount: {self.amount} " + f"Participant: {self.participant} " + f"Notes: {self.notes})>" + ) + + +class SharedList(Base): + __tablename__ = "shared_list" + + id = Column(Integer, primary_key=True, index=True) + event_id = Column(String, ForeignKey("events.id")) + title = Column(String, nullable=True) + + items = relationship("SharedListItem", back_populates="shared_list") + event = relationship("Event", back_populates="shared_list") + + def __repr__(self): + return f"<Shared list {self.id}: " f"Items: {self.items}>" + + class Joke(Base): __tablename__ = "jokes" @@ -497,9 +537,11 @@ class InternationalDays(Base): # insert language data -# Credit to adrihanu https://stackoverflow.com/users/9127249/adrihanu -# https://stackoverflow.com/questions/17461251 + def insert_data(target, session: Session, **kw): + """insert language data + Credit to adrihanu https://stackoverflow.com/users/9127249/adrihanu + https://stackoverflow.com/questions/17461251""" session.execute( target.insert(), {"id": 1, "name": "English"}, diff --git a/app/routers/event.py b/app/routers/event.py index 918f050a..eb65a356 100644 --- a/app/routers/event.py +++ b/app/routers/event.py @@ -2,7 +2,7 @@ import urllib from datetime import datetime as dt from operator import attrgetter -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, NamedTuple, Optional, Tuple from fastapi import APIRouter, Depends, HTTPException, Request from pydantic import BaseModel @@ -11,10 +11,18 @@ from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound from sqlalchemy.sql.elements import Null from starlette import status +from starlette.datastructures import ImmutableMultiDict from starlette.responses import RedirectResponse, Response from starlette.templating import _TemplateResponse -from app.database.models import Comment, Event, User, UserEvent +from app.database.models import ( + Comment, + Event, + SharedList, + SharedListItem, + User, + UserEvent, +) from app.dependencies import get_db, logger, templates from app.internal import comment as cmt from app.internal.emotion import get_emotion @@ -54,6 +62,12 @@ ) +class SharedItem(NamedTuple): + name: str + amount: float + participant: str + + class EventModel(BaseModel): title: str start: dt @@ -134,6 +148,7 @@ async def create_new_event( title, invited_emails, ) + shared_list = extract_shared_list_from_data(event_info=data, db=session) latitude, longitude = None, None if vc_link: @@ -161,6 +176,7 @@ async def create_new_event( category_id=category_id, availability=availability, is_google_event=is_google_event, + shared_list=shared_list, privacy=privacy, ) @@ -429,6 +445,7 @@ def create_event( category_id: Optional[int] = None, availability: bool = True, is_google_event: bool = False, + shared_list: Optional[SharedList] = None, privacy: str = PrivacyKinds.Public.name, ): """Creates an event and an association.""" @@ -453,6 +470,7 @@ def create_event( invitees=invitees_concatenated, all_day=all_day, category_id=category_id, + shared_list=shared_list, availability=availability, is_google_event=is_google_event, ) @@ -551,6 +569,60 @@ def add_new_event(values: dict, db: Session) -> Optional[Event]: return None +def extract_shared_list_from_data( + event_info: ImmutableMultiDict, + db: Session, +) -> Optional[SharedList]: + """Extract shared list items from POST data. + Return: + SharedList: SharedList object stored in the database. + """ + raw_items = zip( + event_info.getlist("item-name"), + event_info.getlist("item-amount"), + event_info.getlist("item-participant"), + ) + items = [] + title = event_info.get("shared-list-title") + for name, amount, participant in raw_items: + item = SharedItem(name, amount, participant) + if _check_item_is_valid(item): + item_dict = item._asdict() + item_dict["amount"] = float(item_dict["amount"]) + items.append(item_dict) + return _create_shared_list({title: items}, db) + + +def _check_item_is_valid(item: SharedItem) -> bool: + return ( + item is not None + and item.amount.isnumeric() + and item.participant is not None + ) + + +def _create_shared_list( + raw_shared_list: Dict[str, Dict[str, Any]], + db: Session, +) -> Optional[SharedList]: + try: + title = list(raw_shared_list.keys())[0] or "Shared List" + except IndexError as e: + logger.exception(e) + return None + shared_list = create_model(db, SharedList, title=title) + try: + items = list(raw_shared_list.values())[0] + for item in items: + item = create_model(db, SharedListItem, **item) + shared_list.items.append(item) + except (IndexError, KeyError) as e: + logger.exception(e) + return None + else: + return shared_list + + def get_template_to_share_event( event_id: int, user_name: str, diff --git a/app/static/event/eventedit.css b/app/static/event/eventedit.css index 9c5d3fda..0193595b 100644 --- a/app/static/event/eventedit.css +++ b/app/static/event/eventedit.css @@ -64,4 +64,12 @@ textarea, input[type="submit"] { width: 100%; -} \ No newline at end of file +} + +.shared-list-item-off { + display: none; +} + +.shared-list-item-on { + display: flex; +} diff --git a/app/static/event/eventview.css b/app/static/event/eventview.css index 0b768405..f3900a04 100644 --- a/app/static/event/eventview.css +++ b/app/static/event/eventview.css @@ -12,13 +12,13 @@ body { flex-direction: column; } -.event_view_wrapper { +.event-view-wrapper { display: flex; flex-direction: column; height: 100%; } -#event_view_tabs { +#event-view-tabs { flex: 1; } @@ -28,23 +28,23 @@ body { flex-direction: column; } -.event_info_row, -.event_info_row_start, -.event_info_row_end { - display: flex +.event-info-row, +.event-info-row-start, +.event-info-row-end { + display: flex; } -.event_info_row_start, -.event_info_row_end { +.event-info-row-start, +.event-info-row-end { flex: 1; } -.event_info_row_end { +.event-info-row-end { justify-content: flex-end; } -div.event_info_row, -.event_info_buttons_row { +div.event-info-row, +.event-info-buttons-row { align-items: center; margin-block-start: 0.2em; margin-block-end: 0.2em; @@ -61,7 +61,7 @@ div.event_info_row, padding-right: 1em; } -.event_info_buttons_row { +.event-info-buttons-row { min-height: 2.25em; max-height: 3.25em; } @@ -70,6 +70,6 @@ button { height: 100%; } -.google_maps_object { +.google-maps-object { width: 100%; } diff --git a/app/static/js/shared_list.js b/app/static/js/shared_list.js new file mode 100644 index 00000000..eab047e1 --- /dev/null +++ b/app/static/js/shared_list.js @@ -0,0 +1,31 @@ +window.addEventListener('load', () => { + document.getElementById("btn-add-item").addEventListener('click', addItem); +}); + + +function addItem() { + const LIST_ITEMS_NUM = document.querySelectorAll("#items > div").length; + const list_items = document.getElementById("items"); + let shared_list_item = document.getElementById("shared-list-item").cloneNode(true); + + shared_list_item.className = "shared-list-item-on"; + shared_list_item.id = shared_list_item.id + LIST_ITEMS_NUM; + for (child of shared_list_item.children) { + if (child.tagName == 'INPUT') { + child.setAttribute('required', 'required'); + } + } + list_items.appendChild(shared_list_item); + document.querySelector(`#${shared_list_item.id} > .remove-btn`).addEventListener('click', () => { + removeItem(shared_list_item, list_items); + }) +} + + +function removeItem(shared_list_item, list_items) { + shared_list_item.remove(); + for (const [index, child] of list_items.childNodes.entries()) + { + child.id = "shared-list-item" + String(index); + } +} diff --git a/app/templates/eventedit.html b/app/templates/eventedit.html index e2f49ed3..06aaf04f 100644 --- a/app/templates/eventedit.html +++ b/app/templates/eventedit.html @@ -1,30 +1,45 @@ -<link href="{{ url_for('static', path='event/eventedit.css') }}" rel="stylesheet"> - <form name="eventeditform" method="POST"> - <!-- Temporary nav layout based on bootstrap --> - <ul class="nav nav-tabs" id="event_edit_nav" role="tablist"> - <li class="nav-item"> - <a class="nav-link active" id="eventdetails-tab" data-toggle="tab" href="#eventdetails" role="tab" aria-controls="eventdetails" aria-selected="true"> - Event Details - </a> - </li> - <!-- Copy commented section to add another navigation item --> - <!-- <li class="nav-item"> - <a class="nav-link" id="(CHANGE_ME)-tab" data-toggle="tab" href="#(CHANGE_ME)" role="tab" aria-controls="(CHANGE_ME)" aria-selected="true"> - (CHANGE_ME) - </a> - </li> --> - </ul> - <div class="tab-content" id="event_edit_tabs"> - <div class="tab-pane fade show active" id="eventdetails" role="tabpanel" aria-labelledby="eventdetails-tab"> - {% include "partials/calendar/event/edit_event_details_tab.html" %} - </div> - <!-- Copy commented section to add another tab --> - <!-- <div class="tab-pane fade" id="(CHANGE_ME)" role="tabpanel" aria-labelledby="(CHANGE_ME)-tab"> - ADD INCLUDE HERE - </div> --> - </div> - <div class="form_row"> - <input type="submit" value="Submit"> +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <link href="{{ url_for('static', path='event/eventedit.css') }}" rel="stylesheet"> + <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl" crossorigin="anonymous"> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"> + <title>Event edit + + +
+ + +
+
+ {% include "partials/calendar/event/edit_event_details_tab.html" %}
- - + + +
+
+ +
+ + + + + + diff --git a/app/templates/eventview.html b/app/templates/eventview.html index ef779db2..06dec1f1 100644 --- a/app/templates/eventview.html +++ b/app/templates/eventview.html @@ -9,9 +9,9 @@ {% endblock head %} {% block content %} -
+
-