From: hylom Date: Wed, 20 Mar 2019 13:35:05 +0000 (+0900) Subject: implement filtering feature for timeline (currently works journal only) X-Git-Tag: v0.1.11 X-Git-Url: http://git.osdn.net/view?a=commitdiff_plain;h=5f382e38c508872851ce8ea0903f9383864f9c7d;p=newslash%2Fnewslash.git implement filtering feature for timeline (currently works journal only) --- diff --git a/src/newslash_web/css/newslash.less b/src/newslash_web/css/newslash.less index b9d6ba33..ca8af4e9 100644 --- a/src/newslash_web/css/newslash.less +++ b/src/newslash_web/css/newslash.less @@ -28,6 +28,7 @@ @import "newslash/messages.less"; @import "newslash/progress_bar.less"; @import "newslash/wiki_content.less"; +@import "newslash/timeline.less"; @import "newslash/ads.less"; @import "newslash/system_error.less"; diff --git a/src/newslash_web/css/newslash/article.less b/src/newslash_web/css/newslash/article.less index f92c89c6..03e1ac01 100644 --- a/src/newslash_web/css/newslash/article.less +++ b/src/newslash_web/css/newslash/article.less @@ -6,6 +6,14 @@ article { header { margin-bottom: 10px; h1 { + &.color-red { border-left: 2px solid red; } + &.color-orange { border-left: 2px solid orange; } + &.color-yellow { border-left: 2px solid yellow; } + &.color-green { border-left: 2px solid green; } + &.color-blue { border-left: 2px solid blue; } + &.color-indigo { border-left: 2px solid indigo; } + &.color-violet { border-left: 2px solid violet; } + &.color-black { border-left: 2px solid black; } &:extend(.rectangle-header); &:extend(.large-text); img { diff --git a/src/newslash_web/css/newslash/timeline.less b/src/newslash_web/css/newslash/timeline.less new file mode 100644 index 00000000..0b86d60a --- /dev/null +++ b/src/newslash_web/css/newslash/timeline.less @@ -0,0 +1,24 @@ +/* timeline related styles */ + +.timeline-filter-ui { + &:extend(.bordered-box); + .filter-colors { + display: inline-block; + .color-indicator { + display: inline-block; + width: 20px; + border: 1px solid @component-border-color; + cursor: pointer; + float: left; + &.red { background-color: red; color: red; } + &.orange { background-color: orange; color: orange; } + &.yellow { background-color: yellow; color: yellow; } + &.green { background-color: green; color: green; } + &.blue { background-color: blue; color: blue; } + &.indigo { background-color: indigo; color: indigo; } + &.violet { background-color: violet; color: violet; } + &.black { background-color: black; color: black; } + &.active { border-width: 3px; border-color: @primary-color; } + } + } +} diff --git a/src/newslash_web/lib/Newslash/Model/Journals.pm b/src/newslash_web/lib/Newslash/Model/Journals.pm index 21d6759a..7ecf36c9 100644 --- a/src/newslash_web/lib/Newslash/Model/Journals.pm +++ b/src/newslash_web/lib/Newslash/Model/Journals.pm @@ -122,6 +122,7 @@ sub select { my $keys = { uid => "journals.uid", user_id => "journals.uid", karma => "users_info.karma", + popularity => "firehose.popularity", }; my $datetime_keys = { create_time => 'journals.date', update_time => 'journals.last_update', diff --git a/src/newslash_web/lib/Newslash/Model/Timeline.pm b/src/newslash_web/lib/Newslash/Model/Timeline.pm index a54997a7..a313e7ee 100644 --- a/src/newslash_web/lib/Newslash/Model/Timeline.pm +++ b/src/newslash_web/lib/Newslash/Model/Timeline.pm @@ -9,7 +9,7 @@ sub select { my $params = {@_}; my $uid = $params->{uid}; - my $type = "global"; + my $type = $params->{type} || "global"; if ($uid) { $type = $params->{type} || "user"; } @@ -17,6 +17,12 @@ sub select { if ($type eq "user") { return $self->_select_user($uid, $params); } + elsif ($type eq "tags") { + return $self->_select_tags($uid, $params); + } + elsif ($type eq "user") { + return $self->_select_user($uid, $params); + } elsif ($type eq "friends") { return $self->_select_friends($uid, $params); } diff --git a/src/newslash_web/lib/Newslash/Plugin/CompositeCache.pm b/src/newslash_web/lib/Newslash/Plugin/CompositeCache.pm index a87b6348..efd798da 100644 --- a/src/newslash_web/lib/Newslash/Plugin/CompositeCache.pm +++ b/src/newslash_web/lib/Newslash/Plugin/CompositeCache.pm @@ -287,6 +287,11 @@ sub _param_to_hash { } +sub select_nocache { + my ($self, @params) = @_; + return $self->model->select(@params); +} + sub select { my ($self, @params) = @_; diff --git a/src/newslash_web/lib/Newslash/Plugin/DefaultConfig.pm b/src/newslash_web/lib/Newslash/Plugin/DefaultConfig.pm index 399e8a74..1b1e65de 100644 --- a/src/newslash_web/lib/Newslash/Plugin/DefaultConfig.pm +++ b/src/newslash_web/lib/Newslash/Plugin/DefaultConfig.pm @@ -69,6 +69,16 @@ my $defaults = { Timeline => { popular_period => { hours => 6 }, item_per_page => 20, + item_per_page_limit => 100, + heatmap => { black => -999, + violet => -20, + indigo => 25, + blue => 93, + green => 138, + yellow => 175, + orange => 200, + red => 240, + }, }, Database => { host => "db", diff --git a/src/newslash_web/lib/Newslash/Web.pm b/src/newslash_web/lib/Newslash/Web.pm index e83ea436..33b705f4 100644 --- a/src/newslash_web/lib/Newslash/Web.pm +++ b/src/newslash_web/lib/Newslash/Web.pm @@ -414,6 +414,8 @@ sub startup { $api->get('/story')->to('API::Story#get'); $api->post('/story')->to('API::Story#post'); + $api->get('/timeline')->to('API::Timeline#get'); + $api->get('/poll')->to('API::Poll#get'); $api->post('/poll')->to('API::Poll#post'); $api->post('/vote')->to('API::Poll#vote', csrf_check_id => 'vote'); diff --git a/src/newslash_web/lib/Newslash/Web/Controller/API/Timeline.pm b/src/newslash_web/lib/Newslash/Web/Controller/API/Timeline.pm new file mode 100644 index 00000000..61efccd8 --- /dev/null +++ b/src/newslash_web/lib/Newslash/Web/Controller/API/Timeline.pm @@ -0,0 +1,186 @@ +package Newslash::Web::Controller::API::Timeline; +use Mojo::Base 'Mojolicious::Controller'; +use Data::Dumper; + + +sub _add_url { + my ($c, $item) = @_; + if ($item->{content_type} eq "journal") { + return "/~$item->{author}/journal/$item->{id}/"; + } + elsif ($item->{content_type} eq "story") { + return "/story/$item->{sid}/"; + } + else { + return "/$item->{content_type}/$item->{id}/"; + } + return; +} + +sub _get_heatmap { + my $c = shift; + my $cfg = $c->config("Timeline"); + my $heatmap = $cfg->{heatmap}; + + if (!$heatmap) { + $c->app->log->error("Timeline: no heatmap defined."); + return; + } + + my @keys = keys %$heatmap; + @keys = sort { $heatmap->{$a} <=> $heatmap->{$b} } @keys; + my $rs = []; + for my $k (@keys) { + push @$rs, { $k => $heatmap->{$k } }; + } + return $rs; +} + +sub _get_primary_topic_icon_url { + my ($c, $item) = @_; + my $cfg = $c->config("Site") || {}; + my $base_url = $cfg->{topic_icon_base_url}; + + if (!$base_url) { + $c->app->log->error("Timeline: Site.topic_icon_base_url is not defined"); + return; + } + my $t = $item->{primary_topic} || {}; + my $image = $t->{image}; + + if ($image) { + return "$base_url/$image"; + } + return; +} + +sub _score_to_heatmap_color { + my ($c, $item) = @_; + my $heatmap = _get_heatmap($c); + if (!$heatmap) { + return; + } + + my $last_color; + for my $i (reverse @$heatmap) { + my @k = keys %$i; + my $color = $last_color = $k[0]; + my $threshold = $i->{$color}; + + if ($item->{popularity} > $threshold) { + return $color; + } + } + return $last_color; +} + +sub _threshold_to_popularity { + my ($c, $threshold) = @_; + my $heatmap = _get_heatmap($c); + if (!$heatmap) { + $c->app->log->error("Timeline::_threshold_to_popularity: no heatmap defined."); + return; + } + + my $color = $heatmap->[$threshold]; + if (!$color) { + $c->app->log->error("Timeline::_threshold_to_popularity: no color for threshold $threshold."); + return; + } + + my @k = keys %$color; + return $color->{$k[0]}; +} + + +sub get { + my $c = shift; + my $user = $c->stash('user'); + my $params = $c->req->query_params->to_hash; + my $target = $params->{target} || "all"; + my $cfg = $c->config("Timeline"); + + my $hide_future = !$user->{is_admin} && !$user->{editor}; + my $public_only = !$user->{is_admin} && !$user->{editor}; + + my $limit = $params->{limit} || $cfg->{item_per_page} || 10; + my $max_limit = $cfg->{item_per_page_limit} || 1000; + $limit = $max_limit if $limit > $max_limit; + + my $skip = $params->{skip} || 0; + my $min_popularity; + if ($params->{threshold}) { + $min_popularity = _threshold_to_popularity($c, $params->{threshold}); + $c->app->log->debug("Timeline::_threshold_to_popularity: use min_pop $min_popularity."); + } + my $result; + my $model; + + if ($target eq "story") { + $model = $c->ccache->model('stories'); + } + elsif ($target eq "journal") { + $model = $c->ccache->model('journals'); + } + elsif ($target eq "comment") { + $model = $c->ccache->model('comments'); + } + elsif ($target eq "poll") { + $model = $c->ccache->model('polls'); + } + elsif ($target eq "submission") { + $model = $c->ccache->model('submissions'); + } + elsif ($target eq "all") { + $model = $c->ccache->model('timeline'); + } + else { + $c->render(json => { error => { code => -1, message => "invalid_request" }}); + $c->rendered(400); + return; + } + + if ($user->{is_login}) { + $result = $model->select_nocache(hide_future => $hide_future, + public_only => $public_only, + limit => $limit, + skip => $skip, + order_by => {create_time => 'desc'}, + popularity => $min_popularity ? { ge => $min_popularity } : undef, + ); + } + else { + $result = $model->select(hide_future => $hide_future, + public_only => $public_only, + limit => $limit, + skip => $skip, + order_by => {create_time => 'desc'}, + popularity => $min_popularity ? { ge => $min_popularity } : undef, + ); + } + + if (!$result) { + $c->render(json => { error => { code => -1, message => "internal_server_error" }}); + $c->rendered(500); + return; + } + + if (!@$result) { + $c->render(json => { error => { code => -1, message => "not_found" }}); + $c->rendered(404); + return; + } + + # add headmap info and topic icon url + for my $item (@$result) { + $item->{color} = _score_to_heatmap_color($c, $item); + $item->{icon_url} = _get_primary_topic_icon_url($c, $item); + $item->{url} = _add_url($c, $item); + } + + + $c->render(json => { result => $result }); +} + + +1; diff --git a/src/newslash_web/lib/Newslash/Web/Controller/Timeline.pm b/src/newslash_web/lib/Newslash/Web/Controller/Timeline.pm index ac4f2c8c..06bc7a5b 100644 --- a/src/newslash_web/lib/Newslash/Web/Controller/Timeline.pm +++ b/src/newslash_web/lib/Newslash/Web/Controller/Timeline.pm @@ -60,6 +60,16 @@ sub _render_timeline { content_type => $params->{content_type}, }; + if ($params->{content_type} eq "journal") { + $self->render("timeline/timeline2", + items => $items, + prev => $prev, + page => $page, + ); + $self->stats->add_event_counter("timeline_view"); + return; + } + $self->render("timeline/base", items => $items, prev => $prev, diff --git a/src/newslash_web/public/js/newslash.js b/src/newslash_web/public/js/newslash.js index 54a40984..68869988 100644 --- a/src/newslash_web/public/js/newslash.js +++ b/src/newslash_web/public/js/newslash.js @@ -201,6 +201,17 @@ function _initNewslash() { return this.post("/journal", data); }; + Newslash.prototype.getTimeline = function getTimeline (target, options) { + if (!target) { target = "all"; } + options = options || {}; + + var url = "/timeline?target=" + target; + if (options.threshold !== undefined) { + url = url + "&threshold=" + options.threshold; + } + + return this.get(url); + }; } _initNewslash(); diff --git a/src/newslash_web/public/js/timeline.js b/src/newslash_web/public/js/timeline.js new file mode 100644 index 00000000..7e6a5ad3 --- /dev/null +++ b/src/newslash_web/public/js/timeline.js @@ -0,0 +1,68 @@ +/* timeline.js */ +var timeline = {}; + +timeline.run = function (params) { + /* define exotic parameters */ + params = params || {}; + var userConfig = params.userConfig || {}; + var siteConfig = params.siteConfig || {}; + var pageInfo = params.pageInfo || {}; + var user = params.user || {}; + + if (!params.el) { + console.log('error in commentTree.run(): no element given'); + return; + } + + /* + * register + */ + Vue.component('timeline-item', { + template: '#timeline-item-template', + props: {item: Object}, + data: function () { return {}; }, + created: function () { return; }, + }); + + /* + * register + */ + Vue.component('timeline-filter-ui', { + template: '#timeline-filter-ui-template', + props: {}, + data: function () { return { threshold: 1}; }, + created: function () { return; }, + methods: { + setThreshold: function setThreshold (threshold) { + if (this.threshold != threshold) { + this.threshold = threshold; + vm.$emit('updateTimeline', {threshold: threshold}); + } + }, + }, + }); + + function updateTimeline(vm, target, threshold) { + newslash.getTimeline(target, {threshold: threshold}).then( + (resp) => { // success + vm.items = resp.result; + }, + (resp) => { // fail + statusIndicator.error("comment_loading_error"); + } + ); + } + + var vm = this.vm = new Vue({ + el: params.el, + data: { items: [] }, + created: function created() { + updateTimeline(this, params.target, 1); + }, + }); + + vm.$on("updateTimeline", function (args) { + updateTimeline(this, params.target, args.threshold); + }); +}; + diff --git a/src/newslash_web/t/api/timeline.t b/src/newslash_web/t/api/timeline.t new file mode 100644 index 00000000..7611bc71 --- /dev/null +++ b/src/newslash_web/t/api/timeline.t @@ -0,0 +1,63 @@ +# -*-Perl-*- +# timeline api tests +use Mojo::Base -strict; +use Mojo::Date; + +use Test::More; +use Test::Mojo; +use Mojo::Util qw(dumper); + +my $t = Test::Mojo->new('Newslash::Web'); + +subtest 'get timeline' => sub { + + # get all items + $t->get_ok("/api/v1/timeline?target=all") + ->status_is(200) + ->content_type_like(qr/application\/json/) + ->json_has('/result') + ->or(sub {diag "message: " . dumper($t->tx->res->json);}); + + # get story items + $t->get_ok("/api/v1/timeline?target=story") + ->status_is(200) + ->content_type_like(qr/application\/json/) + ->json_has('/result') + ->json_has('/result/0/stoid') + ->or(sub {diag "message: " . dumper($t->tx->res->json);}); + + # get comment items + $t->get_ok("/api/v1/timeline?target=comment") + ->status_is(200) + ->content_type_like(qr/application\/json/) + ->json_has('/result') + ->json_has('/result/0/cid') + ->or(sub {diag "message: " . dumper($t->tx->res->json);}); + + # get journal items + $t->get_ok("/api/v1/timeline?target=journal") + ->status_is(200) + ->content_type_like(qr/application\/json/) + ->json_has('/result') + ->json_has('/result/0/journal_id') + ->or(sub {diag "message: " . dumper($t->tx->res->json);}); + + # get submission items + $t->get_ok("/api/v1/timeline?target=submission") + ->status_is(200) + ->content_type_like(qr/application\/json/) + ->json_has('/result') + ->json_has('/result/0/submission_id') + ->or(sub {diag "message: " . dumper($t->tx->res->json);}); + + # get poll items + #$t->get_ok("/api/v1/timeline?target=poll") + # ->status_is(200) + # ->content_type_like(qr/application\/json/) + # ->json_has('/result') + # ->json_has('/result/0/qid') + # ->or(sub {diag "message: " . dumper($t->tx->res->json);}); +}; + + +done_testing(); diff --git a/src/newslash_web/templates/common/article/article.html.tt2 b/src/newslash_web/templates/common/article/article.html.tt2 index 87d2f7f2..224effd1 100644 --- a/src/newslash_web/templates/common/article/article.html.tt2 +++ b/src/newslash_web/templates/common/article/article.html.tt2 @@ -41,7 +41,7 @@ END; -%] -

diff --git a/src/newslash_web/templates/common/components/timeline.html.tt2 b/src/newslash_web/templates/common/components/timeline.html.tt2 new file mode 100644 index 00000000..408f652d --- /dev/null +++ b/src/newslash_web/templates/common/components/timeline.html.tt2 @@ -0,0 +1,86 @@ + + + + + +[% helpers.load_js("timeline.js") %] + diff --git a/src/newslash_web/templates/timeline/timeline2.html.tt2 b/src/newslash_web/templates/timeline/timeline2.html.tt2 new file mode 100644 index 00000000..127fbb30 --- /dev/null +++ b/src/newslash_web/templates/timeline/timeline2.html.tt2 @@ -0,0 +1,34 @@ +[% WRAPPER common/layout enable_sidebar=1 %] + + +[% INCLUDE common/components/timeline %] + +[% END %]