mirror of
https://github.com/logos-co/open-law.git
synced 2025-01-12 15:54:23 +00:00
Merge branch 'develop' into kostia/feature/user_page
This commit is contained in:
commit
79342067b5
@ -25,6 +25,7 @@ def create_app(environment="development"):
|
||||
section_blueprint,
|
||||
vote_blueprint,
|
||||
approve_blueprint,
|
||||
star_blueprint,
|
||||
)
|
||||
from app.models import (
|
||||
User,
|
||||
@ -55,6 +56,7 @@ def create_app(environment="development"):
|
||||
app.register_blueprint(section_blueprint)
|
||||
app.register_blueprint(vote_blueprint)
|
||||
app.register_blueprint(approve_blueprint)
|
||||
app.register_blueprint(star_blueprint)
|
||||
|
||||
# Set up flask login.
|
||||
@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
|
||||
|
||||
|
||||
@ -23,3 +25,12 @@ class Book(BaseModel):
|
||||
@property
|
||||
def last_version(self):
|
||||
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
@ -30,9 +30,9 @@
|
||||
</p>
|
||||
{% endif %}
|
||||
<div class="flex ml-auto align-center justify-center space-x-3">
|
||||
<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="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>
|
||||
<span class="book-star-block space-x-0.5 flex items-center">
|
||||
<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 class="total-stars">{{ book.stars|length }}</p>
|
||||
</span>
|
||||
<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>
|
||||
|
@ -38,8 +38,15 @@
|
||||
<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>
|
||||
<tbody>
|
||||
{% for star 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>
|
||||
{% 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">
|
||||
{{user.username}}
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
{{user.wallet_id}}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -108,9 +108,9 @@
|
||||
<p> Last updated on {{book.versions[-1].updated_at.strftime('%B %d, %Y')}} </p>
|
||||
{% endif %}
|
||||
<div class="flex ml-auto align-center justify-center space-x-3">
|
||||
<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="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>
|
||||
<span class="book-star-block space-x-0.5 flex items-center">
|
||||
<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>
|
||||
<a href={{ url_for('book.statistic_view', book_id=book.id ) }} class="total-stars">{{ book.stars|length }}</a>
|
||||
</span>
|
||||
<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>
|
||||
|
@ -7,3 +7,4 @@ from .home import bp as home_blueprint
|
||||
from .section import bp as section_blueprint
|
||||
from .vote import bp as vote_blueprint
|
||||
from .approve import bp as approve_blueprint
|
||||
from .star import bp as star_blueprint
|
||||
|
@ -154,8 +154,7 @@ def statistic_view(book_id: int):
|
||||
log(log.WARNING, "Book with id [%s] not found", book_id)
|
||||
flash("Book not found", "danger")
|
||||
return redirect(url_for("book.my_library"))
|
||||
else:
|
||||
return render_template("book/stat.html", book=book)
|
||||
return render_template("book/stat.html", book=book)
|
||||
|
||||
|
||||
@bp.route("/<int:book_id>/<int:collection_id>/subcollections", methods=["GET"])
|
||||
|
47
app/views/star.py
Normal file
47
app/views/star.py
Normal 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,
|
||||
}
|
||||
)
|
@ -7,6 +7,7 @@ import {initComments} from './comment';
|
||||
import {initVote} from './vote';
|
||||
import {initTheme} from './theme';
|
||||
import {initApprove} from './approve';
|
||||
import {initStar} from './star';
|
||||
|
||||
initBooks();
|
||||
initContributors();
|
||||
@ -17,3 +18,4 @@ initComments();
|
||||
initVote();
|
||||
initTheme();
|
||||
initApprove();
|
||||
initStar();
|
||||
|
38
src/star.ts
Normal file
38
src/star.ts
Normal 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
58
tests/test_star.py
Normal 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
|
Loading…
x
Reference in New Issue
Block a user