diff --git a/app/models.py b/app/models.py
index d5ccdcc..2c9d267 100644
--- a/app/models.py
+++ b/app/models.py
@@ -558,6 +558,21 @@ class BookmarkCollections(Model):
class BookmarkedPosts(Model):
table = 'bookmarked_posts'
+ @classmethod
+ def get_for_user(cls, post_id, user_id):
+ q = """SELECT
+ bookmarked_posts.id, collection_id, post_id, note
+ FROM
+ bookmarked_posts
+ JOIN
+ bookmark_collections ON bookmark_collections.id = bookmarked_posts.collection_id
+ WHERE
+ post_id = ?
+ AND
+ user_id = ?"""
+ res = db.fetch_one(q, post_id, user_id)
+ return cls.from_data(res) if res is not None else None
+
def get_post(self):
return Posts.find({'id': self.post_id})
@@ -565,6 +580,21 @@ class BookmarkedPosts(Model):
class BookmarkedThreads(Model):
table = 'bookmarked_threads'
+ @classmethod
+ def get_for_user(cls, thread_id, user_id):
+ q = """SELECT
+ bookmarked_threads.id, collection_id, thread_id, note
+ FROM
+ bookmarked_threads
+ JOIN
+ bookmark_collections ON bookmark_collections.id = bookmarked_threads.collection_id
+ WHERE
+ thread_id = ?
+ AND
+ user_id = ?"""
+ res = db.fetch_one(q, thread_id, user_id)
+ return cls.from_data(res) if res is not None else None
+
def get_thread(self):
return Threads.find({'id': self.thread_id})
diff --git a/app/routes/hyperapi.py b/app/routes/hyperapi.py
index 7ffa74f..9a57beb 100644
--- a/app/routes/hyperapi.py
+++ b/app/routes/hyperapi.py
@@ -1,6 +1,6 @@
from flask import Blueprint, render_template, request, url_for
from ..auth import get_active_user, is_logged_in, hard_login_required
-from ..models import BookmarkCollections
+from ..models import BookmarkCollections, BookmarkedPosts, BookmarkedThreads
from functools import wraps
bp = Blueprint('hyperapi', __name__, url_prefix='/hyperapi/')
@@ -26,22 +26,87 @@ def get_bookmark_dropdown():
is_thread = concept_kind == 'thread'
collections = BookmarkCollections.findall({'user_id': user.id})
in_collection = None
+ note = ''
for collection in collections:
callable = collection.has_thread if is_thread else collection.has_post
if callable(concept_id):
in_collection = collection.id
+ concept = 'thread_id' if is_thread else 'post_id'
+ note = (BookmarkedThreads if is_thread else BookmarkedPosts).find({'collection_id': in_collection, concept: concept_id}).note
break
submit_url = url_for('.bookmark_thread' if is_thread else '.bookmark_post')
- return render_template('hyper/bookmark_dropdown.html', collections=collections, in_collection=in_collection, is_thread=is_thread, concept_id=concept_id, submit_url=submit_url)
+ return render_template('hyper/bookmark_dropdown.html', collections=collections, in_collection=in_collection, is_thread=is_thread, concept_id=concept_id, submit_url=submit_url, note=note)
@bp.post('/bookmarks/thread/')
@hard_login_required
@user_required
def bookmark_thread():
+ user = get_active_user()
+ try:
+ thread_id = int(request.form['concept_id'])
+ target_collection_id = int(request.form['target_collection'])
+ except ValueError, KeyError:
+ return 'error', 400
+
+ if target_collection_id == -1:
+ bt = BookmarkedThreads.get_for_user(thread_id, user.id)
+ if bt:
+ bt.delete()
+ return '', 204
+
+ target_collection = BookmarkCollections.find({'id': target_collection_id})
+ note = request.form.get('note', '')
+ if not target_collection:
+ return 'error', 400
+
+ if int(user.id) != int(target_collection.user_id):
+ return 'error', 400
+
+ bt = BookmarkedThreads.get_for_user(thread_id, user.id)
+ if bt:
+ bt.update({'collection_id': target_collection_id, 'note': note})
+ else:
+ BookmarkedThreads.create({
+ 'collection_id': target_collection_id,
+ 'thread_id': thread_id,
+ 'note': note,
+ })
+
return '', 204
@bp.post('/bookmarks/post/')
@hard_login_required
@user_required
def bookmark_post():
+ user = get_active_user()
+ try:
+ post_id = int(request.form['concept_id'])
+ target_collection_id = int(request.form['target_collection'])
+ except ValueError, KeyError:
+ return 'error', 400
+
+ if target_collection_id == -1:
+ bp = BookmarkedPosts.get_for_user(post_id, user.id)
+ if bp:
+ bp.delete()
+ return '', 204
+
+ target_collection = BookmarkCollections.find({'id': target_collection_id})
+ note = request.form.get('note', '')
+ if not target_collection:
+ return 'error', 400
+
+ if int(user.id) != int(target_collection.user_id):
+ return 'error', 400
+
+ bp = BookmarkedPosts.get_for_user(post_id, user.id)
+ if bp:
+ bp.update({'collection_id': target_collection_id, 'note': note})
+ else:
+ BookmarkedPosts.create({
+ 'collection_id': target_collection_id,
+ 'post_id': post_id,
+ 'note': note,
+ })
+
return '', 204
diff --git a/app/templates/base.html b/app/templates/base.html
index 37f6c13..c320607 100644
--- a/app/templates/base.html
+++ b/app/templates/base.html
@@ -15,6 +15,7 @@
+
{%- include 'common/topnav.html' -%}
{%- with messages = get_flashed_messages(with_categories=true) -%}
{%- if messages -%}
diff --git a/app/templates/hyper/bookmark_dropdown.html b/app/templates/hyper/bookmark_dropdown.html
index 0285858..79882da 100644
--- a/app/templates/hyper/bookmark_dropdown.html
+++ b/app/templates/hyper/bookmark_dropdown.html
@@ -18,6 +18,7 @@
{%- endfor -%}
-
+
+
Something went wrong. Try again later.
diff --git a/data/static/js/bits/bookmark-menu.js b/data/static/js/bits/bookmark-menu.js
new file mode 100644
index 0000000..9a5e737
--- /dev/null
+++ b/data/static/js/bits/bookmark-menu.js
@@ -0,0 +1,121 @@
+async function getHTML(endpoint, options = {}) {
+ let query = {};
+ if (options._query !== undefined) {
+ query = options._query;
+ delete options._query;
+ }
+
+ const params = new URLSearchParams(query);
+ const res = await fetch(`${endpoint}?${params}`, options);
+ if (!res.ok) {
+ console.error(res);
+ }
+
+ return { body: await res.text(), status: res.status };
+}
+
+export const b = {
+ bookmarksCollectionEndpoint: '/hyperapi/bookmarks/dropdown/',
+ bookmarkMenuState: {},
+}
+
+export async function showBookmarkMenu(ev, sender, el) {
+ if (b.bookmarkMenuState.state === undefined) {
+ el.addEventListener('toggle', e => {
+ if (e.newState === 'closed') {
+ b.bookmarkMenuState.state = 'closed';
+ }
+ });
+ }
+
+ // dismiss if open and last invoker is the same button that opened it
+ if (b.bookmarkMenuState.state === 'open' && b.bookmarkMenuState.invoker === sender) {
+ el.hidePopover();
+ return;
+ }
+
+ b.bookmarkMenuState.invoker = sender;
+ b.bookmarkMenuState.state = 'open';
+ b.send({ 'plain': 'Loading…' }, 'fillBookmarkMenu');
+ el.showPopover();
+ const bRect = sender.getBoundingClientRect();
+ const menuRect = el.getBoundingClientRect();
+ const preferredLeft = bRect.right - menuRect.width;
+ const enoughSpace = preferredLeft >= 0;
+
+ const scrollY = window.scrollY;
+
+ if (enoughSpace) {
+ el.style.left = `${preferredLeft}px`;
+ } else {
+ el.style.left = `${bRect.left}px`;
+ }
+ el.style.top = `${bRect.bottom + scrollY}px`;
+
+ b.bookmarkMenuState.kind = sender.dataset.conceptKind;
+ b.bookmarkMenuState.id = sender.dataset.conceptId;
+
+ const bookmarkCollections = await getHTML(b.bookmarksCollectionEndpoint, {
+ _query: {
+ concept_kind: b.bookmarkMenuState.kind,
+ concept_id: b.bookmarkMenuState.id,
+ }
+ });
+ b.send({ 'html': bookmarkCollections.body }, 'fillBookmarkMenu');
+}
+
+export function fillBookmarkMenu(payload, __, el) {
+ if (payload.plain) {
+ el.innerText = payload.plain;
+ return;
+ }
+
+ el.innerHTML = payload.html;
+}
+
+export async function bookmarkMenuSubmit(ev, _, el) {
+ ev.preventDefault();
+ const url = el.action;
+ const body = new URLSearchParams(new FormData(el));
+ const options = { body: body, method: 'POST' };
+ const status = (await getHTML(url, options)).status;
+
+ if (status !== 204) {
+ b.trigger('bookmarkMenuShowError');
+ return;
+ }
+
+ const newCollections = await getHTML(b.bookmarksCollectionEndpoint, {
+ _query: {
+ concept_kind: b.bookmarkMenuState.kind,
+ concept_id: b.bookmarkMenuState.id,
+ saved: true,
+ }
+ });
+ b.send({ 'html': newCollections.body }, 'fillBookmarkMenu');
+}
+
+export function bookmarkMenuResetSavedButton(_, __, el) {
+ el.value = 'Save';
+}
+
+export function bookmarkMenuShowError(_, __, el) {
+ if (el === undefined) {
+ return;
+ }
+
+ if (el.classList.contains('hidden')) {
+ el.classList.remove('hidden');
+ setTimeout(() => { b.trigger('bookmarkMenuHideError') }, 4000);
+ }
+}
+
+export function bookmarkMenuHideError(_, __, el) {
+ if (el === undefined) {
+ return;
+ }
+
+ if (!el.classList.contains('hidden')) {
+ el.classList.add('hidden');
+ }
+}
diff --git a/data/static/js/bits/ui.js b/data/static/js/bits/ui.js
index 175c9f8..a94f86a 100644
--- a/data/static/js/bits/ui.js
+++ b/data/static/js/bits/ui.js
@@ -1,9 +1,6 @@
export const b = {
babycodePreviewEndpoint: '/api/babycode-preview/',
- bookmarksCollectionEndpoint: '/hyperapi/bookmarks/dropdown/',
init: 'babycodeEditorCharCountInit localizeTimestamps',
-
- bookmarkState: {},
}
const getThreadId = () => {
@@ -14,22 +11,6 @@ const getThreadId = () => {
return parseInt(scheme[2]);
}
-async function getHTML(endpoint, options = {}) {
- let query = {};
- if (options._query !== undefined) {
- query = options._query;
- delete options._query;
- }
-
- const params = new URLSearchParams(query);
- const res = await fetch(`${endpoint}?${params}`, options);
- if (!res.ok) {
- console.error(res);
- }
-
- return { body: await res.text(), status: res.status };
-}
-
export function setTab(_, sender, el) {
if (sender.ariaSelected === 'true') {
return;
@@ -242,101 +223,3 @@ export function localizeTimestamps(_, __, el) {
const d = new Date(el.dateTime);
el.innerText = d.toLocaleString();
}
-
-export async function showBookmarkMenu(ev, sender, el) {
- if (b.bookmarkState.state === undefined) {
- el.addEventListener('toggle', e => {
- if (e.newState === 'closed') {
- b.bookmarkState.state = 'closed';
- }
- });
- }
-
- // dismiss if open and last invoker is the same button that opened it
- if (b.bookmarkState.state === 'open' && b.bookmarkState.invoker === sender) {
- el.hidePopover();
- return;
- }
-
- b.bookmarkState.invoker = sender;
- b.bookmarkState.state = 'open';
- b.send({ 'plain': 'Loading…' }, 'fillBookmarkMenu');
- el.showPopover();
- const bRect = sender.getBoundingClientRect();
- const menuRect = el.getBoundingClientRect();
- const preferredLeft = bRect.right - menuRect.width;
- const enoughSpace = preferredLeft >= 0;
-
- const scrollY = window.scrollY;
-
- if (enoughSpace) {
- el.style.left = `${preferredLeft}px`;
- } else {
- el.style.left = `${bRect.left}px`;
- }
- el.style.top = `${bRect.bottom + scrollY}px`;
-
- const conceptKind = sender.prop('conceptKind');
- const conceptId = sender.prop('conceptId');
-
- const bookmarkCollections = await getHTML(b.bookmarksCollectionEndpoint, {
- _query: {
- concept_kind: conceptKind,
- concept_id: conceptId,
- }
- });
- b.send({ 'html': bookmarkCollections.body }, 'fillBookmarkMenu');
-}
-
-
-export function fillBookmarkMenu(payload, __, el) {
- if (payload.plain) {
- el.innerText = payload.plain;
- return;
- }
-
- el.innerHTML = payload.html;
-}
-
-export async function bookmarkMenuSubmit(ev, _, el) {
- ev.preventDefault();
- const url = el.action;
- const body = new URLSearchParams(new FormData(el));
- const options = { body: body, method: 'POST' };
- const status = (await getHTML(url, options)).status;
-
- if (status !== 204) {
- b.trigger('bookmarkMenuShowError');
- return;
- }
- b.trigger('bookmarkMenuShowSavedButton');
-}
-
-export function bookmarkMenuShowSavedButton(_, __, el) {
- el.value = 'Saved!';
-}
-
-export function bookmarkMenuResetSavedButton(_, __, el) {
- el.value = 'Save';
-}
-
-export function bookmarkMenuShowError(_, __, el) {
- if (el === undefined) {
- return;
- }
-
- if (el.classList.contains('hidden')) {
- el.classList.remove('hidden');
- setTimeout(() => { b.trigger('bookmarkMenuHideError') }, 4000);
- }
-}
-
-export function bookmarkMenuHideError(_, __, el) {
- if (el === undefined) {
- return;
- }
-
- if (!el.classList.contains('hidden')) {
- el.classList.add('hidden');
- }
-}