1 package Newslash::Model::Stories;
2 use Newslash::Model::Base -base;
4 use Newslash::Model::SlashDB;
6 use Newslash::Model::Stories::Text;
7 use Newslash::Model::Stories::RelatedStories;
10 use DateTime::Format::MySQL;
11 use DateTime::Format::ISO8601;
12 use List::Util q(any);
23 datetime => [qw(time last_update day_published
24 stuckendtime archive_last_update
26 other => [qw(uid dept hits discussion primaryskid
27 tid submitter commentcount hitparade
29 qid body_length word_count sponsor
30 stuck stuckpos fakeemail homepage
32 aliases => { user_id => "uid",
39 use constant FACULTIES => { 1000 => [qw(hits hitparade)] };
43 my $join = 'LEFT JOIN firehose ON (stories.stoid = firehose.srcid AND firehose.type = "story")';
44 my $where = 'firehose.public != "no"';
45 return $self->generic_count(table => "stories",
54 sub text { return shift->_get_model('::Stories::Text'); }
55 sub related_stories { return shift->_get_model('::Stories::RelatedStories'); }
58 my ($self, $class) = @_;
59 $self->{sub_models} ||= {};
60 if (!$self->{sub_models}->{$class}) {
61 $self->{sub_models}->{$class} = $self->new_instance_of($class);
64 if ($self->transaction_mode && !$self->{sub_models}->{$class}->transaction_mode) {
65 $self->{sub_models}->{$class}->use_transaction($self->{_tr_dbh});
68 return $self->{sub_models}->{$class};
71 #========================================================================
73 =head2 select($query_type, $value)
85 query key, "sid" or "stoid"
95 HASH of story contents
106 my $unique_keys = { id => "stories.stoid",
107 story_id => "stories.stoid",
108 stoid => "stories.stoid",
109 sid => "stories.sid",
111 my $keys = { user_id => "stories.uid",
112 uid => "stories.uid",
113 topic_id => "stories.tid",
114 tid => "stories.tid",
115 discussion_id => "stories.discussion",
116 commentcount => "stories.commentcount",
117 hits => "stories.hits",
118 submitter => "stories.submitter",
119 create_time => "stories.time",
120 update_time => "stories.last_update",
121 public => "firehose.public",
123 my $datetime_keys = { create_time => "stories.time",
124 update_time => "stories.last_update",
126 my $timestamp = "stories.time";
128 my ($where_clause, $where_values, $unique) = $self->build_where_clause(unique_keys => $unique_keys,
130 datetime_keys => $datetime_keys,
131 timestamp => $timestamp,
133 my ($limit_clause, $limit_values) = $self->build_limit_clause(params => $params);
134 my ($orderby_clause, $orderby_values) = $self->build_order_by_clause(keys => $keys,
137 # TODO: give reasonable LIMIT Value...
138 $limit_clause = "LIMIT 50" if !$limit_clause;
142 if ($params->{hide_future}) {
143 push @where_clauses, "stories.time <= NOW()";
146 # hide non-public story?
147 if ($params->{public_only}) {
148 push @where_clauses, "firehose.public != 'no'";
151 if (@where_clauses) {
153 $where_clause = $where_clause . " AND ";
156 $where_clause = "WHERE ";
158 $where_clause = $where_clause . join(" AND ", @where_clauses);
162 push @attrs, @$where_values, @$limit_values, @$orderby_values;
164 my $dbh = $self->connect_db;
166 SELECT stories.*, story_text.*, users.nickname as author, firehose.public,
167 discussions.type AS discussion_type, discussions.commentcount AS comment_count
169 LEFT JOIN story_text ON stories.stoid = story_text.stoid
170 LEFT JOIN users ON stories.uid = users.uid
172 ON (stories.stoid = firehose.srcid AND firehose.type = "story")
173 LEFT JOIN discussions ON firehose.discussion = discussions.id
180 #warn(Dumper(@attrs));
182 my $sth = $dbh->prepare($sql);
183 $sth->execute(@attrs);
184 my $stories = $sth->fetchall_arrayref({});
187 $self->disconnect_db();
190 if (@$stories == 0) {
191 $self->disconnect_db();
192 return $unique ? undef : [];
197 SELECT tags.*, tagnames.tagname, target.stoid
198 FROM (SELECT stories.stoid FROM stories
199 LEFT JOIN firehose ON (stories.stoid = firehose.srcid AND firehose.type = "story")
200 $where_clause $orderby_clause $limit_clause) AS target
202 ON target.stoid = globjs.target_id
204 ON globjs.globjid = tags.globjid
206 ON tags.tagnameid = tagnames.tagnameid
207 WHERE globjs.gtid = 1
210 $sth = $dbh->prepare($sql);
211 $sth->execute(@attrs);
212 my $tags_table = $sth->fetchall_arrayref({});
216 SELECT story_topics_rendered.*, story_topics_chosen.weight, topics.*
217 FROM (SELECT stories.stoid FROM stories
218 LEFT JOIN firehose ON (stories.stoid = firehose.srcid AND firehose.type = "story")
219 $where_clause $orderby_clause $limit_clause) AS target
220 LEFT JOIN story_topics_rendered
221 ON target.stoid = story_topics_rendered.stoid
222 LEFT JOIN story_topics_chosen
223 ON story_topics_rendered.stoid = story_topics_chosen.stoid
224 AND story_topics_rendered.tid = story_topics_chosen.tid
226 ON story_topics_rendered.tid = topics.tid
229 $sth = $dbh->prepare($sql);
230 $sth->execute(@attrs);
231 my $topics_table = $sth->fetchall_arrayref({});
236 FROM (SELECT stories.stoid FROM stories
237 LEFT JOIN firehose ON (stories.stoid = firehose.srcid AND firehose.type = "story")
238 $where_clause $orderby_clause $limit_clause) AS target
239 LEFT JOIN story_param
240 ON target.stoid = story_param.stoid
243 $sth = $dbh->prepare($sql);
244 $sth->execute(@attrs);
245 my $params_table = $sth->fetchall_arrayref({});
248 $self->disconnect_db();
252 for my $tag (@$tags_table) {
253 my $stoid = $tag->{stoid};
254 if (!$tags->{$stoid}) {
255 $tags->{$stoid} = [];
257 push @{$tags->{$stoid}}, $tag;
261 for my $topic (@$topics_table) {
262 my $stoid = $topic->{stoid};
263 if (!$topics->{$stoid}) {
264 $topics->{$stoid} = [];
266 push @{$topics->{$stoid}}, $topic;
270 for my $param (@$params_table) {
271 my $stoid = $param->{stoid};
272 if (!$params->{$stoid}) {
273 $params->{$stoid} = [];
275 push @{$params->{$stoid}}, $param;
278 for my $story (@$stories) {
279 my $stoid = $story->{stoid};
280 $story->{tags} = $tags->{$stoid} || [];
281 $story->{topics} = $topics->{$stoid} || [];
282 if ($params->{$stoid}) {
283 for my $param (@{$params->{$stoid}}) {
284 $story->{$param->{name}} = $param->{value};
287 $self->_generalize($story, $params);
290 return $stories->[0] if $unique;
294 sub _check_and_regularize_params {
295 my ($self, $params) = @_;
298 if (defined $params->{title}) {
299 if (length($params->{title}) > $self->{options}->{Story}->{title_max_byte}) {
300 $msg = "title too long. max: $self->{options}->{Story}->{title_max_byte} bytes";
301 $self->set_error($msg, -1);
306 $params->{commentstatus} = $params->{commentstatus} || $params->{comment_status} || "enabled";
307 if (defined $params->{commentstatus}) {
308 if (!grep /\A$params->{commentstatus}\z/, qw(disabled
315 $msg = "invalid comment_status";
316 $self->set_error($msg, -1);
321 # check timestamp. use ISO8601 style timestamp like: 2006-08-14T02:34:56-0600
322 if ($params->{time}) {
323 my $rex_timestamp = qr/
324 ^(\d+)-(\d+)-(\d+)\D+(\d+):(\d+):(\d+(?:\.\d+)?) # datetime
325 (?:Z|([+-])(\d+):(\d+))?$ # tz
327 if ($params->{time} =~ $rex_timestamp) {
328 $params->{time} = "$1-$2-$3 $4:$5:$6";
335 sub _set_tags_from_topics {
336 my ($self, $user, $stoid, $topics) = @_;
341 my $globjs = $self->new_instance_of("Newslash::Model::Globjs");
342 my $globj_id = $globjs->getGlobjidFromTargetIfExists("stories", $stoid);
345 my $tags = $self->new_instance_of("Tags");
346 for my $tid (keys %$topics) {
347 my $ret = $tags->set_tag(uid => $user->{uid} || $user->{user_id},
349 globj_id => $globj_id,
352 #warn "set_tag fault..." if !$ret
360 this implementation uses old slash's updateStory($sid, $data),
361 $sid is takable sid or stoid.
366 #my ($self, $params, $user, $extra_params, $opts) = @_;
369 my $user = $params->{user};
373 my $stoid = $params->{stoid} || $params->{story_id} || $params->{id};
374 my $sid = $params->{sid};
377 $story = $self->select(stoid => $stoid);
380 $story = $self->select(sid => $sid);
384 $sid = $story->{sid};
385 $stoid = $story->{stoid};
389 $self->last_error("no story given");
394 return if !$self->_check_and_regularize_params($params);
396 my $slash_db = Newslash::Model::SlashDB->new($self->{options});
398 my $add_related = $params->{add_related};
399 delete $params->{add_related} if $add_related;
400 my $add_tags = $params->{add_tags};
401 delete $params->{add_tags} if $add_tags;
403 $stoid = $slash_db->updateStory($stoid, $params);
406 $self->_set_tags_from_topics($params->{user}, $stoid, $params->{topics_chosen});
410 my $tags = $self->new_instance_of("Tags");
411 my $globjs = $self->new_instance_of("Newslash::Model::Globjs");
412 my $globj_id = $globjs->getGlobjidFromTargetIfExists("stories", $params->{stoid});
413 for my $tag (@$add_tags) {
414 my $rs = $tags->add(uid => $user->{uid} || $user->{user_id},
415 globj_id => $globj_id,
418 $self->logger->warn("Stories::update: tag $tag set failed: " . $tags->last_error);
423 # insert related story
425 for my $related_sid (@$add_related) {
426 my $rs = $self->add_related_story(sid => $sid,
427 related_sid => $related_sid);
429 $self->logger->warn("Stories::update: insert related story $related_sid to $sid failed: " . $self->last_error);
432 $rs = $self->add_related_story(sid => $related_sid,
433 related_sid => $sid);
435 $self->logger->warn("Stories::update: insert related story $sid to $related_sid failed: " . $self->last_error);
446 return $self->generic_update(params => $params);
450 =head2 create(\%params, $uid)
482 #my ($self, $params, $user, $extra_params, $opts) = @_;
484 return if $self->check_readonly;
487 my $user = $params->{user};
491 $msg = "no_title" if !$params->{title};
492 $msg = "no_introtext" if !$params->{introtext} || $params->{intro_text};
493 $msg = "no_topics" if !defined $params->{topics_chosen};
494 $msg = "invalid_user" if !defined $user->{uid};
496 if (length($params->{title}) > $self->{options}->{Story}->{title_max_byte}) {
497 $msg = "title too long. max: $self->{options}->{Story}->{title_max_byte} bytes";
500 $params->{commentstatus} = $params->{commentstatus} || $params->{comment_status} || "enabled";
501 if (!grep /\A$params->{commentstatus}\z/, qw(disabled enabled friends_only friends_fof_only no_foe no_foe_eof logged_in)) {
502 $msg = "invalid comment_status";
505 # check timestamp. use ISO8601 style timestamp like: 2006-08-14T02:34:56-0600
506 if ($params->{time}) {
507 my $rex_timestamp = qr/
508 ^(\d+)-(\d+)-(\d+)[^ 0-9]+(\d+):(\d+):(\d+(?:\.\d+)?) # datetime
509 (?:Z|([+-])(\d+):(\d+))?$ # tz
511 if ($params->{time} =~ $rex_timestamp) {
512 my $dt = DateTime::Format::ISO8601->parse_datetime($params->{time});
513 $params->{time} = DateTime::Format::MySQL->format_datetime($dt);
517 # check parameters finish
518 if (length($msg) > 0) {
519 $self->set_error($msg, -1);
522 $params->{neverdisplay} ||= 0;
523 $params->{submitter} ||= $user->{uid};
524 $params->{uid} = $user->{uid};
526 # createStory() deletes topics_chosen, so need to save here.
527 my $topics_chosen = $params->{topics_chosen};
529 my $add_related = $params->{add_related};
530 delete $params->{add_related} if $add_related;
531 my $add_tags = $params->{add_tags};
532 delete $params->{add_tags} if $add_tags;
534 my $slash_db = Newslash::Model::SlashDB->new($self->{options});
536 if ($params->{update}) {
537 $stoid = $params->{stoid} || $params->{story_id} || $params->{id};
538 $sid = $slash_db->updateStory($stoid, $params);
539 $self->set_error("updateStory failed");
543 ($sid, $stoid) = $slash_db->createStory($params);
546 my $globjs = $self->new_instance_of("Newslash::Model::Globjs");
547 my $globj_id = $globjs->getGlobjidFromTargetIfExists("stories", $params->{stoid});
550 my $tags = $self->new_instance_of("Tags");
552 for my $tid (keys %$topics_chosen) {
553 my $rs = $tags->add(uid => $user->{uid} || $user->{user_id},
555 globj_id => $globj_id,
559 $self->logger->warn("Stories::create: tag $tid set failed: " . $tags->last_error);
566 for my $tag (@$add_tags) {
567 my $rs = $tags->add(uid => $user->{uid} || $user->{user_id},
568 globj_id => $globj_id,
571 $self->logger->warn("Stories::create: tag $tag set failed: " . $tags->last_error);
576 # insert related story
578 for my $related_sid (@$add_related) {
579 my $rs = $self->add_related_story(sid => $sid,
580 related_sid => $related_sid);
582 $self->logger->warn("insert related story failed: " . $self->last_error);
585 $rs = $self->add_related_story(sid => $related_sid,
586 related_sid => $sid);
588 $self->logger->warn("Stories::create: insert related story failed: " . $self->last_error);
593 # TODO: add firehose item to related_story
595 return wantarray ? ($sid, $stoid) : $stoid;
601 return $self->generic_insert(params => $params);
605 my ($self, @params) = @_;
606 my $params = {@params};
607 my $dt = $params->{base_datetime} || DateTime->now;
609 # create sid from timestamp
610 # my $sid_format = '%02d/%02d/%02d/%02d%0d2%02d';
611 my $sid_format = '%y/%m/%d/%H%M%S';
612 my $sid = $dt->strftime($sid_format);
614 # insert blank story with given sid
615 my $dbh = $self->connect_db;
616 my $sql = "INSERT INTO stories (sid) VALUES (?)";
618 my $n = 100; # retry 100 times
620 my $rs = $dbh->do($sql, undef, $sid);
622 $self->set_error("sid_insert_error", -1);
626 my $stoid = $dbh->last_insert_id(undef, undef, undef, undef);
627 $self->disconnect_db;
628 return ($sid, $stoid);
631 # allocate failed, so recreate sid
632 $dt->subtract( seconds => 1 );
633 $sid = $dt->strftime($sid_format);
635 $self->set_error("sid_allocate_failed", -1);
636 $self->disconnect_db;
643 my ($self, $bogus_sid) = @_;
644 # yes, this format is correct, don't change it :-)
645 my $sidformat = '%02d/%02d/%02d/%02d%0d2%02d';
646 # Create a sid based on the current time.
648 my $start_time = time;
650 # If we were called being told that there's at
651 # least one sid that is invalid (already taken),
652 # then look backwards in time until we find it,
653 # then go one second further.
657 @lt = localtime($start_time);
658 $lt[5] %= 100; $lt[4]++; # year and month
659 last if $bogus_sid eq sprintf($sidformat, @lt[reverse 0..5]);
662 # Found the bogus sid by looking
663 # backwards. Go one second further.
666 # Something's wrong. Skip ahead in
667 # time instead of back (not sure what
669 $start_time = time + 1;
672 @lt = localtime($start_time);
673 $lt[5] %= 100; $lt[4]++; # year and month
674 return sprintf($sidformat, @lt[reverse 0..5]);
685 =head2 get_related_items($stoid)
703 ARRAY of related links
709 sub get_related_items {
712 my $stoid = $params->{stoid} || $params->{story_id} || $params->{id};
715 my $dbh = $self->connect_db;
719 story_text.title as title2,
724 SELECT * FROM related_stories
726 ORDER BY ordernum ASC
728 LEFT JOIN story_text ON story_text.stoid = related.rel_stoid
729 LEFT JOIN firehose ON firehose.id = related.fhid
730 LEFT JOIN stories ON stories.sid = related.rel_sid
731 LEFT JOIN topics ON topics.tid = stories.tid
734 my $sth = $dbh->prepare($sql);
735 $sth->execute($stoid);
736 my $related = $sth->fetchall_arrayref({});
737 $self->disconnect_db();
739 for my $r (@$related) {
740 $r->{create_time} = $r->{time};
741 $r->{title} = $r->{title2} unless $r->{title};
743 $r->{type} = "story";
744 $r->{key_id} = $r->{rel_sid};
746 $r->{type} = "submission";
747 $r->{key_id} = $r->{srcid};
749 $r->{primary_topic} = {};
750 $r->{primary_topic}->{tid} = $r->{tid};
751 for my $k (qw{keyword textname series image width height
752 submittable searchable storypickable usesprite}) {
754 $r->{primary_topic}->{$k} = $r->{$k};
762 sub add_related_story {
766 my $story = $args->{story};
768 if ($args->{stoid}) {
769 $story = $self->select(stoid => $args->{stoid});
770 } elsif ($args->{sid}) {
771 $story = $self->select(sid => $args->{sid});
775 if (!$story || !$story->{stoid} || !$story->{sid}) {
776 $self->last_error("cannot get target story");
780 my $related = $args->{related};
782 if ($args->{related_stoid}) {
783 $related = $self->select(stoid => $args->{related_stoid});
784 } elsif ($args->{related_sid}) {
785 $related = $self->select(sid => $args->{related_sid});
789 if (!$related || !$related->{stoid} || !$related->{sid} ) {
790 $self->last_error("cannot get related story");
794 # get related item count
795 my $dbh = $self->start_transaction;
796 my $sql = "SELECT rel_stoid FROM related_stories WHERE stoid = ?";
797 my $stoids = $dbh->do($sql, undef, $story->{stoid});
798 $stoids = [] if ref($stoids) ne "ARRAY";
800 # check if related story is already set
801 if (any { $_ eq $related->{stoid} } @$stoids) {
806 my $count = @$stoids;
807 my $rs = $self->related_stories->insert(stoid => $story->{stoid},
808 rel_stoid => $related->{stoid},
809 rel_sid => $related->{sid},
810 ordernum => $count + 1);
813 $self->last_error("insert failed: " . $self->related_stories->last_error);
821 =head2 parameters($stoid)
823 get story parameters.
846 my ($self, $stoid) = @_;
848 my $dbh = $self->connect_db;
851 SELECT * FROM story_param WHERE stoid = ?
854 my $sth = $dbh->prepare($sql);
855 $sth->execute($stoid);
856 my $params = $sth->fetchall_hashref('name');
859 # get next/prev story info
860 if ($params->{next_stoid} || $params->{prev_stoid}) {
862 SELECT stories.*, story_text.* FROM stories JOIN story_text ON stories.stoid = story_text.stoid WHERE stories.stoid = ? OR stories.stoid = ?
864 $sth = $dbh->prepare($sql);
865 my $next = $params->{next_stoid}->{value} || 0;
866 my $prev = $params->{prev_stoid}->{value} || 0;
867 $sth->execute($next, $prev);
868 my $sids = $sth->fetchall_hashref('stoid');
869 $params->{next_stoid}->{story} = $sids->{$next} if $sids->{$next};
870 $params->{prev_stoid}->{story} = $sids->{$prev} if $sids->{$prev};
874 $self->disconnect_db();
878 #========================================================================
880 =head2 set_dirty($key, $id)
882 set writestatus dirty for the story.
909 my ($self, $key, $id) = @_;
910 return if $self->check_readonly;
913 if ($key eq 'stoid') {
920 my $dbh = $self->connect_db;
922 INSERT INTO story_dirty (stoid) VALUES (?)
925 my $rs = $dbh->do($sql, undef, $stoid);
933 my ($self, $story, $params) = @_;
937 $story->{id} = $story->{stoid};
938 $story->{story_id} = $story->{stoid};
939 $story->{create_time} = $story->{time};
940 $story->{update_time} = $story->{last_update};
942 for my $t (@{$story->{topics}}) {
943 if ($t->{tid} && $t->{tid} == $story->{tid}) {
944 $story->{primary_topic} = $t;
948 $story->{content_type} = "story";
949 $story->{intro_text} = $story->{introtext};
950 $story->{bodytext} ||= "";
951 $story->{body_text} = $story->{bodytext};
952 if ($story->{body_text}) {
953 $story->{full_text} = join("\n", $story->{intro_text}, $story->{body_text});
956 $story->{full_text} = $story->{intro_text};
958 $story->{fulltext} = $story->{full_text};
960 $story->{discussion_id} = $story->{discussion};
962 # no public flag given, public is 'yes'
963 $story->{public} = 'yes' if !$story->{public};
967 # delete story from database
968 # this method is for test purpose only.
971 return if $self->check_readonly;
974 my $stoid = $params->{story_id};
980 #my $dbh = $self->connect_db({AutoCommit => 0,});
981 my $dbh = $self->start_transaction;
982 for my $table (qw(stories story_param story_text story_topics_chosen story_topics_rendered)) {
983 my $sql = "DELETE FROM $table WHERE stoid = ?";
984 my $rs = $dbh->do($sql, undef, $stoid);
985 if (!defined $rs || $rs == 0) {
986 Mojo::Log->new->warn("DELETE FROM $table failed. stoid is $stoid.");
991 # delete from firehose
992 my $firehose = $self->new_instance_of("Firehose");
993 $firehose->hard_delete("story", $stoid);