1 package Newslash::Model::Stories;
2 use Newslash::Model::Base -base;
4 use Newslash::Model::SlashDB;
6 use DateTime::Format::MySQL;
10 use Newslash::Util::Formatters qw(datetime_to_string);
12 use constant FACULTIES => { 1000 => [qw(hits hitparade)] };
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",
26 #========================================================================
28 =head2 select($query_type, $value)
40 query key, "sid" or "stoid"
50 HASH of story contents
61 my $unique_keys = { id => "stories.stoid",
62 story_id => "stories.stoid",
63 stoid => "stories.stoid",
66 my $keys = { user_id => "stories.uid",
68 topic_id => "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",
78 my $datetime_keys = { create_time => "stories.time",
79 update_time => "stories.last_update",
81 my $timestamp = "stories.time";
83 my ($where_clause, $where_values, $unique) = $self->build_where_clause(unique_keys => $unique_keys,
85 datetime_keys => $datetime_keys,
86 timestamp => $timestamp,
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,
92 # TODO: give reasonable LIMIT Value...
93 $limit_clause = "LIMIT 50" if !$limit_clause;
97 if (!$params->{show_future}) {
98 push @where_clauses, "stories.time <= NOW()";
101 # show non-public story?
102 if (!$params->{show_nonpublic} && !$params->{public}) {
103 push @where_clauses, "firehose.public != 'no'";
106 if (@where_clauses) {
108 $where_clause = $where_clause . " AND ";
111 $where_clause = "WHERE ";
113 $where_clause = $where_clause . join(" AND ", @where_clauses);
117 push @attrs, @$where_values, @$limit_values, @$orderby_values;
119 my $dbh = $self->connect_db;
121 SELECT stories.*, story_text.*, users.nickname as author, firehose.public,
122 discussions.type AS discussion_type, discussions.commentcount AS comment_count
124 LEFT JOIN story_text ON stories.stoid = story_text.stoid
125 LEFT JOIN users ON stories.uid = users.uid
127 ON (stories.stoid = firehose.srcid AND firehose.type = "story")
128 LEFT JOIN discussions ON firehose.discussion = discussions.id
135 #warn(Dumper(@attrs));
137 my $sth = $dbh->prepare($sql);
138 $sth->execute(@attrs);
139 my $stories = $sth->fetchall_arrayref({});
142 $self->disconnect_db();
145 if (@$stories == 0) {
146 $self->disconnect_db();
147 return $unique ? undef : [];
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
157 ON target.stoid = globjs.target_id
159 ON globjs.globjid = tags.globjid
161 ON tags.tagnameid = tagnames.tagnameid
162 WHERE globjs.gtid = 1
165 $sth = $dbh->prepare($sql);
166 $sth->execute(@attrs);
167 my $tags_table = $sth->fetchall_arrayref({});
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
181 ON story_topics_rendered.tid = topics.tid
184 $sth = $dbh->prepare($sql);
185 $sth->execute(@attrs);
186 my $topics_table = $sth->fetchall_arrayref({});
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
198 $sth = $dbh->prepare($sql);
199 $sth->execute(@attrs);
200 my $params_table = $sth->fetchall_arrayref({});
203 $self->disconnect_db();
207 for my $tag (@$tags_table) {
208 my $stoid = $tag->{stoid};
209 if (!$tags->{$stoid}) {
210 $tags->{$stoid} = [];
212 push @{$tags->{$stoid}}, $tag;
216 for my $topic (@$topics_table) {
217 my $stoid = $topic->{stoid};
218 if (!$topics->{$stoid}) {
219 $topics->{$stoid} = [];
221 push @{$topics->{$stoid}}, $topic;
225 for my $param (@$params_table) {
226 my $stoid = $param->{stoid};
227 if (!$params->{$stoid}) {
228 $params->{$stoid} = [];
230 push @{$params->{$stoid}}, $param;
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};
242 $self->_generalize($story, $params);
245 return $stories->[0] if $unique;
249 sub _check_and_regularize_params {
250 my ($self, $params) = @_;
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);
261 $params->{commentstatus} = $params->{commentstatus} || $params->{comment_status} || "enabled";
262 if (defined $params->{commentstatus}) {
263 if (!grep /\A$params->{commentstatus}\z/, qw(disabled
270 $msg = "invalid comment_status";
271 $self->set_error($msg, -1);
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
282 if ($params->{time} =~ $rex_timestamp) {
283 $params->{time} = "$1-$2-$3 $4:$5:$6";
290 sub _set_tags_from_topics {
291 my ($self, $user, $stoid, $topics) = @_;
296 my $globjs = $self->new_instance_of("Newslash::Model::Globjs");
297 my $globj_id = $globjs->getGlobjidFromTargetIfExists("stories", $stoid);
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},
304 globj_id => $globj_id,
307 #warn "set_tag fault..." if !$ret
315 this implementation uses old slash's updateStory($sid, $data),
316 $sid is takable sid or stoid.
321 #my ($self, $params, $user, $extra_params, $opts) = @_;
326 my $id = $params->{stoid} || $params->{story_id} || $params->{id};
328 $self->set_error("story id not given");
333 return if !$self->_check_and_regularize_params($params);
335 my $stoid = $params->{stoid} || $params->{story_id} || $params->{id};
336 my $slash_db = Newslash::Model::SlashDB->new($self->{options});
338 my $sid = $slash_db->updateStory($stoid, $params);
341 $self->_set_tags_from_topics($params->{user}, $stoid, $params->{topics_chosen});
347 =head2 create(\%params, $uid)
379 #my ($self, $params, $user, $extra_params, $opts) = @_;
381 return if $self->check_readonly;
384 my $user = $params->{user};
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';
394 if (length($params->{title}) > $self->{options}->{Story}->{title_max_byte}) {
395 $msg = "title too long. max: $self->{options}->{Story}->{title_max_byte} bytes";
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";
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
409 if ($params->{time} =~ $rex_timestamp) {
410 $params->{time} = "$1-$2-$3 $4:$5:$6";
414 # check parameters finish
415 if (length($msg) > 0) {
416 $self->set_error($msg, -1);
420 $params->{neverdisplay} ||= 0;
422 # createStory() deletes topics_chosen, so need to save here.
423 my $topics_chosen = $params->{topics_chosen};
425 my $slash_db = Newslash::Model::SlashDB->new($self->{options});
427 if ($params->{update}) {
428 $stoid = $params->{stoid} || $params->{story_id} || $params->{id};
429 $sid = $slash_db->updateStory($stoid, $params);
433 ($sid, $stoid) = $slash_db->createStory($params);
436 my $globjs = $self->new_instance_of("Newslash::Model::Globjs");
437 my $globj_id = $globjs->getGlobjidFromTargetIfExists("stories", $params->{stoid});
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},
444 globj_id => $globj_id,
447 #warn "set_tag fault..." if !$ret
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.
459 my $start_time = time;
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.
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]);
473 # Found the bogus sid by looking
474 # backwards. Go one second further.
477 # Something's wrong. Skip ahead in
478 # time instead of back (not sure what
480 $start_time = time + 1;
483 @lt = localtime($start_time);
484 $lt[5] %= 100; $lt[4]++; # year and month
485 return sprintf($sidformat, @lt[reverse 0..5]);
496 =head2 get_related_items($stoid)
514 ARRAY of related links
520 sub get_related_items {
523 my $stoid = $params->{stoid} || $params->{story_id} || $params->{id};
526 my $dbh = $self->connect_db;
530 story_text.title as title2,
534 SELECT * FROM related_stories
536 ORDER BY ordernum ASC
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
544 my $sth = $dbh->prepare($sql);
545 $sth->execute($stoid);
546 my $related = $sth->fetchall_arrayref({});
547 $self->disconnect_db();
549 for my $r (@$related) {
550 $r->{title} = $r->{title2} unless $r->{title};
552 $r->{type} = "story";
553 $r->{key_id} = $r->{rel_sid};
555 $r->{type} = "submission";
556 $r->{key_id} = $r->{srcid};
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}) {
563 $r->{primary_topic}->{$k} = $r->{$k};
571 =head2 parameters($stoid)
573 get story parameters.
596 my ($self, $stoid) = @_;
598 my $dbh = $self->connect_db;
601 SELECT * FROM story_param WHERE stoid = ?
604 my $sth = $dbh->prepare($sql);
605 $sth->execute($stoid);
606 my $params = $sth->fetchall_hashref('name');
609 # get next/prev story info
610 if ($params->{next_stoid} || $params->{prev_stoid}) {
612 SELECT stories.*, story_text.* FROM stories JOIN story_text ON stories.stoid = story_text.stoid WHERE stories.stoid = ? OR stories.stoid = ?
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};
624 $self->disconnect_db();
628 #========================================================================
630 =head2 set_dirty($key, $id)
632 set writestatus dirty for the story.
659 my ($self, $key, $id) = @_;
660 return if $self->check_readonly;
663 if ($key eq 'stoid') {
670 my $dbh = $self->connect_db;
672 INSERT INTO story_dirty (stoid) VALUES (?)
675 my $rs = $dbh->do($sql, undef, $stoid);
683 my ($self, $story, $params) = @_;
687 $story->{id} = $story->{stoid};
688 $story->{story_id} = $story->{stoid};
689 $story->{create_time} = $story->{time};
690 $story->{update_time} = $story->{last_update};
692 for my $t (@{$story->{topics}}) {
693 if ($t->{tid} && $t->{tid} == $story->{tid}) {
694 $story->{primary_topic} = $t;
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});
706 $story->{full_text} = $story->{intro_text};
708 $story->{fulltext} = $story->{full_text};
710 $story->{discussion_id} = $story->{discussion};
712 # no public flag given, public is 'yes'
713 $story->{public} = 'yes' if !$story->{public};
717 # delete story from database
718 # this method is for test purpose only.
721 return if $self->check_readonly;
724 my $stoid = $params->{story_id};
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.");
740 $self->disconnect_db;
744 # delete firehose item
745 # delete firehose_text item
746 # delete firehose_topics_rendererd item