diff --git a/app/database/models.py b/app/database/models.py index 4fa0d667..fbab423c 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -118,3 +118,22 @@ class Quote(Base): id = Column(Integer, primary_key=True, index=True) text = Column(String, nullable=False) author = Column(String) + + +class Zodiac(Base): + __tablename__ = "zodiac-signs" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String, nullable=False) + start_month = Column(Integer, nullable=False) + start_day_in_month = Column(Integer, nullable=False) + end_month = Column(Integer, nullable=False) + end_day_in_month = Column(Integer, nullable=False) + + def __repr__(self): + return ( + f'' + ) diff --git a/app/internal/quotes/daily_quotes.py b/app/internal/daily_quotes.py similarity index 63% rename from app/internal/quotes/daily_quotes.py rename to app/internal/daily_quotes.py index 6bbfef4b..c2b9f846 100644 --- a/app/internal/quotes/daily_quotes.py +++ b/app/internal/daily_quotes.py @@ -1,5 +1,5 @@ from datetime import date -from typing import Optional +from typing import Dict, Optional from app.database.models import Quote @@ -9,6 +9,15 @@ TOTAL_DAYS = 366 +def create_quote_object(quotes_fields: Dict[str, Optional[str]]) -> Quote: + """This function create a quote object from given fields dictionary. + It is used for adding the data from the json into the db""" + return Quote( + text=quotes_fields['text'], + author=quotes_fields['author'] + ) + + def quote_per_day( session: Session, date: date = date.today() ) -> Optional[Quote]: diff --git a/app/internal/json_data_loader.py b/app/internal/json_data_loader.py new file mode 100644 index 00000000..bcf74e7c --- /dev/null +++ b/app/internal/json_data_loader.py @@ -0,0 +1,57 @@ +import json +import os +from typing import Dict, List, Callable, Union + +from loguru import logger +from sqlalchemy.orm import Session + +from app.database.database import Base +from app.database.models import Quote, Zodiac +from app.internal import daily_quotes, zodiac + + +def get_data_from_json(path: str) -> List[Dict[str, Union[str, int, None]]]: + """This function reads all of the data from a specific JSON file. + The json file consists of list of dictionaries""" + try: + with open(path, 'r') as f: + json_content_list = json.load(f) + except (IOError, ValueError): + file_name = os.path.basename(path) + logger.exception( + f"An error occurred during reading of json file: {file_name}") + return [] + return json_content_list + + +def is_table_empty(session: Session, table: Base) -> bool: + return session.query(table).count() == 0 + + +def load_data( + session: Session, path: str, + table: Base, object_creator_function: Callable) -> None: + """This function loads the specific data to the db, + if it wasn't already loaded""" + if not is_table_empty(session, table): + return None + json_objects_list = get_data_from_json(path) + objects = [ + object_creator_function(json_object) + for json_object in json_objects_list] + session.add_all(objects) + session.commit() + + +def load_to_db(session) -> None: + """This function loads all the data for features + based on pre-defind json data""" + load_data( + session, 'app/resources/zodiac.json', + Zodiac, zodiac.create_zodiac_object) + load_data( + session, 'app/resources/quotes.json', + Quote, daily_quotes.create_quote_object) + """The quotes JSON file content is copied from the free API: + 'https://type.fit/api/quotes'. I saved the content so the API won't be + called every time the app is initialized.""" diff --git a/app/internal/quotes/load_quotes.py b/app/internal/quotes/load_quotes.py deleted file mode 100644 index 8f1b0e44..00000000 --- a/app/internal/quotes/load_quotes.py +++ /dev/null @@ -1,41 +0,0 @@ -import json -from typing import Dict, List, Optional - -from app.database.models import Quote - -from sqlalchemy.orm import Session - - -def get_quotes_from_json() -> List[Dict[str, Optional[str]]]: - """This function reads all of the daily quotes from a specific JSON file. - The JSON file content is copied from the free API: - 'https://type.fit/api/quotes'. I saved the content so the API won't be - called every time the app is initialized.""" - try: - with open('app/resources/quotes.json', 'r') as f: - quotes_list = json.load(f) - except (IOError, ValueError): - return [] - return quotes_list - - -def add_quotes_to_db(session: Session) -> None: - """This function reads the quotes and inserts them into the db""" - all_quotes = get_quotes_from_json() - quotes_objects = [ - Quote(text=quote['text'], author=quote['author']) - for quote in all_quotes - ] - session.add_all(quotes_objects) - session.commit() - - -def is_quotes_table_empty(session: Session) -> bool: - return session.query(Quote).count() == 0 - - -def load_daily_quotes(session: Session) -> None: - """This function loads the daily quotes to the db, - if they weren't already loaded""" - if is_quotes_table_empty(session): - add_quotes_to_db(session) diff --git a/app/internal/zodiac.py b/app/internal/zodiac.py new file mode 100644 index 00000000..d8de4738 --- /dev/null +++ b/app/internal/zodiac.py @@ -0,0 +1,42 @@ +from datetime import date +from typing import Dict, Union + +from sqlalchemy.orm import Session +from sqlalchemy import and_, or_ + +from app.database.models import Zodiac + + +def create_zodiac_object(zodiac_fields: Dict[str, Union[str, int]]) -> Zodiac: + """This function create a zodiac object from given fields dictionary. + It is used for adding the data from the json into the db""" + return Zodiac( + name=zodiac_fields['name'], + start_month=zodiac_fields['start_month'], + start_day_in_month=zodiac_fields['start_day_in_month'], + end_month=zodiac_fields['end_month'], + end_day_in_month=zodiac_fields['end_day_in_month'] + ) + + +def get_zodiac_of_day(session: Session, date: date) -> Zodiac: + """This function return a zodiac object + according to the current day.""" + first_month_of_sign_filter = and_( + Zodiac.start_month == date.month, + Zodiac.start_day_in_month <= date.day) + second_month_of_sign_filter = and_( + Zodiac.end_month == date.month, + Zodiac.end_day_in_month >= date.day) + zodiac_obj = session.query(Zodiac).filter( + or_(first_month_of_sign_filter, second_month_of_sign_filter)).first() + return zodiac_obj + + +# TODO: Call this function from the month view +def get_zodiac_of_month(session: Session, date: date) -> Zodiac: + """This function return a zodiac object + according to the current month.""" + zodiac_obj = session.query(Zodiac).filter( + Zodiac.end_month == date.month).first() + return zodiac_obj diff --git a/app/main.py b/app/main.py index aa48c487..19c0ed56 100644 --- a/app/main.py +++ b/app/main.py @@ -6,7 +6,7 @@ from app.database import models from app.database.database import engine, get_db from app.dependencies import (logger, MEDIA_PATH, STATIC_PATH, templates) -from app.internal.quotes import daily_quotes, load_quotes +from app.internal import daily_quotes, json_data_loader from app.routers import ( agenda, dayview, email, event, invitation, profile, search, telegram, whatsapp @@ -30,7 +30,7 @@ def create_tables(engine, psql_environment): app.mount("/static", StaticFiles(directory=STATIC_PATH), name="static") app.mount("/media", StaticFiles(directory=MEDIA_PATH), name="media") -load_quotes.load_daily_quotes(next(get_db())) +json_data_loader.load_to_db(next(get_db())) app.logger = logger diff --git a/app/resources/zodiac.json b/app/resources/zodiac.json new file mode 100644 index 00000000..a860f61e --- /dev/null +++ b/app/resources/zodiac.json @@ -0,0 +1,86 @@ +[ + { + "name": "Capricorn", + "start_month": 12, + "start_day_in_month": 22, + "end_month": 1, + "end_day_in_month": 19 + }, + { + "name": "Aquarius", + "start_month": 1, + "start_day_in_month": 20, + "end_month": 2, + "end_day_in_month": 18 + }, + { + "name": "Pisces", + "start_month": 2, + "start_day_in_month": 19, + "end_month": 3, + "end_day_in_month": 20 + }, + { + "name": "Aries", + "start_month": 3, + "start_day_in_month": 21, + "end_month": 4, + "end_day_in_month": 19 + }, + { + "name": "Taurus", + "start_month": 4, + "start_day_in_month": 20, + "end_month": 5, + "end_day_in_month": 20 + }, + { + "name": "Gemini", + "start_month": 5, + "start_day_in_month": 21, + "end_month": 6, + "end_day_in_month": 20 + }, + { + "name": "Cancer", + "start_month": 6, + "start_day_in_month": 21, + "end_month": 7, + "end_day_in_month": 22 + }, + { + "name": "Leo", + "start_month": 7, + "start_day_in_month": 23, + "end_month": 8, + "end_day_in_month": 22 + }, + { + "name": "Virgo", + "start_month": 8, + "start_day_in_month": 23, + "end_month": 9, + "end_day_in_month": 22 + }, + { + "name": "Libra", + "start_month": 9, + "start_day_in_month": 23, + "end_month": 10, + "end_day_in_month": 22 + }, + { + "name": "Scorpio", + "start_month": 10, + "start_day_in_month": 23, + "end_month": 11, + "end_day_in_month": 21 + }, + { + "name": "Sagittarius", + "start_month": 11, + "start_day_in_month": 22, + "end_month": 12, + "end_day_in_month": 21 + } +] \ No newline at end of file diff --git a/app/routers/dayview.py b/app/routers/dayview.py index b092540b..53fab46f 100644 --- a/app/routers/dayview.py +++ b/app/routers/dayview.py @@ -8,6 +8,7 @@ from app.database.database import get_db from app.database.models import Event, User from app.dependencies import TEMPLATES_PATH +from app.internal import zodiac templates = Jinja2Templates(directory=TEMPLATES_PATH) @@ -104,9 +105,11 @@ async def dayview(request: Request, date: str, db_session=Depends(get_db)): and_(Event.end >= day, Event.end < day_end), and_(Event.start < day_end, day_end < Event.end))) events_n_attrs = [(event, DivAttributes(event, day)) for event in events] + zodiac_obj = zodiac.get_zodiac_of_day(db_session, day) return templates.TemplateResponse("dayview.html", { "request": request, "events": events_n_attrs, "month": day.strftime("%B").upper(), - "day": day.day + "day": day.day, + "zodiac": zodiac_obj }) diff --git a/app/static/dayview.css b/app/static/dayview.css index 655e68ac..1c50d3e3 100644 --- a/app/static/dayview.css +++ b/app/static/dayview.css @@ -89,3 +89,16 @@ html { .action-continer:hover .action-icon { visibility: visible; } + +.zodiac-sign { + position: fixed; + right: 1.2em; + top: 1.6em; + padding-right: 0.4em; + padding-left: 0.4em; + padding-bottom: 0.2em; + border: solid 0.1px var(--primary); + background-color: var(--borders-variant); + border-radius:50px; + box-shadow: 1px 1px 2px #999; +} \ No newline at end of file diff --git a/app/static/images/zodiac/Aquarius.svg b/app/static/images/zodiac/Aquarius.svg new file mode 100644 index 00000000..3f5033fe --- /dev/null +++ b/app/static/images/zodiac/Aquarius.svg @@ -0,0 +1,28 @@ + + + + diff --git a/app/static/images/zodiac/Aries.svg b/app/static/images/zodiac/Aries.svg new file mode 100644 index 00000000..5bad7fc2 --- /dev/null +++ b/app/static/images/zodiac/Aries.svg @@ -0,0 +1,28 @@ + + + + diff --git a/app/static/images/zodiac/Cancer.svg b/app/static/images/zodiac/Cancer.svg new file mode 100644 index 00000000..aa258ac8 --- /dev/null +++ b/app/static/images/zodiac/Cancer.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/static/images/zodiac/Capricorn.svg b/app/static/images/zodiac/Capricorn.svg new file mode 100644 index 00000000..ff40d0ec --- /dev/null +++ b/app/static/images/zodiac/Capricorn.svg @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/app/static/images/zodiac/Gemini.svg b/app/static/images/zodiac/Gemini.svg new file mode 100644 index 00000000..3a8a3ceb --- /dev/null +++ b/app/static/images/zodiac/Gemini.svg @@ -0,0 +1,28 @@ + + + + diff --git a/app/static/images/zodiac/Leo.svg b/app/static/images/zodiac/Leo.svg new file mode 100644 index 00000000..9331b892 --- /dev/null +++ b/app/static/images/zodiac/Leo.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/static/images/zodiac/Libra.svg b/app/static/images/zodiac/Libra.svg new file mode 100644 index 00000000..cd470545 --- /dev/null +++ b/app/static/images/zodiac/Libra.svg @@ -0,0 +1,28 @@ + + + + diff --git a/app/static/images/zodiac/Pisces.svg b/app/static/images/zodiac/Pisces.svg new file mode 100644 index 00000000..1c8802a3 --- /dev/null +++ b/app/static/images/zodiac/Pisces.svg @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/app/static/images/zodiac/Sagittarius.svg b/app/static/images/zodiac/Sagittarius.svg new file mode 100644 index 00000000..b363925c --- /dev/null +++ b/app/static/images/zodiac/Sagittarius.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/static/images/zodiac/Scorpio.svg b/app/static/images/zodiac/Scorpio.svg new file mode 100644 index 00000000..7cba735d --- /dev/null +++ b/app/static/images/zodiac/Scorpio.svg @@ -0,0 +1,28 @@ + + + + diff --git a/app/static/images/zodiac/Taurus.svg b/app/static/images/zodiac/Taurus.svg new file mode 100644 index 00000000..74c319b6 --- /dev/null +++ b/app/static/images/zodiac/Taurus.svg @@ -0,0 +1,28 @@ + + + + diff --git a/app/static/images/zodiac/Virgo.svg b/app/static/images/zodiac/Virgo.svg new file mode 100644 index 00000000..bc699704 --- /dev/null +++ b/app/static/images/zodiac/Virgo.svg @@ -0,0 +1,28 @@ + + + + diff --git a/app/templates/dayview.html b/app/templates/dayview.html index 53d983fb..29a804b6 100644 --- a/app/templates/dayview.html +++ b/app/templates/dayview.html @@ -13,6 +13,11 @@ {{month}} {{day}} + {% if zodiac %} +
+ zodiac sign +
+ {% endif %}
diff --git a/requirements.txt b/requirements.txt index 668d2a03..1b07e2a7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -aiofiles==0.6.0 +aiofiles==0.6.0 aioredis==1.3.1 aiosmtpd==1.2.2 aiosmtplib==1.1.4 @@ -93,4 +93,4 @@ watchgod==0.6 websockets==8.1 word-forms==2.1.0 wsproto==1.0.0 -zipp==3.4.0 \ No newline at end of file +zipp==3.4.0 diff --git a/tests/asyncio_fixture.py b/tests/asyncio_fixture.py index fbc79108..cc2b7cdd 100644 --- a/tests/asyncio_fixture.py +++ b/tests/asyncio_fixture.py @@ -21,7 +21,6 @@ async def telegram_client(): Base.metadata.drop_all(bind=test_engine) -session = get_test_db() today_date = datetime.today().replace(hour=0, minute=0, second=0) @@ -36,7 +35,7 @@ def get_test_placeholder_user(): @pytest.fixture -def fake_user_events(): +def fake_user_events(session): Base.metadata.create_all(bind=test_engine) user = get_test_placeholder_user() session.add(user) diff --git a/tests/conftest.py b/tests/conftest.py index c73a3439..75a61680 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,7 +14,8 @@ 'tests.asyncio_fixture', 'tests.logger_fixture', 'smtpdfix', - 'tests.quotes_fixture' + 'tests.quotes_fixture', + 'tests.zodiac_fixture' ] # When testing in a PostgreSQL environment please make sure that: diff --git a/tests/test_json_data_loader.py b/tests/test_json_data_loader.py new file mode 100644 index 00000000..b0a27938 --- /dev/null +++ b/tests/test_json_data_loader.py @@ -0,0 +1,30 @@ +from app.database.models import Quote, Zodiac +from app.internal import json_data_loader + + +def get_objects_amount(session, table): + return session.query(table).count() + + +def test_load_daily_quotes(session): + json_data_loader.load_to_db(session) + assert get_objects_amount(session, Quote) > 0 + + +def test_load_zodiacs(session): + json_data_loader.load_to_db(session) + assert get_objects_amount(session, Zodiac) > 0 + + +# tests for basic functionality of the json data loader +def test_load_data_with_json_valueerror(mocker, session): + mocker.patch('json.load', side_effect=ValueError) + json_data_loader.load_to_db(session) + assert get_objects_amount(session, Quote) == 0 + + +def test_data_not_load_twice_to_db(session): + json_data_loader.load_to_db(session) + first_quotes_amount = get_objects_amount(session, Quote) + json_data_loader.load_to_db(session) + assert first_quotes_amount == get_objects_amount(session, Quote) diff --git a/tests/test_load_quotes.py b/tests/test_load_quotes.py deleted file mode 100644 index 7e82de2a..00000000 --- a/tests/test_load_quotes.py +++ /dev/null @@ -1,25 +0,0 @@ -from app.database.models import Quote -from app.internal.quotes import load_quotes - - -def get_quotes_amount(session): - return session.query(Quote).count() - - -# Tests for loading the quotes in the db: -def test_load_daily_quotes(session): - load_quotes.load_daily_quotes(session) - assert get_quotes_amount(session) > 0 - - -def test_load_daily_quotes_with_json_valueerror(mocker, session): - mocker.patch('json.load', side_effect=ValueError) - load_quotes.load_daily_quotes(session) - assert get_quotes_amount(session) == 0 - - -def test_quotes_not_load_twice_to_db(session): - load_quotes.load_daily_quotes(session) - first_quotes_amount = get_quotes_amount(session) - load_quotes.load_daily_quotes(session) - assert first_quotes_amount == get_quotes_amount(session) diff --git a/tests/test_quotes.py b/tests/test_quotes.py index 636f0418..7b7f49c8 100644 --- a/tests/test_quotes.py +++ b/tests/test_quotes.py @@ -1,11 +1,19 @@ from datetime import date -from app.internal.quotes import daily_quotes +from app.internal import daily_quotes DATE = date(2021, 1, 1) DATE2 = date(2021, 1, 2) +def test_create_quote_object(): + quotes_fields = { + 'text': 'some_quote', 'author': 'Freud'} + result = daily_quotes.create_quote_object(quotes_fields) + assert result.text == 'some_quote' + assert result.author == 'Freud' + + # Tests for providing a daily-quote from the db: def test_quote_per_day_no_quotes(session): assert daily_quotes.quote_per_day(session, DATE) is None diff --git a/tests/test_zodiac.py b/tests/test_zodiac.py new file mode 100644 index 00000000..2ae34504 --- /dev/null +++ b/tests/test_zodiac.py @@ -0,0 +1,40 @@ +from datetime import date + +from app.internal import zodiac + +DATE = date(2021, 3, 22) +DATE2 = date(2021, 4, 10) + + +def test_create_zodiac_object(): + zodiac_fields = { + 'name': 'aries', + 'start_month': 3, + 'start_day_in_month': 20, + 'end_month': 4, + 'end_day_in_month': 19 + } + result = zodiac.create_zodiac_object(zodiac_fields) + assert result.name == 'aries' + assert result.start_month == 3 + assert str(result) == "" + + +def test_get_correct_zodiac_first_half_month(session, zodiac1): + result = zodiac.get_zodiac_of_day(session, DATE) + assert result.name == zodiac1.name + + +def test_get_correct_zodiac_second_half_month(session, zodiac1): + result = zodiac.get_zodiac_of_day(session, DATE2) + assert result.name == zodiac1.name + + +def test_get_correct_month_zodiac(session, zodiac1): + result = zodiac.get_zodiac_of_month(session, DATE2) + assert result.name == zodiac1.name + + +def test_no_zodiac(session, zodiac1): + result = zodiac.get_zodiac_of_month(session, DATE) + assert result is None diff --git a/tests/zodiac_fixture.py b/tests/zodiac_fixture.py new file mode 100644 index 00000000..515b51e0 --- /dev/null +++ b/tests/zodiac_fixture.py @@ -0,0 +1,19 @@ +import pytest +from sqlalchemy.orm import Session + +from app.database.models import Zodiac +from tests.utils import create_model, delete_instance + + +@pytest.fixture +def zodiac1(session: Session) -> Zodiac: + zodiac = create_model( + session, Zodiac, + name="aries", + start_month=3, + start_day_in_month=20, + end_month=4, + end_day_in_month=19, + ) + yield zodiac + delete_instance(session, zodiac)