buku/bukuserver/views.py
2021-03-14 17:26:47 +02:00

608 lines
24 KiB
Python

"""views module."""
from argparse import Namespace
from collections import Counter
from types import SimpleNamespace
from typing import Any, List, Optional, Tuple
from urllib.parse import urlparse
import itertools
import logging
from flask import current_app, 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: Any, model: Namespace, name: str) -> Markup:
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="{1}">{0}</a>'
for tag in model.tags.split(','):
if tag:
tag_text.append(tag_tmpl.format(tag, url_for(
'bookmark.index_view', flt2_tags_contain=tag)))
if not netloc:
return Markup("""\
{0.title}<br/>{2}<br/>{1}{0.description}
""".format(
model, ''.join(tag_text), Markup.escape(model.url)
))
res = ''
if not current_app.config.get('BUKUSERVER_DISABLE_FAVICON', False):
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;'
open_in_new_tab = current_app.config.get('BUKUSERVER_OPEN_IN_NEW_TAB', False)
if is_scheme_valid and open_in_new_tab:
res += '<a href="{0.url}" target="_blank">{1}</a>'.format(model, title)
elif is_scheme_valid and not open_in_new_tab:
res += '<a href="{0.url}">{1}</a>'.format(model, title)
else:
res += title
if self.url_render_mode == 'netloc':
res += ' (<a href="{1}">{0}</a>)'.format(
netloc,
url_for('bookmark.index_view', flt2_url_netloc_match=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/>'
if self.url_render_mode != 'netloc':
res += tag_tmpl.format(
'netloc:{}'.format(netloc),
url_for('bookmark.index_view', flt2_url_netloc_match=netloc)
)
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_form(self, obj=None):
form = super().create_form(obj)
args = request.args
if 'url' in args.keys() and not args.get("url").startswith('/bookmark/'):
form.url.data = args.get("url")
if 'title' in args.keys():
form.title.data = args.get("title")
if 'description' in args.keys():
form.description.data = args.get("description")
return form
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:
try:
bookmarks = list(chunks(bookmarks, page_size))[page]
except IndexError:
bookmarks = []
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)
if not data:
return Markup('<a href="{}">{}</a>'.format(
url_for('bookmark.index_view', flt2_tags_number_equal=0),
'&lt;EMPTY TAG&gt;'
))
return Markup('<a href="{}">{}</a>'.format(
url_for('bookmark.index_view', flt1_tags_contain=data), data
))
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.DataRequired()])
return CustomForm
def scaffold_list_form(self, widget=None, validators=None):
pass
def get_list(
self,
page: int,
sort_field: str,
sort_desc: bool,
search: Optional[Any],
filters: List[Tuple[int, str, str]],
page_size: int = None) -> Tuple[int, List[SimpleNamespace]]:
bukudb = self.bukudb
tags = bukudb.get_tag_all()[1]
tags = sorted(tags.items())
tags = self._apply_filters(tags, filters)
sort_field_dict = {'usage_count': 1, 'name': 0}
if sort_field in sort_field_dict:
tags = list(sorted(
tags, key=lambda x: x[sort_field_dict[sort_field]], reverse=sort_desc))
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
def __init__(self, *args, **kwargs):
self.bukudb = args[0]
args = list(args[1:])
super().__init__(*args, **kwargs)
@expose('/', methods=('GET', 'POST'))
def index(self):
bukudb = self.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(arr, n):
n = max(1, n)
return (arr[i:i+n] for i in range(0, len(arr), n))