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->{hide_future}) {
98 push @where_clauses, "stories.time <= NOW()";
101 # hide non-public story?
102 if ($params->{public_only}) {
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);
430 $self->set_error("updateStory failed");
434 ($sid, $stoid) = $slash_db->createStory($params);
437 my $globjs = $self->new_instance_of("Newslash::Model::Globjs");
438 my $globj_id = $globjs->getGlobjidFromTargetIfExists("stories", $params->{stoid});
441 my $tags = $self->new_instance_of("Tags");
442 for my $tid (keys %$topics_chosen) {
443 my $ret = $tags->set_tag(uid => $user->{uid} || $user->{user_id},
445 globj_id => $globj_id,
448 #warn "set_tag fault..." if !$ret
455 my ($self, $bogus_sid) = @_;
456 # yes, this format is correct, don't change it :-)
457 my $sidformat = '%02d/%02d/%02d/%02d%0d2%02d';
458 # Create a sid based on the current time.
460 my $start_time = time;
462 # If we were called being told that there's at
463 # least one sid that is invalid (already taken),
464 # then look backwards in time until we find it,
465 # then go one second further.
469 @lt = localtime($start_time);
470 $lt[5] %= 100; $lt[4]++; # year and month
471 last if $bogus_sid eq sprintf($sidformat, @lt[reverse 0..5]);
474 # Found the bogus sid by looking
475 # backwards. Go one second further.
478 # Something's wrong. Skip ahead in
479 # time instead of back (not sure what
481 $start_time = time + 1;
484 @lt = localtime($start_time);
485 $lt[5] %= 100; $lt[4]++; # year and month
486 return sprintf($sidformat, @lt[reverse 0..5]);
497 =head2 get_related_items($stoid)
515 ARRAY of related links
521 sub get_related_items {
524 my $stoid = $params->{stoid} || $params->{story_id} || $params->{id};
527 my $dbh = $self->connect_db;
531 story_text.title as title2,
535 SELECT * FROM related_stories
537 ORDER BY ordernum ASC
539 LEFT JOIN story_text ON story_text.stoid = related.rel_stoid
540 LEFT JOIN firehose ON firehose.id = related.fhid
541 LEFT JOIN stories ON stories.stoid = related.rel_stoid
542 LEFT JOIN topics ON topics.tid = stories.tid
545 my $sth = $dbh->prepare($sql);
546 $sth->execute($stoid);
547 my $related = $sth->fetchall_arrayref({});
548 $self->disconnect_db();
550 for my $r (@$related) {
551 $r->{title} = $r->{title2} unless $r->{title};
553 $r->{type} = "story";
554 $r->{key_id} = $r->{rel_sid};
556 $r->{type} = "submission";
557 $r->{key_id} = $r->{srcid};
559 $r->{primary_topic} = {};
560 $r->{primary_topic}->{tid} = $r->{tid};
561 for my $k (qw{keyword textname series image width height
562 submittable searchable storypickable usesprite}) {
564 $r->{primary_topic}->{$k} = $r->{$k};
572 =head2 parameters($stoid)
574 get story parameters.
597 my ($self, $stoid) = @_;
599 my $dbh = $self->connect_db;
602 SELECT * FROM story_param WHERE stoid = ?
605 my $sth = $dbh->prepare($sql);
606 $sth->execute($stoid);
607 my $params = $sth->fetchall_hashref('name');
610 # get next/prev story info
611 if ($params->{next_stoid} || $params->{prev_stoid}) {
613 SELECT stories.*, story_text.* FROM stories JOIN story_text ON stories.stoid = story_text.stoid WHERE stories.stoid = ? OR stories.stoid = ?
615 $sth = $dbh->prepare($sql);
616 my $next = $params->{next_stoid}->{value} || 0;
617 my $prev = $params->{prev_stoid}->{value} || 0;
618 $sth->execute($next, $prev);
619 my $sids = $sth->fetchall_hashref('stoid');
620 $params->{next_stoid}->{story} = $sids->{$next} if $sids->{$next};
621 $params->{prev_stoid}->{story} = $sids->{$prev} if $sids->{$prev};
625 $self->disconnect_db();
629 #========================================================================
631 =head2 set_dirty($key, $id)
633 set writestatus dirty for the story.
660 my ($self, $key, $id) = @_;
661 return if $self->check_readonly;
664 if ($key eq 'stoid') {
671 my $dbh = $self->connect_db;
673 INSERT INTO story_dirty (stoid) VALUES (?)
676 my $rs = $dbh->do($sql, undef, $stoid);
684 my ($self, $story, $params) = @_;
688 $story->{id} = $story->{stoid};
689 $story->{story_id} = $story->{stoid};
690 $story->{create_time} = $story->{time};
691 $story->{update_time} = $story->{last_update};
693 for my $t (@{$story->{topics}}) {
694 if ($t->{tid} && $t->{tid} == $story->{tid}) {
695 $story->{primary_topic} = $t;
699 $story->{content_type} = "story";
700 $story->{intro_text} = $story->{introtext};
701 $story->{bodytext} ||= "";
702 $story->{body_text} = $story->{bodytext};
703 if ($story->{body_text}) {
704 $story->{full_text} = join("\n", $story->{intro_text}, $story->{body_text});
707 $story->{full_text} = $story->{intro_text};
709 $story->{fulltext} = $story->{full_text};
711 $story->{discussion_id} = $story->{discussion};
713 # no public flag given, public is 'yes'
714 $story->{public} = 'yes' if !$story->{public};
718 # delete story from database
719 # this method is for test purpose only.
722 return if $self->check_readonly;
725 my $stoid = $params->{story_id};
731 #my $dbh = $self->connect_db({AutoCommit => 0,});
732 my $dbh = $self->start_transaction;
733 for my $table (qw(stories story_param story_text story_topics_chosen story_topics_rendered)) {
734 my $sql = "DELETE FROM $table WHERE stoid = ?";
735 my $rs = $dbh->do($sql, undef, $stoid);
736 if (!defined $rs || $rs == 0) {
737 Mojo::Log->new->warn("DELETE FROM $table failed. stoid is $stoid.");
742 # delete from firehose
743 my $firehose = $self->new_instance_of("Firehose");
744 $firehose->hard_delete("story", $stoid);