mirror of https://github.com/logos-co/open-law.git
Merge pull request #52 from Simple2B/svyat/feat/book_star
Svyat/feat/book star
This commit is contained in:
commit
14133b0da3
|
@ -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
|
||||||
|
|
|
@ -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
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
)
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -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
|
Loading…
Reference in New Issue