• R/O
  • HTTP
  • SSH
  • HTTPS

newslash: 提交

newslash


Commit MetaInfo

修订版5f382e38c508872851ce8ea0903f9383864f9c7d (tree)
时间2019-03-20 22:35:05
作者hylom <hylom@user...>
Commiterhylom

Log Message

implement filtering feature for timeline (currently works journal only)

更改概述

差异

--- a/src/newslash_web/css/newslash.less
+++ b/src/newslash_web/css/newslash.less
@@ -28,6 +28,7 @@
2828 @import "newslash/messages.less";
2929 @import "newslash/progress_bar.less";
3030 @import "newslash/wiki_content.less";
31+@import "newslash/timeline.less";
3132
3233 @import "newslash/ads.less";
3334 @import "newslash/system_error.less";
--- a/src/newslash_web/css/newslash/article.less
+++ b/src/newslash_web/css/newslash/article.less
@@ -6,6 +6,14 @@ article {
66 header {
77 margin-bottom: 10px;
88 h1 {
9+ &.color-red { border-left: 2px solid red; }
10+ &.color-orange { border-left: 2px solid orange; }
11+ &.color-yellow { border-left: 2px solid yellow; }
12+ &.color-green { border-left: 2px solid green; }
13+ &.color-blue { border-left: 2px solid blue; }
14+ &.color-indigo { border-left: 2px solid indigo; }
15+ &.color-violet { border-left: 2px solid violet; }
16+ &.color-black { border-left: 2px solid black; }
917 &:extend(.rectangle-header);
1018 &:extend(.large-text);
1119 img {
--- /dev/null
+++ b/src/newslash_web/css/newslash/timeline.less
@@ -0,0 +1,24 @@
1+/* timeline related styles */
2+
3+.timeline-filter-ui {
4+ &:extend(.bordered-box);
5+ .filter-colors {
6+ display: inline-block;
7+ .color-indicator {
8+ display: inline-block;
9+ width: 20px;
10+ border: 1px solid @component-border-color;
11+ cursor: pointer;
12+ float: left;
13+ &.red { background-color: red; color: red; }
14+ &.orange { background-color: orange; color: orange; }
15+ &.yellow { background-color: yellow; color: yellow; }
16+ &.green { background-color: green; color: green; }
17+ &.blue { background-color: blue; color: blue; }
18+ &.indigo { background-color: indigo; color: indigo; }
19+ &.violet { background-color: violet; color: violet; }
20+ &.black { background-color: black; color: black; }
21+ &.active { border-width: 3px; border-color: @primary-color; }
22+ }
23+ }
24+}
--- a/src/newslash_web/lib/Newslash/Model/Journals.pm
+++ b/src/newslash_web/lib/Newslash/Model/Journals.pm
@@ -122,6 +122,7 @@ sub select {
122122 my $keys = { uid => "journals.uid",
123123 user_id => "journals.uid",
124124 karma => "users_info.karma",
125+ popularity => "firehose.popularity",
125126 };
126127 my $datetime_keys = { create_time => 'journals.date',
127128 update_time => 'journals.last_update',
--- a/src/newslash_web/lib/Newslash/Model/Timeline.pm
+++ b/src/newslash_web/lib/Newslash/Model/Timeline.pm
@@ -9,7 +9,7 @@ sub select {
99 my $params = {@_};
1010
1111 my $uid = $params->{uid};
12- my $type = "global";
12+ my $type = $params->{type} || "global";
1313 if ($uid) {
1414 $type = $params->{type} || "user";
1515 }
@@ -17,6 +17,12 @@ sub select {
1717 if ($type eq "user") {
1818 return $self->_select_user($uid, $params);
1919 }
20+ elsif ($type eq "tags") {
21+ return $self->_select_tags($uid, $params);
22+ }
23+ elsif ($type eq "user") {
24+ return $self->_select_user($uid, $params);
25+ }
2026 elsif ($type eq "friends") {
2127 return $self->_select_friends($uid, $params);
2228 }
--- 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 {
287287 }
288288
289289
290+sub select_nocache {
291+ my ($self, @params) = @_;
292+ return $self->model->select(@params);
293+}
294+
290295 sub select {
291296 my ($self, @params) = @_;
292297
--- a/src/newslash_web/lib/Newslash/Plugin/DefaultConfig.pm
+++ b/src/newslash_web/lib/Newslash/Plugin/DefaultConfig.pm
@@ -69,6 +69,16 @@ my $defaults = {
6969
7070 Timeline => { popular_period => { hours => 6 },
7171 item_per_page => 20,
72+ item_per_page_limit => 100,
73+ heatmap => { black => -999,
74+ violet => -20,
75+ indigo => 25,
76+ blue => 93,
77+ green => 138,
78+ yellow => 175,
79+ orange => 200,
80+ red => 240,
81+ },
7282 },
7383
7484 Database => { host => "db",
--- a/src/newslash_web/lib/Newslash/Web.pm
+++ b/src/newslash_web/lib/Newslash/Web.pm
@@ -414,6 +414,8 @@ sub startup {
414414 $api->get('/story')->to('API::Story#get');
415415 $api->post('/story')->to('API::Story#post');
416416
417+ $api->get('/timeline')->to('API::Timeline#get');
418+
417419 $api->get('/poll')->to('API::Poll#get');
418420 $api->post('/poll')->to('API::Poll#post');
419421 $api->post('/vote')->to('API::Poll#vote', csrf_check_id => 'vote');
--- /dev/null
+++ b/src/newslash_web/lib/Newslash/Web/Controller/API/Timeline.pm
@@ -0,0 +1,186 @@
1+package Newslash::Web::Controller::API::Timeline;
2+use Mojo::Base 'Mojolicious::Controller';
3+use Data::Dumper;
4+
5+
6+sub _add_url {
7+ my ($c, $item) = @_;
8+ if ($item->{content_type} eq "journal") {
9+ return "/~$item->{author}/journal/$item->{id}/";
10+ }
11+ elsif ($item->{content_type} eq "story") {
12+ return "/story/$item->{sid}/";
13+ }
14+ else {
15+ return "/$item->{content_type}/$item->{id}/";
16+ }
17+ return;
18+}
19+
20+sub _get_heatmap {
21+ my $c = shift;
22+ my $cfg = $c->config("Timeline");
23+ my $heatmap = $cfg->{heatmap};
24+
25+ if (!$heatmap) {
26+ $c->app->log->error("Timeline: no heatmap defined.");
27+ return;
28+ }
29+
30+ my @keys = keys %$heatmap;
31+ @keys = sort { $heatmap->{$a} <=> $heatmap->{$b} } @keys;
32+ my $rs = [];
33+ for my $k (@keys) {
34+ push @$rs, { $k => $heatmap->{$k } };
35+ }
36+ return $rs;
37+}
38+
39+sub _get_primary_topic_icon_url {
40+ my ($c, $item) = @_;
41+ my $cfg = $c->config("Site") || {};
42+ my $base_url = $cfg->{topic_icon_base_url};
43+
44+ if (!$base_url) {
45+ $c->app->log->error("Timeline: Site.topic_icon_base_url is not defined");
46+ return;
47+ }
48+ my $t = $item->{primary_topic} || {};
49+ my $image = $t->{image};
50+
51+ if ($image) {
52+ return "$base_url/$image";
53+ }
54+ return;
55+}
56+
57+sub _score_to_heatmap_color {
58+ my ($c, $item) = @_;
59+ my $heatmap = _get_heatmap($c);
60+ if (!$heatmap) {
61+ return;
62+ }
63+
64+ my $last_color;
65+ for my $i (reverse @$heatmap) {
66+ my @k = keys %$i;
67+ my $color = $last_color = $k[0];
68+ my $threshold = $i->{$color};
69+
70+ if ($item->{popularity} > $threshold) {
71+ return $color;
72+ }
73+ }
74+ return $last_color;
75+}
76+
77+sub _threshold_to_popularity {
78+ my ($c, $threshold) = @_;
79+ my $heatmap = _get_heatmap($c);
80+ if (!$heatmap) {
81+ $c->app->log->error("Timeline::_threshold_to_popularity: no heatmap defined.");
82+ return;
83+ }
84+
85+ my $color = $heatmap->[$threshold];
86+ if (!$color) {
87+ $c->app->log->error("Timeline::_threshold_to_popularity: no color for threshold $threshold.");
88+ return;
89+ }
90+
91+ my @k = keys %$color;
92+ return $color->{$k[0]};
93+}
94+
95+
96+sub get {
97+ my $c = shift;
98+ my $user = $c->stash('user');
99+ my $params = $c->req->query_params->to_hash;
100+ my $target = $params->{target} || "all";
101+ my $cfg = $c->config("Timeline");
102+
103+ my $hide_future = !$user->{is_admin} && !$user->{editor};
104+ my $public_only = !$user->{is_admin} && !$user->{editor};
105+
106+ my $limit = $params->{limit} || $cfg->{item_per_page} || 10;
107+ my $max_limit = $cfg->{item_per_page_limit} || 1000;
108+ $limit = $max_limit if $limit > $max_limit;
109+
110+ my $skip = $params->{skip} || 0;
111+ my $min_popularity;
112+ if ($params->{threshold}) {
113+ $min_popularity = _threshold_to_popularity($c, $params->{threshold});
114+ $c->app->log->debug("Timeline::_threshold_to_popularity: use min_pop $min_popularity.");
115+ }
116+ my $result;
117+ my $model;
118+
119+ if ($target eq "story") {
120+ $model = $c->ccache->model('stories');
121+ }
122+ elsif ($target eq "journal") {
123+ $model = $c->ccache->model('journals');
124+ }
125+ elsif ($target eq "comment") {
126+ $model = $c->ccache->model('comments');
127+ }
128+ elsif ($target eq "poll") {
129+ $model = $c->ccache->model('polls');
130+ }
131+ elsif ($target eq "submission") {
132+ $model = $c->ccache->model('submissions');
133+ }
134+ elsif ($target eq "all") {
135+ $model = $c->ccache->model('timeline');
136+ }
137+ else {
138+ $c->render(json => { error => { code => -1, message => "invalid_request" }});
139+ $c->rendered(400);
140+ return;
141+ }
142+
143+ if ($user->{is_login}) {
144+ $result = $model->select_nocache(hide_future => $hide_future,
145+ public_only => $public_only,
146+ limit => $limit,
147+ skip => $skip,
148+ order_by => {create_time => 'desc'},
149+ popularity => $min_popularity ? { ge => $min_popularity } : undef,
150+ );
151+ }
152+ else {
153+ $result = $model->select(hide_future => $hide_future,
154+ public_only => $public_only,
155+ limit => $limit,
156+ skip => $skip,
157+ order_by => {create_time => 'desc'},
158+ popularity => $min_popularity ? { ge => $min_popularity } : undef,
159+ );
160+ }
161+
162+ if (!$result) {
163+ $c->render(json => { error => { code => -1, message => "internal_server_error" }});
164+ $c->rendered(500);
165+ return;
166+ }
167+
168+ if (!@$result) {
169+ $c->render(json => { error => { code => -1, message => "not_found" }});
170+ $c->rendered(404);
171+ return;
172+ }
173+
174+ # add headmap info and topic icon url
175+ for my $item (@$result) {
176+ $item->{color} = _score_to_heatmap_color($c, $item);
177+ $item->{icon_url} = _get_primary_topic_icon_url($c, $item);
178+ $item->{url} = _add_url($c, $item);
179+ }
180+
181+
182+ $c->render(json => { result => $result });
183+}
184+
185+
186+1;
--- 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 {
6060 content_type => $params->{content_type},
6161 };
6262
63+ if ($params->{content_type} eq "journal") {
64+ $self->render("timeline/timeline2",
65+ items => $items,
66+ prev => $prev,
67+ page => $page,
68+ );
69+ $self->stats->add_event_counter("timeline_view");
70+ return;
71+ }
72+
6373 $self->render("timeline/base",
6474 items => $items,
6575 prev => $prev,
--- a/src/newslash_web/public/js/newslash.js
+++ b/src/newslash_web/public/js/newslash.js
@@ -201,6 +201,17 @@ function _initNewslash() {
201201 return this.post("/journal", data);
202202 };
203203
204+ Newslash.prototype.getTimeline = function getTimeline (target, options) {
205+ if (!target) { target = "all"; }
206+ options = options || {};
207+
208+ var url = "/timeline?target=" + target;
209+ if (options.threshold !== undefined) {
210+ url = url + "&threshold=" + options.threshold;
211+ }
212+
213+ return this.get(url);
214+ };
204215 }
205216
206217 _initNewslash();
--- /dev/null
+++ b/src/newslash_web/public/js/timeline.js
@@ -0,0 +1,68 @@
1+/* timeline.js */
2+var timeline = {};
3+
4+timeline.run = function (params) {
5+ /* define exotic parameters */
6+ params = params || {};
7+ var userConfig = params.userConfig || {};
8+ var siteConfig = params.siteConfig || {};
9+ var pageInfo = params.pageInfo || {};
10+ var user = params.user || {};
11+
12+ if (!params.el) {
13+ console.log('error in commentTree.run(): no element given');
14+ return;
15+ }
16+
17+ /*
18+ * register <timeline-item>
19+ */
20+ Vue.component('timeline-item', {
21+ template: '#timeline-item-template',
22+ props: {item: Object},
23+ data: function () { return {}; },
24+ created: function () { return; },
25+ });
26+
27+ /*
28+ * register <timeline-filter-ui>
29+ */
30+ Vue.component('timeline-filter-ui', {
31+ template: '#timeline-filter-ui-template',
32+ props: {},
33+ data: function () { return { threshold: 1}; },
34+ created: function () { return; },
35+ methods: {
36+ setThreshold: function setThreshold (threshold) {
37+ if (this.threshold != threshold) {
38+ this.threshold = threshold;
39+ vm.$emit('updateTimeline', {threshold: threshold});
40+ }
41+ },
42+ },
43+ });
44+
45+ function updateTimeline(vm, target, threshold) {
46+ newslash.getTimeline(target, {threshold: threshold}).then(
47+ (resp) => { // success
48+ vm.items = resp.result;
49+ },
50+ (resp) => { // fail
51+ statusIndicator.error("comment_loading_error");
52+ }
53+ );
54+ }
55+
56+ var vm = this.vm = new Vue({
57+ el: params.el,
58+ data: { items: [] },
59+ created: function created() {
60+ updateTimeline(this, params.target, 1);
61+ },
62+ });
63+
64+ vm.$on("updateTimeline", function (args) {
65+ updateTimeline(this, params.target, args.threshold);
66+ });
67+};
68+
--- /dev/null
+++ b/src/newslash_web/t/api/timeline.t
@@ -0,0 +1,63 @@
1+# -*-Perl-*-
2+# timeline api tests
3+use Mojo::Base -strict;
4+use Mojo::Date;
5+
6+use Test::More;
7+use Test::Mojo;
8+use Mojo::Util qw(dumper);
9+
10+my $t = Test::Mojo->new('Newslash::Web');
11+
12+subtest 'get timeline' => sub {
13+
14+ # get all items
15+ $t->get_ok("/api/v1/timeline?target=all")
16+ ->status_is(200)
17+ ->content_type_like(qr/application\/json/)
18+ ->json_has('/result')
19+ ->or(sub {diag "message: " . dumper($t->tx->res->json);});
20+
21+ # get story items
22+ $t->get_ok("/api/v1/timeline?target=story")
23+ ->status_is(200)
24+ ->content_type_like(qr/application\/json/)
25+ ->json_has('/result')
26+ ->json_has('/result/0/stoid')
27+ ->or(sub {diag "message: " . dumper($t->tx->res->json);});
28+
29+ # get comment items
30+ $t->get_ok("/api/v1/timeline?target=comment")
31+ ->status_is(200)
32+ ->content_type_like(qr/application\/json/)
33+ ->json_has('/result')
34+ ->json_has('/result/0/cid')
35+ ->or(sub {diag "message: " . dumper($t->tx->res->json);});
36+
37+ # get journal items
38+ $t->get_ok("/api/v1/timeline?target=journal")
39+ ->status_is(200)
40+ ->content_type_like(qr/application\/json/)
41+ ->json_has('/result')
42+ ->json_has('/result/0/journal_id')
43+ ->or(sub {diag "message: " . dumper($t->tx->res->json);});
44+
45+ # get submission items
46+ $t->get_ok("/api/v1/timeline?target=submission")
47+ ->status_is(200)
48+ ->content_type_like(qr/application\/json/)
49+ ->json_has('/result')
50+ ->json_has('/result/0/submission_id')
51+ ->or(sub {diag "message: " . dumper($t->tx->res->json);});
52+
53+ # get poll items
54+ #$t->get_ok("/api/v1/timeline?target=poll")
55+ # ->status_is(200)
56+ # ->content_type_like(qr/application\/json/)
57+ # ->json_has('/result')
58+ # ->json_has('/result/0/qid')
59+ # ->or(sub {diag "message: " . dumper($t->tx->res->json);});
60+};
61+
62+
63+done_testing();
--- 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;
4141
4242 -%]
4343
44-<article id="[% item.id %]" type="[% item.content_type %]" item-id="[% content_id %]"
44+<article id="[% item.id %]" type="[% item.content_type %]" item-id="[% item.content_id %]"
4545 [% IF !x_template %]v-if="0"[% ELSE %]v-if="mode != 'editing' || enableAutoPreview"[% END %]>
4646 <header>
4747 <h1>
--- /dev/null
+++ b/src/newslash_web/templates/common/components/timeline.html.tt2
@@ -0,0 +1,86 @@
1+<script type="text/x-template" id="timeline-filter-ui-template">
2+ <div class="timeline-filter-ui">
3+ <span>表示するアイテムのしきい値:</span>
4+ <div class="filter-colors">
5+ <span :class="{active: threshold == 0}" class="color-indicator black" title="0" @click="setThreshold(0)">0</span>
6+ <span :class="{active: threshold == 1}" class="color-indicator violet" title="1" @click="setThreshold(1)">1</span>
7+ <span :class="{active: threshold == 2}" class="color-indicator indigo" title="2" @click="setThreshold(2)">2</span>
8+ <span :class="{active: threshold == 3}" class="color-indicator blue" title="3" @click="setThreshold(3)">3</span>
9+ <span :class="{active: threshold == 4}" class="color-indicator green" title="4" @click="setThreshold(4)">4</span>
10+ <span :class="{active: threshold == 5}" class="color-indicator yellow" title="5" @click="setThreshold(5)">5</span>
11+ <span :class="{active: threshold == 6}" class="color-indicator orange" title="6" @click="setThreshold(6)">6</span>
12+ <span :class="{active: threshold == 7}" class="color-indicator red" title="7" @click="setThreshold(7)">7</span>
13+ </div>
14+ </div>
15+</script>
16+
17+<script type="text/x-template" id="timeline-item-template">
18+ <article>
19+ <header>
20+ <h1 :class="item.color ? 'color-' + item.color : ''">
21+ <img :src="item.icon_url" v-if="item.icon_url" />
22+ <a :href="item.url" v-html="item.title" v-if="item.url">
23+ <span v-html="item.title"></span>
24+ </a>
25+ <span v-html="item.title" v-else></span>
26+ </h1>
27+
28+ <div class="property">
29+ <span class="content-type" v-html="item.content_type"></span>
30+ <span class="author">
31+ by <a :href="'/~' + item.author + '/'" v-text="item.author"></a>
32+ </span>
33+ <span class="create-time" v-text="item.create_time"></span>
34+
35+ [%- IF user.is_admin %]
36+ <span class="score">
37+ pop: <span v-text="item.popularity"></span>
38+ epop: <span v-text="item.editorpop"></span>
39+ need: <span v-text="item.neediness"></span>
40+ act: <span v-text="item.activity"></span>
41+ </span>
42+ [%- END %]
43+
44+ <span class="dept" v-if="item.content_type == 'story' && item.dept">
45+ <span class="dept-name" v-text="item.dept"></span> 部門より
46+ </span>
47+ </div><!-- .property -->
48+
49+ [% IF user.author || user.is_admin %]
50+ <div class="alert alert-info" v-if="item.public != 'yes'">この記事は非公開に設定されています</div>
51+ [% END %]
52+ </header>
53+
54+ <div class="body contents-text" v-html="item.intro_text" v-if="item.intro_text"></div>
55+ <div class="body contents-text" v-html="item.body_text" v-if="item.body_text"></div>
56+ <div class="body contents-text" v-html="item.full_text" v-if="item.full_text"></div>
57+ <div class="body contents-text" v-html="item.media" v-if="item.media"></div>
58+ <div class="body contents-text" v-if="item.url"><p><a :href="item.url">情報元へのリンク</a></p></div>
59+
60+ <footer>
61+ <div class="link-to-story">
62+ <a :href="item.url">
63+ <span v-if='item.comment_count > 0'>
64+ <span v-text="item.comment_count"></span>件のコメントを見る
65+ </span>
66+ <span v-else>
67+ 続きを読む
68+ </span>
69+ </a>
70+ </div>
71+
72+ <div class="tag-bar">
73+ <ul class="tags">
74+ <li v-for="tag in item.tags" v-if="tag.private == 'no' && tag.tagname != 'mainpage' && tag.uid == item.uid">
75+ <a :href="'/tag/' + tag.tagname" v-text="tag.tagname"></a>
76+ </li>
77+ </ul>
78+ </div>
79+
80+ </footer>
81+ </article>
82+</script>
83+
84+
85+[% helpers.load_js("timeline.js") %]
86+
--- /dev/null
+++ b/src/newslash_web/templates/timeline/timeline2.html.tt2
@@ -0,0 +1,34 @@
1+[% WRAPPER common/layout enable_sidebar=1 %]
2+
3+<div class="sidebar-wrapper">
4+ [%- helpers.ad_code("timeline-top") %]
5+ <div class="index main-contents" id="timeline">
6+ <timeline-filter-ui></timeline-filter-ui>
7+ <div class="timeline-items" v-if="0">
8+ [%- FOREACH item IN items -%]
9+ [%- INCLUDE common/article/article hide_bodytext=1 %]
10+ [%- END -%]
11+ </div>
12+
13+ <div class="timeline-items" v-else v-for="item in items">
14+ <timeline-item :item="item"></timeline-item>
15+ </div>
16+
17+ [%- IF prev -%]
18+ <div class="pager">
19+ <span class="prev">
20+ <a href="/[% prev.type %]/[% prev.date %]/[% IF prev.id %]#[% prev.id %][% END %]">前の記事</a>
21+ </span>
22+ </div>
23+ [%- END -%]
24+
25+ </div><!-- .index -->
26+
27+ [%- INCLUDE common/sidebar -%]
28+
29+</div><!-- .timeline-wrapper -->
30+[% INCLUDE common/components/timeline %]
31+<script>
32+ timeline.run({el: "#timeline", target: "journal"});
33+</script>
34+[% END %]
Show on old repository browser