OSDN Git Service

move unused (deprecated) files
[newslash/newslash.git] / src / newslash_web / lib / Newslash / Model / Stories.pm
index 2206795..99ea3cd 100644 (file)
@@ -1,13 +1,60 @@
 package Newslash::Model::Stories;
 use Newslash::Model::Base -base;
 
+use Newslash::Model::SlashDB;
+
+use DateTime;
+use DateTime::Format::MySQL;
+use DateTime::Format::ISO8601;
+
 use Data::Dumper;
+use DateTime;
+
+
+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,
+                       }
+           };
+}
+
+use constant FACULTIES => { 1000 => [qw(hits hitparade)] };
+
+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'); }
+
 
 #========================================================================
 
-=head2 latest(\%options)
+=head2 select($query_type, $value)
 
-get latest stories.
+get a story.
 
 =over 4
 
@@ -15,96 +62,323 @@ get latest stories.
 
 =over 4
 
-=item \%options
+=item $query_type
+
+query key, "sid" or "stoid"
 
-options for query.
+=item $value
 
-$options->{show_future}: when 1, return feature stories. default is 0.
-$options->{limit}: number of stories. default is 10.
+value for query
 
 =back
 
 =item Return value
 
-ARRAY of story contents
+HASH of story contents
 
 =back
 
 =cut
 
-sub latest {
-  my ($self, $options) = @_;
-  $options ||= {};
+sub select {
+    my $self = shift;
+    my $params = {@_};
+
+
+    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()";
+    }
 
-  my $show_future = $options->{show_future} || 0;
-  my $limit = $options->{limit} || 10;
+    # hide non-public story?
+    if ($params->{public_only}) {
+        push @where_clauses, "firehose.public != 'no'";
+    }
 
-  my $dbh = $self->connect_db;
+    if (@where_clauses) {
+        if ($where_clause) {
+            $where_clause = $where_clause . " AND ";
+        }
+        else {
+            $where_clause = "WHERE ";
+        }
+        $where_clause = $where_clause . join(" AND ", @where_clauses);
+    }
 
-  # get stories
-  my $where_clause = 'WHERE stories.time <= NOW()';;
-  if ($show_future) {
-    $where_clause = '';
-  }
+    my @attrs;
+    push @attrs, @$where_values, @$limit_values, @$orderby_values;
 
-  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
+    my $dbh = $self->connect_db;
+    my $sql = <<"EOSQL";
+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
+    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
 
-  my $sth = $dbh->prepare($sql);
+    #warn($sql);
+    #warn(Dumper(@attrs));
 
-  $sth->execute($limit);
-  my $rs = $sth->fetchall_arrayref(+{});
-  $sth->finish;
+    my $sth = $dbh->prepare($sql);
+    $sth->execute(@attrs);
+    my $stories = $sth->fetchall_arrayref({});
 
-  if (@$rs == 0) {
-    $dbh->disconnect();
-    return [];
-  }
+    if (!$stories) {
+        $self->disconnect_db();
+        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
 
-  # get tags
-  $sql = <<"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 $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
+  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 = $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->execute($limit);
-  my $topics_table = $sth->fetchall_arrayref(+{});
-  $sth->finish;
-  $dbh->disconnect();
+    $sth = $dbh->prepare($sql);
+    $sth->execute(@attrs);
+    my $params_table = $sth->fetchall_arrayref({});
 
-  my $topics = {};
-  for my $topic (@$topics_table) {
-    if (!$topic->{stoid}) {
-      $topics->{$topic->{stoid}} = [];
+    # 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) {
+        my $stoid = $topic->{stoid};
+        if (!$topics->{$stoid}) {
+            $topics->{$stoid} = [];
+        }
+        push @{$topics->{$stoid}}, $topic;
     }
-    push @{$topics->{$topic->{stoid}}}, $topic;
-  }
 
-  for my $story (@$rs) {
-      my $stoid = $story->{stoid};
-      $story->{topics} = $topics->{$stoid};
-      $self->_generalize($story);
-  }
+    my $params = {};
+    for my $param (@$params_table) {
+        my $stoid = $param->{stoid};
+        if (!$params->{$stoid}) {
+            $params->{$stoid} = [];
+        }
+        push @{$params->{$stoid}}, $param;
+    }
 
-  return $rs;
+    for my $story (@$stories) {
+        my $stoid = $story->{stoid};
+        $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;
+        }
+    }
 
-=head2 select($query_type, $value)
+    $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;
+        }
+    }
 
-get a story.
+    # 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.
 
 =over 4
 
@@ -112,82 +386,192 @@ get a story.
 
 =over 4
 
-=item $query_type
+=item \%params
 
-query key, "sid" or "stoid"
+parameters
 
-=item $value
+$params->{fhid}
+$params->{subid}
 
-value for query
+=item $uid
+
+author's uid
 
 =back
 
 =item Return value
 
-HASH of story contents
+stoid
 
 =back
 
 =cut
 
-sub select {
-  my ($self, $query_type, $value) = @_;
+sub create {
+    #my ($self, $params, $user, $extra_params, $opts) = @_;
+    my $self = shift;
+    return if $self->check_readonly;
 
-  if ($query_type !~ m/\A(sid|stoid)\z/) {
-    return undef;
-  }
+    my $params = {@_};
+    my $user = $params->{user};
 
-  my $dbh = $self->connect_db;
-  my $sql = <<"EOSQL";
-SELECT stories.*, story_text.*, users.nickname as author
-  FROM stories
-    LEFT JOIN story_text ON stories.stoid = story_text.stoid
-    LEFT JOIN users ON stories.uid = users.uid
-  WHERE stories.$query_type = ?
-EOSQL
+    # check parameters
+    my $msg = "";
+    $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};
 
-  my $sth = $dbh->prepare($sql);
+    if (length($params->{title}) > $self->{options}->{Story}->{title_max_byte}) {
+        $msg = "title too long. max: $self->{options}->{Story}->{title_max_byte} bytes";
+    }
 
-  $sth->execute($value);
-  my $story = $sth->fetchrow_hashref;
-  $sth->finish;
+    $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 comment_status";
+    }
 
-  if (!$story) {
-    $dbh->disconnect();
-    return undef;
-  }
+    # check timestamp. use ISO8601 style timestamp like: 2006-08-14T02:34:56-0600
+    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);
+        }
+    }
 
-  my $stoid = $story->{stoid};
-  if (!$stoid) {
-    $dbh->disconnect();
-    return undef;
-  }
+    # check parameters finish
+    if (length($msg) > 0) {
+        $self->set_error($msg, -1);
+        return;
+    }
+    $params->{neverdisplay} ||= 0;
+    $params->{submitter} ||= $user->{uid};
+    $params->{uid} = $user->{uid};
+
+    # 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, $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
+        my $tags = $self->new_instance_of("Tags");
+        for my $tid (keys %$topics_chosen) {
+            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 wantarray ? ($sid, $stoid) : $stoid;
+}
 
-  # get tags
-  $sql = <<"EOSQL";
-SELECT story_topics_rendered.*, story_topics_chosen.weight, topics.*
-  FROM story_topics_rendered
-    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
-  WHERE story_topics_rendered.stoid = ?
-EOSQL
+sub create2 {
+    my $self = shift;
+    my $params = {@_};
+    return $self->generic_insert(params => $params);
+}
 
-  $sth = $dbh->prepare($sql);
+sub allocate_sid {
+    my ($self, @params) = @_;
+    my $params = {@params};
+    my $dt = $params->{base_datetime} || DateTime->now;
 
-  $sth->execute($stoid);
-  my $topics = $sth->fetchall_arrayref(+{});
-  $sth->finish;
-  $dbh->disconnect();
+    # 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);
 
-  $story->{topics} = $topics;
-  $self->_generalize($story);
+    # 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);
+        }
 
-  return $story;
+        # 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;
 }
 
-=head2 related_link($stoid)
+
+# Legacy API
+sub createSid {
+    my ($self, $bogus_sid) = @_;
+    # yes, this format is correct, don't change it :-)
+    my $sidformat = '%02d/%02d/%02d/%02d%0d2%02d';
+    # Create a sid based on the current time.
+    my @lt;
+    my $start_time = time;
+    if ($bogus_sid) {
+        # If we were called being told that there's at
+        # least one sid that is invalid (already taken),
+        # then look backwards in time until we find it,
+        # then go one second further.
+        my $loops = 1000;
+        while (--$loops) {
+            $start_time--;
+            @lt = localtime($start_time);
+            $lt[5] %= 100; $lt[4]++; # year and month
+            last if $bogus_sid eq sprintf($sidformat, @lt[reverse 0..5]);
+        }
+        if ($loops) {
+            # Found the bogus sid by looking
+            # backwards.  Go one second further.
+            $start_time--;
+        } else {
+            # Something's wrong.  Skip ahead in
+            # time instead of back (not sure what
+            # else to do).
+            $start_time = time + 1;
+        }
+    }
+    @lt = localtime($start_time);
+    $lt[5] %= 100; $lt[4]++; # year and month
+    return sprintf($sidformat, @lt[reverse 0..5]);
+}
+
+
+=head2 get_histories
+
+=cut
+
+sub get_histories {
+}
+
+=head2 get_related_items($stoid)
 
 get related links.
 
@@ -211,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";
@@ -242,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;
@@ -300,24 +701,134 @@ EOSQL
         $sth->finish;
     }
 
-    $dbh->disconnect();
+    $self->disconnect_db();
     return $params;
 }
 
+#========================================================================
+
+=head2 set_dirty($key, $id)
+
+set writestatus dirty for the story.
+
+=over 4
+
+=item Parameters
+
+=over 4
+
+=item $key
+
+'stoid'
+
+=item $id
+
+id of the story
+
+=back
+
+=item Return value
+
+1/0
+
+=back
+
+=cut
+
+sub set_dirty {
+    my ($self, $key, $id) = @_;
+    return if $self->check_readonly;
+
+    my $stoid;
+    if ($key eq 'stoid') {
+        $stoid = $id;
+    }
+    else {
+        return;
+    }
+
+    my $dbh = $self->connect_db;
+    my $sql = <<"EOSQL";
+INSERT INTO story_dirty (stoid) VALUES (?)
+EOSQL
+
+    my $rs = $dbh->do($sql, undef, $stoid);
+    if (!$rs) {
+        return;
+    }
+    return 1;
+}
+
 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;