diff --git a/cps/kobo.py b/cps/kobo.py
index 6b70bdf5..ee0aa810 100644
--- a/cps/kobo.py
+++ b/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 .
+import base64
import datetime
+import itertools
+import json
import sys
-import base64
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/", 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//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//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//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//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//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/", methods=["DELETE", "GET"])
-@kobo.route("/v1/library/tags", methods=["POST"])
-@kobo.route("/v1/library/tags/", methods=["POST"])
-@kobo.route("/v1/library/tags/", 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//recommendations", methods=["GET", "POST"])
@kobo.route("/v1/products//nextread", methods=["GET", "POST"])
@kobo.route("/v1/products//reviews", methods=["GET", "POST"])
+@kobo.route("/v1/products/books/series/", methods=["GET", "POST"])
@kobo.route("/v1/products/books/", 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()
diff --git a/cps/opds.py b/cps/opds.py
index 5cfb5348..8877cfb2 100644
--- a/cps/opds.py
+++ b/cps/opds.py
@@ -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:
diff --git a/cps/services/SyncToken.py b/cps/services/SyncToken.py
index 9804fdb3..abc639f1 100644
--- a/cps/services/SyncToken.py
+++ b/cps/services/SyncToken.py
@@ -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)
diff --git a/cps/shelf.py b/cps/shelf.py
index c78059ca..4924a186 100644
--- a/cps/shelf.py
+++ b/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//")
@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/")
@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/")
@shelf.route("/shelf/", defaults={'shelf_type': 1})
@shelf.route("/shelf//")
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:
diff --git a/cps/ub.py b/cps/ub.py
index dbefe217..fbc67629 100644
--- a/cps/ub.py
+++ b/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 '' % (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 '' % 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: