Skip to content

Commit

Permalink
Merge pull request #676 from 2ynn/put_api_tags
Browse files Browse the repository at this point in the history
[#672] [bukuserver API] improve tag replacement/deletion
  • Loading branch information
LeXofLeviafan authored Oct 20, 2023
2 parents 3818639 + c56b2de commit 6369ca9
Show file tree
Hide file tree
Showing 8 changed files with 424 additions and 249 deletions.
28 changes: 17 additions & 11 deletions buku
Original file line number Diff line number Diff line change
Expand Up @@ -1959,7 +1959,7 @@ class BukuDb:

return parse_tags(tags)

def replace_tag(self, orig: str, new: List[str] = []) -> bool:
def replace_tag(self, orig: str, new: List[str] = []):
"""Replace original tag by new tags in all records.
Remove original tag if new tag is empty.
Expand All @@ -1971,22 +1971,26 @@ class BukuDb:
new : list
Replacement tags.
Returns
Raises
-------
bool
True on success, False on failure.
ValueError: Invalid input(s) provided.
RuntimeError: Tag deletion failed.
"""

if DELIM in orig:
raise ValueError("Original tag cannot contain delimiter ({}).".format(DELIM))

orig = delim_wrap(orig)
newtags = parse_tags(new) if new else DELIM
newtags = parse_tags([DELIM.join(new)])

if orig == newtags:
print('Tags are same.')
return False
raise ValueError("Original and replacement tags are the same.")

# Remove original tag from DB if new tagset reduces to delimiter
if newtags == DELIM:
return self.delete_tag_at_index(0, orig)
if not self.delete_tag_at_index(0, orig):
raise RuntimeError("Tag deletion failed.")

# Update bookmarks with original tag
query = 'SELECT id, tags FROM bookmarks WHERE tags LIKE ?'
Expand All @@ -2002,8 +2006,6 @@ class BukuDb:

self.conn.commit()

return True

def get_tagstr_from_taglist(self, id_list, taglist):
"""Get a string of delimiter-separated (and enclosed) string
of tags from a dictionary of tags by matching ids.
Expand Down Expand Up @@ -5974,7 +5976,11 @@ POSITIONAL ARGUMENTS:
if len(args.replace) == 1:
bdb.delete_tag_at_index(0, args.replace[0])
else:
bdb.replace_tag(args.replace[0], args.replace[1:])
try:
bdb.replace_tag(args.replace[0], [' '.join(args.replace[1:])])
except Exception as e:
LOGERR(str(e))
bdb.close_quit(1)

# Export bookmarks
if args.export is not None and not search_opted:
Expand Down
149 changes: 85 additions & 64 deletions bukuserver/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,23 @@
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
from flask import current_app, redirect, request, url_for

try:
from . import forms, response
from response import Response
from forms import ApiBookmarkCreateForm, ApiBookmarkEditForm, ApiBookmarkRangeEditForm, ApiTagForm
except ImportError:
from bukuserver import forms, response
from bukuserver.response import Response
from bukuserver.forms import ApiBookmarkCreateForm, ApiBookmarkEditForm, ApiBookmarkRangeEditForm, ApiTagForm


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 entity(bookmark, id=False):
data = {
Expand Down Expand Up @@ -94,23 +88,35 @@ 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]}
return Response.SUCCESS(data={"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 {"name": tag, "usage_count": tags[1][tag]}
return Response.TAG_NOT_FOUND()
return Response.SUCCESS(data={"name": tag, "usage_count": tags[1][tag]})

def put(self, tag: str):
form = ApiTagForm({})
error_response, data = form.process_data(request.get_json())
if error_response is not None:
return error_response(data=data)
bukudb = get_bukudb()
tags = search_tag(db=bukudb, stag=tag)
if tag not in tags[1]:
return Response.TAG_NOT_FOUND()
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))
bukudb.replace_tag(tag, form.tags.data)
return Response.SUCCESS()
except (ValueError, RuntimeError):
return Response.FAILURE()

def delete(self, tag: str):
if buku.DELIM in tag:
return Response.TAG_NOT_VALID()
bukudb = get_bukudb()
tags = search_tag(db=bukudb, stag=tag)
if tag not in tags[1]:
return Response.TAG_NOT_FOUND()
return Response.from_flag(bukudb.delete_tag_at_index(0, tag, chatty=False))


class ApiBookmarkView(MethodView):
Expand All @@ -121,34 +127,40 @@ def get(self, rec_id: T.Union[int, None]):
all_bookmarks = bukudb.get_rec_all()
result = {'bookmarks': [entity(bookmark, id=not request.path.startswith('/api/'))
for bookmark in all_bookmarks]}
res = jsonify(result)
else:
bukudb = getattr(flask.g, 'bukudb', get_bukudb())
bookmark = bukudb.get_rec_by_id(rec_id)
res = (response_bad() if bookmark is None else jsonify(entity(bookmark)))
return res
if bookmark is None:
return Response.BOOKMARK_NOT_FOUND()
result = entity(bookmark)
return Response.SUCCESS(data=result)

def post(self, rec_id: None = None):
form = ApiBookmarkCreateForm({})
error_response, error_data = form.process_data(request.get_json())
if error_response is not None:
return error_response(data=error_data)
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)
form.url.data,
form.title.data,
form.tags_str,
form.description.data)
return Response.from_flag(result_flag)

def put(self, rec_id: int):
form = ApiBookmarkEditForm({})
error_response, error_data = form.process_data(request.get_json())
if error_response is not None:
return error_response(data=error_data)
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)
form.url.data,
form.title.data,
form.tags_str,
form.description.data)
return Response.from_flag(result_flag)

def delete(self, rec_id: T.Union[int, None]):
if rec_id is None:
Expand All @@ -158,45 +170,57 @@ def delete(self, rec_id: T.Union[int, None]):
else:
bukudb = getattr(flask.g, 'bukudb', get_bukudb())
result_flag = bukudb.delete_rec(rec_id)
return to_response(result_flag)
return Response.from_flag(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() or 0
if starting_id > max_id or ending_id > max_id:
return response_bad()
if starting_id > ending_id or ending_id > max_id:
return Response.RANGE_NOT_VALID()
result = {'bookmarks': {i: entity(bukudb.get_rec_by_id(i))
for i in range(starting_id, ending_id + 1)}}
return jsonify(result)
return Response.SUCCESS(data=result)

def put(self, starting_id: int, ending_id: int):
bukudb = getattr(flask.g, 'bukudb', get_bukudb())
max_id = bukudb.get_max_id() or 0
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()
if starting_id > ending_id or ending_id > max_id:
return Response.RANGE_NOT_VALID()
updates = []
errors = {}
for rec_id in range(starting_id, ending_id + 1):
json = request.get_json().get(str(rec_id))
if json is None:
errors[rec_id] = 'Input required.'
continue
form = ApiBookmarkRangeEditForm({})
error_response, error_data = form.process_data(json)
if error_response is not None:
errors[rec_id] = error_data.get('errors')
updates += [{'index': rec_id,
'url': form.url.data,
'title_in': form.title.data,
'tags_in': form.tags_in,
'desc': form.description.data}]

if errors:
return Response.INPUT_NOT_VALID(data={'errors': errors})
for update in updates:
if not bukudb.update_rec(**update):
return Response.FAILURE()
return Response.SUCCESS()

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


class ApiBookmarkSearchView(MethodView):
Expand All @@ -217,14 +241,11 @@ def get(self):
)
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())
res = None
result = {'bookmarks': [entity(bookmark, id=True)
for bookmark in bukudb.searchdb(keywords, all_keywords, deep, regex)]}
current_app.logger.debug('total bookmarks:{}'.format(len(result['bookmarks'])))
res = jsonify(result)
return res
return Response.SUCCESS(data=result)

def delete(self):
arg_obj = request.form
Expand All @@ -246,8 +267,8 @@ def delete(self):
res = None
for bookmark in bukudb.searchdb(keywords, all_keywords, deep, regex):
if not bukudb.delete_rec(bookmark.id):
res = response_bad()
return res or response_ok()
res = Response.FAILURE()
return res or Response.SUCCESS()


class BookmarkletView(MethodView): # pylint: disable=too-few-public-methods
Expand Down
Loading

0 comments on commit 6369ca9

Please sign in to comment.