OSDN Git Service

Model::Stories: support tag and related stories setting in create()
[newslash/newslash.git] / src / newslash_web / lib / Newslash / Model / Stories.pm
1 package Newslash::Model::Stories;
2 use Newslash::Model::Base -base;
3
4 use Newslash::Model::SlashDB;
5
6 use Newslash::Model::Stories::Text;
7 use Newslash::Model::Stories::RelatedStories;
8
9 use DateTime;
10 use DateTime::Format::MySQL;
11 use DateTime::Format::ISO8601;
12 use List::Util q(any);
13
14 use Data::Dumper;
15 use DateTime;
16
17
18 sub key_definition {
19     return {
20             table => "stories",
21             primary => "stoid",
22             unique => [qw(sid)],
23             datetime => [qw(time last_update day_published
24                             stuckendtime archive_last_update
25                           )],
26             other => [qw(uid dept hits discussion primaryskid
27                          tid submitter commentcount hitparade
28                          is_archived in_trash
29                          qid body_length word_count sponsor
30                          stuck stuckpos fakeemail homepage
31                        )],
32             aliases => { user_id => "uid",
33                          id => "stoid",
34                          create_time => time,
35                        }
36            };
37 }
38
39 use constant FACULTIES => { 1000 => [qw(hits hitparade)] };
40
41 sub count {
42     my $self = shift;
43     my $join = 'LEFT JOIN firehose ON (stories.stoid = firehose.srcid AND firehose.type = "story")';
44     my $where = 'firehose.public != "no"';
45     return $self->generic_count(table => "stories",
46                                 target => "stoid",
47                                 timestamp => "time",
48                                 join => $join,
49                                 where => $where,
50                                 @_);
51 }
52
53 ##### sub models
54 sub text            { return shift->_get_model('::Stories::Text'); }
55 sub related_stories { return shift->_get_model('::Stories::RelatedStories'); }
56
57 sub _get_model {
58     my ($self, $class) = @_;
59     $self->{sub_models} ||= {};
60     if (!$self->{sub_models}->{$class}) {
61         $self->{sub_models}->{$class} = $self->new_instance_of($class);
62     }
63     else {
64         if ($self->transaction_mode && !$self->{sub_models}->{$class}->transaction_mode) {
65             $self->{sub_models}->{$class}->use_transaction($self->{_tr_dbh});
66         }
67     }
68     return $self->{sub_models}->{$class};
69 }
70
71 #========================================================================
72
73 =head2 select($query_type, $value)
74
75 get a story.
76
77 =over 4
78
79 =item Parameters
80
81 =over 4
82
83 =item $query_type
84
85 query key, "sid" or "stoid"
86
87 =item $value
88
89 value for query
90
91 =back
92
93 =item Return value
94
95 HASH of story contents
96
97 =back
98
99 =cut
100
101 sub select {
102     my $self = shift;
103     my $params = {@_};
104
105
106     my $unique_keys = { id => "stories.stoid",
107                         story_id => "stories.stoid",
108                         stoid => "stories.stoid",
109                         sid => "stories.sid",
110                       };
111     my $keys = { user_id => "stories.uid",
112                  uid => "stories.uid",
113                  topic_id => "stories.tid",
114                  tid => "stories.tid",
115                  discussion_id => "stories.discussion",
116                  commentcount => "stories.commentcount",
117                  hits => "stories.hits",
118                  submitter => "stories.submitter",
119                  create_time => "stories.time",
120                  update_time => "stories.last_update",
121                  public => "firehose.public",
122                };
123     my $datetime_keys = { create_time => "stories.time",
124                           update_time => "stories.last_update",
125                         };
126     my $timestamp = "stories.time";
127
128     my ($where_clause, $where_values, $unique) = $self->build_where_clause(unique_keys => $unique_keys,
129                                                                            keys => $keys,
130                                                                            datetime_keys => $datetime_keys,
131                                                                            timestamp => $timestamp,
132                                                                            params => $params);
133     my ($limit_clause, $limit_values) = $self->build_limit_clause(params => $params);
134     my ($orderby_clause, $orderby_values) = $self->build_order_by_clause(keys => $keys,
135                                                                          params => $params);
136
137     # TODO: give reasonable LIMIT Value...
138     $limit_clause = "LIMIT 50" if !$limit_clause;
139
140     # hide future story?
141     my @where_clauses;
142     if ($params->{hide_future}) {
143         push @where_clauses, "stories.time <= NOW()";
144     }
145
146     # hide non-public story?
147     if ($params->{public_only}) {
148         push @where_clauses, "firehose.public != 'no'";
149     }
150
151     if (@where_clauses) {
152         if ($where_clause) {
153             $where_clause = $where_clause . " AND ";
154         }
155         else {
156             $where_clause = "WHERE ";
157         }
158         $where_clause = $where_clause . join(" AND ", @where_clauses);
159     }
160
161     my @attrs;
162     push @attrs, @$where_values, @$limit_values, @$orderby_values;
163
164     my $dbh = $self->connect_db;
165     my $sql = <<"EOSQL";
166 SELECT stories.*, story_text.*, users.nickname as author, firehose.public,
167        discussions.type AS discussion_type, discussions.commentcount AS comment_count
168   FROM stories
169     LEFT JOIN story_text ON stories.stoid = story_text.stoid
170     LEFT JOIN users ON stories.uid = users.uid
171     LEFT JOIN firehose 
172       ON (stories.stoid = firehose.srcid AND firehose.type = "story")
173     LEFT JOIN discussions ON firehose.discussion = discussions.id
174     $where_clause
175     $orderby_clause
176     $limit_clause
177 EOSQL
178
179     #warn($sql);
180     #warn(Dumper(@attrs));
181
182     my $sth = $dbh->prepare($sql);
183     $sth->execute(@attrs);
184     my $stories = $sth->fetchall_arrayref({});
185
186     if (!$stories) {
187         $self->disconnect_db();
188         return;
189     }
190     if (@$stories == 0) {
191         $self->disconnect_db();
192         return $unique ? undef : [];
193     }
194
195     # get tags
196     $sql = <<"EOSQL";
197 SELECT tags.*, tagnames.tagname, target.stoid
198   FROM (SELECT stories.stoid FROM stories 
199         LEFT JOIN firehose ON (stories.stoid = firehose.srcid AND firehose.type = "story")
200         $where_clause $orderby_clause $limit_clause) AS target
201     LEFT JOIN globjs
202       ON target.stoid = globjs.target_id
203     LEFT JOIN tags
204       ON globjs.globjid = tags.globjid
205     LEFT JOIN tagnames
206       ON tags.tagnameid = tagnames.tagnameid
207     WHERE globjs.gtid = 1
208 EOSQL
209
210     $sth = $dbh->prepare($sql);
211     $sth->execute(@attrs);
212     my $tags_table = $sth->fetchall_arrayref({});
213
214     # get topics
215     $sql = <<"EOSQL";
216 SELECT story_topics_rendered.*, story_topics_chosen.weight, topics.*
217   FROM (SELECT stories.stoid FROM stories
218         LEFT JOIN firehose ON (stories.stoid = firehose.srcid AND firehose.type = "story")
219         $where_clause $orderby_clause $limit_clause) AS target
220     LEFT JOIN story_topics_rendered
221       ON target.stoid = story_topics_rendered.stoid
222     LEFT JOIN story_topics_chosen 
223       ON story_topics_rendered.stoid = story_topics_chosen.stoid
224         AND story_topics_rendered.tid = story_topics_chosen.tid
225     LEFT JOIN topics
226       ON story_topics_rendered.tid = topics.tid
227 EOSQL
228
229     $sth = $dbh->prepare($sql);
230     $sth->execute(@attrs);
231     my $topics_table = $sth->fetchall_arrayref({});
232
233     # get params
234     $sql = <<"EOSQL";
235 SELECT story_param.*
236   FROM (SELECT stories.stoid FROM stories
237         LEFT JOIN firehose ON (stories.stoid = firehose.srcid AND firehose.type = "story")
238         $where_clause $orderby_clause $limit_clause) AS target
239     LEFT JOIN story_param
240       ON target.stoid = story_param.stoid
241 EOSQL
242
243     $sth = $dbh->prepare($sql);
244     $sth->execute(@attrs);
245     my $params_table = $sth->fetchall_arrayref({});
246
247     # done
248     $self->disconnect_db();
249
250
251     my $tags = {};
252     for my $tag (@$tags_table) {
253         my $stoid = $tag->{stoid};
254         if (!$tags->{$stoid}) {
255             $tags->{$stoid} = [];
256         }
257         push @{$tags->{$stoid}}, $tag;
258     }
259
260     my $topics = {};
261     for my $topic (@$topics_table) {
262         my $stoid = $topic->{stoid};
263         if (!$topics->{$stoid}) {
264             $topics->{$stoid} = [];
265         }
266         push @{$topics->{$stoid}}, $topic;
267     }
268
269     my $params = {};
270     for my $param (@$params_table) {
271         my $stoid = $param->{stoid};
272         if (!$params->{$stoid}) {
273             $params->{$stoid} = [];
274         }
275         push @{$params->{$stoid}}, $param;
276     }
277
278     for my $story (@$stories) {
279         my $stoid = $story->{stoid};
280         $story->{tags} = $tags->{$stoid} || [];
281         $story->{topics} = $topics->{$stoid} || [];
282         if ($params->{$stoid}) {
283             for my $param (@{$params->{$stoid}}) {
284                 $story->{$param->{name}} = $param->{value};
285             }
286         }
287         $self->_generalize($story, $params);
288     }
289
290     return $stories->[0] if $unique;
291     return $stories;
292 }
293
294 sub _check_and_regularize_params {
295     my ($self, $params) = @_;
296     my $msg;
297
298     if (defined $params->{title}) {
299         if (length($params->{title}) > $self->{options}->{Story}->{title_max_byte}) {
300             $msg = "title too long. max: $self->{options}->{Story}->{title_max_byte} bytes";
301             $self->set_error($msg, -1);
302             return;
303         }
304     }
305
306     $params->{commentstatus} = $params->{commentstatus} || $params->{comment_status} || "enabled";
307     if (defined $params->{commentstatus}) {
308         if (!grep /\A$params->{commentstatus}\z/, qw(disabled
309                                                      enabled
310                                                      friends_only
311                                                      friends_fof_only
312                                                      no_foe
313                                                      no_foe_eof 
314                                                      logged_in)) {
315             $msg = "invalid comment_status";
316             $self->set_error($msg, -1);
317             return;
318         }
319     }
320
321     # check timestamp. use ISO8601 style timestamp like: 2006-08-14T02:34:56-0600
322     if ($params->{time}) {
323         my $rex_timestamp = qr/
324                                   ^(\d+)-(\d+)-(\d+)\D+(\d+):(\d+):(\d+(?:\.\d+)?)   # datetime
325                                   (?:Z|([+-])(\d+):(\d+))?$                          # tz
326                               /xi;
327         if ($params->{time} =~ $rex_timestamp) {
328             $params->{time} = "$1-$2-$3 $4:$5:$6";
329         }
330     }
331
332     return 1;
333 }
334
335 sub _set_tags_from_topics {
336     my ($self, $user, $stoid, $topics) = @_;
337
338     return if !$stoid;
339     return if !$topics;
340
341     my $globjs = $self->new_instance_of("Newslash::Model::Globjs");
342     my $globj_id = $globjs->getGlobjidFromTargetIfExists("stories", $stoid);
343     if ($globj_id) {
344         # set tags
345         my $tags = $self->new_instance_of("Tags");
346         for my $tid (keys %$topics) {
347             my $ret = $tags->set_tag(uid => $user->{uid} || $user->{user_id},
348                                      tagname_id => $tid,
349                                      globj_id => $globj_id,
350                                      private => 0,
351                                     );
352             #warn "set_tag fault..." if !$ret
353         }
354     }
355     return $stoid;
356 }
357
358 =head2 update
359
360 this implementation uses old slash's updateStory($sid, $data),
361 $sid is takable sid or stoid.
362
363 =cut
364
365 sub update {
366     #my ($self, $params, $user, $extra_params, $opts) = @_;
367     my $self = shift;
368     my $params = {@_};
369     my $user = $params->{user};
370
371     # check id
372     my $story;
373     my $stoid = $params->{stoid} || $params->{story_id} || $params->{id};
374     my $sid = $params->{sid};
375     if (!$story) {
376         if ($stoid) {
377             $story = $self->select(stoid => $stoid);
378         }
379         elsif ($sid) {
380             $story = $self->select(sid => $sid);
381         }
382     }
383     if ($story) {
384         $sid = $story->{sid};
385         $stoid = $story->{stoid};
386     }
387
388     if (!$story) {
389         $self->last_error("no story given");
390         return;
391     }
392
393     # check params
394     return if !$self->_check_and_regularize_params($params);
395
396     my $slash_db = Newslash::Model::SlashDB->new($self->{options});
397
398     my $add_related = $params->{add_related};
399     delete $params->{add_related} if $add_related;
400     my $add_tags = $params->{add_tags};
401     delete $params->{add_tags} if $add_tags;
402
403     $stoid = $slash_db->updateStory($stoid, $params);
404     return if !$stoid;
405
406     $self->_set_tags_from_topics($params->{user}, $stoid, $params->{topics_chosen});
407
408     # set tags
409     if ($add_tags) {
410         my $tags = $self->new_instance_of("Tags");
411         my $globjs = $self->new_instance_of("Newslash::Model::Globjs");
412         my $globj_id = $globjs->getGlobjidFromTargetIfExists("stories", $params->{stoid});
413         for my $tag (@$add_tags) {
414             my $rs = $tags->add(uid => $user->{uid} || $user->{user_id},
415                                 globj_id => $globj_id,
416                                 name => $tag);
417             if (!defined $rs) {
418                 $self->logger->warn("Stories::update: tag $tag set failed: " . $tags->last_error);
419             }
420         }
421     }
422
423     # insert related story
424     if ($add_related) {
425         for my $related_sid (@$add_related) {
426             my $rs = $self->add_related_story(sid => $sid,
427                                               related_sid => $related_sid);
428             if (!defined $rs) {
429                 $self->logger->warn("Stories::update: insert related story $related_sid to $sid failed: " . $self->last_error);
430             }
431
432             $rs = $self->add_related_story(sid => $related_sid,
433                                            related_sid => $sid);
434             if (!defined $rs) {
435                 $self->logger->warn("Stories::update: insert related story $sid to $related_sid failed: " . $self->last_error);
436             }
437         }
438     }
439
440     return $stoid;
441 }
442
443 sub update2 {
444     my $self = shift;
445     my $params = {@_};
446     return $self->generic_update(params => $params);
447 }
448
449
450 =head2 create(\%params, $uid)
451
452 create a story.
453
454 =over 4
455
456 =item Parameters
457
458 =over 4
459
460 =item \%params
461
462 parameters
463
464 $params->{fhid}
465 $params->{subid}
466
467 =item $uid
468
469 author's uid
470
471 =back
472
473 =item Return value
474
475 stoid
476
477 =back
478
479 =cut
480
481 sub create {
482     #my ($self, $params, $user, $extra_params, $opts) = @_;
483     my $self = shift;
484     return if $self->check_readonly;
485
486     my $params = {@_};
487     my $user = $params->{user};
488
489     # check parameters
490     my $msg = "";
491     $msg = "no_title" if !$params->{title};
492     $msg = "no_introtext" if !$params->{introtext} || $params->{intro_text};
493     $msg = "no_topics" if !defined $params->{topics_chosen};
494     $msg = "invalid_user" if !defined $user->{uid};
495
496     if (length($params->{title}) > $self->{options}->{Story}->{title_max_byte}) {
497         $msg = "title too long. max: $self->{options}->{Story}->{title_max_byte} bytes";
498     }
499
500     $params->{commentstatus} = $params->{commentstatus} || $params->{comment_status} || "enabled";
501     if (!grep /\A$params->{commentstatus}\z/, qw(disabled enabled friends_only friends_fof_only no_foe no_foe_eof logged_in)) {
502         $msg = "invalid comment_status";
503     }
504
505     # check timestamp. use ISO8601 style timestamp like: 2006-08-14T02:34:56-0600
506     if ($params->{time}) {
507         my $rex_timestamp = qr/
508                                   ^(\d+)-(\d+)-(\d+)[^ 0-9]+(\d+):(\d+):(\d+(?:\.\d+)?)   # datetime
509                                   (?:Z|([+-])(\d+):(\d+))?$                          # tz
510                               /xi;
511         if ($params->{time} =~ $rex_timestamp) {
512             my $dt = DateTime::Format::ISO8601->parse_datetime($params->{time});
513             $params->{time} = DateTime::Format::MySQL->format_datetime($dt);
514         }
515     }
516
517     # check parameters finish
518     if (length($msg) > 0) {
519         $self->set_error($msg, -1);
520         return;
521     }
522     $params->{neverdisplay} ||= 0;
523     $params->{submitter} ||= $user->{uid};
524     $params->{uid} = $user->{uid};
525
526     # createStory() deletes topics_chosen, so need to save here.
527     my $topics_chosen = $params->{topics_chosen};
528
529     my $add_related = $params->{add_related};
530     delete $params->{add_related} if $add_related;
531     my $add_tags = $params->{add_tags};
532     delete $params->{add_tags} if $add_tags;
533
534     my $slash_db = Newslash::Model::SlashDB->new($self->{options});
535     my ($sid, $stoid);
536     if ($params->{update}) {
537         $stoid = $params->{stoid} || $params->{story_id} || $params->{id};
538         $sid = $slash_db->updateStory($stoid, $params);
539         $self->set_error("updateStory failed");
540         return if !$sid;
541     }
542     else {
543         ($sid, $stoid) = $slash_db->createStory($params);
544     }
545
546     my $globjs = $self->new_instance_of("Newslash::Model::Globjs");
547     my $globj_id = $globjs->getGlobjidFromTargetIfExists("stories", $params->{stoid});
548
549     # set topics
550     my $tags = $self->new_instance_of("Tags");
551     if ($globj_id) {
552         for my $tid (keys %$topics_chosen) {
553             my $rs = $tags->add(uid => $user->{uid} || $user->{user_id},
554                                 tagname_id => $tid,
555                                 globj_id => $globj_id,
556                                 private => 0,
557                                );
558             if (!defined $rs) {
559                 $self->logger->warn("Stories::create: tag $tid set failed: " . $tags->last_error);
560             }
561         }
562     }
563
564     # set tags
565     if ($add_tags) {
566         for my $tag (@$add_tags) {
567             my $rs = $tags->add(uid => $user->{uid} || $user->{user_id},
568                                 globj_id => $globj_id,
569                                 name => $tag);
570             if (!defined $rs) {
571                 $self->logger->warn("Stories::create: tag $tag set failed: " . $tags->last_error);
572             }
573         }
574     }
575
576     # insert related story
577     if ($add_related) {
578         for my $related_sid (@$add_related) {
579             my $rs = $self->add_related_story(sid => $sid,
580                                               related_sid => $related_sid);
581             if (!defined $rs) {
582                 $self->logger->warn("insert related story failed: " . $self->last_error);
583             }
584
585             $rs = $self->add_related_story(sid => $related_sid,
586                                            related_sid => $sid);
587             if (!defined $rs) {
588                 $self->logger->warn("Stories::create: insert related story failed: " . $self->last_error);
589             }
590         }
591     }
592
593     # TODO: add firehose item to related_story
594
595     return wantarray ? ($sid, $stoid) : $stoid;
596 }
597
598 sub create2 {
599     my $self = shift;
600     my $params = {@_};
601     return $self->generic_insert(params => $params);
602 }
603
604 sub allocate_sid {
605     my ($self, @params) = @_;
606     my $params = {@params};
607     my $dt = $params->{base_datetime} || DateTime->now;
608
609     # create sid from timestamp
610     # my $sid_format = '%02d/%02d/%02d/%02d%0d2%02d';
611     my $sid_format = '%y/%m/%d/%H%M%S';
612     my $sid = $dt->strftime($sid_format);
613
614     # insert blank story with given sid
615     my $dbh = $self->connect_db;
616     my $sql = "INSERT INTO stories (sid) VALUES (?)";
617
618     my $n = 100; # retry 100 times
619     while (--$n) {
620         my $rs = $dbh->do($sql, undef, $sid);
621         if (!defined $rs) {
622             $self->set_error("sid_insert_error", -1);
623             return;
624         }
625         if ($rs) {
626             my $stoid = $dbh->last_insert_id(undef, undef, undef, undef);
627             $self->disconnect_db;
628             return ($sid, $stoid);
629         }
630
631         # allocate failed, so recreate sid
632         $dt->subtract( seconds => 1 );
633         $sid = $dt->strftime($sid_format);
634     }
635     $self->set_error("sid_allocate_failed", -1);
636     $self->disconnect_db;
637     return;
638 }
639
640
641 # Legacy API
642 sub createSid {
643     my ($self, $bogus_sid) = @_;
644     # yes, this format is correct, don't change it :-)
645     my $sidformat = '%02d/%02d/%02d/%02d%0d2%02d';
646     # Create a sid based on the current time.
647     my @lt;
648     my $start_time = time;
649     if ($bogus_sid) {
650         # If we were called being told that there's at
651         # least one sid that is invalid (already taken),
652         # then look backwards in time until we find it,
653         # then go one second further.
654         my $loops = 1000;
655         while (--$loops) {
656             $start_time--;
657             @lt = localtime($start_time);
658             $lt[5] %= 100; $lt[4]++; # year and month
659             last if $bogus_sid eq sprintf($sidformat, @lt[reverse 0..5]);
660         }
661         if ($loops) {
662             # Found the bogus sid by looking
663             # backwards.  Go one second further.
664             $start_time--;
665         } else {
666             # Something's wrong.  Skip ahead in
667             # time instead of back (not sure what
668             # else to do).
669             $start_time = time + 1;
670         }
671     }
672     @lt = localtime($start_time);
673     $lt[5] %= 100; $lt[4]++; # year and month
674     return sprintf($sidformat, @lt[reverse 0..5]);
675 }
676
677
678 =head2 get_histories
679
680 =cut
681
682 sub get_histories {
683 }
684
685 =head2 get_related_items($stoid)
686
687 get related links.
688
689 =over 4
690
691 =item Parameters
692
693 =over 4
694
695 =item $stoid
696
697 story id
698
699 =back
700
701 =item Return value
702
703 ARRAY of related links
704
705 =back
706
707 =cut
708
709 sub get_related_items {
710     my $self = shift;
711     my $params = {@_};
712     my $stoid = $params->{stoid} || $params->{story_id} || $params->{id};
713     return if !$stoid;
714
715     my $dbh = $self->connect_db;
716
717     my $sql = <<"EOSQL";
718 SELECT related.*, 
719        story_text.title as title2,
720        firehose.*,
721        stories.*,
722        topics.*
723   FROM (
724     SELECT * FROM related_stories
725       WHERE stoid = ?
726       ORDER BY ordernum ASC
727     ) AS related
728   LEFT JOIN story_text ON story_text.stoid = related.rel_stoid
729   LEFT JOIN firehose ON firehose.id = related.fhid
730   LEFT JOIN stories ON stories.sid = related.rel_sid
731   LEFT JOIN topics ON topics.tid = stories.tid
732 EOSQL
733
734     my $sth = $dbh->prepare($sql);
735     $sth->execute($stoid);
736     my $related = $sth->fetchall_arrayref({});
737     $self->disconnect_db();
738
739     for my $r (@$related) {
740         $r->{create_time} = $r->{time};
741         $r->{title} = $r->{title2} unless $r->{title};
742         if ($r->{rel_sid}) {
743             $r->{type} = "story";
744             $r->{key_id} = $r->{rel_sid};
745         } else {
746             $r->{type} = "submission";
747             $r->{key_id} = $r->{srcid};
748         }
749         $r->{primary_topic} = {};
750         $r->{primary_topic}->{tid} = $r->{tid};
751         for my $k (qw{keyword textname series image width height
752                       submittable searchable storypickable usesprite}) {
753             next if !$r->{$k};
754             $r->{primary_topic}->{$k} = $r->{$k};
755             delete $r->{$k};
756         }
757     }
758
759     return $related;
760 }
761
762 sub add_related_story {
763     my $self = shift;
764     my $args = {@_};
765
766     my $story = $args->{story};
767     if (!$story) {
768         if ($args->{stoid}) {
769             $story = $self->select(stoid => $args->{stoid});
770         } elsif ($args->{sid}) {
771             $story = $self->select(sid => $args->{sid});
772         }
773     }
774
775     if (!$story || !$story->{stoid} || !$story->{sid}) {
776         $self->last_error("cannot get target story");
777         return;
778     }
779
780     my $related = $args->{related};
781     if (!$related) {
782         if ($args->{related_stoid}) {
783             $related = $self->select(stoid => $args->{related_stoid});
784         } elsif ($args->{related_sid}) {
785             $related = $self->select(sid => $args->{related_sid});
786         }
787     }
788
789     if (!$related || !$related->{stoid} || !$related->{sid} ) {
790         $self->last_error("cannot get related story");
791         return;
792     }
793
794     # get related item count
795     my $dbh = $self->start_transaction;
796     my $sql = "SELECT rel_stoid FROM related_stories WHERE stoid = ?";
797     my $stoids = $dbh->do($sql, undef, $story->{stoid});
798     $stoids = [] if ref($stoids) ne "ARRAY";
799
800     # check if related story is already set
801     if (any { $_ eq $related->{stoid} } @$stoids) {
802         return 0;
803     }
804
805     # insert
806     my $count = @$stoids;
807     my $rs = $self->related_stories->insert(stoid => $story->{stoid},
808                                             rel_stoid => $related->{stoid},
809                                             rel_sid => $related->{sid},
810                                             ordernum => $count + 1);
811
812     if (!defined $rs) {
813         $self->last_error("insert failed: " . $self->related_stories->last_error);
814         $dbh->rollback;
815         return;
816     }
817     $dbh->commit;
818     return 1;
819 }
820
821 =head2 parameters($stoid)
822
823 get story parameters.
824
825 =over 4
826
827 =item Parameters
828
829 =over 4
830
831 =item $stoid
832
833 story id
834
835 =back
836
837 =item Return value
838
839 HASH of parameters
840
841 =back
842
843 =cut
844
845 sub parameters {
846     my ($self, $stoid) = @_;
847
848     my $dbh = $self->connect_db;
849
850     my $sql = <<"EOSQL";
851 SELECT * FROM story_param WHERE stoid = ?
852 EOSQL
853
854     my $sth = $dbh->prepare($sql);
855     $sth->execute($stoid);
856     my $params = $sth->fetchall_hashref('name');
857     $sth->finish;
858
859     # get next/prev story info
860     if ($params->{next_stoid} || $params->{prev_stoid}) {
861         $sql = <<"EOSQL";
862 SELECT stories.*, story_text.* FROM stories JOIN story_text ON stories.stoid = story_text.stoid WHERE stories.stoid = ? OR stories.stoid = ?
863 EOSQL
864         $sth = $dbh->prepare($sql);
865         my $next = $params->{next_stoid}->{value} || 0;
866         my $prev = $params->{prev_stoid}->{value} || 0;
867         $sth->execute($next, $prev);
868         my $sids = $sth->fetchall_hashref('stoid');
869         $params->{next_stoid}->{story} = $sids->{$next} if $sids->{$next};
870         $params->{prev_stoid}->{story} = $sids->{$prev} if $sids->{$prev};
871         $sth->finish;
872     }
873
874     $self->disconnect_db();
875     return $params;
876 }
877
878 #========================================================================
879
880 =head2 set_dirty($key, $id)
881
882 set writestatus dirty for the story.
883
884 =over 4
885
886 =item Parameters
887
888 =over 4
889
890 =item $key
891
892 'stoid'
893
894 =item $id
895
896 id of the story
897
898 =back
899
900 =item Return value
901
902 1/0
903
904 =back
905
906 =cut
907
908 sub set_dirty {
909     my ($self, $key, $id) = @_;
910     return if $self->check_readonly;
911
912     my $stoid;
913     if ($key eq 'stoid') {
914         $stoid = $id;
915     }
916     else {
917         return;
918     }
919
920     my $dbh = $self->connect_db;
921     my $sql = <<"EOSQL";
922 INSERT INTO story_dirty (stoid) VALUES (?)
923 EOSQL
924
925     my $rs = $dbh->do($sql, undef, $stoid);
926     if (!$rs) {
927         return;
928     }
929     return 1;
930 }
931
932 sub _generalize {
933     my ($self, $story, $params) = @_;
934     $params ||= {};
935
936     # NTO-nized
937     $story->{id} = $story->{stoid};
938     $story->{story_id} = $story->{stoid};
939     $story->{create_time} = $story->{time};
940     $story->{update_time} = $story->{last_update};
941
942     for my $t (@{$story->{topics}}) {
943         if ($t->{tid} && $t->{tid} == $story->{tid}) {
944             $story->{primary_topic} = $t;
945         }
946     }
947
948     $story->{content_type} = "story";
949     $story->{intro_text} = $story->{introtext};
950     $story->{bodytext} ||= "";
951     $story->{body_text} = $story->{bodytext};
952     if ($story->{body_text}) {
953         $story->{full_text} = join("\n", $story->{intro_text}, $story->{body_text});
954     }
955     else {
956         $story->{full_text} = $story->{intro_text};
957     }
958     $story->{fulltext} = $story->{full_text};
959
960     $story->{discussion_id} = $story->{discussion};
961
962     # no public flag given, public is 'yes'
963     $story->{public} = 'yes' if !$story->{public};
964
965 }
966
967 # delete story from database
968 # this method is for test purpose only.
969 sub hard_delete {
970     my $self = shift;
971     return if $self->check_readonly;
972     my $params = {@_};
973
974     my $stoid = $params->{story_id};
975     return if !$stoid;
976
977     my $error = 0;
978     my $sql;
979
980     #my $dbh = $self->connect_db({AutoCommit => 0,});
981     my $dbh = $self->start_transaction;
982     for my $table (qw(stories story_param story_text story_topics_chosen story_topics_rendered)) {
983         my $sql = "DELETE FROM $table WHERE stoid = ?";
984         my $rs = $dbh->do($sql, undef, $stoid);
985         if (!defined $rs || $rs == 0) {
986             Mojo::Log->new->warn("DELETE FROM $table failed. stoid is $stoid.");
987             $error = 1;
988         }
989     }
990
991     # delete from firehose
992     my $firehose = $self->new_instance_of("Firehose");
993     $firehose->hard_delete("story", $stoid);
994
995     $self->commit;
996     return !$error;
997
998     # delete globjs
999     # delete tags
1000
1001     return;
1002 }
1003
1004 1;