OSDN Git Service

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