OSDN Git Service

* Model::Stories: fix full_text property
[newslash/newslash.git] / src / newslash_web / lib / Newslash / Model / Stories.pm
1 package Newslash::Model::Stories;
2 use Newslash::Model::Base -base;
3
4 use Newslash::Model::SlashDB;
5
6 use DateTime::Format::MySQL;
7
8 use Data::Dumper;
9 use DateTime;
10 use Newslash::Util::Formatters qw(datetime_to_string);
11
12 use constant FACULTIES => { 1000 => [qw(hits hitparade)] };
13
14 sub count {
15     my $self = shift;
16     my $join = 'LEFT JOIN firehose ON (stories.stoid = firehose.srcid AND firehose.type = "story")';
17     my $where = 'firehose.public != "no"';
18     return $self->generic_count(table => "stories",
19                                 target => "stoid",
20                                 timestamp => "time",
21                                 join => $join,
22                                 where => $where,
23                                 @_);
24 }
25
26 #========================================================================
27
28 =head2 select($query_type, $value)
29
30 get a story.
31
32 =over 4
33
34 =item Parameters
35
36 =over 4
37
38 =item $query_type
39
40 query key, "sid" or "stoid"
41
42 =item $value
43
44 value for query
45
46 =back
47
48 =item Return value
49
50 HASH of story contents
51
52 =back
53
54 =cut
55
56 sub select {
57     my $self = shift;
58     my $params = {@_};
59
60
61     my $unique_keys = { id => "stories.stoid",
62                         story_id => "stories.stoid",
63                         stoid => "stories.stoid",
64                         sid => "stories.sid",
65                       };
66     my $keys = { user_id => "stories.uid",
67                  uid => "stories.uid",
68                  topic_id => "stories.tid",
69                  tid => "stories.tid",
70                  discussion_id => "stories.discussion",
71                  commentcount => "stories.commentcount",
72                  hits => "stories.hits",
73                  submitter => "stories.submitter",
74                  create_time => "stories.time",
75                  update_time => "stories.last_update",
76                  public => "firehose.public",
77                };
78     my $datetime_keys = { create_time => "stories.time",
79                           update_time => "stories.last_update",
80                         };
81     my $timestamp = "stories.time";
82
83     my ($where_clause, $where_values, $unique) = $self->build_where_clause(unique_keys => $unique_keys,
84                                                                            keys => $keys,
85                                                                            datetime_keys => $datetime_keys,
86                                                                            timestamp => $timestamp,
87                                                                            params => $params);
88     my ($limit_clause, $limit_values) = $self->build_limit_clause(params => $params);
89     my ($orderby_clause, $orderby_values) = $self->build_order_by_clause(keys => $keys,
90                                                                          params => $params);
91
92     # TODO: give reasonable LIMIT Value...
93     $limit_clause = "LIMIT 50" if !$limit_clause;
94
95     # show future story?
96     my @where_clauses;
97     if (!$params->{show_future}) {
98         push @where_clauses, "stories.time <= NOW()";
99     }
100
101     # show non-public story?
102     if (!$params->{show_nonpublic} && !$params->{public}) {
103         push @where_clauses, "firehose.public != 'no'";
104     }
105
106     if (@where_clauses) {
107         if ($where_clause) {
108             $where_clause = $where_clause . " AND ";
109         }
110         else {
111             $where_clause = "WHERE ";
112         }
113         $where_clause = $where_clause . join(" AND ", @where_clauses);
114     }
115
116     my @attrs;
117     push @attrs, @$where_values, @$limit_values, @$orderby_values;
118
119     my $dbh = $self->connect_db;
120     my $sql = <<"EOSQL";
121 SELECT stories.*, story_text.*, users.nickname as author, firehose.public,
122        discussions.type AS discussion_type, discussions.commentcount AS comment_count
123   FROM stories
124     LEFT JOIN story_text ON stories.stoid = story_text.stoid
125     LEFT JOIN users ON stories.uid = users.uid
126     LEFT JOIN firehose 
127       ON (stories.stoid = firehose.srcid AND firehose.type = "story")
128     LEFT JOIN discussions ON firehose.discussion = discussions.id
129     $where_clause
130     $orderby_clause
131     $limit_clause
132 EOSQL
133
134     #warn($sql);
135     #warn(Dumper(@attrs));
136
137     my $sth = $dbh->prepare($sql);
138     $sth->execute(@attrs);
139     my $stories = $sth->fetchall_arrayref({});
140
141     if (!$stories) {
142         $self->disconnect_db();
143         return;
144     }
145     if (@$stories == 0) {
146         $self->disconnect_db();
147         return $unique ? undef : [];
148     }
149
150     # get tags
151     $sql = <<"EOSQL";
152 SELECT tags.*, tagnames.tagname, target.stoid
153   FROM (SELECT stories.stoid FROM stories 
154         LEFT JOIN firehose ON (stories.stoid = firehose.srcid AND firehose.type = "story")
155         $where_clause $orderby_clause $limit_clause) AS target
156     LEFT JOIN globjs
157       ON target.stoid = globjs.target_id
158     LEFT JOIN tags
159       ON globjs.globjid = tags.globjid
160     LEFT JOIN tagnames
161       ON tags.tagnameid = tagnames.tagnameid
162     WHERE globjs.gtid = 1
163 EOSQL
164
165     $sth = $dbh->prepare($sql);
166     $sth->execute(@attrs);
167     my $tags_table = $sth->fetchall_arrayref({});
168
169     # get topics
170     $sql = <<"EOSQL";
171 SELECT story_topics_rendered.*, story_topics_chosen.weight, topics.*
172   FROM (SELECT stories.stoid FROM stories
173         LEFT JOIN firehose ON (stories.stoid = firehose.srcid AND firehose.type = "story")
174         $where_clause $orderby_clause $limit_clause) AS target
175     LEFT JOIN story_topics_rendered
176       ON target.stoid = story_topics_rendered.stoid
177     LEFT JOIN story_topics_chosen 
178       ON story_topics_rendered.stoid = story_topics_chosen.stoid
179         AND story_topics_rendered.tid = story_topics_chosen.tid
180     LEFT JOIN topics
181       ON story_topics_rendered.tid = topics.tid
182 EOSQL
183
184     $sth = $dbh->prepare($sql);
185     $sth->execute(@attrs);
186     my $topics_table = $sth->fetchall_arrayref({});
187
188     # get params
189     $sql = <<"EOSQL";
190 SELECT story_param.*
191   FROM (SELECT stories.stoid FROM stories
192         LEFT JOIN firehose ON (stories.stoid = firehose.srcid AND firehose.type = "story")
193         $where_clause $orderby_clause $limit_clause) AS target
194     LEFT JOIN story_param
195       ON target.stoid = story_param.stoid
196 EOSQL
197
198     $sth = $dbh->prepare($sql);
199     $sth->execute(@attrs);
200     my $params_table = $sth->fetchall_arrayref({});
201
202     # done
203     $self->disconnect_db();
204
205
206     my $tags = {};
207     for my $tag (@$tags_table) {
208         my $stoid = $tag->{stoid};
209         if (!$tags->{$stoid}) {
210             $tags->{$stoid} = [];
211         }
212         push @{$tags->{$stoid}}, $tag;
213     }
214
215     my $topics = {};
216     for my $topic (@$topics_table) {
217         my $stoid = $topic->{stoid};
218         if (!$topics->{$stoid}) {
219             $topics->{$stoid} = [];
220         }
221         push @{$topics->{$stoid}}, $topic;
222     }
223
224     my $params = {};
225     for my $param (@$params_table) {
226         my $stoid = $param->{stoid};
227         if (!$params->{$stoid}) {
228             $params->{$stoid} = [];
229         }
230         push @{$params->{$stoid}}, $param;
231     }
232
233     for my $story (@$stories) {
234         my $stoid = $story->{stoid};
235         $story->{tags} = $tags->{$stoid} if $tags->{$stoid};
236         $story->{topics} = $topics->{$stoid} if $topics->{$stoid};
237         if ($params->{$stoid}) {
238             for my $param (@{$params->{$stoid}}) {
239                 $story->{$param->{name}} = $param->{value};
240             }
241         }
242         $self->_generalize($story, $params);
243     }
244
245     return $stories->[0] if $unique;
246     return $stories;
247 }
248
249 sub _check_and_regularize_params {
250     my ($self, $params) = @_;
251     my $msg;
252
253     if (defined $params->{title}) {
254         if (length($params->{title}) > $self->{options}->{Story}->{title_max_byte}) {
255             $msg = "title too long. max: $self->{options}->{Story}->{title_max_byte} bytes";
256             $self->set_error($msg, -1);
257             return;
258         }
259     }
260
261     $params->{commentstatus} = $params->{commentstatus} || $params->{comment_status} || "enabled";
262     if (defined $params->{commentstatus}) {
263         if (!grep /\A$params->{commentstatus}\z/, qw(disabled
264                                                      enabled
265                                                      friends_only
266                                                      friends_fof_only
267                                                      no_foe
268                                                      no_foe_eof 
269                                                      logged_in)) {
270             $msg = "invalid comment_status";
271             $self->set_error($msg, -1);
272             return;
273         }
274     }
275
276     # check timestamp. use ISO8601 style timestamp like: 2006-08-14T02:34:56-0600
277     if ($params->{time}) {
278         my $rex_timestamp = qr/
279                                   ^(\d+)-(\d+)-(\d+)\D+(\d+):(\d+):(\d+(?:\.\d+)?)   # datetime
280                                   (?:Z|([+-])(\d+):(\d+))?$                          # tz
281                               /xi;
282         if ($params->{time} =~ $rex_timestamp) {
283             $params->{time} = "$1-$2-$3 $4:$5:$6";
284         }
285     }
286
287     return 1;
288 }
289
290 sub _set_tags_from_topics {
291     my ($self, $user, $stoid, $topics) = @_;
292
293     return if !$stoid;
294     return if !$topics;
295
296     my $globjs = $self->new_instance_of("Newslash::Model::Globjs");
297     my $globj_id = $globjs->getGlobjidFromTargetIfExists("stories", $stoid);
298     if ($globj_id) {
299         # set tags
300         my $tags = $self->new_instance_of("Tags");
301         for my $tid (keys %$topics) {
302             my $ret = $tags->set_tag(uid => $user->{uid} || $user->{user_id},
303                                      tagname_id => $tid,
304                                      globj_id => $globj_id,
305                                      private => 0,
306                                     );
307             #warn "set_tag fault..." if !$ret
308         }
309     }
310     return $stoid;
311 }
312
313 =head2 update
314
315 this implementation uses old slash's updateStory($sid, $data),
316 $sid is takable sid or stoid.
317
318 =cut
319
320 sub update {
321     #my ($self, $params, $user, $extra_params, $opts) = @_;
322     my $self = shift;
323     my $params = {@_};
324
325     # check id
326     my $id = $params->{stoid} || $params->{story_id} || $params->{id};
327     if (!$id) {
328         $self->set_error("story id not given");
329         return;
330     }
331
332     # check params
333     return if !$self->_check_and_regularize_params($params);
334
335     my $stoid = $params->{stoid} || $params->{story_id} || $params->{id};
336     my $slash_db = Newslash::Model::SlashDB->new($self->{options});
337
338     my $sid = $slash_db->updateStory($stoid, $params);
339     return if !$sid;
340
341     $self->_set_tags_from_topics($params->{user}, $stoid, $params->{topics_chosen});
342     return $stoid;
343
344 }
345
346
347 =head2 create(\%params, $uid)
348
349 create a story.
350
351 =over 4
352
353 =item Parameters
354
355 =over 4
356
357 =item \%params
358
359 parameters
360
361 $params->{fhid}
362 $params->{subid}
363
364 =item $uid
365
366 author's uid
367
368 =back
369
370 =item Return value
371
372 stoid
373
374 =back
375
376 =cut
377
378 sub create {
379     #my ($self, $params, $user, $extra_params, $opts) = @_;
380     my $self = shift;
381     return if $self->check_readonly;
382
383     my $params = {@_};
384     my $user = $params->{user};
385
386     # check parameters
387     my $msg = "";
388     $msg = "no title" if !$params->{title};
389     $msg = "no introtext" if !$params->{introtext} || $params->{intro_text};
390     $msg = "no uid" if !$params->{uid} || $params->{user_id};
391     $msg = "no topics" if !defined $params->{topics_chosen};
392     $msg = "invalid user" if ref($user) ne 'HASH';
393
394     if (length($params->{title}) > $self->{options}->{Story}->{title_max_byte}) {
395         $msg = "title too long. max: $self->{options}->{Story}->{title_max_byte} bytes";
396     }
397
398     $params->{commentstatus} = $params->{commentstatus} || $params->{comment_status} || "enabled";
399     if (!grep /\A$params->{commentstatus}\z/, qw(disabled enabled friends_only friends_fof_only no_foe no_foe_eof logged_in)) {
400         $msg = "invalid comment_status";
401     }
402
403     # check timestamp. use ISO8601 style timestamp like: 2006-08-14T02:34:56-0600
404     if ($params->{time}) {
405         my $rex_timestamp = qr/
406                                   ^(\d+)-(\d+)-(\d+)\D+(\d+):(\d+):(\d+(?:\.\d+)?)   # datetime
407                                   (?:Z|([+-])(\d+):(\d+))?$                          # tz
408                               /xi;
409         if ($params->{time} =~ $rex_timestamp) {
410             $params->{time} = "$1-$2-$3 $4:$5:$6";
411         }
412     }
413
414     # check parameters finish
415     if (length($msg) > 0) {
416         $self->set_error($msg, -1);
417         return;
418     }
419
420     $params->{neverdisplay} ||= 0;
421
422     # createStory() deletes topics_chosen, so need to save here.
423     my $topics_chosen = $params->{topics_chosen};
424
425     my $slash_db = Newslash::Model::SlashDB->new($self->{options});
426     my ($sid, $stoid);
427     if ($params->{update}) {
428         $stoid = $params->{stoid} || $params->{story_id} || $params->{id};
429         $sid = $slash_db->updateStory($stoid, $params);
430         return if !$sid;
431     }
432     else {
433         ($sid, $stoid) = $slash_db->createStory($params);
434     }
435
436     my $globjs = $self->new_instance_of("Newslash::Model::Globjs");
437     my $globj_id = $globjs->getGlobjidFromTargetIfExists("stories", $params->{stoid});
438     if ($globj_id) {
439         # set tags
440         my $tags = $self->new_instance_of("Tags");
441         for my $tid (keys %$topics_chosen) {
442             my $ret = $tags->set_tag(uid => $user->{uid} || $user->{user_id},
443                                      tagname_id => $tid,
444                                      globj_id => $globj_id,
445                                      private => 0,
446                                     );
447             #warn "set_tag fault..." if !$ret
448         }
449     }
450     return $stoid;
451 }
452
453 sub createSid {
454     my ($self, $bogus_sid) = @_;
455     # yes, this format is correct, don't change it :-)
456     my $sidformat = '%02d/%02d/%02d/%02d%0d2%02d';
457     # Create a sid based on the current time.
458     my @lt;
459     my $start_time = time;
460     if ($bogus_sid) {
461         # If we were called being told that there's at
462         # least one sid that is invalid (already taken),
463         # then look backwards in time until we find it,
464         # then go one second further.
465         my $loops = 1000;
466         while (--$loops) {
467             $start_time--;
468             @lt = localtime($start_time);
469             $lt[5] %= 100; $lt[4]++; # year and month
470             last if $bogus_sid eq sprintf($sidformat, @lt[reverse 0..5]);
471         }
472         if ($loops) {
473             # Found the bogus sid by looking
474             # backwards.  Go one second further.
475             $start_time--;
476         } else {
477             # Something's wrong.  Skip ahead in
478             # time instead of back (not sure what
479             # else to do).
480             $start_time = time + 1;
481         }
482     }
483     @lt = localtime($start_time);
484     $lt[5] %= 100; $lt[4]++; # year and month
485     return sprintf($sidformat, @lt[reverse 0..5]);
486 }
487
488
489 =head2 get_histories
490
491 =cut
492
493 sub get_histories {
494 }
495
496 =head2 get_related_items($stoid)
497
498 get related links.
499
500 =over 4
501
502 =item Parameters
503
504 =over 4
505
506 =item $stoid
507
508 story id
509
510 =back
511
512 =item Return value
513
514 ARRAY of related links
515
516 =back
517
518 =cut
519
520 sub get_related_items {
521     my $self = shift;
522     my $params = {@_};
523     my $stoid = $params->{stoid} || $params->{story_id} || $params->{id};
524     return if !$stoid;
525
526     my $dbh = $self->connect_db;
527
528     my $sql = <<"EOSQL";
529 SELECT related.*, 
530        story_text.title as title2,
531        firehose.srcid,
532        topics.*
533   FROM (
534     SELECT * FROM related_stories
535       WHERE stoid = ?
536       ORDER BY ordernum ASC
537     ) AS related
538   LEFT JOIN story_text ON story_text.stoid = related.rel_stoid
539   LEFT JOIN firehose ON firehose.id = related.fhid
540   LEFT JOIN stories ON stories.stoid = related.rel_stoid
541   LEFT JOIN topics ON topics.tid = stories.tid
542 EOSQL
543
544     my $sth = $dbh->prepare($sql);
545     $sth->execute($stoid);
546     my $related = $sth->fetchall_arrayref({});
547     $self->disconnect_db();
548
549     for my $r (@$related) {
550         $r->{title} = $r->{title2} unless $r->{title};
551         if ($r->{rel_sid}) {
552             $r->{type} = "story";
553             $r->{key_id} = $r->{rel_sid};
554         } else {
555             $r->{type} = "submission";
556             $r->{key_id} = $r->{srcid};
557         }
558         $r->{primary_topic} = {};
559         $r->{primary_topic}->{tid} = $r->{tid};
560         for my $k (qw{keyword textname series image width height
561                       submittable searchable storypickable usesprite}) {
562             next if !$r->{$k};
563             $r->{primary_topic}->{$k} = $r->{$k};
564             delete $r->{$k};
565         }
566     }
567
568     return $related;
569 }
570
571 =head2 parameters($stoid)
572
573 get story parameters.
574
575 =over 4
576
577 =item Parameters
578
579 =over 4
580
581 =item $stoid
582
583 story id
584
585 =back
586
587 =item Return value
588
589 HASH of parameters
590
591 =back
592
593 =cut
594
595 sub parameters {
596     my ($self, $stoid) = @_;
597
598     my $dbh = $self->connect_db;
599
600     my $sql = <<"EOSQL";
601 SELECT * FROM story_param WHERE stoid = ?
602 EOSQL
603
604     my $sth = $dbh->prepare($sql);
605     $sth->execute($stoid);
606     my $params = $sth->fetchall_hashref('name');
607     $sth->finish;
608
609     # get next/prev story info
610     if ($params->{next_stoid} || $params->{prev_stoid}) {
611         $sql = <<"EOSQL";
612 SELECT stories.*, story_text.* FROM stories JOIN story_text ON stories.stoid = story_text.stoid WHERE stories.stoid = ? OR stories.stoid = ?
613 EOSQL
614         $sth = $dbh->prepare($sql);
615         my $next = $params->{next_stoid}->{value} || 0;
616         my $prev = $params->{prev_stoid}->{value} || 0;
617         $sth->execute($next, $prev);
618         my $sids = $sth->fetchall_hashref('stoid');
619         $params->{next_stoid}->{story} = $sids->{$next} if $sids->{$next};
620         $params->{prev_stoid}->{story} = $sids->{$prev} if $sids->{$prev};
621         $sth->finish;
622     }
623
624     $self->disconnect_db();
625     return $params;
626 }
627
628 #========================================================================
629
630 =head2 set_dirty($key, $id)
631
632 set writestatus dirty for the story.
633
634 =over 4
635
636 =item Parameters
637
638 =over 4
639
640 =item $key
641
642 'stoid'
643
644 =item $id
645
646 id of the story
647
648 =back
649
650 =item Return value
651
652 1/0
653
654 =back
655
656 =cut
657
658 sub set_dirty {
659     my ($self, $key, $id) = @_;
660     return if $self->check_readonly;
661
662     my $stoid;
663     if ($key eq 'stoid') {
664         $stoid = $id;
665     }
666     else {
667         return;
668     }
669
670     my $dbh = $self->connect_db;
671     my $sql = <<"EOSQL";
672 INSERT INTO story_dirty (stoid) VALUES (?)
673 EOSQL
674
675     my $rs = $dbh->do($sql, undef, $stoid);
676     if (!$rs) {
677         return;
678     }
679     return 1;
680 }
681
682 sub _generalize {
683     my ($self, $story, $params) = @_;
684     $params ||= {};
685
686     # NTO-nized
687     $story->{id} = $story->{stoid};
688     $story->{story_id} = $story->{stoid};
689     $story->{create_time} = $story->{time};
690     $story->{update_time} = $story->{last_update};
691
692     for my $t (@{$story->{topics}}) {
693         if ($t->{tid} && $t->{tid} == $story->{tid}) {
694             $story->{primary_topic} = $t;
695         }
696     }
697
698     $story->{content_type} = "story";
699     $story->{intro_text} = $story->{introtext};
700     $story->{bodytext} ||= "";
701     $story->{body_text} = $story->{bodytext};
702     if ($story->{body_text}) {
703         $story->{full_text} = join("\n", $story->{intro_text}, $story->{body_text});
704     }
705     else {
706         $story->{full_text} = $story->{intro_text};
707     }
708     $story->{fulltext} = $story->{full_text};
709
710     $story->{discussion_id} = $story->{discussion};
711
712     # no public flag given, public is 'yes'
713     $story->{public} = 'yes' if !$story->{public};
714
715 }
716
717 # delete story from database
718 # this method is for test purpose only.
719 sub hard_delete {
720     my $self = shift;
721     return if $self->check_readonly;
722     my $params = {@_};
723
724     my $stoid = $params->{story_id};
725     return if !$stoid;
726
727     my $error = 0;
728     my $sql;
729
730     my $dbh = $self->connect_db({AutoCommit => 0,});
731     for my $table (qw(stories story_param story_text story_topics_chosen story_topics_rendered)) {
732         my $sql = "DELETE FROM $table WHERE stoid = ?";
733         my $rs = $dbh->do($sql, undef, $stoid);
734         if (!defined $rs || $rs == 0) {
735             Mojo::Log->new->warn("DELETE FROM $table failed. stoid is $stoid.");
736             $error = 1;
737         }
738     }
739     $dbh->commit;
740     $self->disconnect_db;
741
742     return !$error;
743
744     # delete firehose item
745     # delete firehose_text item
746     # delete firehose_topics_rendererd item
747     # delete globjs
748     # delete tags
749
750     return;
751 }
752
753 1;