Feature/server (#289)
* new: dev: version flag fix: dev: pylint error * chg: dev: sort route !cosmetic * new: dev: custom BaseModelView for buku bookmark * new: dev: formatted entry * new: dev: url render mode * new: dev: bookmark edit !wip * chg: dev: use existing form for bookmark * chg: dev: form name !refactor * new: dev: enable details views * new: dev: views module * new: dev: tag model view * chg: dev: only split page_size defined * chg: dev: use SelectMultipleField * fix: dev: Bookmark tags choices * chg dev: configure tags * chg: dev: tag edit form !wip * chg: dev: update bookmark model * chg: dev: remove unused function * new: dev: add flask wtf and admin * chg: dev: use SimpleNamespace instead namedtuple * new: dev: delete tags feature * new: dev: chatty parameter for delete_tag_at_index * fix: dev: skip confirmation when deleting tag * fix: dev: when update bookmark model * new: dev: update tag * chg: dev: use parse_tags method * new: dev: select2 field for tagsfield * chg: dev: remove unused code * fix: dev: syntax * fix: dev: update bookmark model * chg: dev: change api based on flask-api * fix: dev: new tags on tag_detail PUT * chg: dev: raise error when parsing failed * chg: dev: move server required package * new: dev: create_model * chg: dev: override abstract method model view class * chg: dev: delete model for bookmark * fix: dev: pylint ignore !cosmetic * new: dev: filter for tag * chg: dev: more filter for TagModel * new: dev: new filter for tag model * chg: dev: deduplicate filter * fix: dev: pylint !cosmetic * chg: dev: generalize tag, bookmark filter * chg: dev: add filters for bookmark * fix: dev: not equal filter * new: dev: url basic filter * chg: dev: configure bookmark model view * chg: dev: reorder bookmark view method * new: dev: tags number filter * chg: dev: bookmark url with unknown scheme * new: dev: network handle api * new: dev: modal edit/create for bookmark * chg: dev: link tag bookmark tag search * fix: dev: empty tag contain search * chg: dev: buku search option * new: dev: buku search to filter * chg: dev: front page search * chg: dev: move Statistic page to views module * fix: dev: bookmark search * new: dev: title filter * fix: dev: statistic label * fix: dev: link on statistic page * chg: dev: strip search value * fix: dev: bookmark entry fix * fix: dev: netloc modal on * fix: dev: pylint !cosmetic * chg: dev: remove duplicate package * chg: dev: move admin to root * fix: dev: link on statistic page * chg: dev: pin pyyaml package
This commit is contained in:
parent
c3d38cb17b
commit
be50451d1d
11
buku.py
11
buku.py
@ -697,7 +697,7 @@ class BukuDb:
|
||||
|
||||
return True
|
||||
|
||||
def delete_tag_at_index(self, index, tags_in, delay_commit=False):
|
||||
def delete_tag_at_index(self, index, tags_in, delay_commit=False, chatty=True):
|
||||
"""Delete tags from bookmark tagset at index.
|
||||
|
||||
Parameters
|
||||
@ -709,6 +709,8 @@ class BukuDb:
|
||||
delay_commit : bool, optional
|
||||
True if record should not be committed to the DB,
|
||||
leaving commit responsibility to caller. Default is False.
|
||||
chatty: bool, optional
|
||||
Skip confirmation when set to False.
|
||||
|
||||
Returns
|
||||
-------
|
||||
@ -719,9 +721,10 @@ class BukuDb:
|
||||
tags_to_delete = tags_in.strip(DELIM).split(DELIM)
|
||||
|
||||
if index == 0:
|
||||
resp = read_in('Delete the tag(s) from ALL bookmarks? (y/n): ')
|
||||
if resp != 'y':
|
||||
return False
|
||||
if chatty:
|
||||
resp = read_in('Delete the tag(s) from ALL bookmarks? (y/n): ')
|
||||
if resp != 'y':
|
||||
return False
|
||||
|
||||
count = 0
|
||||
match = "'%' || ? || '%'"
|
||||
|
235
bukuserver/filters.py
Normal file
235
bukuserver/filters.py
Normal file
@ -0,0 +1,235 @@
|
||||
from enum import Enum
|
||||
|
||||
from flask_admin.model import filters
|
||||
|
||||
|
||||
class BookmarkField(Enum):
|
||||
ID = 0
|
||||
URL = 1
|
||||
TITLE = 2
|
||||
TAGS = 3
|
||||
DESCRIPTION = 4
|
||||
|
||||
|
||||
def equal_func(query, value, index):
|
||||
return filter(lambda x: x[index] == value, query)
|
||||
|
||||
|
||||
def not_equal_func(query, value, index):
|
||||
return filter(lambda x: x[index] != value, query)
|
||||
|
||||
|
||||
def greater_func(query, value, index):
|
||||
return filter(lambda x: x[index] > value, query)
|
||||
|
||||
|
||||
def smaller_func(query, value, index):
|
||||
return filter(lambda x: x[index] > value, query)
|
||||
|
||||
|
||||
def in_list_func(query, value, index):
|
||||
return filter(lambda x: x[index] in value, query)
|
||||
|
||||
|
||||
def not_in_list_func(query, value, index):
|
||||
return filter(lambda x: x[index] not in value, query)
|
||||
|
||||
|
||||
def top_x_func(query, value, index):
|
||||
items = sorted(set(x[index] for x in query), reverse=True)
|
||||
top_x = items[:value]
|
||||
return filter(lambda x: x[index] in top_x, query)
|
||||
|
||||
|
||||
def bottom_x_func(query, value, index):
|
||||
items = sorted(set(x[index] for x in query), reverse=False)
|
||||
top_x = items[:value]
|
||||
return filter(lambda x: x[index] in top_x, query)
|
||||
|
||||
|
||||
class FilterType(Enum):
|
||||
|
||||
EQUAL = {'func': equal_func, 'text':'equals'}
|
||||
NOT_EQUAL = {'func': not_equal_func, 'text':'not equal'}
|
||||
GREATER = {'func': greater_func, 'text':'greater than'}
|
||||
SMALLER = {'func': smaller_func, 'text':'smaller than'}
|
||||
IN_LIST = {'func': in_list_func, 'text':'in list'}
|
||||
NOT_IN_LIST = {'func': not_in_list_func, 'text':'not in list'}
|
||||
TOP_X = {'func': top_x_func, 'text': 'top x'}
|
||||
BOTTOM_X = {'func': bottom_x_func, 'text': 'bottom x'}
|
||||
|
||||
|
||||
class BaseFilter(filters.BaseFilter):
|
||||
|
||||
def operation(self):
|
||||
return getattr(self, 'operation_text')
|
||||
|
||||
def apply(self, query, value):
|
||||
return getattr(self, 'apply_func')(query, value, getattr(self, 'index'))
|
||||
|
||||
|
||||
class TagBaseFilter(BaseFilter):
|
||||
|
||||
def __init__(self, name, operation_text=None, apply_func=None, filter_type=None, options=None, data_type=None):
|
||||
if operation_text in ('in list', 'not in list'):
|
||||
super().__init__(name, options, data_type='select2-tags')
|
||||
else:
|
||||
super().__init__(name, options, data_type)
|
||||
if name == 'name':
|
||||
self.index = 0
|
||||
elif name == 'usage_count':
|
||||
self.index = 1
|
||||
else:
|
||||
raise ValueError('name: {}'.format(name))
|
||||
self.filter_type = None
|
||||
if filter_type:
|
||||
self.apply_func = filter_type.value['func']
|
||||
self.operation_text = filter_type.value['text']
|
||||
self.filter_type = filter_type
|
||||
else:
|
||||
self.apply_func = apply_func
|
||||
self.operation_text = operation_text
|
||||
|
||||
def clean(self, value):
|
||||
if self.filter_type in (FilterType.IN_LIST, FilterType.NOT_IN_LIST) and self.name == 'usage_count':
|
||||
value = [int(v.strip()) for v in value.split(',') if v.strip()]
|
||||
elif self.filter_type in (FilterType.IN_LIST, FilterType.NOT_IN_LIST):
|
||||
value = [v.strip() for v in value.split(',') if v.strip()]
|
||||
elif self.name == 'usage_count':
|
||||
value = int(value)
|
||||
if self.filter_type in (FilterType.TOP_X, FilterType.BOTTOM_X) and value < 1:
|
||||
raise ValueError
|
||||
if isinstance(value, str):
|
||||
return value.strip()
|
||||
return value
|
||||
|
||||
|
||||
class BookmarkBukuFilter(BaseFilter):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.keys = {
|
||||
'all_keywords': 'match all',
|
||||
'deep': 'deep',
|
||||
'regex': 'regex'
|
||||
}
|
||||
for key, value in kwargs.items():
|
||||
if key in self.keys and value:
|
||||
setattr(self, key, value)
|
||||
else:
|
||||
setattr(self, key, False)
|
||||
list(map(lambda x: kwargs.pop(x), self.keys))
|
||||
super().__init__('buku', *args, **kwargs)
|
||||
|
||||
def operation(self):
|
||||
parts = []
|
||||
for key, value in self.keys.items():
|
||||
if getattr(self, key):
|
||||
parts.append(value)
|
||||
if not parts:
|
||||
return 'search'
|
||||
return 'search ' + ', '.join(parts)
|
||||
|
||||
def apply(self, query, value):
|
||||
return query
|
||||
|
||||
|
||||
class BookmarkBaseFilter(BaseFilter):
|
||||
|
||||
def __init__(self, name, operation_text=None, apply_func=None, filter_type=None, options=None, data_type=None):
|
||||
if operation_text in ('in list', 'not in list'):
|
||||
super().__init__(name, options, data_type='select2-tags')
|
||||
else:
|
||||
super().__init__(name, options, data_type)
|
||||
bm_fields_dict = {x.name.lower(): x.value for x in BookmarkField}
|
||||
if name in bm_fields_dict:
|
||||
self.index = bm_fields_dict[name]
|
||||
else:
|
||||
raise ValueError('name: {}'.format(name))
|
||||
self.filter_type = None
|
||||
if filter_type:
|
||||
self.apply_func = filter_type.value['func']
|
||||
self.operation_text = filter_type.value['text']
|
||||
else:
|
||||
self.apply_func = apply_func
|
||||
self.operation_text = operation_text
|
||||
|
||||
def clean(self, value):
|
||||
if self.filter_type in (FilterType.IN_LIST, FilterType.NOT_IN_LIST) and self.name == BookmarkField.ID.name.lower():
|
||||
value = [int(v.strip()) for v in value.split(',') if v.strip()]
|
||||
elif self.filter_type in (FilterType.IN_LIST, FilterType.NOT_IN_LIST):
|
||||
value = [v.strip() for v in value.split(',') if v.strip()]
|
||||
elif self.name == BookmarkField.ID.name.lower():
|
||||
value = int(value)
|
||||
if self.filter_type in (FilterType.TOP_X, FilterType.BOTTOM_X) and value < 1:
|
||||
raise ValueError
|
||||
if isinstance(value, str):
|
||||
return value.strip()
|
||||
return value
|
||||
|
||||
|
||||
class BookmarkTagNumberEqualFilter(BookmarkBaseFilter):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def apply_func(query, value, index):
|
||||
for item in query:
|
||||
tags = [tag for tag in item[index].split(',') if tag]
|
||||
if len(tags) == value:
|
||||
yield item
|
||||
|
||||
self.apply_func = apply_func
|
||||
|
||||
def clean(self, value):
|
||||
value = int(value)
|
||||
if value < 0:
|
||||
raise ValueError
|
||||
return value
|
||||
|
||||
|
||||
class BookmarkTagNumberGreaterFilter(BookmarkTagNumberEqualFilter):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def apply_func(query, value, index):
|
||||
for item in query:
|
||||
tags = [tag for tag in item[index].split(',') if tag]
|
||||
if len(tags) > value:
|
||||
yield item
|
||||
|
||||
self.apply_func = apply_func
|
||||
|
||||
|
||||
class BookmarkTagNumberNotEqualFilter(BookmarkTagNumberEqualFilter):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def apply_func(query, value, index):
|
||||
for item in query:
|
||||
tags = [tag for tag in item[index].split(',') if tag]
|
||||
if len(tags) != value:
|
||||
yield item
|
||||
|
||||
self. apply_func = apply_func
|
||||
|
||||
|
||||
class BookmarkTagNumberSmallerFilter(BookmarkBaseFilter):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def apply_func(query, value, index):
|
||||
for item in query:
|
||||
tags = [tag for tag in item[index].split(',') if tag]
|
||||
if len(tags) < value:
|
||||
yield item
|
||||
|
||||
self.apply_func = apply_func
|
||||
|
||||
def clean(self, value):
|
||||
value = int(value)
|
||||
if value < 1:
|
||||
raise ValueError
|
||||
return value
|
@ -1,18 +1,23 @@
|
||||
"""Forms module."""
|
||||
# pylint: disable=too-few-public-methods, missing-docstring
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, FieldList, BooleanField, validators
|
||||
import wtforms
|
||||
|
||||
|
||||
class SearchBookmarksForm(FlaskForm):
|
||||
keywords = FieldList(StringField('Keywords'), min_entries=1)
|
||||
all_keywords = BooleanField('Match all keywords')
|
||||
deep = BooleanField('Deep search')
|
||||
regex = BooleanField('Regex')
|
||||
keywords = wtforms.FieldList(wtforms.StringField('Keywords'), min_entries=1)
|
||||
all_keywords = wtforms.BooleanField('Match all keywords')
|
||||
deep = wtforms.BooleanField('Deep search')
|
||||
regex = wtforms.BooleanField('Regex')
|
||||
|
||||
|
||||
class CreateBookmarksForm(FlaskForm):
|
||||
url = StringField(validators=[validators.required(), validators.URL(require_tld=False)])
|
||||
title = StringField()
|
||||
tags = StringField()
|
||||
description = StringField()
|
||||
class HomeForm(SearchBookmarksForm):
|
||||
keyword = wtforms.StringField('Keyword')
|
||||
|
||||
|
||||
class BookmarkForm(FlaskForm):
|
||||
url = wtforms.StringField(
|
||||
validators=[wtforms.validators.required(), wtforms.validators.URL(require_tld=False)])
|
||||
title = wtforms.StringField()
|
||||
tags = wtforms.StringField()
|
||||
description = wtforms.TextAreaField()
|
||||
|
@ -2,12 +2,14 @@
|
||||
# pylint: disable=wrong-import-order, ungrouped-imports
|
||||
"""Server module."""
|
||||
import os
|
||||
import sys
|
||||
from collections import Counter
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from buku import BukuDb
|
||||
from buku import BukuDb, __version__, network_handler
|
||||
from flask.cli import FlaskGroup
|
||||
from flask_api import status
|
||||
from flask_admin import Admin
|
||||
from flask_api import exceptions, FlaskAPI, status
|
||||
from flask_bootstrap import Bootstrap
|
||||
from flask_paginate import Pagination, get_page_parameter, get_per_page_parameter
|
||||
from markupsafe import Markup
|
||||
@ -15,10 +17,10 @@ import arrow
|
||||
import click
|
||||
import flask
|
||||
from flask import (
|
||||
__version__ as flask_version,
|
||||
abort,
|
||||
current_app,
|
||||
flash,
|
||||
Flask,
|
||||
jsonify,
|
||||
redirect,
|
||||
render_template,
|
||||
@ -27,13 +29,11 @@ from flask import (
|
||||
)
|
||||
|
||||
try:
|
||||
from . import response, forms
|
||||
from . import response, forms, views
|
||||
except ImportError:
|
||||
from bukuserver import response, forms
|
||||
from bukuserver import response, forms, views
|
||||
|
||||
|
||||
DEFAULT_PER_PAGE = 10
|
||||
DEFAULT_URL_RENDER_MODE = 'full'
|
||||
STATISTIC_DATA = None
|
||||
|
||||
|
||||
@ -50,6 +50,46 @@ def get_tags():
|
||||
return res
|
||||
|
||||
|
||||
def network_handle_detail():
|
||||
failed_resp = response.response_template['failure'], status.HTTP_400_BAD_REQUEST
|
||||
url = request.data.get('url', None)
|
||||
if not url:
|
||||
return failed_resp
|
||||
try:
|
||||
res = network_handler(url)
|
||||
return {'title': res[0], 'recognized mime': res[1], 'bad url': res[2]}
|
||||
except Exception as e:
|
||||
current_app.logger.debug(str(e))
|
||||
return failed_resp
|
||||
|
||||
|
||||
def tag_list():
|
||||
tags = BukuDb().get_tag_all()
|
||||
result = {'tags': tags[0]}
|
||||
return result
|
||||
|
||||
|
||||
def tag_detail(tag):
|
||||
bukudb = BukuDb()
|
||||
if request.method == 'GET':
|
||||
tags = bukudb.get_tag_all()
|
||||
if tag not in tags[1]:
|
||||
raise exceptions.NotFound()
|
||||
res = dict(name=tag, usage_count=tags[1][tag])
|
||||
elif request.method == 'PUT':
|
||||
res = None
|
||||
try:
|
||||
new_tags = request.data.get('tags').split(',')
|
||||
except AttributeError as e:
|
||||
raise exceptions.ParseError(detail=str(e))
|
||||
result_flag = bukudb.replace_tag(tag, new_tags)
|
||||
if result_flag:
|
||||
res = response.response_template['success'], status.HTTP_200_OK
|
||||
else:
|
||||
res = response.response_template['failure'], status.HTTP_400_BAD_REQUEST
|
||||
return res
|
||||
|
||||
|
||||
def update_tag(tag):
|
||||
res = None
|
||||
if request.method in ('PUT', 'POST'):
|
||||
@ -71,11 +111,6 @@ def update_tag(tag):
|
||||
return res
|
||||
|
||||
|
||||
def chunks(l, n):
|
||||
n = max(1, n)
|
||||
return (l[i:i+n] for i in range(0, len(l), n))
|
||||
|
||||
|
||||
def bookmarks():
|
||||
"""Bookmarks."""
|
||||
res = None
|
||||
@ -85,10 +120,10 @@ def bookmarks():
|
||||
get_per_page_parameter(),
|
||||
type=int,
|
||||
default=int(
|
||||
current_app.config.get('BUKUSERVER_PER_PAGE', DEFAULT_PER_PAGE))
|
||||
current_app.config.get('BUKUSERVER_PER_PAGE', views.DEFAULT_PER_PAGE))
|
||||
)
|
||||
url_render_mode = current_app.config['BUKUSERVER_URL_RENDER_MODE']
|
||||
create_bookmarks_form = forms.CreateBookmarksForm()
|
||||
create_bookmarks_form = forms.BookmarkForm()
|
||||
if request.method == 'GET':
|
||||
all_bookmarks = bukudb.get_rec_all()
|
||||
result = {
|
||||
@ -116,7 +151,7 @@ def bookmarks():
|
||||
current_app.logger.debug('total bookmarks:{}'.format(len(result['bookmarks'])))
|
||||
current_app.logger.debug('per page:{}'.format(per_page))
|
||||
pagination_total = len(result['bookmarks'])
|
||||
bms = list(chunks(result['bookmarks'], per_page))
|
||||
bms = list(views.chunks(result['bookmarks'], per_page))
|
||||
try:
|
||||
result['bookmarks'] = bms[page-1]
|
||||
except IndexError as err:
|
||||
@ -191,7 +226,7 @@ def bookmark_api(id):
|
||||
return jsonify(response.response_template['failure']), status.HTTP_400_BAD_REQUEST, \
|
||||
{'ContentType': 'application/json'}
|
||||
bukudb = getattr(flask.g, 'bukudb', BukuDb())
|
||||
bookmark_form = forms.CreateBookmarksForm()
|
||||
bookmark_form = forms.BookmarkForm()
|
||||
is_html_post_request = request.method == 'POST' and not request.path.startswith('/api/')
|
||||
if request.method == 'GET':
|
||||
bookmark = bukudb.get_rec_by_id(id)
|
||||
@ -383,7 +418,7 @@ def search_bookmarks():
|
||||
get_per_page_parameter(),
|
||||
type=int,
|
||||
default=int(
|
||||
current_app.config.get('BUKUSERVER_PER_PAGE', DEFAULT_PER_PAGE))
|
||||
current_app.config.get('BUKUSERVER_PER_PAGE', views.DEFAULT_PER_PAGE))
|
||||
)
|
||||
|
||||
res = None
|
||||
@ -403,7 +438,7 @@ def search_bookmarks():
|
||||
res = jsonify(result)
|
||||
else:
|
||||
pagination_total = len(result['bookmarks'])
|
||||
bms = list(chunks(result['bookmarks'], per_page))
|
||||
bms = list(views.chunks(result['bookmarks'], per_page))
|
||||
try:
|
||||
result['bookmarks'] = bms[page-1]
|
||||
except IndexError as err:
|
||||
@ -418,7 +453,7 @@ def search_bookmarks():
|
||||
'bukuserver/bookmarks.html',
|
||||
result=result, pagination=pagination,
|
||||
search_bookmarks_form=search_bookmarks_form,
|
||||
create_bookmarks_form=forms.CreateBookmarksForm(),
|
||||
create_bookmarks_form=forms.BookmarkForm(),
|
||||
)
|
||||
elif request.method == 'DELETE':
|
||||
if found_bookmarks is not None:
|
||||
@ -520,13 +555,13 @@ def view_statistic():
|
||||
|
||||
def create_app(config_filename=None):
|
||||
"""create app."""
|
||||
app = Flask(__name__)
|
||||
per_page = int(os.getenv('BUKUSERVER_PER_PAGE', DEFAULT_PER_PAGE))
|
||||
per_page = per_page if per_page > 0 else DEFAULT_PER_PAGE
|
||||
app = FlaskAPI(__name__)
|
||||
per_page = int(os.getenv('BUKUSERVER_PER_PAGE', views.DEFAULT_PER_PAGE))
|
||||
per_page = per_page if per_page > 0 else views.DEFAULT_PER_PAGE
|
||||
app.config['BUKUSERVER_PER_PAGE'] = per_page
|
||||
url_render_mode = os.getenv('BUKUSERVER_URL_RENDER_MODE', DEFAULT_URL_RENDER_MODE)
|
||||
url_render_mode = os.getenv('BUKUSERVER_URL_RENDER_MODE', views.DEFAULT_URL_RENDER_MODE)
|
||||
if url_render_mode not in ('full', 'netloc'):
|
||||
url_render_mode = DEFAULT_URL_RENDER_MODE
|
||||
url_render_mode = views.DEFAULT_URL_RENDER_MODE
|
||||
app.config['BUKUSERVER_URL_RENDER_MODE'] = url_render_mode
|
||||
app.config['SECRET_KEY'] = os.getenv('BUKUSERVER_SECRET_KEY') or os.urandom(24)
|
||||
bukudb = BukuDb()
|
||||
@ -541,16 +576,20 @@ def create_app(config_filename=None):
|
||||
app.jinja_env.filters['netloc'] = lambda x: urlparse(x).netloc # pylint: disable=no-member
|
||||
|
||||
Bootstrap(app)
|
||||
admin = Admin(
|
||||
app, name='Buku Server', template_mode='bootstrap3',
|
||||
index_view=views.CustomAdminIndexView(
|
||||
template='bukuserver/home.html', url='/'
|
||||
)
|
||||
)
|
||||
# routing
|
||||
app.add_url_rule('/api/tags', 'get_tags', get_tags, methods=['GET'])
|
||||
app.add_url_rule('/tags', 'get_tags-html', get_tags, methods=['GET'])
|
||||
app.add_url_rule('/api/tags/<tag>', 'update_tag', update_tag, methods=['PUT'])
|
||||
app.add_url_rule('/tags/<tag>', 'update_tag-html', update_tag, methods=['POST'])
|
||||
# api
|
||||
app.add_url_rule('/api/tags', 'get_tags', tag_list, methods=['GET'])
|
||||
app.add_url_rule('/api/tags/<tag>', 'update_tag', tag_detail, methods=['GET', 'PUT'])
|
||||
app.add_url_rule('/api/network_handle', 'networkk_handle', network_handle_detail, methods=['POST'])
|
||||
app.add_url_rule('/api/bookmarks', 'bookmarks', bookmarks, methods=['GET', 'POST', 'DELETE'])
|
||||
app.add_url_rule('/bookmarks', 'bookmarks-html', bookmarks, methods=['GET', 'POST', 'DELETE'])
|
||||
app.add_url_rule('/api/bookmarks/refresh', 'refresh_bookmarks', refresh_bookmarks, methods=['POST'])
|
||||
app.add_url_rule('/api/bookmarks/<id>', 'bookmark_api', bookmark_api, methods=['GET', 'PUT', 'DELETE'])
|
||||
app.add_url_rule('/bookmarks/<id>', 'bookmark_api-html', bookmark_api, methods=['GET', 'POST'])
|
||||
app.add_url_rule('/api/bookmarks/<id>/refresh', 'refresh_bookmark', refresh_bookmark, methods=['POST'])
|
||||
app.add_url_rule('/api/bookmarks/<id>/tiny', 'get_tiny_url', get_tiny_url, methods=['GET'])
|
||||
app.add_url_rule('/api/bookmarks/<id>/long', 'get_long_url', get_long_url, methods=['GET'])
|
||||
@ -558,14 +597,36 @@ def create_app(config_filename=None):
|
||||
'/api/bookmarks/<starting_id>/<ending_id>',
|
||||
'bookmark_range_operations', bookmark_range_operations, methods=['GET', 'PUT', 'DELETE'])
|
||||
app.add_url_rule('/api/bookmarks/search', 'search_bookmarks', search_bookmarks, methods=['GET', 'DELETE'])
|
||||
app.add_url_rule('/bookmarks/search', 'search_bookmarks-html', search_bookmarks, methods=['GET'])
|
||||
app.add_url_rule('/', 'index', lambda: render_template(
|
||||
'bukuserver/index.html', search_bookmarks_form=forms.SearchBookmarksForm()))
|
||||
app.add_url_rule('/statistic', 'statistic', view_statistic, methods=['GET', 'POST'])
|
||||
# non api
|
||||
admin.add_view(views.BookmarkModelView(
|
||||
bukudb, 'Bookmarks', page_size=per_page, url_render_mode=url_render_mode))
|
||||
admin.add_view(views.TagModelView(
|
||||
bukudb, 'Tags', page_size=per_page))
|
||||
admin.add_view(views.StatisticView('Statistic', endpoint='statistic'))
|
||||
return app
|
||||
|
||||
|
||||
@click.group(cls=FlaskGroup, create_app=create_app)
|
||||
class CustomFlaskGroup(FlaskGroup): # pylint: disable=too-few-public-methods
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.params[0].help = 'Show the program version'
|
||||
self.params[0].callback = get_custom_version
|
||||
|
||||
|
||||
def get_custom_version(ctx, param, value):
|
||||
if not value or ctx.resilient_parsing:
|
||||
return
|
||||
message = '%(app_name)s %(app_version)s\nFlask %(version)s\nPython %(python_version)s'
|
||||
click.echo(message % {
|
||||
'app_name': 'Buku',
|
||||
'app_version': __version__,
|
||||
'version': flask_version,
|
||||
'python_version': sys.version,
|
||||
}, color=ctx.color)
|
||||
ctx.exit()
|
||||
|
||||
|
||||
@click.group(cls=CustomFlaskGroup, create_app=create_app)
|
||||
def cli():
|
||||
"""This is a management script for the wiki application."""
|
||||
|
||||
|
8
bukuserver/static/bukuserver/js/bookmark.js
Normal file
8
bukuserver/static/bukuserver/js/bookmark.js
Normal file
@ -0,0 +1,8 @@
|
||||
$(document).ready(function() {
|
||||
$.getJSON( "/api/tags", function( json ) {
|
||||
$('input#tags').select2({
|
||||
tags: json.tags,
|
||||
tokenSeparators: [','],
|
||||
});
|
||||
});
|
||||
});
|
@ -25,7 +25,7 @@
|
||||
<li><a href="{{url_for('index')}}">Home</a></li>
|
||||
<li><a href="{{url_for('bookmarks-html')}}">Bookmarks</a></li>
|
||||
<li><a href="{{url_for('get_tags-html')}}">Tags</a></li>
|
||||
<li><a href="{{url_for('statistic')}}">Statistics</a></li>
|
||||
<li><a href="{{url_for('statistic.index')}}">Statistics</a></li>
|
||||
</ul>
|
||||
<form class="navbar-form navbar-right" action="{{url_for('search_bookmarks-html')}}" method="GET">
|
||||
<div class="form-group">
|
||||
|
6
bukuserver/templates/bukuserver/bookmark_create.html
Normal file
6
bukuserver/templates/bukuserver/bookmark_create.html
Normal file
@ -0,0 +1,6 @@
|
||||
{% extends 'admin/model/create.html' %}
|
||||
|
||||
{% block tail %}
|
||||
{{ super() }}
|
||||
<script src="{{ url_for('static', filename='bukuserver/js/bookmark.js') }}"></script>
|
||||
{% endblock %}
|
@ -0,0 +1,6 @@
|
||||
{% extends 'admin/model/modals/create.html' %}
|
||||
|
||||
{% block tail %}
|
||||
{{ super() }}
|
||||
<script src="{{ url_for('static', filename='bukuserver/js/bookmark.js') }}"></script>
|
||||
{% endblock %}
|
@ -1,14 +1,6 @@
|
||||
{% extends "bukuserver/base.html" %}
|
||||
{% extends 'admin/model/edit.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1 style="padding-top: 70px;">Edit Bookmarks</h1>
|
||||
<form method="POST" action="{{url_for('bookmark_api-html', id=bookmark_id)}}">
|
||||
<div class="form-group"> {{ bookmark_form.title.label }} {{ bookmark_form.title(class_="form-control") }} </div>
|
||||
<div class="form-group"> {{ bookmark_form.url.label }} {{ bookmark_form.url(class_="form-control") }} </div>
|
||||
<div class="form-group"> {{ bookmark_form.tags.label }} {{ bookmark_form.tags(class_="form-control") }} </div>
|
||||
<div class="form-group"> {{ bookmark_form.description.label }} {{ bookmark_form.description(class_="form-control") }} </div>
|
||||
<button type="submit" class="btn btn-default">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
{% block tail %}
|
||||
{{ super() }}
|
||||
<script src="{{ url_for('static', filename='bukuserver/js/bookmark.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
6
bukuserver/templates/bukuserver/bookmark_edit_modal.html
Normal file
6
bukuserver/templates/bukuserver/bookmark_edit_modal.html
Normal file
@ -0,0 +1,6 @@
|
||||
{% extends 'admin/model/modals/edit.html' %}
|
||||
|
||||
{% block tail %}
|
||||
{{ super() }}
|
||||
<script src="{{ url_for('static', filename='bukuserver/js/bookmark.js') }}"></script>
|
||||
{% endblock %}
|
39
bukuserver/templates/bukuserver/home.html
Normal file
39
bukuserver/templates/bukuserver/home.html
Normal file
@ -0,0 +1,39 @@
|
||||
{% extends "admin/index.html" %}
|
||||
|
||||
{% block menu_links %}
|
||||
{{ super() }}
|
||||
<form class="navbar-form navbar-right" action="{{url_for('bookmark.index_view')}}" method="GET">
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" id="inputKeywords" placeholder="Search bookmark" name="flt1_buku_search">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-default">Search</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{{ super() }}
|
||||
<div class="container">
|
||||
<div style="padding: 40px 15px; text-align: center;">
|
||||
<h1>BUKU</h1>
|
||||
<p class="lead">Bookmark manager like a text-based mini-web</p>
|
||||
<p>
|
||||
<a class="btn btn-lg btn-success" href="{{url_for('bookmark.index_view')}}" role="button">Bookmarks</a>
|
||||
<a class="btn btn-lg btn-success" href="{{url_for('tag.index_view')}}" role="button">Tags</a>
|
||||
<a class="btn btn-lg btn-success" href="{{url_for('statistic.index')}}" role="button">Statistics</a>
|
||||
</p>
|
||||
<div class=" col-md-4 col-md-offset-4">
|
||||
<form class="form-horizontal" action="{{url_for('admin.search')}}" method="POST">
|
||||
<div class="form-group">
|
||||
{{form.keyword.label}}
|
||||
{{form.keyword()}}
|
||||
</div>
|
||||
<div class="text-left col-sm-offset-2">
|
||||
<div class="form-group"> {{form.deep()}} {{form.deep.label}} </div>
|
||||
<div class="form-group"> {{form.regex()}} {{form.regex.label}} </div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-default">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -8,7 +8,7 @@
|
||||
<p>
|
||||
<a class="btn btn-lg btn-success" href="{{url_for('bookmarks-html')}}" role="button">Bookmarks</a>
|
||||
<a class="btn btn-lg btn-success" href="{{url_for('get_tags-html')}}" role="button">Tags</a>
|
||||
<a class="btn btn-lg btn-success" href="{{url_for('statistic')}}" role="button">Statistics</a>
|
||||
<a class="btn btn-lg btn-success" href="{{url_for('statistic.index')}}" role="button">Statistics</a>
|
||||
</p>
|
||||
<div class=" col-md-4 col-md-offset-4">
|
||||
{{show_bookmarks_search_form(checkbox_class="text-left col-sm-offset-2")}}
|
||||
|
@ -1,9 +1,8 @@
|
||||
{% extends "bukuserver/base.html" %}
|
||||
{% extends "bukuserver/home.html" %}
|
||||
|
||||
{% block content %}
|
||||
{% block body %}
|
||||
<div class="container">
|
||||
<h2 style="padding-top: 70px;">Statistics</h2>
|
||||
<form class="form-inline" action="{{url_for('statistic')}}" method="POST">
|
||||
<form class="form-inline" action="{{url_for('statistic.index')}}" method="POST">
|
||||
Data created
|
||||
<span rel="tooltip" title="{{datetime}}">{{datetime_text}}</span>
|
||||
<button type="submit" class="btn btn-default btn-sm">refresh</button>
|
||||
@ -30,7 +29,7 @@
|
||||
{% for item, number, _ in most_common_netlocs %}
|
||||
<tr>
|
||||
<td>{{loop.index}}</td>
|
||||
<td> <a href="{{url_for('search_bookmarks-html')}}?keywords-0={{item}}">{{item}}</a> </td>
|
||||
<td> <a href="{{url_for('bookmark.index_view', flt1_url_netloc_match=item)}}">{{item}}</a> </td>
|
||||
<td class="text-right">{{number}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@ -60,8 +59,8 @@
|
||||
<tr>
|
||||
<td>{{loop.index}}</td>
|
||||
<td>
|
||||
{% if netloc %}
|
||||
<a href="{{url_for('search_bookmarks-html')}}?keywords-0={{item}}">{{item}}</a>
|
||||
{% if item %}
|
||||
<a href="{{url_for('bookmark.index_view', flt1_url_netloc_match=item)}}">{{item}}</a>
|
||||
{% else %}
|
||||
<span class="btn btn-default" disabled="disabled">(No Netloc)</span>
|
||||
{% endif %}
|
||||
@ -100,7 +99,7 @@
|
||||
<tr>
|
||||
<td>{{loop.index}}</td>
|
||||
<td>
|
||||
<a href="{{url_for('bookmarks-html', tag=item)}}">{{item}}</a>
|
||||
<a href="{{url_for('bookmark.index_view', flt3_tags_contain=item)}}">{{item}}</a>
|
||||
</td>
|
||||
<td class="text-right">{{number}}</td>
|
||||
</tr>
|
||||
@ -129,7 +128,7 @@
|
||||
{% for item, number in tag_counter.most_common() %}
|
||||
<tr>
|
||||
<td>{{loop.index}}</td>
|
||||
<td> <a href="{{url_for('bookmarks-html', tag=item)}}">{{item}}</a> </td>
|
||||
<td> <a href="{{url_for('bookmark.index_view', flt3_tags_contain=item)}}">{{item}}</a> </td>
|
||||
<td class="text-right">{{number}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@ -164,7 +163,7 @@
|
||||
<td>{{loop.index}}</td>
|
||||
<td>
|
||||
{% if item %}
|
||||
<a href="{{url_for('search_bookmarks-html')}}?keywords-0={{item}}">{{item}}</a>
|
||||
<a href="{{url_for('bookmark.index_view', flt0_title_equals=item)}}">{{item}}</a>
|
||||
{% else %}
|
||||
(No Title)
|
||||
{% endif %}
|
||||
@ -199,7 +198,7 @@
|
||||
<td>{{loop.index}}</td>
|
||||
<td style="word-break:break-all;">
|
||||
{% if item %}
|
||||
<a href="{{url_for('search_bookmarks-html')}}?keywords-0={{item}}">{{item}}</a>
|
||||
<a href="{{url_for('bookmark.index_view', flt0_title_equals=item)}}">{{item}}</a>
|
||||
{% else %}
|
||||
<span class="btn btn-default" disabled="disabled">(No Title)</span>
|
||||
{% endif %}
|
||||
@ -214,7 +213,11 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block tail %}
|
||||
{{ super() }}
|
||||
<script src="{{url_for('static', filename='bukuserver/js/Chart.min.js')}}"></script>
|
||||
<script>
|
||||
var ctx = document.getElementById("mostCommonChart").getContext('2d');
|
||||
var netlocChart = new Chart(ctx, {
|
||||
@ -239,12 +242,12 @@
|
||||
var form = $('<form></form>');
|
||||
|
||||
form.attr("method", "get");
|
||||
form.attr("action", "{{url_for('search_bookmarks-html')}}");
|
||||
form.attr("action", "{{url_for('bookmark.index_view')}}");
|
||||
|
||||
var field = $('<input></input>');
|
||||
|
||||
field.attr("type", "hidden");
|
||||
field.attr("name", "keywords-0");
|
||||
field.attr("name", "flt1_url_netloc_match");
|
||||
field.attr("value", value);
|
||||
form.append(field);
|
||||
|
||||
@ -276,7 +279,7 @@
|
||||
options: {
|
||||
'onClick' : function (evt, item) {
|
||||
var tagStr = this.data.labels[item[0]._index];
|
||||
var url = "{{url_for('bookmarks-html')}}?tag=" + tagStr;
|
||||
var url = "{{url_for('bookmark.index_view')}}?flt3_tags_contain=" + tagStr;
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
@ -305,12 +308,12 @@
|
||||
var form = $('<form></form>');
|
||||
|
||||
form.attr("method", "get");
|
||||
form.attr("action", "{{url_for('search_bookmarks-html')}}");
|
||||
form.attr("action", "{{url_for('bookmark.index_view')}}");
|
||||
|
||||
var field = $('<input></input>');
|
||||
|
||||
field.attr("type", "hidden");
|
||||
field.attr("name", "keywords-0");
|
||||
field.attr("name", "flt0_title_equals");
|
||||
field.attr("value", value);
|
||||
form.append(field);
|
||||
|
||||
@ -327,8 +330,3 @@
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
{{super()}}
|
||||
<script src="{{url_for('static', filename='bukuserver/js/Chart.min.js')}}"></script>
|
||||
{% endblock %}
|
||||
|
562
bukuserver/views.py
Normal file
562
bukuserver/views.py
Normal file
@ -0,0 +1,562 @@
|
||||
"""views module."""
|
||||
from collections import Counter
|
||||
from types import SimpleNamespace
|
||||
from urllib.parse import urlparse
|
||||
import itertools
|
||||
import logging
|
||||
|
||||
from buku import BukuDb
|
||||
from flask import flash, redirect, request, url_for
|
||||
from flask_admin.babel import gettext
|
||||
from flask_admin.base import AdminIndexView, BaseView, expose
|
||||
from flask_admin.model import BaseModelView
|
||||
from flask_wtf import FlaskForm
|
||||
from jinja2 import Markup
|
||||
import arrow
|
||||
import wtforms
|
||||
|
||||
try:
|
||||
from . import forms, filters as bs_filters
|
||||
from .filters import BookmarkField, FilterType
|
||||
except ImportError:
|
||||
from bukuserver import forms, filters as bs_filters
|
||||
from bukuserver.filters import BookmarkField, FilterType
|
||||
|
||||
|
||||
STATISTIC_DATA = None
|
||||
DEFAULT_URL_RENDER_MODE = 'full'
|
||||
DEFAULT_PER_PAGE = 10
|
||||
log = logging.getLogger("bukuserver.views")
|
||||
|
||||
|
||||
class CustomAdminIndexView(AdminIndexView):
|
||||
|
||||
@expose('/')
|
||||
def index(self):
|
||||
return self.render('bukuserver/home.html', form=forms.HomeForm())
|
||||
|
||||
@expose('/', methods=['POST',])
|
||||
def search(self):
|
||||
"redirect to bookmark search"
|
||||
form = forms.HomeForm()
|
||||
bbm_filter = bs_filters.BookmarkBukuFilter(
|
||||
all_keywords=False, deep=form.deep.data, regex=form.regex.data)
|
||||
op_text = bbm_filter.operation()
|
||||
values_combi = sorted(itertools.product([True, False], repeat=3))
|
||||
for idx, (all_keywords, deep, regex) in enumerate(values_combi):
|
||||
if deep == form.deep.data and regex == form.regex.data and not all_keywords:
|
||||
choosen_idx = idx
|
||||
url_op_text = op_text.replace(', ', '_').replace(' ', ' ').replace(' ', '_')
|
||||
key = ''.join(['flt', str(choosen_idx), '_buku_', url_op_text])
|
||||
kwargs = {key: form.keyword.data}
|
||||
url = url_for('bookmark.index_view', **kwargs)
|
||||
return redirect(url)
|
||||
|
||||
|
||||
class CustomBukuDbModel: # pylint: disable=too-few-public-methods
|
||||
|
||||
def __init__(self, bukudb_inst, name):
|
||||
self.bukudb = bukudb_inst
|
||||
self.name = name
|
||||
|
||||
@property
|
||||
def __name__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class BookmarkModelView(BaseModelView):
|
||||
|
||||
def _apply_filters(self, models, filters):
|
||||
for idx, flt_name, value in filters:
|
||||
flt = self._filters[idx]
|
||||
clean_value = flt.clean(value)
|
||||
models = list(flt.apply(models, clean_value))
|
||||
return models
|
||||
|
||||
def _create_ajax_loader(self, name, options):
|
||||
pass
|
||||
|
||||
def _list_entry(self, context, model, name):
|
||||
parsed_url = urlparse(model.url)
|
||||
netloc, scheme = parsed_url.netloc, parsed_url.scheme
|
||||
is_scheme_valid = scheme in ('http', 'https')
|
||||
tag_text = []
|
||||
tag_tmpl = '<a class="btn btn-default" href="#">{0}</a>'
|
||||
for tag in model.tags.split(','):
|
||||
if tag:
|
||||
tag_text.append(tag_tmpl.format(tag))
|
||||
if not netloc:
|
||||
return Markup("""\
|
||||
{0.title}<br/>{2}<br/>{1}{0.description}
|
||||
""".format(
|
||||
model, ''.join(tag_text), Markup.escape(model.url)
|
||||
))
|
||||
netloc_tmpl = '<img src="{}{}"/> '
|
||||
res = netloc_tmpl.format(
|
||||
'http://www.google.com/s2/favicons?domain=', netloc)
|
||||
title = model.title if model.title else '<EMPTY TITLE>'
|
||||
if is_scheme_valid:
|
||||
res += '<a href="{0.url}">{1}</a>'.format(model, title)
|
||||
else:
|
||||
res += title
|
||||
if self.url_render_mode == 'netloc':
|
||||
res += ' ({})'.format(netloc)
|
||||
res += '<br/>'
|
||||
if not is_scheme_valid:
|
||||
res += model.url
|
||||
elif self.url_render_mode is None or self.url_render_mode == 'full':
|
||||
res += '<a href="{0.url}">{0.url}</a>'.format(model)
|
||||
res += '<br/>'
|
||||
res += ''.join(tag_text)
|
||||
description = model.description
|
||||
if description:
|
||||
res += '<br/>'
|
||||
res += description.replace('\n', '<br/>')
|
||||
return Markup(res)
|
||||
|
||||
can_set_page_size = True
|
||||
can_view_details = True
|
||||
column_filters = ['buku', 'id', 'url', 'title', 'tags']
|
||||
column_formatters = {'Entry': _list_entry,}
|
||||
column_list = ['Entry']
|
||||
create_modal = True
|
||||
create_modal_template = 'bukuserver/bookmark_create_modal.html'
|
||||
create_template = 'bukuserver/bookmark_create.html'
|
||||
details_modal = True
|
||||
edit_modal = True
|
||||
edit_modal_template = 'bukuserver/bookmark_edit_modal.html'
|
||||
edit_template = 'bukuserver/bookmark_edit.html'
|
||||
named_filter_urls = True
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.bukudb = args[0]
|
||||
custom_model = CustomBukuDbModel(args[0], 'bookmark')
|
||||
args = [custom_model, ] + list(args[1:])
|
||||
self.page_size = kwargs.pop('page_size', DEFAULT_PER_PAGE)
|
||||
self.url_render_mode = kwargs.pop('url_render_mode', DEFAULT_URL_RENDER_MODE)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def create_model(self, form):
|
||||
try:
|
||||
model = SimpleNamespace(id=None, url=None, title=None, tags=None, description=None)
|
||||
form.populate_obj(model)
|
||||
vars(model).pop('id')
|
||||
self._on_model_change(form, model, True)
|
||||
tags_in = model.tags
|
||||
if not tags_in.startswith(','):
|
||||
tags_in = ',{}'.format(tags_in)
|
||||
if not tags_in.endswith(','):
|
||||
tags_in = '{},'.format(tags_in)
|
||||
self.model.bukudb.add_rec(
|
||||
url=model.url, title_in=model.title, tags_in=tags_in, desc=model.description)
|
||||
except Exception as ex:
|
||||
if not self.handle_view_exception(ex):
|
||||
flash(gettext('Failed to create record. %(error)s', error=str(ex)), 'error')
|
||||
log.exception('Failed to create record.')
|
||||
return False
|
||||
else:
|
||||
self.after_model_change(form, model, True)
|
||||
return model
|
||||
|
||||
def delete_model(self, model):
|
||||
try:
|
||||
self.on_model_delete(model)
|
||||
res = self.bukudb.delete_rec(model.id)
|
||||
except Exception as ex:
|
||||
if not self.handle_view_exception(ex):
|
||||
flash(gettext('Failed to delete record. %(error)s', error=str(ex)), 'error')
|
||||
log.exception('Failed to delete record.')
|
||||
return False
|
||||
else:
|
||||
self.after_model_delete(model)
|
||||
return res
|
||||
|
||||
def get_list(self, page, sort_field, sort_desc, search, filters, page_size=None):
|
||||
bukudb = self.bukudb
|
||||
contain_buku_search = any(x[1] == 'buku' for x in filters)
|
||||
if contain_buku_search:
|
||||
mode_id = [x[0] for x in filters]
|
||||
if len(list(set(mode_id))) > 1:
|
||||
flash(gettext('Invalid search mode combination'), 'error')
|
||||
return 0, []
|
||||
keywords = [x[2] for x in filters]
|
||||
for idx, flt_name, value in filters:
|
||||
if flt_name == 'buku':
|
||||
flt = self._filters[idx]
|
||||
bookmarks = bukudb.searchdb(
|
||||
keywords, all_keywords=flt.all_keywords, deep=flt.deep, regex=flt.regex)
|
||||
else:
|
||||
bookmarks = bukudb.get_rec_all()
|
||||
bookmarks = self._apply_filters(bookmarks, filters)
|
||||
if sort_field:
|
||||
key_idx = [x.value for x in BookmarkField if x.name.lower() == sort_field][0]
|
||||
bookmarks = sorted(bookmarks, key=lambda x:x[key_idx], reverse=sort_desc)
|
||||
count = len(bookmarks)
|
||||
if page_size and bookmarks:
|
||||
bookmarks = list(chunks(bookmarks, page_size))[page]
|
||||
data = []
|
||||
for bookmark in bookmarks:
|
||||
bm_sns = SimpleNamespace(id=None, url=None, title=None, tags=None, description=None)
|
||||
for field in list(BookmarkField):
|
||||
if field == BookmarkField.TAGS:
|
||||
value = bookmark[field.value]
|
||||
if value.startswith(','):
|
||||
value = value[1:]
|
||||
if value.endswith(','):
|
||||
value = value[:-1]
|
||||
setattr(bm_sns, field.name.lower(), value)
|
||||
else:
|
||||
setattr(bm_sns, field.name.lower(), bookmark[field.value])
|
||||
data.append(bm_sns)
|
||||
return count, data
|
||||
|
||||
def get_one(self, id):
|
||||
bookmark = self.model.bukudb.get_rec_by_id(id)
|
||||
bm_sns = SimpleNamespace(id=None, url=None, title=None, tags=None, description=None)
|
||||
for field in list(BookmarkField):
|
||||
if field == BookmarkField.TAGS and bookmark[field.value].startswith(','):
|
||||
value = bookmark[field.value]
|
||||
if value.startswith(','):
|
||||
value = value[1:]
|
||||
if value.endswith(','):
|
||||
value = value[:-1]
|
||||
setattr(bm_sns, field.name.lower(), value)
|
||||
else:
|
||||
setattr(bm_sns, field.name.lower(), bookmark[field.value])
|
||||
return bm_sns
|
||||
|
||||
def get_pk_value(self, model):
|
||||
return model.id
|
||||
|
||||
def scaffold_list_columns(self):
|
||||
return [x.name.lower() for x in BookmarkField]
|
||||
|
||||
def scaffold_list_form(self, widget=None, validators=None):
|
||||
pass
|
||||
|
||||
def scaffold_sortable_columns(self):
|
||||
return {x:x for x in self.scaffold_list_columns()}
|
||||
|
||||
def scaffold_filters(self, name):
|
||||
res = []
|
||||
if name == 'buku':
|
||||
values_combi = sorted(itertools.product([True, False], repeat=3))
|
||||
for all_keywords, deep, regex in values_combi:
|
||||
res.append(
|
||||
bs_filters.BookmarkBukuFilter(all_keywords=all_keywords, deep=deep, regex=regex)
|
||||
)
|
||||
elif name == BookmarkField.ID.name.lower():
|
||||
res.extend([
|
||||
bs_filters.BookmarkBaseFilter(name, filter_type=FilterType.EQUAL),
|
||||
bs_filters.BookmarkBaseFilter(name, filter_type=FilterType.NOT_EQUAL),
|
||||
bs_filters.BookmarkBaseFilter(name, filter_type=FilterType.IN_LIST),
|
||||
bs_filters.BookmarkBaseFilter(name, filter_type=FilterType.NOT_IN_LIST),
|
||||
bs_filters.BookmarkBaseFilter(name, filter_type=FilterType.GREATER),
|
||||
bs_filters.BookmarkBaseFilter(name, filter_type=FilterType.SMALLER),
|
||||
bs_filters.BookmarkBaseFilter(name, filter_type=FilterType.TOP_X),
|
||||
bs_filters.BookmarkBaseFilter(name, filter_type=FilterType.BOTTOM_X),
|
||||
])
|
||||
elif name == BookmarkField.URL.name.lower():
|
||||
def netloc_match_func(query, value, index):
|
||||
return filter(lambda x: urlparse(x[index]).netloc == value, query)
|
||||
|
||||
res.extend([
|
||||
bs_filters.BookmarkBaseFilter(name, 'netloc match', netloc_match_func),
|
||||
bs_filters.BookmarkBaseFilter(name, filter_type=FilterType.EQUAL),
|
||||
bs_filters.BookmarkBaseFilter(name, filter_type=FilterType.NOT_EQUAL),
|
||||
bs_filters.BookmarkBaseFilter(name, filter_type=FilterType.IN_LIST),
|
||||
bs_filters.BookmarkBaseFilter(name, filter_type=FilterType.NOT_IN_LIST),
|
||||
])
|
||||
elif name == BookmarkField.TITLE.name.lower():
|
||||
res.extend([
|
||||
bs_filters.BookmarkBaseFilter(name, filter_type=FilterType.EQUAL),
|
||||
bs_filters.BookmarkBaseFilter(name, filter_type=FilterType.NOT_EQUAL),
|
||||
bs_filters.BookmarkBaseFilter(name, filter_type=FilterType.IN_LIST),
|
||||
bs_filters.BookmarkBaseFilter(name, filter_type=FilterType.NOT_IN_LIST),
|
||||
])
|
||||
elif name == BookmarkField.TAGS.name.lower():
|
||||
def tags_contain_func(query, value, index):
|
||||
for item in query:
|
||||
for tag in item[index].split(','):
|
||||
if tag and tag == value:
|
||||
yield item
|
||||
|
||||
def tags_not_contain_func(query, value, index):
|
||||
for item in query:
|
||||
for tag in item[index].split(','):
|
||||
if tag and tag == value:
|
||||
yield item
|
||||
|
||||
res.extend([
|
||||
bs_filters.BookmarkBaseFilter(name, 'contain', tags_contain_func),
|
||||
bs_filters.BookmarkBaseFilter(name, 'not contain', tags_not_contain_func),
|
||||
bs_filters.BookmarkTagNumberEqualFilter(name, 'number equal'),
|
||||
bs_filters.BookmarkTagNumberNotEqualFilter(name, 'number not equal'),
|
||||
bs_filters.BookmarkTagNumberGreaterFilter(name, 'number greater than'),
|
||||
bs_filters.BookmarkTagNumberSmallerFilter(name, 'number smaller than'),
|
||||
])
|
||||
elif name in self.scaffold_list_columns():
|
||||
pass
|
||||
else:
|
||||
return super().scaffold_filters(name)
|
||||
return res
|
||||
|
||||
def scaffold_form(self):
|
||||
cls = forms.BookmarkForm
|
||||
return cls
|
||||
|
||||
def update_model(self, form, model):
|
||||
res = False
|
||||
try:
|
||||
original_tags = model.tags
|
||||
form.populate_obj(model)
|
||||
self._on_model_change(form, model, False)
|
||||
self.bukudb.delete_tag_at_index(model.id, original_tags)
|
||||
tags_in = model.tags
|
||||
if not tags_in.startswith(','):
|
||||
tags_in = ',{}'.format(tags_in)
|
||||
if not tags_in.endswith(','):
|
||||
tags_in = '{},'.format(tags_in)
|
||||
res = self.bukudb.update_rec(
|
||||
model.id, url=model.url, title_in=model.title, tags_in=tags_in,
|
||||
desc=model.description)
|
||||
except Exception as ex:
|
||||
if not self.handle_view_exception(ex):
|
||||
flash(gettext('Failed to update record. %(error)s', error=str(ex)), 'error')
|
||||
log.exception('Failed to update record.')
|
||||
return False
|
||||
else:
|
||||
self.after_model_change(form, model, False)
|
||||
return res
|
||||
|
||||
|
||||
class TagModelView(BaseModelView):
|
||||
|
||||
def _create_ajax_loader(self, name, options):
|
||||
pass
|
||||
|
||||
def _apply_filters(self, models, filters):
|
||||
for idx, flt_name, value in filters:
|
||||
flt = self._filters[idx]
|
||||
clean_value = flt.clean(value)
|
||||
models = list(flt.apply(models, clean_value))
|
||||
return models
|
||||
|
||||
def _name_formatter(self, context, model, name):
|
||||
data = getattr(model, name)
|
||||
return Markup('<a href="{}">{}</a>'.format(
|
||||
url_for('bookmark.index_view', flt1_tags_contain=data),
|
||||
data if data else '<EMPTY TAG>'
|
||||
))
|
||||
|
||||
can_create = False
|
||||
can_set_page_size = True
|
||||
column_filters = ['name', 'usage_count']
|
||||
column_formatters = {'name': _name_formatter,}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.bukudb = args[0]
|
||||
custom_model = CustomBukuDbModel(args[0], 'tag')
|
||||
args = [custom_model, ] + list(args[1:])
|
||||
self.page_size = kwargs.pop('page_size', DEFAULT_PER_PAGE)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def scaffold_list_columns(self):
|
||||
return ['name', 'usage_count']
|
||||
|
||||
def scaffold_sortable_columns(self):
|
||||
return {x:x for x in self.scaffold_list_columns()}
|
||||
|
||||
def scaffold_form(self):
|
||||
class CustomForm(FlaskForm): # pylint: disable=too-few-public-methods
|
||||
name = wtforms.StringField(validators=[wtforms.validators.required()])
|
||||
|
||||
return CustomForm
|
||||
|
||||
def scaffold_list_form(self, widget=None, validators=None):
|
||||
pass
|
||||
|
||||
def get_list(self, page, sort_field, sort_desc, search, filters, page_size=None):
|
||||
bukudb = self.bukudb
|
||||
tags = bukudb.get_tag_all()[1]
|
||||
tags = [(x, y) for x, y in tags.items()]
|
||||
tags = self._apply_filters(tags, filters)
|
||||
if sort_field == 'usage_count':
|
||||
tags = sorted(tags, key=lambda x: x[1], reverse=sort_desc)
|
||||
elif sort_field == 'name':
|
||||
tags = sorted(tags, key=lambda x: x[0], reverse=sort_desc)
|
||||
tags = list(tags)
|
||||
count = len(tags)
|
||||
if page_size and tags:
|
||||
tags = list(chunks(tags, page_size))[page]
|
||||
data = []
|
||||
for name, usage_count in tags:
|
||||
tag_sns = SimpleNamespace(name=None, usage_count=None)
|
||||
tag_sns.name, tag_sns.usage_count = name, usage_count
|
||||
data.append(tag_sns)
|
||||
return count, data
|
||||
|
||||
def get_pk_value(self, model):
|
||||
return model.name
|
||||
|
||||
def get_one(self, id):
|
||||
tags = self.bukudb.get_tag_all()[1]
|
||||
tag_sns = SimpleNamespace(name=id, usage_count=tags[id])
|
||||
return tag_sns
|
||||
|
||||
def scaffold_filters(self, name):
|
||||
res = []
|
||||
|
||||
def top_most_common_func(query, value, index):
|
||||
counter = Counter(x[index] for x in query)
|
||||
most_common = counter.most_common(value)
|
||||
most_common_item = [x[0] for x in most_common]
|
||||
return filter(lambda x: x[index] in most_common_item, query)
|
||||
|
||||
res.extend([
|
||||
bs_filters.TagBaseFilter(name, filter_type=FilterType.EQUAL),
|
||||
bs_filters.TagBaseFilter(name, filter_type=FilterType.NOT_EQUAL),
|
||||
bs_filters.TagBaseFilter(name, filter_type=FilterType.IN_LIST),
|
||||
bs_filters.TagBaseFilter(name, filter_type=FilterType.NOT_IN_LIST),
|
||||
])
|
||||
if name == 'usage_count':
|
||||
res.extend([
|
||||
bs_filters.TagBaseFilter(name, filter_type=FilterType.GREATER),
|
||||
bs_filters.TagBaseFilter(name, filter_type=FilterType.SMALLER),
|
||||
bs_filters.TagBaseFilter(name, filter_type=FilterType.TOP_X),
|
||||
bs_filters.TagBaseFilter(name, filter_type=FilterType.BOTTOM_X),
|
||||
bs_filters.TagBaseFilter(name, 'top most common', top_most_common_func),
|
||||
])
|
||||
elif name == 'name':
|
||||
pass
|
||||
else:
|
||||
return super().scaffold_filters(name)
|
||||
return res
|
||||
|
||||
def delete_model(self, model):
|
||||
res = None
|
||||
try:
|
||||
self.on_model_delete(model)
|
||||
res = self.bukudb.delete_tag_at_index(0, model.name, chatty=False)
|
||||
except Exception as ex:
|
||||
if not self.handle_view_exception(ex):
|
||||
flash(gettext('Failed to delete record. %(error)s', error=str(ex)), 'error')
|
||||
log.exception('Failed to delete record.')
|
||||
return False
|
||||
else:
|
||||
self.after_model_delete(model)
|
||||
return res
|
||||
|
||||
def update_model(self, form, model):
|
||||
res = None
|
||||
try:
|
||||
original_name = model.name
|
||||
form.populate_obj(model)
|
||||
self._on_model_change(form, model, False)
|
||||
res = self.bukudb.replace_tag(original_name, [model.name])
|
||||
except Exception as ex:
|
||||
if not self.handle_view_exception(ex):
|
||||
flash(gettext('Failed to update record. %(error)s', error=str(ex)), 'error')
|
||||
log.exception('Failed to update record.')
|
||||
return False
|
||||
else:
|
||||
self.after_model_change(form, model, False)
|
||||
return res
|
||||
|
||||
def create_model(self, form):
|
||||
pass
|
||||
|
||||
|
||||
class StatisticView(BaseView): # pylint: disable=too-few-public-methods
|
||||
|
||||
@expose('/')
|
||||
def index(self):
|
||||
bukudb = BukuDb()
|
||||
global STATISTIC_DATA
|
||||
statistic_data = STATISTIC_DATA
|
||||
if not statistic_data or request.method == 'POST':
|
||||
all_bookmarks = bukudb.get_rec_all()
|
||||
netloc = [urlparse(x[1]).netloc for x in all_bookmarks]
|
||||
tag_set = [x[3] for x in all_bookmarks]
|
||||
tag_items = []
|
||||
for tags in tag_set:
|
||||
tag_items.extend([x.strip() for x in tags.split(',') if x.strip()])
|
||||
tag_counter = Counter(tag_items)
|
||||
title_items = [x[2] for x in all_bookmarks]
|
||||
title_counter = Counter(title_items)
|
||||
statistic_datetime = arrow.now()
|
||||
STATISTIC_DATA = {
|
||||
'datetime': statistic_datetime,
|
||||
'netloc': netloc,
|
||||
'tag_counter': tag_counter,
|
||||
'title_counter': title_counter,
|
||||
}
|
||||
else:
|
||||
netloc = statistic_data['netloc']
|
||||
statistic_datetime = statistic_data['datetime']
|
||||
tag_counter = statistic_data['tag_counter']
|
||||
title_counter = statistic_data['title_counter']
|
||||
|
||||
netloc_counter = Counter(netloc)
|
||||
unique_netloc_len = len(set(netloc))
|
||||
colors = [
|
||||
"#F7464A", "#46BFBD", "#FDB45C", "#FEDCBA",
|
||||
"#ABCDEF", "#DDDDDD", "#ABCABC", "#4169E1",
|
||||
"#C71585", "#FF4500", "#FEDCBA", "#46BFBD"]
|
||||
show_netloc_table = False
|
||||
if unique_netloc_len > len(colors):
|
||||
max_netloc_item = len(colors)
|
||||
netloc_colors = colors
|
||||
show_netloc_table = True
|
||||
else:
|
||||
netloc_colors = colors[:unique_netloc_len]
|
||||
max_netloc_item = unique_netloc_len
|
||||
most_common_netlocs = netloc_counter.most_common(max_netloc_item)
|
||||
most_common_netlocs = [
|
||||
[val[0], val[1], netloc_colors[idx]] for idx, val in enumerate(most_common_netlocs)]
|
||||
|
||||
unique_tag_len = len(tag_counter)
|
||||
show_tag_rank_table = False
|
||||
if unique_tag_len > len(colors):
|
||||
max_tag_item = len(colors)
|
||||
tag_colors = colors
|
||||
show_tag_rank_table = True
|
||||
else:
|
||||
tag_colors = colors[:unique_tag_len]
|
||||
max_tag_item = unique_tag_len
|
||||
most_common_tags = tag_counter.most_common(max_tag_item)
|
||||
most_common_tags = [
|
||||
[val[0], val[1], tag_colors[idx]] for idx, val in enumerate(most_common_tags)]
|
||||
|
||||
unique_title_len = len(title_counter)
|
||||
show_title_rank_table = False
|
||||
if unique_title_len > len(colors):
|
||||
max_title_item = len(colors)
|
||||
title_colors = colors
|
||||
show_title_rank_table = True
|
||||
else:
|
||||
title_colors = colors[:unique_title_len]
|
||||
max_title_item = unique_title_len
|
||||
most_common_titles = title_counter.most_common(max_title_item)
|
||||
most_common_titles = [
|
||||
[val[0], val[1], title_colors[idx]] for idx, val in enumerate(most_common_titles)]
|
||||
|
||||
return self.render(
|
||||
'bukuserver/statistic.html',
|
||||
most_common_netlocs=most_common_netlocs,
|
||||
netloc_counter=netloc_counter,
|
||||
show_netloc_table=show_netloc_table,
|
||||
most_common_tags=most_common_tags,
|
||||
tag_counter=tag_counter,
|
||||
show_tag_rank_table=show_tag_rank_table,
|
||||
most_common_titles=most_common_titles,
|
||||
title_counter=title_counter,
|
||||
show_title_rank_table=show_title_rank_table,
|
||||
datetime=statistic_datetime,
|
||||
datetime_text=statistic_datetime.humanize(arrow.now(), granularity='second'),
|
||||
)
|
||||
|
||||
|
||||
def chunks(l, n):
|
||||
n = max(1, n)
|
||||
return (l[i:i+n] for i in range(0, len(l), n))
|
Loading…
x
Reference in New Issue
Block a user