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
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
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."""
# 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()

View File

@ -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."""

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('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">

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

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>
<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")}}

View File

@ -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
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',
'pytest-cov',
'pytest>=3.4.2',
'PyYAML>=3.12',
'PyYAML==3.12',
]
server_require = [
'arrow>=0.12.1',
'click>=6.7',
'Flask-Admin==1.5.1',
'Flask-API>=0.6.9',
'Flask-Bootstrap>=3.3.7.1',
'flask-paginate>=0.5.1',