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:
rachmadani haryono 2018-06-28 22:04:35 +08:00 committed by Arun Prakash Jana
parent c3d38cb17b
commit be50451d1d
15 changed files with 1007 additions and 85 deletions

11
buku.py
View File

@ -697,7 +697,7 @@ class BukuDb:
return True 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. """Delete tags from bookmark tagset at index.
Parameters Parameters
@ -709,6 +709,8 @@ class BukuDb:
delay_commit : bool, optional delay_commit : bool, optional
True if record should not be committed to the DB, True if record should not be committed to the DB,
leaving commit responsibility to caller. Default is False. leaving commit responsibility to caller. Default is False.
chatty: bool, optional
Skip confirmation when set to False.
Returns Returns
------- -------
@ -719,9 +721,10 @@ class BukuDb:
tags_to_delete = tags_in.strip(DELIM).split(DELIM) tags_to_delete = tags_in.strip(DELIM).split(DELIM)
if index == 0: if index == 0:
resp = read_in('Delete the tag(s) from ALL bookmarks? (y/n): ') if chatty:
if resp != 'y': resp = read_in('Delete the tag(s) from ALL bookmarks? (y/n): ')
return False if resp != 'y':
return False
count = 0 count = 0
match = "'%' || ? || '%'" match = "'%' || ? || '%'"

235
bukuserver/filters.py Normal file
View 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

View File

@ -1,18 +1,23 @@
"""Forms module.""" """Forms module."""
# pylint: disable=too-few-public-methods, missing-docstring # pylint: disable=too-few-public-methods, missing-docstring
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField, FieldList, BooleanField, validators import wtforms
class SearchBookmarksForm(FlaskForm): class SearchBookmarksForm(FlaskForm):
keywords = FieldList(StringField('Keywords'), min_entries=1) keywords = wtforms.FieldList(wtforms.StringField('Keywords'), min_entries=1)
all_keywords = BooleanField('Match all keywords') all_keywords = wtforms.BooleanField('Match all keywords')
deep = BooleanField('Deep search') deep = wtforms.BooleanField('Deep search')
regex = BooleanField('Regex') regex = wtforms.BooleanField('Regex')
class CreateBookmarksForm(FlaskForm): class HomeForm(SearchBookmarksForm):
url = StringField(validators=[validators.required(), validators.URL(require_tld=False)]) keyword = wtforms.StringField('Keyword')
title = StringField()
tags = StringField()
description = StringField() 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()

View File

@ -2,12 +2,14 @@
# pylint: disable=wrong-import-order, ungrouped-imports # pylint: disable=wrong-import-order, ungrouped-imports
"""Server module.""" """Server module."""
import os import os
import sys
from collections import Counter from collections import Counter
from urllib.parse import urlparse from urllib.parse import urlparse
from buku import BukuDb from buku import BukuDb, __version__, network_handler
from flask.cli import FlaskGroup 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_bootstrap import Bootstrap
from flask_paginate import Pagination, get_page_parameter, get_per_page_parameter from flask_paginate import Pagination, get_page_parameter, get_per_page_parameter
from markupsafe import Markup from markupsafe import Markup
@ -15,10 +17,10 @@ import arrow
import click import click
import flask import flask
from flask import ( from flask import (
__version__ as flask_version,
abort, abort,
current_app, current_app,
flash, flash,
Flask,
jsonify, jsonify,
redirect, redirect,
render_template, render_template,
@ -27,13 +29,11 @@ from flask import (
) )
try: try:
from . import response, forms from . import response, forms, views
except ImportError: 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 STATISTIC_DATA = None
@ -50,6 +50,46 @@ def get_tags():
return res 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): def update_tag(tag):
res = None res = None
if request.method in ('PUT', 'POST'): if request.method in ('PUT', 'POST'):
@ -71,11 +111,6 @@ def update_tag(tag):
return res 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(): def bookmarks():
"""Bookmarks.""" """Bookmarks."""
res = None res = None
@ -85,10 +120,10 @@ def bookmarks():
get_per_page_parameter(), get_per_page_parameter(),
type=int, type=int,
default=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'] url_render_mode = current_app.config['BUKUSERVER_URL_RENDER_MODE']
create_bookmarks_form = forms.CreateBookmarksForm() create_bookmarks_form = forms.BookmarkForm()
if request.method == 'GET': if request.method == 'GET':
all_bookmarks = bukudb.get_rec_all() all_bookmarks = bukudb.get_rec_all()
result = { result = {
@ -116,7 +151,7 @@ def bookmarks():
current_app.logger.debug('total bookmarks:{}'.format(len(result['bookmarks']))) current_app.logger.debug('total bookmarks:{}'.format(len(result['bookmarks'])))
current_app.logger.debug('per page:{}'.format(per_page)) current_app.logger.debug('per page:{}'.format(per_page))
pagination_total = len(result['bookmarks']) pagination_total = len(result['bookmarks'])
bms = list(chunks(result['bookmarks'], per_page)) bms = list(views.chunks(result['bookmarks'], per_page))
try: try:
result['bookmarks'] = bms[page-1] result['bookmarks'] = bms[page-1]
except IndexError as err: except IndexError as err:
@ -191,7 +226,7 @@ def bookmark_api(id):
return jsonify(response.response_template['failure']), status.HTTP_400_BAD_REQUEST, \ return jsonify(response.response_template['failure']), status.HTTP_400_BAD_REQUEST, \
{'ContentType': 'application/json'} {'ContentType': 'application/json'}
bukudb = getattr(flask.g, 'bukudb', BukuDb()) 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/') is_html_post_request = request.method == 'POST' and not request.path.startswith('/api/')
if request.method == 'GET': if request.method == 'GET':
bookmark = bukudb.get_rec_by_id(id) bookmark = bukudb.get_rec_by_id(id)
@ -383,7 +418,7 @@ def search_bookmarks():
get_per_page_parameter(), get_per_page_parameter(),
type=int, type=int,
default=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 res = None
@ -403,7 +438,7 @@ def search_bookmarks():
res = jsonify(result) res = jsonify(result)
else: else:
pagination_total = len(result['bookmarks']) pagination_total = len(result['bookmarks'])
bms = list(chunks(result['bookmarks'], per_page)) bms = list(views.chunks(result['bookmarks'], per_page))
try: try:
result['bookmarks'] = bms[page-1] result['bookmarks'] = bms[page-1]
except IndexError as err: except IndexError as err:
@ -418,7 +453,7 @@ def search_bookmarks():
'bukuserver/bookmarks.html', 'bukuserver/bookmarks.html',
result=result, pagination=pagination, result=result, pagination=pagination,
search_bookmarks_form=search_bookmarks_form, search_bookmarks_form=search_bookmarks_form,
create_bookmarks_form=forms.CreateBookmarksForm(), create_bookmarks_form=forms.BookmarkForm(),
) )
elif request.method == 'DELETE': elif request.method == 'DELETE':
if found_bookmarks is not None: if found_bookmarks is not None:
@ -520,13 +555,13 @@ def view_statistic():
def create_app(config_filename=None): def create_app(config_filename=None):
"""create app.""" """create app."""
app = Flask(__name__) app = FlaskAPI(__name__)
per_page = int(os.getenv('BUKUSERVER_PER_PAGE', DEFAULT_PER_PAGE)) per_page = int(os.getenv('BUKUSERVER_PER_PAGE', views.DEFAULT_PER_PAGE))
per_page = per_page if per_page > 0 else DEFAULT_PER_PAGE per_page = per_page if per_page > 0 else views.DEFAULT_PER_PAGE
app.config['BUKUSERVER_PER_PAGE'] = 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'): 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['BUKUSERVER_URL_RENDER_MODE'] = url_render_mode
app.config['SECRET_KEY'] = os.getenv('BUKUSERVER_SECRET_KEY') or os.urandom(24) app.config['SECRET_KEY'] = os.getenv('BUKUSERVER_SECRET_KEY') or os.urandom(24)
bukudb = BukuDb() 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 app.jinja_env.filters['netloc'] = lambda x: urlparse(x).netloc # pylint: disable=no-member
Bootstrap(app) Bootstrap(app)
admin = Admin(
app, name='Buku Server', template_mode='bootstrap3',
index_view=views.CustomAdminIndexView(
template='bukuserver/home.html', url='/'
)
)
# routing # routing
app.add_url_rule('/api/tags', 'get_tags', get_tags, methods=['GET']) # api
app.add_url_rule('/tags', 'get_tags-html', get_tags, methods=['GET']) app.add_url_rule('/api/tags', 'get_tags', tag_list, methods=['GET'])
app.add_url_rule('/api/tags/<tag>', 'update_tag', update_tag, methods=['PUT']) app.add_url_rule('/api/tags/<tag>', 'update_tag', tag_detail, methods=['GET', 'PUT'])
app.add_url_rule('/tags/<tag>', 'update_tag-html', update_tag, methods=['POST']) 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('/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/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('/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>/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>/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']) 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>', '/api/bookmarks/<starting_id>/<ending_id>',
'bookmark_range_operations', bookmark_range_operations, methods=['GET', 'PUT', 'DELETE']) '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('/api/bookmarks/search', 'search_bookmarks', search_bookmarks, methods=['GET', 'DELETE'])
app.add_url_rule('/bookmarks/search', 'search_bookmarks-html', search_bookmarks, methods=['GET']) # non api
app.add_url_rule('/', 'index', lambda: render_template( admin.add_view(views.BookmarkModelView(
'bukuserver/index.html', search_bookmarks_form=forms.SearchBookmarksForm())) bukudb, 'Bookmarks', page_size=per_page, url_render_mode=url_render_mode))
app.add_url_rule('/statistic', 'statistic', view_statistic, methods=['GET', 'POST']) admin.add_view(views.TagModelView(
bukudb, 'Tags', page_size=per_page))
admin.add_view(views.StatisticView('Statistic', endpoint='statistic'))
return app 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(): def cli():
"""This is a management script for the wiki application.""" """This is a management script for the wiki application."""

View File

@ -0,0 +1,8 @@
$(document).ready(function() {
$.getJSON( "/api/tags", function( json ) {
$('input#tags').select2({
tags: json.tags,
tokenSeparators: [','],
});
});
});

View File

@ -25,7 +25,7 @@
<li><a href="{{url_for('index')}}">Home</a></li> <li><a href="{{url_for('index')}}">Home</a></li>
<li><a href="{{url_for('bookmarks-html')}}">Bookmarks</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('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> </ul>
<form class="navbar-form navbar-right" action="{{url_for('search_bookmarks-html')}}" method="GET"> <form class="navbar-form navbar-right" action="{{url_for('search_bookmarks-html')}}" method="GET">
<div class="form-group"> <div class="form-group">

View 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 %}

View File

@ -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 %}

View File

@ -1,14 +1,6 @@
{% extends "bukuserver/base.html" %} {% extends 'admin/model/edit.html' %}
{% block content %} {% block tail %}
<div class="container"> {{ super() }}
<h1 style="padding-top: 70px;">Edit Bookmarks</h1> <script src="{{ url_for('static', filename='bukuserver/js/bookmark.js') }}"></script>
<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>
{% endblock %} {% endblock %}

View 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 %}

View 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 %}

View File

@ -8,7 +8,7 @@
<p> <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('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('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> </p>
<div class=" col-md-4 col-md-offset-4"> <div class=" col-md-4 col-md-offset-4">
{{show_bookmarks_search_form(checkbox_class="text-left col-sm-offset-2")}} {{show_bookmarks_search_form(checkbox_class="text-left col-sm-offset-2")}}

View File

@ -1,9 +1,8 @@
{% extends "bukuserver/base.html" %} {% extends "bukuserver/home.html" %}
{% block content %} {% block body %}
<div class="container"> <div class="container">
<h2 style="padding-top: 70px;">Statistics</h2> <form class="form-inline" action="{{url_for('statistic.index')}}" method="POST">
<form class="form-inline" action="{{url_for('statistic')}}" method="POST">
Data created Data created
<span rel="tooltip" title="{{datetime}}">{{datetime_text}}</span> <span rel="tooltip" title="{{datetime}}">{{datetime_text}}</span>
<button type="submit" class="btn btn-default btn-sm">refresh</button> <button type="submit" class="btn btn-default btn-sm">refresh</button>
@ -30,7 +29,7 @@
{% for item, number, _ in most_common_netlocs %} {% for item, number, _ in most_common_netlocs %}
<tr> <tr>
<td>{{loop.index}}</td> <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> <td class="text-right">{{number}}</td>
</tr> </tr>
{% endfor %} {% endfor %}
@ -60,8 +59,8 @@
<tr> <tr>
<td>{{loop.index}}</td> <td>{{loop.index}}</td>
<td> <td>
{% if netloc %} {% if item %}
<a href="{{url_for('search_bookmarks-html')}}?keywords-0={{item}}">{{item}}</a> <a href="{{url_for('bookmark.index_view', flt1_url_netloc_match=item)}}">{{item}}</a>
{% else %} {% else %}
<span class="btn btn-default" disabled="disabled">(No Netloc)</span> <span class="btn btn-default" disabled="disabled">(No Netloc)</span>
{% endif %} {% endif %}
@ -100,7 +99,7 @@
<tr> <tr>
<td>{{loop.index}}</td> <td>{{loop.index}}</td>
<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>
<td class="text-right">{{number}}</td> <td class="text-right">{{number}}</td>
</tr> </tr>
@ -129,7 +128,7 @@
{% for item, number in tag_counter.most_common() %} {% for item, number in tag_counter.most_common() %}
<tr> <tr>
<td>{{loop.index}}</td> <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> <td class="text-right">{{number}}</td>
</tr> </tr>
{% endfor %} {% endfor %}
@ -164,7 +163,7 @@
<td>{{loop.index}}</td> <td>{{loop.index}}</td>
<td> <td>
{% if item %} {% 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 %} {% else %}
(No Title) (No Title)
{% endif %} {% endif %}
@ -199,7 +198,7 @@
<td>{{loop.index}}</td> <td>{{loop.index}}</td>
<td style="word-break:break-all;"> <td style="word-break:break-all;">
{% if item %} {% 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 %} {% else %}
<span class="btn btn-default" disabled="disabled">(No Title)</span> <span class="btn btn-default" disabled="disabled">(No Title)</span>
{% endif %} {% endif %}
@ -214,7 +213,11 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% endblock %}
{% block tail %}
{{ super() }}
<script src="{{url_for('static', filename='bukuserver/js/Chart.min.js')}}"></script>
<script> <script>
var ctx = document.getElementById("mostCommonChart").getContext('2d'); var ctx = document.getElementById("mostCommonChart").getContext('2d');
var netlocChart = new Chart(ctx, { var netlocChart = new Chart(ctx, {
@ -239,12 +242,12 @@
var form = $('<form></form>'); var form = $('<form></form>');
form.attr("method", "get"); form.attr("method", "get");
form.attr("action", "{{url_for('search_bookmarks-html')}}"); form.attr("action", "{{url_for('bookmark.index_view')}}");
var field = $('<input></input>'); var field = $('<input></input>');
field.attr("type", "hidden"); field.attr("type", "hidden");
field.attr("name", "keywords-0"); field.attr("name", "flt1_url_netloc_match");
field.attr("value", value); field.attr("value", value);
form.append(field); form.append(field);
@ -276,7 +279,7 @@
options: { options: {
'onClick' : function (evt, item) { 'onClick' : function (evt, item) {
var tagStr = this.data.labels[item[0]._index]; 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; window.location.href = url;
} }
} }
@ -305,12 +308,12 @@
var form = $('<form></form>'); var form = $('<form></form>');
form.attr("method", "get"); form.attr("method", "get");
form.attr("action", "{{url_for('search_bookmarks-html')}}"); form.attr("action", "{{url_for('bookmark.index_view')}}");
var field = $('<input></input>'); var field = $('<input></input>');
field.attr("type", "hidden"); field.attr("type", "hidden");
field.attr("name", "keywords-0"); field.attr("name", "flt0_title_equals");
field.attr("value", value); field.attr("value", value);
form.append(field); form.append(field);
@ -327,8 +330,3 @@
</div> </div>
{% endblock %} {% endblock %}
{% block head %}
{{super()}}
<script src="{{url_for('static', filename='bukuserver/js/Chart.min.js')}}"></script>
{% endblock %}

562
bukuserver/views.py Normal file
View 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 '&lt;EMPTY TITLE&gt;'
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 '&lt;EMPTY TAG&gt;'
))
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))

View File

@ -24,13 +24,14 @@ tests_require = [
'pylint>=1.7.2', 'pylint>=1.7.2',
'pytest-cov', 'pytest-cov',
'pytest>=3.4.2', 'pytest>=3.4.2',
'PyYAML>=3.12', 'PyYAML==3.12',
] ]
server_require = [ server_require = [
'arrow>=0.12.1', 'arrow>=0.12.1',
'click>=6.7', 'click>=6.7',
'Flask-Admin==1.5.1',
'Flask-API>=0.6.9', 'Flask-API>=0.6.9',
'Flask-Bootstrap>=3.3.7.1', 'Flask-Bootstrap>=3.3.7.1',
'flask-paginate>=0.5.1', 'flask-paginate>=0.5.1',