OSDN Git Service

move unused (deprecated) files
[newslash/newslash.git] / src / newslash_web / lib / Newslash / Model / Stories.pm
index caaab76..99ea3cd 100644 (file)
@@ -3,104 +3,52 @@ use Newslash::Model::Base -base;
 
 use Newslash::Model::SlashDB;
 
-use Data::Dumper;
 use DateTime;
+use DateTime::Format::MySQL;
+use DateTime::Format::ISO8601;
 
-#========================================================================
-
-=head2 latest(\%options)
-
-get latest stories.
-
-=over 4
-
-=item Parameters
-
-=over 4
-
-=item \%options
-
-options for query.
-
-$options->{show_future}: when 1, return feature stories. default is 0.
-$options->{limit}: number of stories. default is 10.
-
-=back
-
-=item Return value
-
-ARRAY of story contents
-
-=back
-
-=cut
-
-sub latest {
-  my ($self, $options) = @_;
-  $options ||= {};
-
-  my $show_future = $options->{show_future} || 0;
-  my $limit = $options->{limit} || 10;
-
-  my $dbh = $self->connect_db;
-
-  # get stories
-  my $where_clause = 'WHERE stories.time <= NOW()';;
-  if ($show_future) {
-    $where_clause = '';
-  }
-
-  my $sql = <<"EOSQL";
-SELECT latest.*, story_text.*, users.nickname as author
-  FROM (SELECT * from stories $where_clause ORDER BY time DESC LIMIT ?) AS latest
-    LEFT JOIN story_text ON latest.stoid = story_text.stoid
-    LEFT JOIN users ON latest.uid = users.uid
-EOSQL
-
-  my $sth = $dbh->prepare($sql);
-
-  $sth->execute($limit);
-  my $rs = $sth->fetchall_arrayref(+{});
-
-  if (@$rs == 0) {
-    $dbh->disconnect();
-    return [];
-  }
-
-  # get tags
-  $sql = <<"EOSQL";
-SELECT story_topics_rendered.*, story_topics_chosen.weight, topics.*
-  FROM (SELECT stoid FROM stories $where_clause ORDER BY time DESC LIMIT ?) AS latest
-    INNER JOIN story_topics_rendered ON latest.stoid = story_topics_rendered.stoid
-    LEFT JOIN story_topics_chosen ON story_topics_rendered.stoid = story_topics_chosen.stoid
-      AND story_topics_rendered.tid = story_topics_chosen.tid
-    LEFT JOIN topics ON story_topics_rendered.tid = topics.tid
-EOSQL
-
-  $sth = $dbh->prepare($sql);
+use Data::Dumper;
+use DateTime;
 
-  $sth->execute($limit);
-  my $topics_table = $sth->fetchall_arrayref(+{});
-  $sth->finish;
-  $dbh->disconnect();
 
-  my $topics = {};
-  for my $topic (@$topics_table) {
-    if (!$topic->{stoid}) {
-      $topics->{$topic->{stoid}} = [];
-    }
-    push @{$topics->{$topic->{stoid}}}, $topic;
-  }
+sub key_definition {
+    return {
+            table => "stories",
+            primary => "stoid",
+            unique => [qw(sid)],
+            datetime => [qw(time last_update day_published
+                            stuckendtime archive_last_update
+                          )],
+            other => [qw(uid dept hits discussion primaryskid
+                         tid submitter commentcount hitparade
+                         is_archived in_trash
+                         qid body_length word_count sponsor
+                         stuck stuckpos fakeemail homepage
+                       )],
+            aliases => { user_id => "uid",
+                         id => "stoid",
+                         create_time => time,
+                       }
+           };
+}
 
-  for my $story (@$rs) {
-      my $stoid = $story->{stoid};
-      $story->{topics} = $topics->{$stoid};
-      $self->_generalize($story);
-  }
+use constant FACULTIES => { 1000 => [qw(hits hitparade)] };
 
-  return $rs;
+sub count {
+    my $self = shift;
+    my $join = 'LEFT JOIN firehose ON (stories.stoid = firehose.srcid AND firehose.type = "story")';
+    my $where = 'firehose.public != "no"';
+    return $self->generic_count(table => "stories",
+                                target => "stoid",
+                                timestamp => "time",
+                                join => $join,
+                                where => $where,
+                                @_);
 }
 
+##### sub models
+sub text { return shift->new_instance_of('::Stories::Text'); }
+
 
 #========================================================================
 
@@ -135,124 +83,299 @@ HASH of story contents
 sub select {
     my $self = shift;
     my $params = {@_};
-    my $query_type;
-    my $value;
-    my $return_single = 0;
-
-    for my $k (qw(sid stoid)) {
-        if ($params->{$k}) {
-            $query_type = $k;
-            $value = $params->{$k};
-            $return_single = 1;
-        }
-    }
 
-    my @where_clauses;
-    my @query_param;
 
-    if ($query_type && $value) {
-        push @where_clauses, "stories.$query_type = ?";
-        push @query_param, $value;
+    my $unique_keys = { id => "stories.stoid",
+                        story_id => "stories.stoid",
+                        stoid => "stories.stoid",
+                        sid => "stories.sid",
+                      };
+    my $keys = { user_id => "stories.uid",
+                 uid => "stories.uid",
+                 topic_id => "stories.tid",
+                 tid => "stories.tid",
+                 discussion_id => "stories.discussion",
+                 commentcount => "stories.commentcount",
+                 hits => "stories.hits",
+                 submitter => "stories.submitter",
+                 create_time => "stories.time",
+                 update_time => "stories.last_update",
+                 public => "firehose.public",
+               };
+    my $datetime_keys = { create_time => "stories.time",
+                          update_time => "stories.last_update",
+                        };
+    my $timestamp = "stories.time";
+
+    my ($where_clause, $where_values, $unique) = $self->build_where_clause(unique_keys => $unique_keys,
+                                                                           keys => $keys,
+                                                                           datetime_keys => $datetime_keys,
+                                                                           timestamp => $timestamp,
+                                                                           params => $params);
+    my ($limit_clause, $limit_values) = $self->build_limit_clause(params => $params);
+    my ($orderby_clause, $orderby_values) = $self->build_order_by_clause(keys => $keys,
+                                                                         params => $params);
+
+    # TODO: give reasonable LIMIT Value...
+    $limit_clause = "LIMIT 50" if !$limit_clause;
+
+    # hide future story?
+    my @where_clauses;
+    if ($params->{hide_future}) {
+        push @where_clauses, "stories.time <= NOW()";
     }
 
-    # show future story?
-    if (!$params->{show_future}) {
-        push @where_clauses, "stories.time <= NOW()";
+    # hide non-public story?
+    if ($params->{public_only}) {
+        push @where_clauses, "firehose.public != 'no'";
     }
 
-    # target period
-    my $date_limit = "";
-    for my $term (qw(years months weeks days hours minutes)) {
-        if (defined $params->{$term}) {
-            my $dt = DateTime->now();
-            $dt->subtract($term => $params->{$term});
-            $date_limit = $dt->ymd("-") . " " . $dt->hms(":");
+    if (@where_clauses) {
+        if ($where_clause) {
+            $where_clause = $where_clause . " AND ";
         }
-    }
-    if (length $date_limit) {
-        push @where_clauses, "stories.time > ?";
-        push @query_param, $date_limit;
-    }
-
-    my @order_clauses;
-    # set ORDER BY clause
-    my @safe_params = qw(commentcount hits);
-    if (defined $params->{order_by}) {
-        # check order_by's value
-        my $k = $params->{order_by};
-        if (grep {$_ eq $k} @safe_params) {
-            my $order = "DESC";
-            if (defined $params->{order} && $params->{order} eq "ASC") {
-                $order = "ASC";
-            }
-            push @order_clauses, "ORDER BY $k $order";
+        else {
+            $where_clause = "WHERE ";
         }
+        $where_clause = $where_clause . join(" AND ", @where_clauses);
     }
 
-    # set LIMIT clause
-    my $limit_clause = "";
-    if (defined $params->{limit}) {
-        push @order_clauses, "LIMIT ?";
-        push @query_param, $params->{limit};
-    }
+    my @attrs;
+    push @attrs, @$where_values, @$limit_values, @$orderby_values;
 
-    my $where_clause = "WHERE " . join("\n AND ", @where_clauses) . "\n";
-    my $sub_clauses = $where_clause . join("\n", @order_clauses);
     my $dbh = $self->connect_db;
     my $sql = <<"EOSQL";
-SELECT stories.*, story_text.*, users.nickname as author
+SELECT stories.*, story_text.*, users.nickname as author, firehose.public,
+       discussions.type AS discussion_type, discussions.commentcount AS comment_count
   FROM stories
     LEFT JOIN story_text ON stories.stoid = story_text.stoid
     LEFT JOIN users ON stories.uid = users.uid
-    $sub_clauses
+    LEFT JOIN firehose 
+      ON (stories.stoid = firehose.srcid AND firehose.type = "story")
+    LEFT JOIN discussions ON firehose.discussion = discussions.id
+    $where_clause
+    $orderby_clause
+    $limit_clause
 EOSQL
 
+    #warn($sql);
+    #warn(Dumper(@attrs));
+
     my $sth = $dbh->prepare($sql);
-    $sth->execute(@query_param);
-    my $rs = $sth->fetchall_arrayref({});
+    $sth->execute(@attrs);
+    my $stories = $sth->fetchall_arrayref({});
 
-    if (!$rs) {
-        $dbh->disconnect();
+    if (!$stories) {
+        $self->disconnect_db();
         return;
     }
-    if (@$rs == 0) {
-        $dbh->disconnect();
-        return;
+    if (@$stories == 0) {
+        $self->disconnect_db();
+        return $unique ? undef : [];
     }
 
     # get tags
     $sql = <<"EOSQL";
+SELECT tags.*, tagnames.tagname, target.stoid
+  FROM (SELECT stories.stoid FROM stories 
+        LEFT JOIN firehose ON (stories.stoid = firehose.srcid AND firehose.type = "story")
+        $where_clause $orderby_clause $limit_clause) AS target
+    LEFT JOIN globjs
+      ON target.stoid = globjs.target_id
+    LEFT JOIN tags
+      ON globjs.globjid = tags.globjid
+    LEFT JOIN tagnames
+      ON tags.tagnameid = tagnames.tagnameid
+    WHERE globjs.gtid = 1
+EOSQL
+
+    $sth = $dbh->prepare($sql);
+    $sth->execute(@attrs);
+    my $tags_table = $sth->fetchall_arrayref({});
+
+    # get topics
+    $sql = <<"EOSQL";
 SELECT story_topics_rendered.*, story_topics_chosen.weight, topics.*
-  FROM (SELECT stoid FROM stories $sub_clauses) AS latest
-    INNER JOIN story_topics_rendered ON latest.stoid = story_topics_rendered.stoid
-    LEFT JOIN story_topics_chosen ON story_topics_rendered.stoid = story_topics_chosen.stoid
-      AND story_topics_rendered.tid = story_topics_chosen.tid
-    LEFT JOIN topics ON story_topics_rendered.tid = topics.tid
+  FROM (SELECT stories.stoid FROM stories
+        LEFT JOIN firehose ON (stories.stoid = firehose.srcid AND firehose.type = "story")
+        $where_clause $orderby_clause $limit_clause) AS target
+    LEFT JOIN story_topics_rendered
+      ON target.stoid = story_topics_rendered.stoid
+    LEFT JOIN story_topics_chosen 
+      ON story_topics_rendered.stoid = story_topics_chosen.stoid
+        AND story_topics_rendered.tid = story_topics_chosen.tid
+    LEFT JOIN topics
+      ON story_topics_rendered.tid = topics.tid
+EOSQL
+
+    $sth = $dbh->prepare($sql);
+    $sth->execute(@attrs);
+    my $topics_table = $sth->fetchall_arrayref({});
+
+    # get params
+    $sql = <<"EOSQL";
+SELECT story_param.*
+  FROM (SELECT stories.stoid FROM stories
+        LEFT JOIN firehose ON (stories.stoid = firehose.srcid AND firehose.type = "story")
+        $where_clause $orderby_clause $limit_clause) AS target
+    LEFT JOIN story_param
+      ON target.stoid = story_param.stoid
 EOSQL
 
     $sth = $dbh->prepare($sql);
-    $sth->execute(@query_param);
-    my $topics_table = $sth->fetchall_arrayref(+{});
-    $dbh->disconnect();
+    $sth->execute(@attrs);
+    my $params_table = $sth->fetchall_arrayref({});
+
+    # done
+    $self->disconnect_db();
+
+
+    my $tags = {};
+    for my $tag (@$tags_table) {
+        my $stoid = $tag->{stoid};
+        if (!$tags->{$stoid}) {
+            $tags->{$stoid} = [];
+        }
+        push @{$tags->{$stoid}}, $tag;
+    }
 
     my $topics = {};
     for my $topic (@$topics_table) {
-        if (!$topic->{stoid}) {
-            $topics->{$topic->{stoid}} = [];
+        my $stoid = $topic->{stoid};
+        if (!$topics->{$stoid}) {
+            $topics->{$stoid} = [];
+        }
+        push @{$topics->{$stoid}}, $topic;
+    }
+
+    my $params = {};
+    for my $param (@$params_table) {
+        my $stoid = $param->{stoid};
+        if (!$params->{$stoid}) {
+            $params->{$stoid} = [];
         }
-        push @{$topics->{$topic->{stoid}}}, $topic;
+        push @{$params->{$stoid}}, $param;
     }
 
-    for my $story (@$rs) {
+    for my $story (@$stories) {
         my $stoid = $story->{stoid};
-        $story->{topics} = $topics->{$stoid};
-        $self->_generalize($story);
+        $story->{tags} = $tags->{$stoid} if $tags->{$stoid};
+        $story->{topics} = $topics->{$stoid} if $topics->{$stoid};
+        if ($params->{$stoid}) {
+            for my $param (@{$params->{$stoid}}) {
+                $story->{$param->{name}} = $param->{value};
+            }
+        }
+        $self->_generalize($story, $params);
+    }
+
+    return $stories->[0] if $unique;
+    return $stories;
+}
+
+sub _check_and_regularize_params {
+    my ($self, $params) = @_;
+    my $msg;
+
+    if (defined $params->{title}) {
+        if (length($params->{title}) > $self->{options}->{Story}->{title_max_byte}) {
+            $msg = "title too long. max: $self->{options}->{Story}->{title_max_byte} bytes";
+            $self->set_error($msg, -1);
+            return;
+        }
+    }
+
+    $params->{commentstatus} = $params->{commentstatus} || $params->{comment_status} || "enabled";
+    if (defined $params->{commentstatus}) {
+        if (!grep /\A$params->{commentstatus}\z/, qw(disabled
+                                                     enabled
+                                                     friends_only
+                                                     friends_fof_only
+                                                     no_foe
+                                                     no_foe_eof 
+                                                     logged_in)) {
+            $msg = "invalid comment_status";
+            $self->set_error($msg, -1);
+            return;
+        }
     }
 
-    return $rs->[0] if $return_single;
-    return $rs;
+    # check timestamp. use ISO8601 style timestamp like: 2006-08-14T02:34:56-0600
+    if ($params->{time}) {
+        my $rex_timestamp = qr/
+                                  ^(\d+)-(\d+)-(\d+)\D+(\d+):(\d+):(\d+(?:\.\d+)?)   # datetime
+                                  (?:Z|([+-])(\d+):(\d+))?$                          # tz
+                              /xi;
+        if ($params->{time} =~ $rex_timestamp) {
+            $params->{time} = "$1-$2-$3 $4:$5:$6";
+        }
+    }
+
+    return 1;
+}
+
+sub _set_tags_from_topics {
+    my ($self, $user, $stoid, $topics) = @_;
+
+    return if !$stoid;
+    return if !$topics;
+
+    my $globjs = $self->new_instance_of("Newslash::Model::Globjs");
+    my $globj_id = $globjs->getGlobjidFromTargetIfExists("stories", $stoid);
+    if ($globj_id) {
+        # set tags
+        my $tags = $self->new_instance_of("Tags");
+        for my $tid (keys %$topics) {
+            my $ret = $tags->set_tag(uid => $user->{uid} || $user->{user_id},
+                                     tagname_id => $tid,
+                                     globj_id => $globj_id,
+                                     private => 0,
+                                    );
+            #warn "set_tag fault..." if !$ret
+        }
+    }
+    return $stoid;
 }
 
+=head2 update
+
+this implementation uses old slash's updateStory($sid, $data),
+$sid is takable sid or stoid.
+
+=cut
+
+sub update {
+    #my ($self, $params, $user, $extra_params, $opts) = @_;
+    my $self = shift;
+    my $params = {@_};
+
+    # check id
+    my $id = $params->{stoid} || $params->{story_id} || $params->{id};
+    if (!$id) {
+        $self->set_error("story id not given");
+        return;
+    }
+
+    # check params
+    return if !$self->_check_and_regularize_params($params);
+
+    my $stoid = $params->{stoid} || $params->{story_id} || $params->{id};
+    my $slash_db = Newslash::Model::SlashDB->new($self->{options});
+
+    my $sid = $slash_db->updateStory($stoid, $params);
+    return if !$sid;
+
+    $self->_set_tags_from_topics($params->{user}, $stoid, $params->{topics_chosen});
+    return $stoid;
+}
+
+sub update2 {
+    my $self = shift;
+    my $params = {@_};
+    return $self->generic_update(params => $params);
+}
+
+
 =head2 create(\%params, $uid)
 
 create a story.
@@ -285,31 +408,39 @@ stoid
 =cut
 
 sub create {
-    my ($self, $params, $user, $extra_params, $opts) = @_;
+    #my ($self, $params, $user, $extra_params, $opts) = @_;
+    my $self = shift;
+    return if $self->check_readonly;
+
+    my $params = {@_};
+    my $user = $params->{user};
 
     # check parameters
     my $msg = "";
-    $msg = "no title" if !$params->{title};
-    $msg = "no title" if !$params->{title};
-    $msg = "no uid" if !$params->{uid};
-    $msg = "no topics" if !defined $params->{topics_chosen};
+    $msg = "no_title" if !$params->{title};
+    $msg = "no_introtext" if !$params->{introtext} || $params->{intro_text};
+    $msg = "no_topics" if !defined $params->{topics_chosen};
+    $msg = "invalid_user" if !defined $user->{uid};
 
     if (length($params->{title}) > $self->{options}->{Story}->{title_max_byte}) {
         $msg = "title too long. max: $self->{options}->{Story}->{title_max_byte} bytes";
     }
 
-    $params->{commentstatus} ||= "enabled";
+    $params->{commentstatus} = $params->{commentstatus} || $params->{comment_status} || "enabled";
     if (!grep /\A$params->{commentstatus}\z/, qw(disabled enabled friends_only friends_fof_only no_foe no_foe_eof logged_in)) {
-        $msg = "invalid commentstatus";
+        $msg = "invalid comment_status";
     }
 
     # check timestamp. use ISO8601 style timestamp like: 2006-08-14T02:34:56-0600
-    my $rex_timestamp = qr/
-                         ^(\d+)-(\d+)-(\d+)\D+(\d+):(\d+):(\d+(?:\.\d+)?)   # datetime
-                         (?:Z|([+-])(\d+):(\d+))?$                          # tz
-                          /xi;
-    if ($params->{time} =~ $rex_timestamp) {
-        $params->{time} = "$1-$2-$3 $4:$5:$6";
+    if ($params->{time}) {
+        my $rex_timestamp = qr/
+                                  ^(\d+)-(\d+)-(\d+)[^ 0-9]+(\d+):(\d+):(\d+(?:\.\d+)?)   # datetime
+                                  (?:Z|([+-])(\d+):(\d+))?$                          # tz
+                              /xi;
+        if ($params->{time} =~ $rex_timestamp) {
+            my $dt = DateTime::Format::ISO8601->parse_datetime($params->{time});
+            $params->{time} = DateTime::Format::MySQL->format_datetime($dt);
+        }
     }
 
     # check parameters finish
@@ -317,33 +448,86 @@ sub create {
         $self->set_error($msg, -1);
         return;
     }
-
     $params->{neverdisplay} ||= 0;
+    $params->{submitter} ||= $user->{uid};
+    $params->{uid} = $user->{uid};
 
-    # createStory deletes topics_chosen, so save before.
+    # createStory() deletes topics_chosen, so need to save here.
     my $topics_chosen = $params->{topics_chosen};
 
     my $slash_db = Newslash::Model::SlashDB->new($self->{options});
-    my $sid = $slash_db->createStory($params);
+    my ($sid, $stoid);
+    if ($params->{update}) {
+        $stoid = $params->{stoid} || $params->{story_id} || $params->{id};
+        $sid = $slash_db->updateStory($stoid, $params);
+        $self->set_error("updateStory failed");
+        return if !$sid;
+    }
+    else {
+        ($sid, $stoid) = $slash_db->createStory($params);
+    }
 
     my $globjs = $self->new_instance_of("Newslash::Model::Globjs");
     my $globj_id = $globjs->getGlobjidFromTargetIfExists("stories", $params->{stoid});
     if ($globj_id) {
         # set tags
-        use Newslash::Model::Tags;
-        my $tags = $self->new_instance_of("Newslash::Model::Tags");
+        my $tags = $self->new_instance_of("Tags");
         for my $tid (keys %$topics_chosen) {
-            my $ret = $tags->set_tag(uid => $user->{uid},
+            my $ret = $tags->set_tag(uid => $user->{uid} || $user->{user_id},
                                      tagname_id => $tid,
                                      globj_id => $globj_id,
                                      private => 0,
                                     );
-            warn "set_tag fault..." if !$ret
+            #warn "set_tag fault..." if !$ret
         }
     }
-    return $sid;
+    return wantarray ? ($sid, $stoid) : $stoid;
 }
 
+sub create2 {
+    my $self = shift;
+    my $params = {@_};
+    return $self->generic_insert(params => $params);
+}
+
+sub allocate_sid {
+    my ($self, @params) = @_;
+    my $params = {@params};
+    my $dt = $params->{base_datetime} || DateTime->now;
+
+    # create sid from timestamp
+    # my $sid_format = '%02d/%02d/%02d/%02d%0d2%02d';
+    my $sid_format = '%y/%m/%d/%H%M%S';
+    my $sid = $dt->strftime($sid_format);
+
+    # insert blank story with given sid
+    my $dbh = $self->connect_db;
+    my $sql = "INSERT INTO stories (sid) VALUES (?)";
+
+    my $n = 100; # retry 100 times
+    while (--$n) {
+        my $rs = $dbh->do($sql, undef, $sid);
+        if (!defined $rs) {
+            $self->set_error("sid_insert_error", -1);
+            return;
+        }
+        if ($rs) {
+            my $stoid = $dbh->last_insert_id(undef, undef, undef, undef);
+            $self->disconnect_db;
+            return ($sid, $stoid);
+        }
+
+        # allocate failed, so recreate sid
+        $dt->subtract( seconds => 1 );
+        $sid = $dt->strftime($sid_format);
+    }
+    $self->set_error("sid_allocate_failed", -1);
+    $self->disconnect_db;
+    return;
+}
+
+
+# Legacy API
 sub createSid {
     my ($self, $bogus_sid) = @_;
     # yes, this format is correct, don't change it :-)
@@ -379,403 +563,15 @@ sub createSid {
     return sprintf($sidformat, @lt[reverse 0..5]);
 }
 
-=pod
-
-sub grantStorySubmissionKarma {
-    my($self, $story) = @_;
-    #my $constants = getCurrentStatic();
-    my $db = $self->new_instance_of('LegacyDB');
-    #if ($constants->{plugin}{FireHose}) {
-    {
-        my $fhid;
-        #my $firehose = getObject("Slash::FireHose");
-        if ($story->{fhid}) {
-            $fhid = $story->{fhid};
-        } elsif ($story->{subid}) {
-            #my $subid_q = $self->sqlQuote($story->{subid});
-            #($fhid) = $self->sqlSelect('id', 'firehose', "type='submission' and srcid=$subid_q");
-            my $subid_q = $db->sqlQuote($story->{subid});
-            ($fhid) = $db->sqlSelect('id', 'firehose', "type='submission' and srcid=$subid_q");
-        }
-        #$firehose->setFireHose($fhid, { accepted => "yes" }) if $fhid;
-        $self->setFireHose($fhid, { accepted => "yes" }) if $fhid;
-    }
-    return 0 unless $story->{subid};
-    #my($submitter_uid) = $self->sqlSelect(
-    my($submitter_uid) = $db->sqlSelect(
-                                        'uid', 'submissions',
-                                        # 'subid=' . $self->sqlQuote($story->{subid})
-                                        'subid=' . $db->sqlQuote($story->{subid})
-                                       );
-
-    if (!isAnon($submitter_uid)) {
-        #my $constants = getCurrentStatic();
-        my $maxkarma = 50;
-        #$self->sqlUpdate('users_info',
-        $db->sqlUpdate('users_info',
-                       { -karma => "LEAST(karma + $constants->{submission_bonus}, $maxkarma)" },
-                       "uid=$submitter_uid");
-
-        #$self->clearRookie($submitter_uid);
-        my $users = $self->new_instance_of('Users');
-        $users->clearRookie($submitter_uid);
-
-        #$self->validateSubmitter($submitter_uid);
-        $users->update(target => 'class',
-                       field => 'validated_submitter',
-                       now => 1);
-        #$self->setUser_delete_memcached($submitter_uid);
-    }
-
-    #my $submission_info = { del => 2 };
-    #$submission_info->{stoid} = $story->{stoid} if $story->{stoid};
-    #$submission_info->{sid}   = $story->{sid}   if $story->{sid};
-    #$self->setSubmission($story->{subid}, $submission_info);
-    my $submissions = $self->new_instance_of('Submissions');
-    $submissions->update(subid => $story->{subid},
-                         field => 'del',
-                         value => 2);
-    $submissions->upsert_param(params => {
-                                          stoid => $story->{stoid},
-                                          sid => $story->{sid},
-                                         },
-                               subid => $story->{subid});
-}
 
+=head2 get_histories
 
 =cut
 
-########################################################
-
-=pod
-
-sub createStory {
-    my($self, $story) = @_;
-
-    #my $constants = getCurrentStatic();
-    my $db = $self->new_instance_of('LegacyDB');
-    #$self->sqlDo("SET AUTOCOMMIT=0");
-    $db->sqlDo("SET AUTOCOMMIT=0");
-
-    my $error = "";
-
-    $story->{submitter} = $story->{submitter} ?  $story->{submitter} : $story->{uid};
-    $story->{is_dirty} = 1;
-
-    if (!defined($story->{title})) {
-        $error = "createStory needs a defined title";
-    } else {
-        # Rather than call truncateStringForCharColumn() here,
-        # we prefer to throw an error.  Unlike createComment,
-        # we would prefer that overlong subjects not be silently
-        # chopped off.  Consider the consequences of saving a
-        # story with the headline "Chris Nandor is a Freakishly
-        # Ugly Twisted Criminal, Claims National Enquirer" and
-        # later realizing it had been truncated after 50 chars.
-        #my $title_len = $self->sqlGetCharColumnLength('story_text', 'title');
-        my $title_len = $db->sqlGetCharColumnLength('story_text', 'title');
-        if ($title_len && length($story->{title}) > $title_len) {
-            $error = "createStory title too long: " . length($story->{title}) . " > $title_len";
-        }
-    }
-
-    my $stoid;
-    if (!$error) {
-        #$story->{sid} = createSid();
-        $story->{sid} = $self->createSid;
-        my $sid_ok = 0;
-        while ($sid_ok == 0) {
-            # we rely on logic in setStory() later to properly
-            # set up the data for a story, so we can't someday
-            # just change this to do an insert of all the story
-            # data, we do need to continue pass it through
-            # setStory()
-            #$sid_ok = $self->sqlInsert('stories',
-            $sid_ok = $db->sqlInsert('stories',
-                                     { sid => $story->{sid} },
-                                     { ignore => 1 } ); # don't need error messages
-            if ($sid_ok == 0) { # returns 0E0 on collision, which == 0
-                # Keep looking...
-                $story->{sid} = $self->createSid($story->{sid});
-            }
-        }
-        # If this came from a submission, update submission and grant
-        # karma to the user
-        $stoid = $self->getLastInsertId({ table => 'stories', prime => 'stoid' });
-        $story->{stoid} = $stoid;
-        $self->grantStorySubmissionKarma($story);
-    }
-
-    if (!$error) {
-        #if (! $self->sqlInsert('story_text', { stoid => $stoid })) {
-        if (! $db->sqlInsert('story_text', { stoid => $stoid })) {
-            $error = "sqlInsert failed for story_text: " . $self->sqlError();
-        }
-    }
-
-    # Write the chosen topics into story_topics_chosen.  We do this
-    # here because it returns the primaryskid and we will write that
-    # into the stories table with setStory in just a moment.
-       my($primaryskid, $tids);
-       if (!$error) {
-               my $success = $self->setStoryTopicsChosen($stoid, $story->{topics_chosen});
-               $error = "Failed to set chosen topics for story '$stoid'\n" if !$success;
-       }
-       if (!$error) {
-               my $info_hr = { };
-               $info_hr->{neverdisplay} = 1 if $story->{neverdisplay};
-               ($primaryskid, $tids) = $self->setStoryRenderedFromChosen($stoid,
-                       $story->{topics_chosen}, $info_hr);
-               $error = "Failed to set rendered topics for story '$stoid'\n" if !defined($primaryskid);
-       }
-       delete $story->{topics_chosen};
-       my $commentstatus = delete $story->{commentstatus};
-
-       if (!$error) {
-               if ($story->{fhid} && $constants->{plugin}{FireHose}) {
-                       my $firehose = getObject("Slash::FireHose");
-                       my $item = $firehose->getFireHose($story->{fhid});
-                       $firehose->setFireHose($story->{fhid}, { stoid => $stoid });
-                       if ($item && $item->{type} eq "journal") {
-                               $story->{discussion} = $item->{discussion};
-                               $story->{journal_id} = $item->{srcid};
-
-                               if ($story->{journal_id}) {
-                                       if (!$self->sqlCount("journal_transfer", "id = ".$self->sqlQuote($story->{journal_id}))) {
-                                               $self->sqlInsert("journal_transfer", {
-                                                       id => $story->{journal_id}
-                                               });
-                                       }
-                               }
-                       }
-
-               } elsif ($story->{subid}) {
-                       if ($self->sqlSelect('id', 'journal_transfer',
-                               'subid=' . $self->sqlQuote($story->{subid})
-                       )) {
-                               my $sub = $self->getSubmission($story->{subid});
-                               if ($sub) {
-                                       for (qw(discussion journal_id by by_url)) {
-                                               $story->{$_} = $sub->{$_};
-                                       }
-                               }
-                       }
-               }
-
-               $story->{body_length} = defined($story->{bodytext}) ? length($story->{bodytext}) : 0;
-               $story->{word_count} = countWords($story->{introtext}) + countWords($story->{bodytext});
-               $story->{primaryskid} = $primaryskid;
-               $story->{tid} = $tids->[0];
-
-               if (! $self->setStory($stoid, $story)) {
-                       $error = "setStory failed after creation: " . $self->sqlError();
-               }
-       }
-       if (!$error) {
-               my $rootdir;
-               if ($story->{primaryskid}) {
-                    
-                       my $storyskin = $self->getSkin($story->{primaryskid});
-                       $rootdir = $storyskin->{rootdir};
-               } else {
-                       # The story is set never-display so its discussion's rootdir
-                       # probably doesn't matter.  Just go with the default.
-                       my $storyskin = $self->getSkin($constants->{mainpage_skid});
-                       $rootdir = $storyskin->{rootdir};
-               }
-               my $comment_codes = $self->getDescriptions('commentcodes_extended');
-
-               my $discussion = {
-                       kind            => 'story',
-                       uid             => $story->{uid},
-                       title           => $story->{title},
-                       primaryskid     => $primaryskid,
-                       topic           => $tids->[0],
-                       url             => $self->getUrlFromSid(
-                                               $story->{sid},
-                                               $story->{primaryskid},
-                                               $tids->[0],
-                                               $story->{title}
-                                          ),
-                       stoid           => $stoid,
-                       sid             => $story->{sid},
-                       commentstatus   => $comment_codes->{$commentstatus}
-                                          ? $commentstatus
-                                          : $constants->{defaultcommentstatus},
-                       ts              => $story->{'time'}
-               };
-
-               my $id;
-               if ($story->{discussion} && $story->{journal_id}) {
-                       # updating now for journals tips off users that this will
-                       # be a story soon, esp. ts, url, title, kind ... i don't
-                       # care personally, does it matter?  if so we can task some
-                       # of these changes, if we need to make them -- pudge
-
-                       # update later in task
-                       delete @{$discussion}{qw(title url ts)};
-                       delete $discussion->{uid}; # leave it "owned" by poster
-
-                       $id = $story->{discussion};
-                       $discussion->{kind} = 'journal-story';
-                       $discussion->{type} = 'open'; # should be already
-                       $discussion->{archivable} = 'yes'; # for good measure
-
-                       if (!$self->setDiscussion($id, $discussion)) {
-                               $error = "Failed to set discussion data for story\n";
-
-                       } elsif ($story->{journal_id}) {
-                               $self->sqlUpdate('journal_transfer', {
-                                       stoid   => $stoid,
-                                       updated => 0,
-                               }, 'id=' . $self->sqlQuote($story->{journal_id}));
-                       }
-
-               } else {
-                       $id = $self->createDiscussion($discussion);
-                       if (!$id) {
-                               $error = "Failed to create discussion for story";
-                       }
-               }
-               if (!$error && !$self->setStory($stoid, { discussion => $id })) {
-                       $error = "Failed to set discussion '$id' for story '$stoid'\n";
-               }
-       }
-
-       if ($error) {
-               # Rollback doesn't even work in 4.0.x, since some tables
-               # are non-transactional...
-               $self->sqlDo("ROLLBACK");
-               $self->sqlDo("SET AUTOCOMMIT=1");
-               chomp $error;
-               print STDERR scalar(localtime) . " createStory error: $error\n";
-               return "";
-       }
-
-       $self->sqlDo("COMMIT");
-       $self->sqlDo("SET AUTOCOMMIT=1");
-
-       if ($constants->{plugin}{FireHose}) {
-               my $firehose = getObject("Slash::FireHose");
-               $firehose->createItemFromStory($stoid);
-       }
-
-       return $story->{sid};
-}
-
-sub editCreateStory {
-    my($self, $preview, $fhitem) = @_;
-    my $constants = getCurrentStatic();
-    my $user = getCurrentUser();
-    my $form = getCurrentForm();
-    my $data;
-
-    my $tagsdb = getObject("Slash::Tags");
-    my $admindb = getObject("Slash::Admin");
-
-    my @topics;
-    push @topics, $fhitem->{tid} if $fhitem->{tid};
-
-    my $chosen_hr = $tagsdb->extractChosenFromTags($fhitem->{globjid}, 'admin');
-
-    my $save_extras = $self->getExtrasToSaveForChosen($chosen_hr, $preview);
-
-    my $is_sectiononly = $tagsdb->isAdminTagged($fhitem->{globjid}, 'sectiononly');
-    $save_extras->{offmainpage} = 1 if $is_sectiononly;
-
-    $data = {
-             uid             => $fhitem->{uid},
-             #sid
-             title           => $preview->{title},
-             #section
-             submitter       => $preview->{submitter},
-             topics_chosen   => $chosen_hr,
-             dept            => $fhitem->{dept},
-             'time'          => $admindb->findTheTime($fhitem->{createtime}, $preview->{fastforward}),
-             bodytext        => $preview->{bodytext},
-             introtext       => $preview->{introtext},
-             #relatedtext
-             media           => $fhitem->{media},
-             commentstatus   => $preview->{commentstatus},
-             thumb           => $fhitem->{thumb},
-             -rendered       => 'NULL',
-             neverdisplay    => $preview->{neverdisplay},
-             sponsor         => $preview->{sponsor},
-             stuck           => $preview->{stuck},
-             stuckpos        => $preview->{stuckpos},
-             stuckendtime    => $preview->{stuckendtime},
-            };
-
-    foreach my $key (keys %$save_extras) {
-        $data->{$key} = $save_extras->{$key};
-    }
-
-    $data->{subid} = $preview->{subid} if $preview->{subid};
-    $data->{fhid} = $preview->{fhid} if $preview->{fhid};
-
-    for (qw(dept bodytext relatedtext)) {
-        $data->{$_} = '' unless defined $data->{$_};  # allow to blank out
-    }
-
-    for my $field (qw( introtext bodytext media )) {
-        local $Slash::Utility::Data::approveTag::admin = 2;
-
-        # XXXEdit check this
-        $data->{$field} = $self->autoUrl($form->{section}, $data->{$field});
-        $data->{$field} = cleanSlashTags($data->{$field});
-        $data->{$field} = strip_html($data->{$field});
-        $data->{$field} = slashizeLinks($data->{$field});
-        $data->{$field} = parseSlashizedLinks($data->{$field});
-        $data->{$field} = balanceTags($data->{$field});
-        $data->{$field} = adjustStoryTags($data->{$field});
-    }
-
-    for (qw(dept bodytext relatedtext)) {
-        $data->{$_} = '' unless defined $data->{$_};  # allow to blank out
-    }
-
-    my $sid = $self->createStory($data);
-
-    if ($sid) {
-        my $st = $self->getStory($sid);
-        $self->setRelated($sid);
-        slashHook('admin_save_story_success', { story => $data });
-        my $stoid = $st->{stoid};
-        my $story_globjid = $self->getGlobjidCreate('stories', $stoid);
-
-        # XXXEdit Do we have to worry about user editing vs author uid on transfer
-        $tagsdb->transferTags($fhitem->{globjid}, $story_globjid);
-
-        #Don't automatically signoff with new editor, this makes it automatically disapper for an admin on first refresh
-        $self->createSignoff($st->{stoid}, $user->{uid}, "created", { no_filter => 1});
-
-        #XXXEdit Tags Auto save?
-        my $admindb = getObject("Slash::Admin");
-        if ($admindb) {
-            $admindb->grantStoryPostingAchievements($data->{uid}, $data->{submitter});
-            $admindb->addSpriteForSid($sid);
-        }
-
-        #XXX Move this to Slash::DB
-        my $sfids = $self->sqlSelect('value', 'preview_param', "name = 'sfid' and preview_id = " . $preview->{preview_id});
-        if ($sfids && $stoid) {
-            $self->sqlUpdate('static_files', { stoid => $stoid, fhid => 0 }, 'fhid = ' . $preview->{preview_fhid});
-        }
-    }
-
-    return $sid;
+sub get_histories {
 }
 
-
-
-
-=cut
-
-
-
-
-
-=head2 related_link($stoid)
+=head2 get_related_items($stoid)
 
 get related links.
 
@@ -799,29 +595,38 @@ ARRAY of related links
 
 =cut
 
-sub related {
-    my ($self, $stoid) = @_;
+sub get_related_items {
+    my $self = shift;
+    my $params = {@_};
+    my $stoid = $params->{stoid} || $params->{story_id} || $params->{id};
+    return if !$stoid;
 
     my $dbh = $self->connect_db;
 
     my $sql = <<"EOSQL";
-SELECT related.*, story_text.title as title2, firehose.srcid
+SELECT related.*, 
+       story_text.title as title2,
+       firehose.*,
+       stories.*,
+       topics.*
   FROM (
     SELECT * FROM related_stories
       WHERE stoid = ?
       ORDER BY ordernum ASC
     ) AS related
-  LEFT JOIN story_text ON related.rel_stoid = story_text.stoid
+  LEFT JOIN story_text ON story_text.stoid = related.rel_stoid
   LEFT JOIN firehose ON firehose.id = related.fhid
+  LEFT JOIN stories ON stories.sid = related.rel_sid
+  LEFT JOIN topics ON topics.tid = stories.tid
 EOSQL
 
     my $sth = $dbh->prepare($sql);
     $sth->execute($stoid);
     my $related = $sth->fetchall_arrayref({});
-    $sth->finish;
-    $dbh->disconnect();
+    $self->disconnect_db();
 
     for my $r (@$related) {
+        $r->{create_time} = $r->{time};
         $r->{title} = $r->{title2} unless $r->{title};
         if ($r->{rel_sid}) {
             $r->{type} = "story";
@@ -830,6 +635,14 @@ EOSQL
             $r->{type} = "submission";
             $r->{key_id} = $r->{srcid};
         }
+        $r->{primary_topic} = {};
+        $r->{primary_topic}->{tid} = $r->{tid};
+        for my $k (qw{keyword textname series image width height
+                      submittable searchable storypickable usesprite}) {
+            next if !$r->{$k};
+            $r->{primary_topic}->{$k} = $r->{$k};
+            delete $r->{$k};
+        }
     }
 
     return $related;
@@ -888,7 +701,7 @@ EOSQL
         $sth->finish;
     }
 
-    $dbh->disconnect();
+    $self->disconnect_db();
     return $params;
 }
 
@@ -924,6 +737,7 @@ id of the story
 
 sub set_dirty {
     my ($self, $key, $id) = @_;
+    return if $self->check_readonly;
 
     my $stoid;
     if ($key eq 'stoid') {
@@ -946,19 +760,75 @@ EOSQL
 }
 
 sub _generalize {
-    my ($self, $story) = @_;
+    my ($self, $story, $params) = @_;
+    $params ||= {};
 
-    $story->{content_type} = "story";
+    # NTO-nized
+    $story->{id} = $story->{stoid};
+    $story->{story_id} = $story->{stoid};
+    $story->{create_time} = $story->{time};
+    $story->{update_time} = $story->{last_update};
 
-    #my $max_weight = 0;
     for my $t (@{$story->{topics}}) {
-        if ($t->{tid} == $story->{tid}) {
+        if ($t->{tid} && $t->{tid} == $story->{tid}) {
             $story->{primary_topic} = $t;
         }
-        #if ($t->{weight} && $t->{weight} > $max_weight) {
-        #    $max_weight = $t->{weight};
-        #}
     }
+
+    $story->{content_type} = "story";
+    $story->{intro_text} = $story->{introtext};
+    $story->{bodytext} ||= "";
+    $story->{body_text} = $story->{bodytext};
+    if ($story->{body_text}) {
+        $story->{full_text} = join("\n", $story->{intro_text}, $story->{body_text});
+    }
+    else {
+        $story->{full_text} = $story->{intro_text};
+    }
+    $story->{fulltext} = $story->{full_text};
+
+    $story->{discussion_id} = $story->{discussion};
+
+    # no public flag given, public is 'yes'
+    $story->{public} = 'yes' if !$story->{public};
+
+}
+
+# delete story from database
+# this method is for test purpose only.
+sub hard_delete {
+    my $self = shift;
+    return if $self->check_readonly;
+    my $params = {@_};
+
+    my $stoid = $params->{story_id};
+    return if !$stoid;
+
+    my $error = 0;
+    my $sql;
+
+    #my $dbh = $self->connect_db({AutoCommit => 0,});
+    my $dbh = $self->start_transaction;
+    for my $table (qw(stories story_param story_text story_topics_chosen story_topics_rendered)) {
+        my $sql = "DELETE FROM $table WHERE stoid = ?";
+        my $rs = $dbh->do($sql, undef, $stoid);
+        if (!defined $rs || $rs == 0) {
+            Mojo::Log->new->warn("DELETE FROM $table failed. stoid is $stoid.");
+            $error = 1;
+        }
+    }
+
+    # delete from firehose
+    my $firehose = $self->new_instance_of("Firehose");
+    $firehose->hard_delete("story", $stoid);
+
+    $self->commit;
+    return !$error;
+
+    # delete globjs
+    # delete tags
+
+    return;
 }
 
 1;