1 package Newslash::Model::Stories;
2 use Newslash::Model::Base -base;
4 use Newslash::Model::SlashDB;
8 #========================================================================
10 =head2 latest(\%options)
24 $options->{show_future}: when 1, return feature stories. default is 0.
25 $options->{limit}: number of stories. default is 10.
31 ARRAY of story contents
38 my ($self, $options) = @_;
41 my $show_future = $options->{show_future} || 0;
42 my $limit = $options->{limit} || 10;
44 my $dbh = $self->connect_db;
47 my $where_clause = 'WHERE stories.time <= NOW()';;
53 SELECT latest.*, story_text.*, users.nickname as author
54 FROM (SELECT * from stories $where_clause ORDER BY time DESC LIMIT ?) AS latest
55 LEFT JOIN story_text ON latest.stoid = story_text.stoid
56 LEFT JOIN users ON latest.uid = users.uid
59 my $sth = $dbh->prepare($sql);
61 $sth->execute($limit);
62 my $rs = $sth->fetchall_arrayref(+{});
72 SELECT story_topics_rendered.*, story_topics_chosen.weight, topics.*
73 FROM (SELECT stoid FROM stories $where_clause ORDER BY time DESC LIMIT ?) AS latest
74 INNER JOIN story_topics_rendered ON latest.stoid = story_topics_rendered.stoid
75 LEFT JOIN story_topics_chosen ON story_topics_rendered.stoid = story_topics_chosen.stoid
76 AND story_topics_rendered.tid = story_topics_chosen.tid
77 LEFT JOIN topics ON story_topics_rendered.tid = topics.tid
80 $sth = $dbh->prepare($sql);
82 $sth->execute($limit);
83 my $topics_table = $sth->fetchall_arrayref(+{});
88 for my $topic (@$topics_table) {
89 if (!$topic->{stoid}) {
90 $topics->{$topic->{stoid}} = [];
92 push @{$topics->{$topic->{stoid}}}, $topic;
95 for my $story (@$rs) {
96 my $stoid = $story->{stoid};
97 $story->{topics} = $topics->{$stoid};
98 $self->_generalize($story);
105 #========================================================================
107 =head2 select($query_type, $value)
119 query key, "sid" or "stoid"
129 HASH of story contents
136 my ($self, $query_type, $value) = @_;
138 if ($query_type !~ m/\A(sid|stoid)\z/) {
142 my $dbh = $self->connect_db;
144 SELECT stories.*, story_text.*, users.nickname as author
146 LEFT JOIN story_text ON stories.stoid = story_text.stoid
147 LEFT JOIN users ON stories.uid = users.uid
148 WHERE stories.$query_type = ?
151 my $sth = $dbh->prepare($sql);
153 $sth->execute($value);
154 my $story = $sth->fetchrow_hashref;
162 my $stoid = $story->{stoid};
171 SELECT story_topics_rendered.*, story_topics_chosen.weight, topics.*
172 FROM story_topics_rendered
173 LEFT JOIN story_topics_chosen ON story_topics_rendered.stoid = story_topics_chosen.stoid
174 AND story_topics_rendered.tid = story_topics_chosen.tid
175 LEFT JOIN topics ON story_topics_rendered.tid = topics.tid
176 WHERE story_topics_rendered.stoid = ?
179 $sth = $dbh->prepare($sql);
181 $sth->execute($stoid);
182 my $topics = $sth->fetchall_arrayref(+{});
186 $story->{topics} = $topics;
187 $self->_generalize($story);
192 =head2 create(\%params, $uid)
224 my ($self, $params, $user, $extra_params, $opts) = @_;
228 $msg = "no title" if !$params->{title};
229 $msg = "no title" if !$params->{title};
230 $msg = "no uid" if !$params->{uid};
231 $msg = "no topics" if !defined $params->{topics_chosen};
233 if (length($params->{title}) > $self->{options}->{Story}->{title_max_byte}) {
234 $msg = "title too long. max: $self->{options}->{Story}->{title_max_byte} bytes";
237 $params->{commentstatus} ||= "enabled";
238 if (!grep /\A$params->{commentstatus}\z/, qw(disabled enabled friends_only friends_fof_only no_foe no_foe_eof logged_in)) {
239 $msg = "invalid commentstatus";
242 # check timestamp. use ISO8601 style timestamp like: 2006-08-14T02:34:56-0600
243 my $rex_timestamp = qr/
244 ^(\d+)-(\d+)-(\d+)\D+(\d+):(\d+):(\d+(?:\.\d+)?) # datetime
245 (?:Z|([+-])(\d+):(\d+))?$ # tz
247 if ($params->{time} =~ $rex_timestamp) {
248 $params->{time} = "$1-$2-$3 $4:$5:$6";
251 # check parameters finish
252 if (length($msg) > 0) {
253 $self->set_error($msg, -1);
257 $params->{neverdisplay} ||= 0;
259 # createStory deletes topics_chosen, so save before.
260 my $topics_chosen = $params->{topics_chosen};
262 my $slash_db = Newslash::Model::SlashDB->new($self->{options});
263 my $sid = $slash_db->createStory($params);
265 my $globjs = $self->new_instance_of("Newslash::Model::Globjs");
266 my $globj_id = $globjs->getGlobjidFromTargetIfExists("stories", $params->{stoid});
269 use Newslash::Model::Tags;
270 my $tags = $self->new_instance_of("Newslash::Model::Tags");
271 for my $tid (keys %$topics_chosen) {
272 my $ret = $tags->set_tag(uid => $user->{uid},
274 globj_id => $globj_id,
277 warn "set_tag fault..." if !$ret
284 my ($self, $bogus_sid) = @_;
285 # yes, this format is correct, don't change it :-)
286 my $sidformat = '%02d/%02d/%02d/%02d%0d2%02d';
287 # Create a sid based on the current time.
289 my $start_time = time;
291 # If we were called being told that there's at
292 # least one sid that is invalid (already taken),
293 # then look backwards in time until we find it,
294 # then go one second further.
298 @lt = localtime($start_time);
299 $lt[5] %= 100; $lt[4]++; # year and month
300 last if $bogus_sid eq sprintf($sidformat, @lt[reverse 0..5]);
303 # Found the bogus sid by looking
304 # backwards. Go one second further.
307 # Something's wrong. Skip ahead in
308 # time instead of back (not sure what
310 $start_time = time + 1;
313 @lt = localtime($start_time);
314 $lt[5] %= 100; $lt[4]++; # year and month
315 return sprintf($sidformat, @lt[reverse 0..5]);
320 sub grantStorySubmissionKarma {
321 my($self, $story) = @_;
322 #my $constants = getCurrentStatic();
323 my $db = $self->new_instance_of('LegacyDB');
324 #if ($constants->{plugin}{FireHose}) {
327 #my $firehose = getObject("Slash::FireHose");
328 if ($story->{fhid}) {
329 $fhid = $story->{fhid};
330 } elsif ($story->{subid}) {
331 #my $subid_q = $self->sqlQuote($story->{subid});
332 #($fhid) = $self->sqlSelect('id', 'firehose', "type='submission' and srcid=$subid_q");
333 my $subid_q = $db->sqlQuote($story->{subid});
334 ($fhid) = $db->sqlSelect('id', 'firehose', "type='submission' and srcid=$subid_q");
336 #$firehose->setFireHose($fhid, { accepted => "yes" }) if $fhid;
337 $self->setFireHose($fhid, { accepted => "yes" }) if $fhid;
339 return 0 unless $story->{subid};
340 #my($submitter_uid) = $self->sqlSelect(
341 my($submitter_uid) = $db->sqlSelect(
342 'uid', 'submissions',
343 # 'subid=' . $self->sqlQuote($story->{subid})
344 'subid=' . $db->sqlQuote($story->{subid})
347 if (!isAnon($submitter_uid)) {
348 #my $constants = getCurrentStatic();
350 #$self->sqlUpdate('users_info',
351 $db->sqlUpdate('users_info',
352 { -karma => "LEAST(karma + $constants->{submission_bonus}, $maxkarma)" },
353 "uid=$submitter_uid");
355 #$self->clearRookie($submitter_uid);
356 my $users = $self->new_instance_of('Users');
357 $users->clearRookie($submitter_uid);
359 #$self->validateSubmitter($submitter_uid);
360 $users->update(target => 'class',
361 field => 'validated_submitter',
363 #$self->setUser_delete_memcached($submitter_uid);
366 #my $submission_info = { del => 2 };
367 #$submission_info->{stoid} = $story->{stoid} if $story->{stoid};
368 #$submission_info->{sid} = $story->{sid} if $story->{sid};
369 #$self->setSubmission($story->{subid}, $submission_info);
370 my $submissions = $self->new_instance_of('Submissions');
371 $submissions->update(subid => $story->{subid},
374 $submissions->upsert_param(params => {
375 stoid => $story->{stoid},
376 sid => $story->{sid},
378 subid => $story->{subid});
384 ########################################################
389 my($self, $story) = @_;
391 #my $constants = getCurrentStatic();
392 my $db = $self->new_instance_of('LegacyDB');
393 #$self->sqlDo("SET AUTOCOMMIT=0");
394 $db->sqlDo("SET AUTOCOMMIT=0");
398 $story->{submitter} = $story->{submitter} ? $story->{submitter} : $story->{uid};
399 $story->{is_dirty} = 1;
401 if (!defined($story->{title})) {
402 $error = "createStory needs a defined title";
404 # Rather than call truncateStringForCharColumn() here,
405 # we prefer to throw an error. Unlike createComment,
406 # we would prefer that overlong subjects not be silently
407 # chopped off. Consider the consequences of saving a
408 # story with the headline "Chris Nandor is a Freakishly
409 # Ugly Twisted Criminal, Claims National Enquirer" and
410 # later realizing it had been truncated after 50 chars.
411 #my $title_len = $self->sqlGetCharColumnLength('story_text', 'title');
412 my $title_len = $db->sqlGetCharColumnLength('story_text', 'title');
413 if ($title_len && length($story->{title}) > $title_len) {
414 $error = "createStory title too long: " . length($story->{title}) . " > $title_len";
420 #$story->{sid} = createSid();
421 $story->{sid} = $self->createSid;
423 while ($sid_ok == 0) {
424 # we rely on logic in setStory() later to properly
425 # set up the data for a story, so we can't someday
426 # just change this to do an insert of all the story
427 # data, we do need to continue pass it through
429 #$sid_ok = $self->sqlInsert('stories',
430 $sid_ok = $db->sqlInsert('stories',
431 { sid => $story->{sid} },
432 { ignore => 1 } ); # don't need error messages
433 if ($sid_ok == 0) { # returns 0E0 on collision, which == 0
435 $story->{sid} = $self->createSid($story->{sid});
438 # If this came from a submission, update submission and grant
440 $stoid = $self->getLastInsertId({ table => 'stories', prime => 'stoid' });
441 $story->{stoid} = $stoid;
442 $self->grantStorySubmissionKarma($story);
446 #if (! $self->sqlInsert('story_text', { stoid => $stoid })) {
447 if (! $db->sqlInsert('story_text', { stoid => $stoid })) {
448 $error = "sqlInsert failed for story_text: " . $self->sqlError();
452 # Write the chosen topics into story_topics_chosen. We do this
453 # here because it returns the primaryskid and we will write that
454 # into the stories table with setStory in just a moment.
455 my($primaryskid, $tids);
457 my $success = $self->setStoryTopicsChosen($stoid, $story->{topics_chosen});
458 $error = "Failed to set chosen topics for story '$stoid'\n" if !$success;
462 $info_hr->{neverdisplay} = 1 if $story->{neverdisplay};
463 ($primaryskid, $tids) = $self->setStoryRenderedFromChosen($stoid,
464 $story->{topics_chosen}, $info_hr);
465 $error = "Failed to set rendered topics for story '$stoid'\n" if !defined($primaryskid);
467 delete $story->{topics_chosen};
468 my $commentstatus = delete $story->{commentstatus};
471 if ($story->{fhid} && $constants->{plugin}{FireHose}) {
472 my $firehose = getObject("Slash::FireHose");
473 my $item = $firehose->getFireHose($story->{fhid});
474 $firehose->setFireHose($story->{fhid}, { stoid => $stoid });
475 if ($item && $item->{type} eq "journal") {
476 $story->{discussion} = $item->{discussion};
477 $story->{journal_id} = $item->{srcid};
479 if ($story->{journal_id}) {
480 if (!$self->sqlCount("journal_transfer", "id = ".$self->sqlQuote($story->{journal_id}))) {
481 $self->sqlInsert("journal_transfer", {
482 id => $story->{journal_id}
488 } elsif ($story->{subid}) {
489 if ($self->sqlSelect('id', 'journal_transfer',
490 'subid=' . $self->sqlQuote($story->{subid})
492 my $sub = $self->getSubmission($story->{subid});
494 for (qw(discussion journal_id by by_url)) {
495 $story->{$_} = $sub->{$_};
501 $story->{body_length} = defined($story->{bodytext}) ? length($story->{bodytext}) : 0;
502 $story->{word_count} = countWords($story->{introtext}) + countWords($story->{bodytext});
503 $story->{primaryskid} = $primaryskid;
504 $story->{tid} = $tids->[0];
506 if (! $self->setStory($stoid, $story)) {
507 $error = "setStory failed after creation: " . $self->sqlError();
512 if ($story->{primaryskid}) {
514 my $storyskin = $self->getSkin($story->{primaryskid});
515 $rootdir = $storyskin->{rootdir};
517 # The story is set never-display so its discussion's rootdir
518 # probably doesn't matter. Just go with the default.
519 my $storyskin = $self->getSkin($constants->{mainpage_skid});
520 $rootdir = $storyskin->{rootdir};
522 my $comment_codes = $self->getDescriptions('commentcodes_extended');
526 uid => $story->{uid},
527 title => $story->{title},
528 primaryskid => $primaryskid,
530 url => $self->getUrlFromSid(
532 $story->{primaryskid},
537 sid => $story->{sid},
538 commentstatus => $comment_codes->{$commentstatus}
540 : $constants->{defaultcommentstatus},
541 ts => $story->{'time'}
545 if ($story->{discussion} && $story->{journal_id}) {
546 # updating now for journals tips off users that this will
547 # be a story soon, esp. ts, url, title, kind ... i don't
548 # care personally, does it matter? if so we can task some
549 # of these changes, if we need to make them -- pudge
551 # update later in task
552 delete @{$discussion}{qw(title url ts)};
553 delete $discussion->{uid}; # leave it "owned" by poster
555 $id = $story->{discussion};
556 $discussion->{kind} = 'journal-story';
557 $discussion->{type} = 'open'; # should be already
558 $discussion->{archivable} = 'yes'; # for good measure
560 if (!$self->setDiscussion($id, $discussion)) {
561 $error = "Failed to set discussion data for story\n";
563 } elsif ($story->{journal_id}) {
564 $self->sqlUpdate('journal_transfer', {
567 }, 'id=' . $self->sqlQuote($story->{journal_id}));
571 $id = $self->createDiscussion($discussion);
573 $error = "Failed to create discussion for story";
576 if (!$error && !$self->setStory($stoid, { discussion => $id })) {
577 $error = "Failed to set discussion '$id' for story '$stoid'\n";
582 # Rollback doesn't even work in 4.0.x, since some tables
583 # are non-transactional...
584 $self->sqlDo("ROLLBACK");
585 $self->sqlDo("SET AUTOCOMMIT=1");
587 print STDERR scalar(localtime) . " createStory error: $error\n";
591 $self->sqlDo("COMMIT");
592 $self->sqlDo("SET AUTOCOMMIT=1");
594 if ($constants->{plugin}{FireHose}) {
595 my $firehose = getObject("Slash::FireHose");
596 $firehose->createItemFromStory($stoid);
599 return $story->{sid};
602 sub editCreateStory {
603 my($self, $preview, $fhitem) = @_;
604 my $constants = getCurrentStatic();
605 my $user = getCurrentUser();
606 my $form = getCurrentForm();
609 my $tagsdb = getObject("Slash::Tags");
610 my $admindb = getObject("Slash::Admin");
613 push @topics, $fhitem->{tid} if $fhitem->{tid};
615 my $chosen_hr = $tagsdb->extractChosenFromTags($fhitem->{globjid}, 'admin');
617 my $save_extras = $self->getExtrasToSaveForChosen($chosen_hr, $preview);
619 my $is_sectiononly = $tagsdb->isAdminTagged($fhitem->{globjid}, 'sectiononly');
620 $save_extras->{offmainpage} = 1 if $is_sectiononly;
623 uid => $fhitem->{uid},
625 title => $preview->{title},
627 submitter => $preview->{submitter},
628 topics_chosen => $chosen_hr,
629 dept => $fhitem->{dept},
630 'time' => $admindb->findTheTime($fhitem->{createtime}, $preview->{fastforward}),
631 bodytext => $preview->{bodytext},
632 introtext => $preview->{introtext},
634 media => $fhitem->{media},
635 commentstatus => $preview->{commentstatus},
636 thumb => $fhitem->{thumb},
638 neverdisplay => $preview->{neverdisplay},
639 sponsor => $preview->{sponsor},
640 stuck => $preview->{stuck},
641 stuckpos => $preview->{stuckpos},
642 stuckendtime => $preview->{stuckendtime},
645 foreach my $key (keys %$save_extras) {
646 $data->{$key} = $save_extras->{$key};
649 $data->{subid} = $preview->{subid} if $preview->{subid};
650 $data->{fhid} = $preview->{fhid} if $preview->{fhid};
652 for (qw(dept bodytext relatedtext)) {
653 $data->{$_} = '' unless defined $data->{$_}; # allow to blank out
656 for my $field (qw( introtext bodytext media )) {
657 local $Slash::Utility::Data::approveTag::admin = 2;
660 $data->{$field} = $self->autoUrl($form->{section}, $data->{$field});
661 $data->{$field} = cleanSlashTags($data->{$field});
662 $data->{$field} = strip_html($data->{$field});
663 $data->{$field} = slashizeLinks($data->{$field});
664 $data->{$field} = parseSlashizedLinks($data->{$field});
665 $data->{$field} = balanceTags($data->{$field});
666 $data->{$field} = adjustStoryTags($data->{$field});
669 for (qw(dept bodytext relatedtext)) {
670 $data->{$_} = '' unless defined $data->{$_}; # allow to blank out
673 my $sid = $self->createStory($data);
676 my $st = $self->getStory($sid);
677 $self->setRelated($sid);
678 slashHook('admin_save_story_success', { story => $data });
679 my $stoid = $st->{stoid};
680 my $story_globjid = $self->getGlobjidCreate('stories', $stoid);
682 # XXXEdit Do we have to worry about user editing vs author uid on transfer
683 $tagsdb->transferTags($fhitem->{globjid}, $story_globjid);
685 #Don't automatically signoff with new editor, this makes it automatically disapper for an admin on first refresh
686 $self->createSignoff($st->{stoid}, $user->{uid}, "created", { no_filter => 1});
688 #XXXEdit Tags Auto save?
689 my $admindb = getObject("Slash::Admin");
691 $admindb->grantStoryPostingAchievements($data->{uid}, $data->{submitter});
692 $admindb->addSpriteForSid($sid);
695 #XXX Move this to Slash::DB
696 my $sfids = $self->sqlSelect('value', 'preview_param', "name = 'sfid' and preview_id = " . $preview->{preview_id});
697 if ($sfids && $stoid) {
698 $self->sqlUpdate('static_files', { stoid => $stoid, fhid => 0 }, 'fhid = ' . $preview->{preview_fhid});
714 =head2 related_link($stoid)
732 ARRAY of related links
739 my ($self, $stoid) = @_;
741 my $dbh = $self->connect_db;
744 SELECT related.*, story_text.title as title2, firehose.srcid
746 SELECT * FROM related_stories
748 ORDER BY ordernum ASC
750 LEFT JOIN story_text ON related.rel_stoid = story_text.stoid
751 LEFT JOIN firehose ON firehose.id = related.fhid
754 my $sth = $dbh->prepare($sql);
755 $sth->execute($stoid);
756 my $related = $sth->fetchall_arrayref({});
760 for my $r (@$related) {
761 $r->{title} = $r->{title2} unless $r->{title};
763 $r->{type} = "story";
764 $r->{key_id} = $r->{rel_sid};
766 $r->{type} = "submission";
767 $r->{key_id} = $r->{srcid};
774 =head2 parameters($stoid)
776 get story parameters.
799 my ($self, $stoid) = @_;
801 my $dbh = $self->connect_db;
804 SELECT * FROM story_param WHERE stoid = ?
807 my $sth = $dbh->prepare($sql);
808 $sth->execute($stoid);
809 my $params = $sth->fetchall_hashref('name');
812 # get next/prev story info
813 if ($params->{next_stoid} || $params->{prev_stoid}) {
815 SELECT stories.*, story_text.* FROM stories JOIN story_text ON stories.stoid = story_text.stoid WHERE stories.stoid = ? OR stories.stoid = ?
817 $sth = $dbh->prepare($sql);
818 my $next = $params->{next_stoid}->{value} || 0;
819 my $prev = $params->{prev_stoid}->{value} || 0;
820 $sth->execute($next, $prev);
821 my $sids = $sth->fetchall_hashref('stoid');
822 $params->{next_stoid}->{story} = $sids->{$next} if $sids->{$next};
823 $params->{prev_stoid}->{story} = $sids->{$prev} if $sids->{$prev};
831 #========================================================================
833 =head2 set_dirty($key, $id)
835 set writestatus dirty for the story.
862 my ($self, $key, $id) = @_;
865 if ($key eq 'stoid') {
872 my $dbh = $self->connect_db;
874 INSERT INTO story_dirty (stoid) VALUES (?)
877 my $rs = $dbh->do($sql, undef, $stoid);
885 my ($self, $story) = @_;
887 $story->{content_type} = "story";
890 for my $t (@{$story->{topics}}) {
891 if ($t->{tid} == $story->{tid}) {
892 $story->{primary_topic} = $t;
894 #if ($t->{weight} && $t->{weight} > $max_weight) {
895 # $max_weight = $t->{weight};