[Kobo] Add Shelf/Collection support.
Implements the v1/library/tags set of endpoints to sync CalibreWeb shelves with the Kobo device. Drive-by: Refactors shelf.py to consolidate how user permissions are checked. Drive-by: Fix issue with the sync endpoint that arrises when a book is hard-deleted.
This commit is contained in:
parent
3e1c34efe6
commit
41a3623fcc
256
cps/kobo.py
256
cps/kobo.py
|
@ -17,9 +17,11 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import datetime
|
||||
import sys
|
||||
import base64
|
||||
import datetime
|
||||
import itertools
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
import uuid
|
||||
from time import gmtime, strftime
|
||||
|
@ -45,7 +47,7 @@ from sqlalchemy import func
|
|||
from sqlalchemy.sql.expression import and_, or_
|
||||
import requests
|
||||
|
||||
from . import config, logger, kobo_auth, db, helper, ub
|
||||
from . import config, logger, kobo_auth, db, helper, shelf as shelf_lib, ub
|
||||
from .services import SyncToken as SyncToken
|
||||
from .web import download_required
|
||||
from .kobo_auth import requires_kobo_auth
|
||||
|
@ -120,6 +122,7 @@ def redirect_or_proxy_request():
|
|||
def convert_to_kobo_timestamp_string(timestamp):
|
||||
return timestamp.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
|
||||
@kobo.route("/v1/library/sync")
|
||||
@requires_kobo_auth
|
||||
@download_required
|
||||
|
@ -203,24 +206,23 @@ def HandleSyncRequest():
|
|||
ub.KoboReadingState.user_id == current_user.id,
|
||||
ub.KoboReadingState.book_id.notin_(reading_states_in_new_entitlements))))
|
||||
for kobo_reading_state in changed_reading_states.all():
|
||||
book = db.session.query(db.Books).filter(db.Books.id == kobo_reading_state.book_id).one()
|
||||
sync_results.append({
|
||||
"ChangedReadingState": {
|
||||
"ReadingState": get_kobo_reading_state_response(book, kobo_reading_state)
|
||||
}
|
||||
})
|
||||
new_reading_state_last_modified = max(new_reading_state_last_modified, kobo_reading_state.last_modified)
|
||||
book = db.session.query(db.Books).filter(db.Books.id == kobo_reading_state.book_id).one_or_none()
|
||||
if book:
|
||||
sync_results.append({
|
||||
"ChangedReadingState": {
|
||||
"ReadingState": get_kobo_reading_state_response(book, kobo_reading_state)
|
||||
}
|
||||
})
|
||||
new_reading_state_last_modified = max(new_reading_state_last_modified, kobo_reading_state.last_modified)
|
||||
|
||||
sync_shelves(sync_token, sync_results)
|
||||
|
||||
sync_token.books_last_created = new_books_last_created
|
||||
sync_token.books_last_modified = new_books_last_modified
|
||||
sync_token.archive_last_modified = new_archived_last_modified
|
||||
sync_token.reading_state_last_modified = new_reading_state_last_modified
|
||||
|
||||
if config.config_kobo_proxy:
|
||||
return generate_sync_response(request, sync_token, sync_results)
|
||||
|
||||
return make_response(jsonify(sync_results))
|
||||
# Missing feature: Detect server-side book deletions.
|
||||
return generate_sync_response(request, sync_token, sync_results)
|
||||
|
||||
|
||||
def generate_sync_response(request, sync_token, sync_results):
|
||||
|
@ -392,6 +394,222 @@ def get_metadata(book):
|
|||
return metadata
|
||||
|
||||
|
||||
@kobo.route("/v1/library/tags", methods=["POST"])
|
||||
@login_required
|
||||
# Creates a Shelf with the given items, and returns the shelf's uuid.
|
||||
def HandleTagCreate():
|
||||
shelf_request = request.json
|
||||
name, items = None, None
|
||||
try:
|
||||
name = shelf_request["Name"]
|
||||
items = shelf_request["Items"]
|
||||
except KeyError:
|
||||
log.debug("Received malformed v1/library/tags request.")
|
||||
abort(400, description="Malformed tags POST request. Data is missing 'Name' or 'Items' field")
|
||||
|
||||
shelf = ub.session.query(ub.Shelf).filter(and_(ub.Shelf.name) == name, ub.Shelf.user_id ==
|
||||
current_user.id).one_or_none()
|
||||
if shelf and not shelf_lib.check_shelf_edit_permissions(shelf):
|
||||
abort(401, description="User is unauthaurized to edit shelf.")
|
||||
|
||||
if not shelf:
|
||||
shelf = ub.Shelf(user_id=current_user.id, name=name, uuid=uuid.uuid4())
|
||||
ub.session.add(shelf)
|
||||
|
||||
items_unknown_to_calibre = add_items_to_shelf(items, shelf)
|
||||
if items_unknown_to_calibre:
|
||||
log.debug("Received request to add unknown books to a collection. Silently ignoring items.")
|
||||
ub.session.commit()
|
||||
|
||||
return make_response(jsonify(str(shelf.uuid)), 201)
|
||||
|
||||
|
||||
@kobo.route("/v1/library/tags/<tag_id>", methods=["DELETE", "PUT"])
|
||||
def HandleTagUpdate(tag_id):
|
||||
shelf = ub.session.query(ub.Shelf).filter(and_(ub.Shelf.uuid) == tag_id,
|
||||
ub.Shelf.user_id == current_user.id).one_or_none()
|
||||
if not shelf:
|
||||
log.debug("Received Kobo tag update request on a collection unknown to CalibreWeb")
|
||||
if config.config_kobo_proxy:
|
||||
return redirect_or_proxy_request()
|
||||
else:
|
||||
abort(404, description="Collection isn't known to CalibreWeb")
|
||||
|
||||
if not shelf_lib.check_shelf_edit_permissions(shelf):
|
||||
abort(401, description="User is unauthaurized to edit shelf.")
|
||||
|
||||
if request.method == "DELETE":
|
||||
shelf_lib.delete_shelf_helper(shelf)
|
||||
else:
|
||||
shelf_request = request.json
|
||||
name = None
|
||||
try:
|
||||
name = shelf_request["Name"]
|
||||
except KeyError:
|
||||
log.debug("Received malformed v1/library/tags rename request.")
|
||||
abort(400, description="Malformed tags POST request. Data is missing 'Name' field")
|
||||
|
||||
shelf.name = name
|
||||
ub.session.merge(shelf)
|
||||
ub.session.commit()
|
||||
|
||||
return make_response(' ', 200)
|
||||
|
||||
|
||||
# Adds items to the given shelf.
|
||||
def add_items_to_shelf(items, shelf):
|
||||
book_ids_already_in_shelf = set([book_shelf.book_id for book_shelf in shelf.books])
|
||||
items_unknown_to_calibre = []
|
||||
for item in items:
|
||||
if item["Type"] != "ProductRevisionTagItem":
|
||||
items_unknown_to_calibre.append(item)
|
||||
continue
|
||||
|
||||
book = db.session.query(db.Books).filter(db.Books.uuid == item["RevisionId"]).one_or_none()
|
||||
if not book:
|
||||
items_unknown_to_calibre.append(item)
|
||||
continue
|
||||
|
||||
book_id = book.id
|
||||
if book_id not in book_ids_already_in_shelf:
|
||||
shelf.books.append(ub.BookShelf(book_id=book_id))
|
||||
return items_unknown_to_calibre
|
||||
|
||||
|
||||
@kobo.route("/v1/library/tags/<tag_id>/items", methods=["POST"])
|
||||
@login_required
|
||||
def HandleTagAddItem(tag_id):
|
||||
tag_request = request.json
|
||||
items = None
|
||||
try:
|
||||
items = tag_request["Items"]
|
||||
except KeyError:
|
||||
log.debug("Received malformed v1/library/tags/<tag_id>/items/delete request.")
|
||||
abort(400, description="Malformed tags POST request. Data is missing 'Items' field")
|
||||
|
||||
shelf = ub.session.query(ub.Shelf).filter(and_(ub.Shelf.uuid) == tag_id,
|
||||
ub.Shelf.user_id == current_user.id).one_or_none()
|
||||
if not shelf:
|
||||
log.debug("Received Kobo request on a collection unknown to CalibreWeb")
|
||||
abort(404, description="Collection isn't known to CalibreWeb")
|
||||
|
||||
if not shelf_lib.check_shelf_edit_permissions(shelf):
|
||||
abort(401, description="User is unauthaurized to edit shelf.")
|
||||
|
||||
items_unknown_to_calibre = add_items_to_shelf(items, shelf)
|
||||
if items_unknown_to_calibre:
|
||||
log.debug("Received request to add an unknown book to a collecition. Silently ignoring item.")
|
||||
|
||||
ub.session.merge(shelf)
|
||||
ub.session.commit()
|
||||
|
||||
return make_response('', 201)
|
||||
|
||||
|
||||
@kobo.route("/v1/library/tags/<tag_id>/items/delete", methods=["POST"])
|
||||
@login_required
|
||||
def HandleTagRemoveItem(tag_id):
|
||||
tag_request = request.json
|
||||
items = None
|
||||
try:
|
||||
items = tag_request["Items"]
|
||||
except KeyError:
|
||||
log.debug("Received malformed v1/library/tags/<tag_id>/items/delete request.")
|
||||
abort(400, description="Malformed tags POST request. Data is missing 'Items' field")
|
||||
|
||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.uuid == tag_id,
|
||||
ub.Shelf.user_id == current_user.id).one_or_none()
|
||||
if not shelf:
|
||||
log.debug(
|
||||
"Received a request to remove an item from a Collection unknown to CalibreWeb.")
|
||||
abort(404, description="Collection isn't known to CalibreWeb")
|
||||
|
||||
if not shelf_lib.check_shelf_edit_permissions(shelf):
|
||||
abort(401, description="User is unauthaurized to edit shelf.")
|
||||
|
||||
items_unknown_to_calibre = []
|
||||
for item in items:
|
||||
if item["Type"] != "ProductRevisionTagItem":
|
||||
items_unknown_to_calibre.append(item)
|
||||
continue
|
||||
|
||||
book = db.session.query(db.Books).filter(db.Books.uuid == item["RevisionId"]).one_or_none()
|
||||
if not book:
|
||||
items_unknown_to_calibre.append(item)
|
||||
continue
|
||||
|
||||
shelf.books.filter(ub.BookShelf.book_id == book.id).delete()
|
||||
ub.session.commit()
|
||||
|
||||
if items_unknown_to_calibre:
|
||||
log.debug("Received request to remove an unknown book to a collecition. Silently ignoring item.")
|
||||
|
||||
return make_response('', 200)
|
||||
|
||||
|
||||
# Add new, changed, or deleted shelves to the sync_results.
|
||||
# Note: Public shelves that aren't owned by the user aren't supported.
|
||||
def sync_shelves(sync_token, sync_results):
|
||||
new_tags_last_modified = sync_token.tags_last_modified
|
||||
|
||||
for shelf in ub.session.query(ub.ShelfArchive).filter(func.datetime(ub.ShelfArchive.last_modified) > sync_token.tags_last_modified, ub.ShelfArchive.user_id == current_user.id):
|
||||
new_tags_last_modified = max(shelf.last_modified, new_tags_last_modified)
|
||||
|
||||
sync_results.append({
|
||||
"DeletedTag": {
|
||||
"Tag": {
|
||||
"Id": shelf.uuid,
|
||||
"LastModified": convert_to_kobo_timestamp_string(shelf.last_modified)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
for shelf in ub.session.query(ub.Shelf).filter(func.datetime(ub.Shelf.last_modified) > sync_token.tags_last_modified, ub.Shelf.user_id == current_user.id):
|
||||
if not shelf_lib.check_shelf_view_permissions(shelf):
|
||||
continue
|
||||
|
||||
new_tags_last_modified = max(shelf.last_modified, new_tags_last_modified)
|
||||
|
||||
tag = create_kobo_tag(shelf)
|
||||
if not tag:
|
||||
continue
|
||||
|
||||
if shelf.created > sync_token.tags_last_modified:
|
||||
sync_results.append({
|
||||
"NewTag": tag
|
||||
})
|
||||
else:
|
||||
sync_results.append({
|
||||
"ChangedTag": tag
|
||||
})
|
||||
sync_token.tags_last_modified = new_tags_last_modified
|
||||
ub.session.commit()
|
||||
|
||||
|
||||
# Creates a Kobo "Tag" object from a ub.Shelf object
|
||||
def create_kobo_tag(shelf):
|
||||
tag = {
|
||||
"Created": convert_to_kobo_timestamp_string(shelf.created),
|
||||
"Id": shelf.uuid,
|
||||
"Items": [],
|
||||
"LastModified": convert_to_kobo_timestamp_string(shelf.last_modified),
|
||||
"Name": shelf.name,
|
||||
"Type": "UserTag"
|
||||
}
|
||||
for book_shelf in shelf.books:
|
||||
book = db.session.query(db.Books).filter(db.Books.id == book_shelf.book_id).one_or_none()
|
||||
if not book:
|
||||
log.info(u"Book (id: %s) in BookShelf (id: %s) not found in book database", book_shelf.book_id, shelf.id)
|
||||
return None
|
||||
tag["Items"].append(
|
||||
{
|
||||
"RevisionId": book.uuid,
|
||||
"Type": "ProductRevisionTagItem"
|
||||
}
|
||||
)
|
||||
return {"Tag": tag}
|
||||
|
||||
|
||||
@kobo.route("/v1/library/<book_uuid>/state", methods=["GET", "PUT"])
|
||||
@login_required
|
||||
def HandleStateRequest(book_uuid):
|
||||
|
@ -589,10 +807,7 @@ def HandleBookDeletionRequest(book_uuid):
|
|||
|
||||
# TODO: Implement the following routes
|
||||
@kobo.route("/v1/library/<dummy>", methods=["DELETE", "GET"])
|
||||
@kobo.route("/v1/library/tags", methods=["POST"])
|
||||
@kobo.route("/v1/library/tags/<shelf_name>", methods=["POST"])
|
||||
@kobo.route("/v1/library/tags/<tag_id>", methods=["DELETE"])
|
||||
def HandleUnimplementedRequest(dummy=None, book_uuid=None, shelf_name=None, tag_id=None):
|
||||
def HandleUnimplementedRequest(dummy=None):
|
||||
log.debug("Unimplemented Library Request received: %s", request.base_url)
|
||||
return redirect_or_proxy_request()
|
||||
|
||||
|
@ -612,6 +827,7 @@ def HandleUserRequest(dummy=None):
|
|||
@kobo.route("/v1/products/<dummy>/recommendations", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products/<dummy>/nextread", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products/<dummy>/reviews", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products/books/series/<dummy>", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products/books/<dummy>", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products/dailydeal", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products", methods=["GET", "POST"])
|
||||
|
@ -624,7 +840,7 @@ def HandleProductsRequest(dummy=None):
|
|||
def handle_404(err):
|
||||
# This handler acts as a catch-all for endpoints that we don't have an interest in
|
||||
# implementing (e.g: v1/analytics/gettests, v1/user/recommendations, etc)
|
||||
log.debug("Unknown Request received: %s", request.base_url)
|
||||
log.debug("Unknown Request received: %s, method: %s, data: %s", request.base_url, request.method, request.data)
|
||||
return redirect_or_proxy_request()
|
||||
|
||||
|
||||
|
|
|
@ -326,12 +326,12 @@ def feed_shelfindex():
|
|||
def feed_shelf(book_id):
|
||||
off = request.args.get("offset") or 0
|
||||
if current_user.is_anonymous:
|
||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1, ub.Shelf.id == book_id).first()
|
||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1, ub.Shelf.id == book_id, not ub.Shelf.deleted).first()
|
||||
else:
|
||||
shelf = ub.session.query(ub.Shelf).filter(or_(and_(ub.Shelf.user_id == int(current_user.id),
|
||||
ub.Shelf.id == book_id),
|
||||
and_(ub.Shelf.is_public == 1,
|
||||
ub.Shelf.id == book_id))).first()
|
||||
ub.Shelf.id == book_id)), not ub.Shelf.deleted).first()
|
||||
result = list()
|
||||
# user is allowed to access shelf
|
||||
if shelf:
|
||||
|
|
|
@ -78,6 +78,7 @@ class SyncToken():
|
|||
"books_last_created": {"type": "string"},
|
||||
"archive_last_modified": {"type": "string"},
|
||||
"reading_state_last_modified": {"type": "string"},
|
||||
"tags_last_modified": {"type": "string"},
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -88,13 +89,14 @@ class SyncToken():
|
|||
books_last_modified=datetime.min,
|
||||
archive_last_modified=datetime.min,
|
||||
reading_state_last_modified=datetime.min,
|
||||
tags_last_modified=datetime.min,
|
||||
):
|
||||
self.raw_kobo_store_token = raw_kobo_store_token
|
||||
self.books_last_created = books_last_created
|
||||
self.books_last_modified = books_last_modified
|
||||
self.archive_last_modified = archive_last_modified
|
||||
self.reading_state_last_modified = reading_state_last_modified
|
||||
|
||||
self.tags_last_modified = tags_last_modified
|
||||
|
||||
@staticmethod
|
||||
def from_headers(headers):
|
||||
|
@ -128,6 +130,7 @@ class SyncToken():
|
|||
books_last_created = get_datetime_from_json(data_json, "books_last_created")
|
||||
archive_last_modified = get_datetime_from_json(data_json, "archive_last_modified")
|
||||
reading_state_last_modified = get_datetime_from_json(data_json, "reading_state_last_modified")
|
||||
tags_last_modified = get_datetime_from_json(data_json, "tags_last_modified")
|
||||
except TypeError:
|
||||
log.error("SyncToken timestamps don't parse to a datetime.")
|
||||
return SyncToken(raw_kobo_store_token=raw_kobo_store_token)
|
||||
|
@ -137,7 +140,8 @@ class SyncToken():
|
|||
books_last_created=books_last_created,
|
||||
books_last_modified=books_last_modified,
|
||||
archive_last_modified=archive_last_modified,
|
||||
reading_state_last_modified=reading_state_last_modified
|
||||
reading_state_last_modified=reading_state_last_modified,
|
||||
tags_last_modified=tags_last_modified
|
||||
)
|
||||
|
||||
def set_kobo_store_header(self, store_headers):
|
||||
|
@ -159,7 +163,8 @@ class SyncToken():
|
|||
"books_last_modified": to_epoch_timestamp(self.books_last_modified),
|
||||
"books_last_created": to_epoch_timestamp(self.books_last_created),
|
||||
"archive_last_modified": to_epoch_timestamp(self.archive_last_modified),
|
||||
"reading_state_last_modified": to_epoch_timestamp(self.reading_state_last_modified)
|
||||
"reading_state_last_modified": to_epoch_timestamp(self.reading_state_last_modified),
|
||||
"tags_last_modified": to_epoch_timestamp(self.tags_last_modified)
|
||||
},
|
||||
}
|
||||
return b64encode_json(token)
|
||||
|
|
99
cps/shelf.py
99
cps/shelf.py
|
@ -35,6 +35,24 @@ from .helper import common_filters
|
|||
shelf = Blueprint('shelf', __name__)
|
||||
log = logger.create()
|
||||
|
||||
def check_shelf_edit_permissions(cur_shelf):
|
||||
if not cur_shelf.is_public and not cur_shelf.user_id == int(current_user.id):
|
||||
log.error("User %s not allowed to edit shelf %s", current_user, cur_shelf)
|
||||
return False
|
||||
if cur_shelf.is_public and not current_user.role_edit_shelfs():
|
||||
log.info("User %s not allowed to edit public shelves", current_user)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def check_shelf_view_permissions(cur_shelf):
|
||||
if cur_shelf.is_public:
|
||||
return True
|
||||
if current_user.is_anonymous or cur_shelf.user_id != current_user.id:
|
||||
log.error("User is unauthorized to view non-public shelf: %s", cur_shelf)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@shelf.route("/shelf/add/<int:shelf_id>/<int:book_id>")
|
||||
@login_required
|
||||
|
@ -48,21 +66,13 @@ def add_to_shelf(shelf_id, book_id):
|
|||
return redirect(url_for('web.index'))
|
||||
return "Invalid shelf specified", 400
|
||||
|
||||
if not shelf.is_public and not shelf.user_id == int(current_user.id):
|
||||
log.error("User %s not allowed to add a book to %s", current_user, shelf)
|
||||
if not check_shelf_edit_permissions(shelf):
|
||||
if not xhr:
|
||||
flash(_(u"Sorry you are not allowed to add a book to the the shelf: %(shelfname)s", shelfname=shelf.name),
|
||||
category="error")
|
||||
return redirect(url_for('web.index'))
|
||||
return "Sorry you are not allowed to add a book to the the shelf: %s" % shelf.name, 403
|
||||
|
||||
if shelf.is_public and not current_user.role_edit_shelfs():
|
||||
log.info("User %s not allowed to edit public shelves", current_user)
|
||||
if not xhr:
|
||||
flash(_(u"You are not allowed to edit public shelves"), category="error")
|
||||
return redirect(url_for('web.index'))
|
||||
return "User is not allowed to edit public shelves", 403
|
||||
|
||||
book_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id,
|
||||
ub.BookShelf.book_id == book_id).first()
|
||||
if book_in_shelf:
|
||||
|
@ -78,8 +88,8 @@ def add_to_shelf(shelf_id, book_id):
|
|||
else:
|
||||
maxOrder = maxOrder[0]
|
||||
|
||||
ins = ub.BookShelf(shelf=shelf.id, book_id=book_id, order=maxOrder + 1)
|
||||
ub.session.add(ins)
|
||||
shelf.books.append(ub.BookShelf(shelf=shelf.id, book_id=book_id, order=maxOrder + 1))
|
||||
ub.session.merge(shelf)
|
||||
ub.session.commit()
|
||||
if not xhr:
|
||||
flash(_(u"Book has been added to shelf: %(sname)s", sname=shelf.name), category="success")
|
||||
|
@ -99,16 +109,10 @@ def search_to_shelf(shelf_id):
|
|||
flash(_(u"Invalid shelf specified"), category="error")
|
||||
return redirect(url_for('web.index'))
|
||||
|
||||
if not shelf.is_public and not shelf.user_id == int(current_user.id):
|
||||
log.error("User %s not allowed to add a book to %s", current_user, shelf)
|
||||
if not check_shelf_edit_permissions(shelf):
|
||||
flash(_(u"You are not allowed to add a book to the the shelf: %(name)s", name=shelf.name), category="error")
|
||||
return redirect(url_for('web.index'))
|
||||
|
||||
if shelf.is_public and not current_user.role_edit_shelfs():
|
||||
log.error("User %s not allowed to edit public shelves", current_user)
|
||||
flash(_(u"User is not allowed to edit public shelves"), category="error")
|
||||
return redirect(url_for('web.index'))
|
||||
|
||||
if current_user.id in searched_ids and searched_ids[current_user.id]:
|
||||
books_for_shelf = list()
|
||||
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).all()
|
||||
|
@ -135,8 +139,8 @@ def search_to_shelf(shelf_id):
|
|||
|
||||
for book in books_for_shelf:
|
||||
maxOrder = maxOrder + 1
|
||||
ins = ub.BookShelf(shelf=shelf.id, book_id=book, order=maxOrder)
|
||||
ub.session.add(ins)
|
||||
shelf.books.append(ub.BookShelf(shelf=shelf.id, book_id=book, order=maxOrder))
|
||||
ub.session.merge(shelf)
|
||||
ub.session.commit()
|
||||
flash(_(u"Books have been added to shelf: %(sname)s", sname=shelf.name), category="success")
|
||||
else:
|
||||
|
@ -163,8 +167,7 @@ def remove_from_shelf(shelf_id, book_id):
|
|||
# true 0 x 1
|
||||
# false 0 x 0
|
||||
|
||||
if (not shelf.is_public and shelf.user_id == int(current_user.id)) \
|
||||
or (shelf.is_public and current_user.role_edit_shelfs()):
|
||||
if check_shelf_edit_permissions(shelf):
|
||||
book_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id,
|
||||
ub.BookShelf.book_id == book_id).first()
|
||||
|
||||
|
@ -185,7 +188,6 @@ def remove_from_shelf(shelf_id, book_id):
|
|||
return redirect(url_for('web.index'))
|
||||
return "", 204
|
||||
else:
|
||||
log.error("User %s not allowed to remove a book from %s", current_user, shelf)
|
||||
if not xhr:
|
||||
flash(_(u"Sorry you are not allowed to remove a book from this shelf: %(sname)s", sname=shelf.name),
|
||||
category="error")
|
||||
|
@ -238,7 +240,7 @@ def create_shelf():
|
|||
@login_required
|
||||
def edit_shelf(shelf_id):
|
||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||
if request.method == "POST":
|
||||
if request.method == "POST":
|
||||
to_save = request.form.to_dict()
|
||||
|
||||
is_shelf_name_unique = False
|
||||
|
@ -275,41 +277,33 @@ def edit_shelf(shelf_id):
|
|||
return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"Edit a shelf"), page="shelfedit")
|
||||
|
||||
|
||||
def delete_shelf_helper(cur_shelf):
|
||||
if not cur_shelf or not check_shelf_edit_permissions(cur_shelf):
|
||||
return
|
||||
shelf_id = cur_shelf.id
|
||||
ub.session.delete(cur_shelf)
|
||||
ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).delete()
|
||||
ub.session.add(ub.ShelfArchive(uuid = cur_shelf.uuid, user_id = cur_shelf.uuid))
|
||||
ub.session.commit()
|
||||
log.info("successfully deleted %s", cur_shelf)
|
||||
|
||||
|
||||
@shelf.route("/shelf/delete/<int:shelf_id>")
|
||||
@login_required
|
||||
def delete_shelf(shelf_id):
|
||||
cur_shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||
deleted = None
|
||||
if current_user.role_admin():
|
||||
deleted = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).delete()
|
||||
else:
|
||||
if (not cur_shelf.is_public and cur_shelf.user_id == int(current_user.id)) \
|
||||
or (cur_shelf.is_public and current_user.role_edit_shelfs()):
|
||||
deleted = ub.session.query(ub.Shelf).filter(or_(and_(ub.Shelf.user_id == int(current_user.id),
|
||||
ub.Shelf.id == shelf_id),
|
||||
and_(ub.Shelf.is_public == 1,
|
||||
ub.Shelf.id == shelf_id))).delete()
|
||||
|
||||
if deleted:
|
||||
ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).delete()
|
||||
ub.session.commit()
|
||||
log.info("successfully deleted %s", cur_shelf)
|
||||
delete_shelf_helper(cur_shelf)
|
||||
return redirect(url_for('web.index'))
|
||||
|
||||
# @shelf.route("/shelfdown/<int:shelf_id>")
|
||||
@shelf.route("/shelf/<int:shelf_id>", defaults={'shelf_type': 1})
|
||||
@shelf.route("/shelf/<int:shelf_id>/<int:shelf_type>")
|
||||
def show_shelf(shelf_type, shelf_id):
|
||||
if current_user.is_anonymous:
|
||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1, ub.Shelf.id == shelf_id).first()
|
||||
else:
|
||||
shelf = ub.session.query(ub.Shelf).filter(or_(and_(ub.Shelf.user_id == int(current_user.id),
|
||||
ub.Shelf.id == shelf_id),
|
||||
and_(ub.Shelf.is_public == 1,
|
||||
ub.Shelf.id == shelf_id))).first()
|
||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||
|
||||
result = list()
|
||||
# user is allowed to access shelf
|
||||
if shelf:
|
||||
if shelf and check_shelf_view_permissions(shelf):
|
||||
page = "shelf.html" if shelf_type == 1 else 'shelfdown.html'
|
||||
|
||||
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id)\
|
||||
|
@ -344,15 +338,10 @@ def order_shelf(shelf_id):
|
|||
setattr(book, 'order', to_save[str(book.book_id)])
|
||||
counter += 1
|
||||
ub.session.commit()
|
||||
if current_user.is_anonymous:
|
||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1, ub.Shelf.id == shelf_id).first()
|
||||
else:
|
||||
shelf = ub.session.query(ub.Shelf).filter(or_(and_(ub.Shelf.user_id == int(current_user.id),
|
||||
ub.Shelf.id == shelf_id),
|
||||
and_(ub.Shelf.is_public == 1,
|
||||
ub.Shelf.id == shelf_id))).first()
|
||||
|
||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||
result = list()
|
||||
if shelf:
|
||||
if shelf and check_shelf_view_permissions(shelf):
|
||||
books_in_shelf2 = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id) \
|
||||
.order_by(ub.BookShelf.order.asc()).all()
|
||||
for book in books_in_shelf2:
|
||||
|
|
38
cps/ub.py
38
cps/ub.py
|
@ -21,6 +21,7 @@ from __future__ import division, print_function, unicode_literals
|
|||
import os
|
||||
import datetime
|
||||
import itertools
|
||||
import uuid
|
||||
from binascii import hexlify
|
||||
|
||||
from flask import g
|
||||
|
@ -35,8 +36,8 @@ except ImportError:
|
|||
from sqlalchemy import create_engine, exc, exists, event
|
||||
from sqlalchemy import Column, ForeignKey
|
||||
from sqlalchemy import String, Integer, SmallInteger, Boolean, DateTime, Float
|
||||
from sqlalchemy.orm import backref, foreign, relationship, remote, sessionmaker, Session
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import backref, foreign, relationship, remote, sessionmaker, Session
|
||||
from sqlalchemy.sql.expression import and_
|
||||
from werkzeug.security import generate_password_hash
|
||||
|
||||
|
@ -265,9 +266,13 @@ class Shelf(Base):
|
|||
__tablename__ = 'shelf'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
uuid = Column(String, default=lambda : str(uuid.uuid4()))
|
||||
name = Column(String)
|
||||
is_public = Column(Integer, default=0)
|
||||
user_id = Column(Integer, ForeignKey('user.id'))
|
||||
books = relationship("BookShelf", backref="ub_shelf", cascade="all, delete-orphan", lazy="dynamic")
|
||||
created = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
last_modified = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Shelf %d:%r>' % (self.id, self.name)
|
||||
|
@ -281,11 +286,22 @@ class BookShelf(Base):
|
|||
book_id = Column(Integer)
|
||||
order = Column(Integer)
|
||||
shelf = Column(Integer, ForeignKey('shelf.id'))
|
||||
date_added = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Book %r>' % self.id
|
||||
|
||||
|
||||
# This table keeps track of deleted Shelves so that deletes can be propagated to any paired Kobo device.
|
||||
class ShelfArchive(Base):
|
||||
__tablename__ = 'shelf_archive'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
uuid = Column(String)
|
||||
user_id = Column(Integer, ForeignKey('user.id'))
|
||||
last_modified = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
|
||||
class ReadBook(Base):
|
||||
__tablename__ = 'book_read_link'
|
||||
|
||||
|
@ -370,6 +386,10 @@ def receive_before_flush(session, flush_context, instances):
|
|||
if isinstance(change, (ReadBook, KoboStatistics, KoboBookmark)):
|
||||
if change.kobo_reading_state:
|
||||
change.kobo_reading_state.last_modified = datetime.datetime.utcnow()
|
||||
# Maintain the last_modified bit for the Shelf table.
|
||||
for change in itertools.chain(session.new, session.deleted):
|
||||
if isinstance(change, BookShelf):
|
||||
change.ub_shelf.last_modified = datetime.datetime.utcnow()
|
||||
|
||||
|
||||
# Baseclass representing Downloads from calibre-web in app.db
|
||||
|
@ -463,7 +483,21 @@ def migrate_Database(session):
|
|||
conn.execute("ALTER TABLE book_read_link ADD column 'last_time_started_reading' DATETIME")
|
||||
conn.execute("ALTER TABLE book_read_link ADD column 'times_started_reading' INTEGER DEFAULT 0")
|
||||
session.commit()
|
||||
|
||||
try:
|
||||
session.query(exists().where(Shelf.uuid)).scalar()
|
||||
except exc.OperationalError:
|
||||
conn = engine.connect()
|
||||
conn.execute("ALTER TABLE shelf ADD column 'uuid' STRING")
|
||||
conn.execute("ALTER TABLE shelf ADD column 'created' DATETIME")
|
||||
conn.execute("ALTER TABLE shelf ADD column 'last_modified' DATETIME")
|
||||
conn.execute("ALTER TABLE book_shelf_link ADD column 'date_added' DATETIME")
|
||||
for shelf in session.query(Shelf).all():
|
||||
shelf.uuid = str(uuid.uuid4())
|
||||
shelf.created = datetime.datetime.now()
|
||||
shelf.last_modified = datetime.datetime.now()
|
||||
for book_shelf in session.query(BookShelf).all():
|
||||
book_shelf.date_added = datetime.datetime.now()
|
||||
session.commit()
|
||||
# Handle table exists, but no content
|
||||
cnt = session.query(Registration).count()
|
||||
if not cnt:
|
||||
|
|
Loading…
Reference in New Issue
Block a user