Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Feature/favorite quotes #299

Open
wants to merge 74 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 71 commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
fef70ba
feature: add favorite quotes
subscorp Feb 15, 2021
da92003
fix: fixed conflicts
subscorp Feb 15, 2021
4d4ac98
fix: fixed flake8 error
subscorp Feb 15, 2021
9a103e6
Merge branch 'develop' of https://github.com/PythonFreeCourse/calenda…
subscorp Feb 15, 2021
8392bb2
fix: fixed pytest issues
subscorp Feb 15, 2021
c728f0c
a small fix
subscorp Feb 15, 2021
f62f8d3
fix: fixed display issue in favorite_quotes.html
subscorp Feb 15, 2021
82d79fd
fix: moved favorite quotes from navigation bar to profile
subscorp Feb 15, 2021
26f6a91
fix: fixed issues according to comments
subscorp Feb 16, 2021
076ab07
fix: fixed conflicts
subscorp Feb 17, 2021
701228e
fix: fixed issues according to comments
subscorp Feb 18, 2021
a9fe632
fix: fixed conflicts
subscorp Feb 18, 2021
fbe286c
fix: fixed flake8 issues
subscorp Feb 18, 2021
3026dd7
fix: fixed issue in pytest
subscorp Feb 18, 2021
2238a8d
fix: a tiny improvement
subscorp Feb 18, 2021
d8ce7b5
fix: small fix
subscorp Feb 18, 2021
47b0b5f
fix: another tiny improvement
subscorp Feb 18, 2021
1308754
fix: fixed issues according to comments
subscorp Feb 20, 2021
fef81a7
fix: fixed conflicts
subscorp Feb 20, 2021
e53fab9
fix: fixed pytest issues
subscorp Feb 20, 2021
fedc394
fix: a small fix
subscorp Feb 20, 2021
ee7ec43
fix: another small fix
subscorp Feb 20, 2021
c012d64
Update: now using the new login system
subscorp Feb 20, 2021
ac4a818
Update: now using the new login system
subscorp Feb 20, 2021
1800655
fix: fixed low coverage in pytest
subscorp Feb 20, 2021
a975a1c
fix: moved styles to css
subscorp Feb 21, 2021
bdba51f
fix: fixed conflicts
subscorp Feb 21, 2021
34d1ee6
Update: moved Favorite Quotes to profile
subscorp Feb 21, 2021
3098862
fix: fixed issues according to comments
subscorp Feb 22, 2021
1791fa6
fix: fixed conflicts
subscorp Feb 22, 2021
b722eea
fix: fixed pytest issue
subscorp Feb 22, 2021
5e5f6c3
fix: fixed issues in pytest
subscorp Feb 22, 2021
5d243e3
fix: small fix
subscorp Feb 22, 2021
56b0988
fix: fixed conflicts
subscorp Feb 23, 2021
097271a
fix: fixed issues according to comments
subscorp Feb 23, 2021
667b4a3
Merge branch 'develop' of https://github.com/PythonFreeCourse/calenda…
subscorp Feb 23, 2021
8a0464e
fix: a small fix
subscorp Feb 23, 2021
1fa587a
fix: fixed issues in pytest
subscorp Feb 23, 2021
de61e04
fix: flake8 error fix
subscorp Feb 24, 2021
f6355dc
fix: flake8 error fix
subscorp Feb 24, 2021
9c3a795
fix: flake8 error fix
subscorp Feb 24, 2021
f2c7d7a
fix: fixed conflicts
subscorp Feb 24, 2021
0e0f704
fix: revert some changes due to a bug
subscorp Feb 24, 2021
6bb30e1
Update: moved functions out of main
subscorp Feb 24, 2021
0a846f7
fix: a small fix
subscorp Feb 24, 2021
a059d81
Updates: A. heart now appears only when logged in. B. landing page no…
subscorp Feb 24, 2021
9a76d3f
fix: some small fixes and mainly added tests to both the audio featur…
subscorp Feb 25, 2021
37ec594
fix: fixed conflicts
subscorp Feb 25, 2021
0ea5847
fix: fixed issues according to comments
subscorp Feb 25, 2021
9f55f15
fix: fixed issues according to comments
subscorp Feb 25, 2021
99f5e33
fix: a small, final fix
subscorp Feb 25, 2021
c390fea
fix: fixed issues according to comments
subscorp Feb 25, 2021
3e33aa2
fix: fixed merge issue :)
subscorp Feb 26, 2021
79a8e6d
fix: fixed conflicts
subscorp Feb 26, 2021
19aae78
fix: trying to fix a bug in pytest
subscorp Feb 26, 2021
c671d5f
fix: trying to fix the bug
subscorp Feb 26, 2021
1e4c089
fix: trying to fix the bug
subscorp Feb 26, 2021
dccf688
fix: trying to fix the bug
subscorp Feb 26, 2021
095fcea
Merge branch 'develop' of https://github.com/PythonFreeCourse/calenda…
subscorp Feb 26, 2021
60af145
trying to fix the bug
subscorp Feb 26, 2021
07f6cc6
fix: trying to fix the bug (it's only in github)
subscorp Feb 26, 2021
970f296
fix: trying to fix the bug (it's only in github)
subscorp Feb 26, 2021
e5bcdc5
fix: trying to fix the bug
subscorp Feb 26, 2021
0504e22
fix: trying to fix the bug
subscorp Feb 26, 2021
12481b4
fix: trying to fix the bug
subscorp Feb 26, 2021
0d4042f
fix: trying to fix the bug
subscorp Feb 26, 2021
de05d1a
fix: trying to fix the bug
subscorp Feb 26, 2021
4abe14d
fix: trying to fix the bug
subscorp Feb 26, 2021
404e195
fix: trying to fix the bug
subscorp Feb 26, 2021
23becd6
fix: trying to fix the bug
subscorp Feb 26, 2021
943162a
fix: trying to fix the bug
subscorp Feb 26, 2021
9f5244e
fix: trying to fix the bug
subscorp Feb 26, 2021
cac2432
Merge branch 'develop' of https://github.com/PythonFreeCourse/calenda…
subscorp Feb 26, 2021
5fd6929
fix: fixed issues according to comments
subscorp Feb 26, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions app/database/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ class UserFeature(Base):
__tablename__ = "user_feature"

id = Column(Integer, primary_key=True, index=True)
feature_id = Column('feature_id', Integer, ForeignKey('features.id'))
user_id = Column('user_id', Integer, ForeignKey('users.id'))
feature_id = Column("feature_id", Integer, ForeignKey("features.id"))
user_id = Column("user_id", Integer, ForeignKey("users.id"))

is_enable = Column(Boolean, default=False)

Expand Down Expand Up @@ -485,6 +485,7 @@ class Quote(Base):
id = Column(Integer, primary_key=True, index=True)
text = Column(String, nullable=False)
author = Column(String)
is_favorite = Column(Boolean, default=False, nullable=False)


class Country(Base):
Expand Down Expand Up @@ -596,3 +597,11 @@ def insert_data(target, session: Session, **kw):


event.listen(Language.__table__, "after_create", insert_data)


class UserQuotes(Base):
__tablename__ = "user_quotes"

id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
quote_id = Column(Integer, ForeignKey("quotes.id"), nullable=False)
50 changes: 40 additions & 10 deletions app/internal/daily_quotes.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
from datetime import date
from typing import Dict, Optional
from os.path import join, relpath
from typing import Dict, List, Optional

from sqlalchemy.orm import Session
from sqlalchemy.sql.expression import func

from app.database.models import Quote
from app.database.models import Quote, UserQuotes
from app.dependencies import MEDIA_PATH

EMPTY_HEART_PATH = relpath(join(MEDIA_PATH, "empty_heart.png"), "app")
FULL_HEART_PATH = relpath(join(MEDIA_PATH, "full_heart.png"), "app")
FAVORITES_PATH = join("../..", FULL_HEART_PATH)
subscorp marked this conversation as resolved.
Show resolved Hide resolved
TOTAL_DAYS = 366


Expand All @@ -19,13 +24,15 @@ def get_quote(quote_: Dict[str, Optional[str]]) -> Quote:
A new Quote object.
"""
return Quote(
text=quote_['text'],
author=quote_['author'],
text=quote_["text"],
author=quote_["author"],
is_favorite=False,
)


def get_quote_of_day(
session: Session, requested_date: date = date.today()
session: Session,
requested_date: date = date.today(),
) -> Optional[Quote]:
"""Returns the Quote object for the specific day.

Expand All @@ -39,9 +46,32 @@ def get_quote_of_day(
A Quote object.
"""
day_number = requested_date.timetuple().tm_yday
quote = (session.query(Quote)
.filter(Quote.id % TOTAL_DAYS == day_number)
.order_by(func.random())
.first()
)
quote = (
session.query(Quote)
.filter(Quote.id % TOTAL_DAYS == day_number)
.order_by(func.random())
.first()
)
return quote


def get_quotes(session: Session, user_id: int) -> List[Quote]:
"""Retrieves the users' favorite quotes from the database."""
return session.query(Quote).filter_by(id=UserQuotes.quote_id).all()


def is_quote_favorite(
session: Session,
user_id: int,
quote_of_day: Quote,
) -> bool:
"""Checks if the daily quote is in favorites list."""
if not quote_of_day:
return False

exists = (
session.query(UserQuotes)
.filter(user_id == user_id, UserQuotes.quote_id == quote_of_day.id)
.all()
)
return bool(exists)
subscorp marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 2 additions & 2 deletions app/internal/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ def add_countries_to_db(session: Session) -> None:
session.commit()


@functools.lru_cache
@functools.lru_cache(maxsize=None)
def get_all_countries_names(session: Session) -> List[str]:
"""
Returns a cached list of the countries names.
Expand Down Expand Up @@ -174,4 +174,4 @@ async def get_location_coordinates(
name=geolocation.raw["display_name"],
)
return location
return address
return address # delete db test to make sure
6 changes: 4 additions & 2 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,20 @@
from fastapi.staticfiles import StaticFiles
from sqlalchemy.orm import Session

import app.internal.features as internal_features
from app import config
from app.database import engine, models
from app.dependencies import (
MEDIA_PATH,
SOUNDS_PATH,
STATIC_PATH,
UPLOAD_PATH,
SessionLocal,
get_db,
logger,
templates,
SessionLocal,
)
from app.internal import daily_quotes, json_data_loader
import app.internal.features as internal_features
from app.internal.languages import set_ui_language
from app.internal.security.ouath2 import auth_exception_handler
from app.routers.salary import routes as salary
Expand Down Expand Up @@ -83,6 +83,7 @@ def create_tables(engine, psql_environment):
search,
settings,
telegram,
todo_list,
user,
weekview,
weight,
Expand Down Expand Up @@ -137,6 +138,7 @@ async def swagger_ui_redirect():
search.router,
settings.router,
telegram.router,
todo_list.router,
user.router,
weekview.router,
weight.router,
Expand Down
Binary file added app/media/empty_heart.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/media/full_heart.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
56 changes: 56 additions & 0 deletions app/routers/favorite_quotes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from fastapi import APIRouter, Depends, Form, Request
from sqlalchemy.orm import Session

from app.database import models
from app.dependencies import get_db, templates
from app.internal import daily_quotes
from app.internal.security.dependencies import current_user

router = APIRouter(
prefix="/quotes",
tags=["quotes"],
responses={404: {"description": "Not found"}},
)


@router.post("/save")
async def save_quote(
user: models.User = Depends(current_user),
quote_id: int = Form(...),
db: Session = Depends(get_db),
) -> None:
"""Saves a quote in the database."""
db.merge(models.UserQuotes(user_id=user.user_id, quote_id=quote_id))
db.commit()


@router.delete("/delete")
async def delete_quote(
user: models.User = Depends(current_user),
quote_id: int = Form(...),
db: Session = Depends(get_db),
) -> None:
"""Deletes a quote from the database."""
db.query(models.UserQuotes).filter(
models.UserQuotes.user_id == user.user_id,
models.UserQuotes.quote_id == quote_id,
).delete()
db.commit()


@router.get("/favorites")
async def favorite_quotes(
request: Request,
db: Session = Depends(get_db),
user: models.User = Depends(current_user),
) -> templates.TemplateResponse:
"""html page for displaying the users' favorite quotes."""
quotes = daily_quotes.get_quotes(db, user.user_id)
return templates.TemplateResponse(
"favorite_quotes.html",
{
"request": request,
"quotes": quotes,
"full_heart": daily_quotes.FAVORITES_PATH,
},
)
68 changes: 68 additions & 0 deletions app/static/favorite_quotes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
const FULL_HEART = "../../media/full_heart.png";
const EMPTY_HEART = "../../media/empty_heart.png";

// Adding event listener
window.addEventListener("load", function () {
const quoteContainer = document.getElementById("quote-container");
if (!quoteContainer) {
return;
}
const isConnected = quoteContainer.dataset.connected;
if (isConnected !== "True") {
return;
}
const fullHeart = document.getElementsByClassName("full-heart")[0];
const emptyHeart = document.getElementsByClassName("empty-heart")[0];
if (fullHeart) {
fullHeart.classList.toggle("full-heart");
} else if (emptyHeart) {
emptyHeart.classList.toggle("empty-heart");
}

let hearts = Array.from(document.getElementsByClassName("heart"));
hearts.forEach((heart_element) => {
if (heart_element) {
heart_element.addEventListener("click", function () {
onHeartClick(heart_element);
});
}
});
});

/**
* @summary This function is a handler for the event of heart-click.
* Whenever a user clicks on a heart icon, in case of empty heart:
* saves quote in favorites, as well as changing
* the heart icon from empty to full.
* In case of full heart:
* Removes it and switch back to empty heart icon.
* Uses the save_or_remove_quote function to handle db operations.
*/
function onHeartClick(heart_element) {
const quote_id = heart_element.dataset.qid;
if (heart_element.dataset.heart == "off") {
heart_element.src = FULL_HEART;
heart_element.dataset.heart = "on";
save_or_remove_quote(quote_id, true);
} else {
heart_element.src = EMPTY_HEART;
heart_element.dataset.heart = "off";
save_or_remove_quote(quote_id, false);
if (heart_element.classList.contains("favorites")) {
heart_element.parentNode.parentNode.remove();
}
}
}

/**
* @summary Saves or removes a quote from favorites.
*/
function save_or_remove_quote(quote_id, to_save) {
const method = to_save ? "post" : "delete";
const url = method == "post" ? "/quotes/save" : "/quotes/delete";
const xhr = new XMLHttpRequest();
quote_id = parseInt(quote_id);
xhr.open(method, url);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send(`quote_id=${quote_id}`);
}
30 changes: 30 additions & 0 deletions app/static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ p {
justify-content: space-around;
}

p#p-quote {
align-items: center;
justify-content: center;
}

#author {
font-size: small;
}

#inner:hover {
cursor: pointer;
padding: 50px;
Expand Down Expand Up @@ -156,6 +165,27 @@ p {
font-size: 1.25rem;
}

.subtitle {
font-size: 1.25rem;
}

.heart {
width: 1.5em;
height: 1.5em;
subscorp marked this conversation as resolved.
Show resolved Hide resolved
}

.full-heart {
display: none;
}

.empty-heart {
display: none;
}

.favorite-quotes {
margin-bottom: 1.5em;
}

.error-message {
line-height: 0;
color: red;
Expand Down
1 change: 1 addition & 0 deletions app/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
<!-- This bootstrap version is needed here because of the toggler bug in the beta version-->
<script src="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha2/js/bootstrap.bundle.min.js" integrity="sha384-BOsAfwzjNJHrJ8cZidOg56tcQWfp6y72vEJ8xQ9w6Quywb24iOsW913URv1IS4GD" crossorigin="anonymous"></script>
<script src="{{ url_for('static', path='/horoscope.js') }}"></script>
<script src="{{ url_for('static', path='/favorite_quotes.js') }}"></script>
<script src="{{ url_for('static', path='/graph.js') }}"></script>
<script type="text/javascript" src="{{ url_for( 'static', path='/audio_settings.js' ) }}"></script>
<script src="{{ url_for('static', path='/js/darkmode.js') }}"></script>
Expand Down
20 changes: 20 additions & 0 deletions app/templates/favorite_quotes.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{% extends "base.html" %}

{% block content %}
<div class="d-flex position-relative" id="quote-container" data-connected="True">
<div class="container mx-auto gap-3">
<h2 id="favorite-quotes">Favorite Quotes</h2>
{% for quote in quotes %}
<p id="p-quote"><cite><q id="quote">{{ quote.text }}</q></cite>
{% if quote.author %}
<span id="author">&nbsp; \ {{ quote.author }}
{% else %}
<span>&nbsp;
{% endif %}
<img src="{{full_heart}}" data-heart="on" data-qid="{{quote.id}}" class="heart favorites">
</span>
</p>
{% endfor %}
</div>
</div>
{% endblock %}
38 changes: 0 additions & 38 deletions app/templates/home.html

This file was deleted.

Loading