Skip to content

Commit

Permalink
Merge pull request #643 from LeXofLeviafan/refactor
Browse files Browse the repository at this point in the history
[bukuserver] Bookmarklet, optional fetch, refactor
  • Loading branch information
jarun authored Dec 14, 2022
2 parents f5bf26b + eba8fca commit e49d61d
Show file tree
Hide file tree
Showing 19 changed files with 454 additions and 414 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ buku.py
/tests/test_bukuDb/places.sqlite*
/tests/vcr_cassettes/test_search_and_open_all_in_browser.yaml
/tests/vcr_cassettes/tests.test_bukuDb/
/bookmarks.db
9 changes: 6 additions & 3 deletions bukuserver/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,9 @@ See more option on `bukuserver run --help` and `bukuserver --help`

### Configuration

Following are available os env config available for bukuserver.
The following are os env config variables available for bukuserver.

| Name (without prefix) | Description | Value |
| Name (_without prefix_) | Description | Value |
| --- | --- | --- |
| PER_PAGE | bookmarks per page | positive integer [default: 10] |
| SECRET_KEY | [flask secret key](https://flask.palletsprojects.com/config/#SECRET_KEY) | string [default: os.urandom(24)] |
Expand All @@ -123,8 +123,11 @@ Following are available os env config available for bukuserver.
| DISABLE_FAVICON | disable bookmark [favicons](https://wikipedia.org/wiki/Favicon) | boolean [default: `true`] |
| OPEN_IN_NEW_TAB | url link open in new tab | boolean [default: `false`] |
| REVERSE_PROXY_PATH | reverse proxy path | string |
| LOCALE | GUI language (partial support) | string [default: `en`] |

Note: `BUKUSERVER_` is the common prefix.
Note: `BUKUSERVER_` is the common prefix (_every variable starts with it_).

Note: Valid boolean values are `true`, `false`, `1`, `0` (case-insensitive).

Note: if input is invalid, the default value will be used if defined

Expand Down
290 changes: 290 additions & 0 deletions bukuserver/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
#!/usr/bin/env python
# pylint: disable=wrong-import-order, ungrouped-imports
"""Server module."""
import collections
import typing as T
from typing import Any, Dict, Union # NOQA; type: ignore
from unittest import mock

from flask.views import MethodView
from flask_api import exceptions, status

import buku
from buku import BukuDb

import flask
from flask import current_app, jsonify, redirect, request, url_for

try:
from . import forms, response
except ImportError:
from bukuserver import forms, response


STATISTIC_DATA = None

response_ok = lambda: (jsonify(response.response_template['success']),
status.HTTP_200_OK,
{'ContentType': 'application/json'})
response_bad = lambda: (jsonify(response.response_template['failure']),
status.HTTP_400_BAD_REQUEST,
{'ContentType': 'application/json'})
to_response = lambda ok: response_ok() if ok else response_bad()


def get_bukudb():
"""get bukudb instance"""
db_file = current_app.config.get('BUKUSERVER_DB_FILE', None)
return BukuDb(dbfile=db_file)


def search_tag(
db: BukuDb, stag: T.Optional[str] = None, limit: T.Optional[int] = None
) -> T.Tuple[T.List[str], T.Dict[str, int]]:
"""search tag.
db:
buku db instance
stag:
search tag
limit:
positive integer limit
Returns
-------
tuple
list of unique tags sorted alphabetically and dictionary of tag and its usage count
Raises
------
ValueError
if limit is not positive
"""
if limit is not None and limit < 1:
raise ValueError("limit must be positive")
tags: T.Set[str] = set()
counter = collections.Counter()
query_list = ["SELECT DISTINCT tags , COUNT(tags) FROM bookmarks"]
if stag:
query_list.append("where tags LIKE :search_tag")
query_list.append("GROUP BY tags")
row: T.Tuple[str, int]
for row in db.cur.execute(" ".join(query_list), {"search_tag": f"%{stag}%"}):
for tag in row[0].strip(buku.DELIM).split(buku.DELIM):
if not tag:
continue
tags.add(tag)
counter[tag] += row[1]
return list(sorted(tags)), dict(counter.most_common(limit))


class ApiTagView(MethodView):

def get(self, tag: T.Optional[str]):
bukudb = get_bukudb()
if tag is None:
return {"tags": search_tag(db=bukudb, limit=5)[0]}
tags = search_tag(db=bukudb, stag=tag)
if tag not in tags[1]:
raise exceptions.NotFound()
return dict(name=tag, usage_count=tags[1][tag])

def put(self, tag: str):
bukudb = get_bukudb()
try:
new_tags = request.data.get('tags') # type: ignore
if new_tags:
new_tags = new_tags.split(',')
else:
return response_bad()
except AttributeError as e:
raise exceptions.ParseError(detail=str(e))
return to_response(bukudb.replace_tag(tag, new_tags))


class ApiBookmarkView(MethodView):

def get(self, rec_id: Union[int, None]):
if rec_id is None:
bukudb = getattr(flask.g, 'bukudb', get_bukudb())
all_bookmarks = bukudb.get_rec_all()
result = {'bookmarks': []} # type: Dict[str, Any]
for bookmark in all_bookmarks:
result_bookmark = {
'url': bookmark[1],
'title': bookmark[2],
'tags': [x for x in bookmark[3].split(',') if x],
'description': bookmark[4]
}
if not request.path.startswith('/api/'):
result_bookmark['id'] = bookmark[0]
result['bookmarks'].append(result_bookmark)
res = jsonify(result)
else:
bukudb = getattr(flask.g, 'bukudb', get_bukudb())
bookmark = bukudb.get_rec_by_id(rec_id)
if bookmark is None:
res = response_bad()
else:
res = jsonify({
'url': bookmark[1],
'title': bookmark[2],
'tags': [x for x in bookmark[3].split(',') if x],
'description': bookmark[4]
})
return res

def post(self, rec_id: None = None):
bukudb = getattr(flask.g, 'bukudb', get_bukudb())
create_bookmarks_form = forms.ApiBookmarkForm()
url_data = create_bookmarks_form.url.data
result_flag = bukudb.add_rec(
url_data,
create_bookmarks_form.title.data,
create_bookmarks_form.tags.data,
create_bookmarks_form.description.data
)
return to_response(result_flag != -1)

def put(self, rec_id: int):
bukudb = getattr(flask.g, 'bukudb', get_bukudb())
result_flag = bukudb.update_rec(
rec_id,
request.form.get('url'),
request.form.get('title'),
request.form.get('tags'),
request.form.get('description'))
return to_response(result_flag)

def delete(self, rec_id: Union[int, None]):
if rec_id is None:
bukudb = getattr(flask.g, 'bukudb', get_bukudb())
with mock.patch('buku.read_in', return_value='y'):
result_flag = bukudb.cleardb()
else:
bukudb = getattr(flask.g, 'bukudb', get_bukudb())
result_flag = bukudb.delete_rec(rec_id)
return to_response(result_flag)


class ApiBookmarkRangeView(MethodView):

def get(self, starting_id: int, ending_id: int):
bukudb = getattr(flask.g, 'bukudb', get_bukudb())
max_id = bukudb.get_max_id()
if starting_id > max_id or ending_id > max_id:
return response_bad()
result = {'bookmarks': {}} # type: ignore
for i in range(starting_id, ending_id + 1, 1):
bookmark = bukudb.get_rec_by_id(i)
result['bookmarks'][i] = {
'url': bookmark[1],
'title': bookmark[2],
'tags': [x for x in bookmark[3].split(',') if x],
'description': bookmark[4]
}
return jsonify(result)

def put(self, starting_id: int, ending_id: int):
bukudb = getattr(flask.g, 'bukudb', get_bukudb())
max_id = bukudb.get_max_id()
if starting_id > max_id or ending_id > max_id:
return response_bad()
for i in range(starting_id, ending_id + 1, 1):
updated_bookmark = request.data.get(str(i)) # type: ignore
result_flag = bukudb.update_rec(
i,
updated_bookmark.get('url'),
updated_bookmark.get('title'),
updated_bookmark.get('tags'),
updated_bookmark.get('description'))
if result_flag is False:
return response_bad()
return response_ok()

def delete(self, starting_id: int, ending_id: int):
bukudb = getattr(flask.g, 'bukudb', get_bukudb())
max_id = bukudb.get_max_id()
if starting_id > max_id or ending_id > max_id:
return response_bad()
idx = min([starting_id, ending_id])
result_flag = bukudb.delete_rec(idx, starting_id, ending_id, is_range=True)
return to_response(result_flag)


class ApiBookmarkSearchView(MethodView):

def get(self):
arg_obj = request.args
keywords = arg_obj.getlist('keywords')
all_keywords = arg_obj.get('all_keywords')
deep = arg_obj.get('deep')
regex = arg_obj.get('regex')
# api request is more strict
all_keywords = False if all_keywords is None else all_keywords
deep = False if deep is None else deep
regex = False if regex is None else regex
all_keywords = (
all_keywords if isinstance(all_keywords, bool) else
all_keywords.lower() == 'true'
)
deep = deep if isinstance(deep, bool) else deep.lower() == 'true'
regex = regex if isinstance(regex, bool) else regex.lower() == 'true'

result = {'bookmarks': []}
bukudb = getattr(flask.g, 'bukudb', get_bukudb())
found_bookmarks = bukudb.searchdb(keywords, all_keywords, deep, regex)
found_bookmarks = [] if found_bookmarks is None else found_bookmarks
res = None
if found_bookmarks is not None:
for bookmark in found_bookmarks:
result_bookmark = {
'id': bookmark[0],
'url': bookmark[1],
'title': bookmark[2],
'tags': list(filter(lambda x: x, bookmark[3].split(','))),
'description': bookmark[4]
}
result['bookmarks'].append(result_bookmark)
current_app.logger.debug('total bookmarks:{}'.format(len(result['bookmarks'])))
res = jsonify(result)
return res

def delete(self):
arg_obj = request.form
keywords = arg_obj.getlist('keywords')
all_keywords = arg_obj.get('all_keywords')
deep = arg_obj.get('deep')
regex = arg_obj.get('regex')
# api request is more strict
all_keywords = False if all_keywords is None else all_keywords
deep = False if deep is None else deep
regex = False if regex is None else regex
all_keywords = (
all_keywords if isinstance(all_keywords, bool) else
all_keywords.lower() == 'true'
)
deep = deep if isinstance(deep, bool) else deep.lower() == 'true'
regex = regex if isinstance(regex, bool) else regex.lower() == 'true'
bukudb = getattr(flask.g, 'bukudb', get_bukudb())
found_bookmarks = bukudb.searchdb(keywords, all_keywords, deep, regex)
found_bookmarks = [] if found_bookmarks is None else found_bookmarks
res = None
if found_bookmarks is not None:
for bookmark in found_bookmarks:
if not bukudb.delete_rec(bookmark[0]):
res = response_bad()
return res or response_ok()


class BookmarkletView(MethodView): # pylint: disable=too-few-public-methods
def get(self):
url = request.args.get('url')
title = request.args.get('title')
description = request.args.get('description')

bukudb = getattr(flask.g, 'bukudb', get_bukudb())
rec_id = bukudb.get_rec_id(url)
if rec_id >= 0:
return redirect(url_for('bookmark.edit_view', id=rec_id))
return redirect(url_for('bookmark.create_view', link=url, title=title, description=description))
2 changes: 1 addition & 1 deletion bukuserver/bookmarklet.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

var url = location.href;
var title = document.title.trim() || "";
var desc = document.getSelection().toString().trim();
var desc = document.getSelection().toString().trim() || (document.querySelector('meta[name$=description i], meta[property$=description i]')||{}).content || "";
if(desc.length > 4000){
desc = desc.substr(0,4000) + '...';
alert('The selected text is too long, it will be truncated.');
Expand Down
3 changes: 2 additions & 1 deletion bukuserver/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ class HomeForm(SearchBookmarksForm):


class BookmarkForm(FlaskForm):
url = wtforms.StringField('Url', name='link', validators=[wtforms.validators.DataRequired()])
url = wtforms.StringField('Url', name='link', validators=[wtforms.validators.InputRequired()])
title = wtforms.StringField()
tags = wtforms.StringField()
description = wtforms.TextAreaField()
fetch = wtforms.HiddenField(filters=[bool])

class ApiBookmarkForm(BookmarkForm):
url = wtforms.StringField(validators=[wtforms.validators.DataRequired()])
Loading

0 comments on commit e49d61d

Please sign in to comment.