Merge pull request #52 from Simple2B/svyat/feat/book_star

Svyat/feat/book star
This commit is contained in:
Костя Столярский 2023-05-12 17:29:20 +03:00 committed by GitHub
commit 14133b0da3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 178 additions and 13 deletions

View File

@ -25,6 +25,7 @@ def create_app(environment="development"):
section_blueprint, section_blueprint,
vote_blueprint, vote_blueprint,
approve_blueprint, approve_blueprint,
star_blueprint,
) )
from app.models import ( from app.models import (
User, User,
@ -55,6 +56,7 @@ def create_app(environment="development"):
app.register_blueprint(section_blueprint) app.register_blueprint(section_blueprint)
app.register_blueprint(vote_blueprint) app.register_blueprint(vote_blueprint)
app.register_blueprint(approve_blueprint) app.register_blueprint(approve_blueprint)
app.register_blueprint(star_blueprint)
# Set up flask login. # Set up flask login.
@login_manager.user_loader @login_manager.user_loader

View File

@ -1,4 +1,6 @@
from app import db from flask_login import current_user
from app import db, models as m
from app.models.utils import BaseModel from app.models.utils import BaseModel
@ -23,3 +25,12 @@ class Book(BaseModel):
@property @property
def last_version(self): def last_version(self):
return self.versions[-1] return self.versions[-1]
@property
def current_user_has_star(self):
if current_user.is_authenticated:
book_star: m.BookStar = m.BookStar.query.filter_by(
user_id=current_user.id, book_id=self.id
).first()
if book_star:
return True

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -29,9 +29,9 @@
</p> </p>
{% endif %} {% endif %}
<div class="flex ml-auto align-center justify-center space-x-3"> <div class="flex ml-auto align-center justify-center space-x-3">
<span class="space-x-0.5 flex items-center"> <span class="book-star-block space-x-0.5 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 22 22" stroke-width="1" stroke="currentColor" class="w-4 h-4 inline-flex mr-1"> <path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z" /> </svg> <svg class="star-btn cursor-pointer w-4 h-4 inline-flex mr-1 {% if book.current_user_has_star %}fill-yellow-300{% endif %}" data-book-id={{ book.id }} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 22 22" stroke-width="1" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z" /> </svg>
<p>{{ book.stars|length }}</p> <p class="total-stars">{{ book.stars|length }}</p>
</span> </span>
<span class="space-x-0.5 flex items-center"> <span class="space-x-0.5 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 22 22" stroke-width="1" stroke="currentColor" class="w-4 h-4 inline-flex mr-1"> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" /> </svg> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 22 22" stroke-width="1" stroke="currentColor" class="w-4 h-4 inline-flex mr-1"> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" /> </svg>

View File

@ -38,8 +38,15 @@
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400"> <table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400"> <tr> <th scope="col" class="px-6 py-3"> Username </th> <th scope="col" class="px-6 py-3"> Address </th> </tr> </thead> <thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400"> <tr> <th scope="col" class="px-6 py-3"> Username </th> <th scope="col" class="px-6 py-3"> Address </th> </tr> </thead>
<tbody> <tbody>
{% for star in book.stars %} {% for user in book.stars %}
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700"> <th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"> {{star.user.username}} </th> <td class="px-6 py-4"> {{star.user.wallet_id}} </td> </tr> <tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
{{user.username}}
</th>
<td class="px-6 py-4">
{{user.wallet_id}}
</td>
</tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>

View File

@ -108,9 +108,9 @@
<p> Last updated on {{book.versions[-1].updated_at.strftime('%B %d, %Y')}} </p> <p> Last updated on {{book.versions[-1].updated_at.strftime('%B %d, %Y')}} </p>
{% endif %} {% endif %}
<div class="flex ml-auto align-center justify-center space-x-3"> <div class="flex ml-auto align-center justify-center space-x-3">
<span class="space-x-0.5 flex items-center"> <span class="book-star-block space-x-0.5 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 22 22" stroke-width="1" stroke="currentColor" class="w-4 h-4 inline-flex mr-1"> <path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z" /> </svg> <svg class="star-btn cursor-pointer w-4 h-4 inline-flex mr-1 {% if book.current_user_has_star %}fill-yellow-300{% endif %}" data-book-id={{ book.id }} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 22 22" stroke-width="1" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z" /> </svg>
<p>{{ book.stars|length }}</p> <a href={{ url_for('book.statistic_view', book_id=book.id ) }} class="total-stars">{{ book.stars|length }}</a>
</span> </span>
<span class="space-x-0.5 flex items-center"> <span class="space-x-0.5 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 22 22" stroke-width="1" stroke="currentColor" class="w-4 h-4 inline-flex mr-1"> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" /> </svg> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 22 22" stroke-width="1" stroke="currentColor" class="w-4 h-4 inline-flex mr-1"> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" /> </svg>

View File

@ -7,3 +7,4 @@ from .home import bp as home_blueprint
from .section import bp as section_blueprint from .section import bp as section_blueprint
from .vote import bp as vote_blueprint from .vote import bp as vote_blueprint
from .approve import bp as approve_blueprint from .approve import bp as approve_blueprint
from .star import bp as star_blueprint

View File

@ -154,7 +154,6 @@ def statistic_view(book_id: int):
log(log.WARNING, "Book with id [%s] not found", book_id) log(log.WARNING, "Book with id [%s] not found", book_id)
flash("Book not found", "danger") flash("Book not found", "danger")
return redirect(url_for("book.my_library")) return redirect(url_for("book.my_library"))
else:
return render_template("book/stat.html", book=book) return render_template("book/stat.html", book=book)

47
app/views/star.py Normal file
View File

@ -0,0 +1,47 @@
from flask import (
Blueprint,
jsonify,
)
from flask_login import login_required, current_user
from app import models as m, db
from app.logger import log
bp = Blueprint("star", __name__, url_prefix="/star")
@bp.route(
"/<int:book_id>",
methods=["POST"],
)
@login_required
def star_book(book_id: int):
book: m.Book = db.session.get(m.Book, book_id)
if not book:
log(log.WARNING, "Book with id [%s] not found", book_id)
return jsonify({"message": "Book not found"}), 404
book_star: m.BookStar = m.BookStar.query.filter_by(
user_id=current_user.id, book_id=book_id
).first()
current_user_star = True
if book_star:
current_user_star = False
db.session.delete(book_star)
db.session.commit()
else:
book_star = m.BookStar(user_id=current_user.id, book_id=book_id)
log(
log.INFO,
"User [%s]. Add book [%s] star",
current_user,
book,
)
book_star.save()
return jsonify(
{
"stars_count": len(book.stars),
"current_user_star": current_user_star,
}
)

View File

@ -7,6 +7,7 @@ import {initComments} from './comment';
import {initVote} from './vote'; import {initVote} from './vote';
import {initTheme} from './theme'; import {initTheme} from './theme';
import {initApprove} from './approve'; import {initApprove} from './approve';
import {initStar} from './star';
initBooks(); initBooks();
initContributors(); initContributors();
@ -17,3 +18,4 @@ initComments();
initVote(); initVote();
initTheme(); initTheme();
initApprove(); initApprove();
initStar();

38
src/star.ts Normal file
View File

@ -0,0 +1,38 @@
const starClickEventListener = async (btn: Element, totalStars: Element) => {
const bookId = btn.getAttribute('data-book-id');
const requestUrl = '/star/' + bookId;
const response = await fetch(requestUrl, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
});
const json = await response.json();
const currentUserStar = json.current_user_star;
const starsCount = json.stars_count;
totalStars.innerHTML = starsCount;
btn.classList.remove('fill-yellow-300');
if (currentUserStar) {
btn.classList.add('fill-yellow-300');
} else {
btn.classList.remove('fill-yellow-300');
}
};
export function initStar() {
const bookStarsBlocks = document.querySelectorAll('.book-star-block');
bookStarsBlocks.forEach(bookStarsBlock => {
const bookStarBtn = bookStarsBlock.querySelector('.star-btn');
const totalStarsDiv = bookStarsBlock.querySelector('.total-stars');
bookStarBtn.addEventListener('click', () => {
starClickEventListener(bookStarBtn, totalStarsDiv);
});
});
}

58
tests/test_star.py Normal file
View File

@ -0,0 +1,58 @@
from flask import current_app as Response
from flask.testing import FlaskClient
from app import models as m
from tests.utils import login
def test_star(client: FlaskClient):
_, user = login(client)
response: Response = client.post(
"/star/999",
data=dict(
positive=True,
),
follow_redirects=True,
)
assert response
assert response.status_code == 404
assert response.json["message"] == "Book not found"
book = m.Book(
label="Test Interpretation 1 Label",
user_id=user.id,
).save()
assert len(book.stars) == 0
response: Response = client.post(
f"/star/{book.id}",
follow_redirects=True,
)
assert response
assert response.status_code == 200
json = response.json
assert json
assert "stars_count" in json
assert json["stars_count"] == 1
assert "current_user_star" in json
assert json["current_user_star"]
assert len(book.stars) == 1
response: Response = client.post(
f"/star/{book.id}",
follow_redirects=True,
)
assert response
assert response.status_code == 200
json = response.json
assert json
assert "stars_count" in json
assert json["stars_count"] == 0
assert "current_user_star" in json
assert not json["current_user_star"]
assert len(book.stars) == 0