root/branches/release-34/lib/MT/CMS/Dashboard.pm @ 1857

Revision 1857, 15.6 kB (checked in by bchoate, 20 months ago)

Optimizations for dashboard tag cloud for blogs with lotsa tags.

  • Property svn:keywords set to Id Revision
Line 
1package MT::CMS::Dashboard;
2
3use strict;
4use MT::Util qw( epoch2ts );
5
6sub dashboard {
7    my $app = shift;
8    my (%param) = @_;
9
10    if ( $app->request('fresh_login') ) {
11        if ( !$app->param('blog_id') ) {
12
13            # return to the last blog they visted, if any
14            my $fav_blogs = $app->user->favorite_blogs || [];
15            my $blog_id = $fav_blogs->[0] if @$fav_blogs;
16            $app->param( 'blog_id', $blog_id ) if $blog_id;
17            $app->delete_param('blog_id') unless $app->is_authorized;
18        }
19    }
20
21    my $param = \%param;
22
23    $param->{redirect}   ||= $app->param('redirect');
24    $param->{permission} ||= $app->param('permission');
25    $param->{saved}      ||= $app->param('saved');
26
27    $param->{system_overview_nav} = $app->param('blog_id') ? 0 : defined($app->param('blog_id')) ? 1 : 0;
28    $param->{quick_search}        = 0;
29    $param->{no_breadcrumbs}      = 1;
30    $param->{screen_class}        = "dashboard";
31    $param->{screen_id}           = "dashboard";
32
33    my $default_widgets = {
34        'blog_stats' =>
35          { param => { tab => 'entry' }, order => 1, set => 'main' },
36        'this_is_you-1' => { order => 1, set => 'sidebar' },
37        'mt_shortcuts'  => { order => 2, set => 'sidebar' },
38        'mt_news'       => { order => 3, set => 'sidebar' },
39    };
40
41    require MT::FileMgr;
42    my $fmgr = MT::FileMgr->new('Local');
43    $param->{support_path} =
44        File::Spec->catdir( $app->static_file_path, 'support', 'uploads' );
45    if ( $fmgr->exists( $param->{support_path} ) ) {
46        $param->{has_uploads_path} = 1;
47    } else {
48        $fmgr->mkpath( $param->{support_path} );
49        if ( $fmgr->exists( $param->{support_path} )
50             && $fmgr->can_write( $param->{support_path} ) )
51        {
52            $param->{has_uploads_path} = 1;
53        }
54    }
55
56    # We require that the determination of the 'single blog mode'
57    # state be done PRIOR to the generation of the widgets
58    $app->build_blog_selector($param);
59    $app->load_widget_list( 'dashboard', $param, $default_widgets );
60    $param = $app->load_widgets( 'dashboard', $param, $default_widgets );
61    return $app->load_tmpl( "dashboard.tmpl", $param );
62}
63
64sub new_version_widget {
65    my $app = shift;
66    my ( $tmpl, $param ) = @_;
67
68    push @{ $param->{feature_loop} ||= [] },
69      {
70        feature_label => MT->translate('Better, Stronger, Faster'),
71        feature_url  => $app->help_url('mt415/known-issues.html'),
72        feature_description => MT->translate('A concerted effort has been made to make significant improvements to Movable Type\'s performance and reliability with added features like Server Side Includes and Template Module Caching.'),
73      },
74      {
75        feature_label => MT->translate('Improved Template Management'),
76        feature_url  => $app->help_url('mt415/whats-new.html#design-changes'),
77        feature_description => MT->translate('The template editing interface has been enhanced to make designers more efficient at updating their site\'s design.'),
78      },
79      {
80        feature_label => MT->translate('Threaded Comments'),
81        feature_url  => $app->help_url('mt415/threaded-comments.html'),
82        feature_description => MT->translate('Allow commenters on your blog to reply to each other increasing user engagement and creating more dynamic conversations.'),
83      };
84}
85
86sub this_is_you_widget {
87    my $app = shift;
88    my ( $tmpl, $param ) = @_;
89
90    my $user = $app->user;
91
92    # User profile data
93    # Number of posts by this user
94    require MT::Entry;
95    $param->{publish_count} = MT::Entry->count( { author_id => $user->id, } );
96    $param->{draft_count} = MT::Entry->count(
97        {
98            author_id => $user->id,
99            status    => MT::Entry::HOLD(),
100        }
101    );
102    if ( $param->{publish_count} ) {
103        my $iter = MT::Entry->sum_group_by({
104            author_id => $user->id,
105        }, { sum => 'comment_count', group => ['author_id'] });
106        my ($count, $author_id) = $iter->();
107        $param->{comment_count} = $count;
108    }
109
110    my $last_post = MT::Entry->load(
111        {
112            author_id => $user->id,
113            status    => MT::Entry::RELEASE(),
114        },
115        {
116            sort      => 'authored_on',
117            direction => 'descend',
118            limit     => 1,
119        }
120    );
121    if ($last_post) {
122        $param->{last_post_id}      = $last_post->id;
123        $param->{last_post_blog_id} = $last_post->blog_id;
124        $param->{last_post_blog_name} = $last_post->blog->name;
125        $param->{last_post_ts}      = $last_post->authored_on;
126    }
127
128    if (my ($url) = $user->userpic_url()) {
129        $param->{author_userpic_url}    = $url;
130    }
131    $param->{author_userpic_width}  = 50;
132    $param->{author_userpic_height} = 50;
133}
134
135sub mt_news_widget {
136    my $app = shift;
137    my ( $tmpl, $param ) = @_;
138
139    $param->{news_html} = get_newsbox_content($app) || '';
140    $param->{learning_mt_news_html} = get_lmt_content($app) || '';
141}
142
143sub get_newsbox_content {
144    my $app = shift;
145    my $newsbox_url = $app->config('NewsboxURL');
146    if ( $newsbox_url && $newsbox_url ne 'disable' ) {
147        return MT::Util::get_newsbox_html($newsbox_url, 'NW');
148    }
149    return q();
150}
151
152sub get_lmt_content {
153    my $app = shift;
154    my $newsbox_url = $app->config('LearningNewsURL');
155    if ( $newsbox_url && $newsbox_url ne 'disable' ) {
156        return MT::Util::get_newsbox_html($newsbox_url, 'LW');
157    }
158    return q();
159}
160
161sub mt_blog_stats_widget {
162    my $app = shift;
163    my ( $tmpl, $param ) = @_;
164
165    # For stats shown on this page
166    generate_dashboard_stats($app, $param) or return;
167
168    my $tabs = $app->registry('blog_stats_tabs') or return;
169    $tabs = $app->filter_conditional_list($tabs, 'dashboard', ($param->{widget_scope} || ''));
170
171    $param->{tab_html_head} = '';
172    {
173        local $param->{main};
174        local $param->{html_head};
175
176        my %cfgs;
177        my $stat_url = delete $param->{stat_url};
178        while (my ($tab_id, $url) = each %$stat_url) {
179            $param->{has_stat_urls} = 1;
180            $cfgs{$tab_id} = { param => { stat_url => $url } };
181        }
182        $app->build_widgets(
183            set            => 'blog_stats',
184            param          => $param,
185            widgets        => $tabs,
186            widget_cfgs    => \%cfgs,
187            passthru_param => [qw( html_head js_include tabs active_stats_panel_updates )],
188        ) or return;
189
190        $param->{blog_stats} = $param->{main};
191        $param->{tab_html_head} .= $param->{html_head};
192    }
193}
194
195sub mt_blog_stats_widget_entry_tab {
196    my ($app, $tmpl, $param) = @_;
197
198    my $user    = $app->user;
199    my $blog    = $app->blog;
200    my $blog_id = $blog->id if $blog;
201
202    $param->{editable} = $user->is_superuser;
203    if ( $blog && !$param->{editable} ) {
204        $param->{editable} = $user->permissions($blog_id)->can_edit_all_posts;
205    }
206
207    my $entries = sub {
208        my $args = {
209            limit     => 10,
210            sort      => 'authored_on',
211            direction => 'descend',
212        };
213        if ( !$user->is_superuser && !$blog_id ) {
214            $args->{join} = MT::Permission->join_on(
215                undef,
216                {
217                    blog_id   => \'= entry_blog_id',
218                    author_id => $user->id
219                },
220            );
221        }
222        my @e =
223          MT::Entry->load( { ( $blog_id ? ( blog_id => $blog_id ) : () ), },
224            $args );
225        \@e;
226    };
227
228    require MT::Promise;
229    my $ctx = $tmpl->context;
230    $ctx->stash( 'entries',  MT::Promise::delay($entries) );
231}
232
233sub generate_dashboard_stats {
234    my $app = shift;
235    my ($param) = @_;
236
237    my $cache_time = 60 * 15;    # cache for 15 minutes
238
239    my $blog_id = $app->blog ? $app->blog->id : 0;
240    my $user    = $app->user;
241    my $user_id = $user->id;
242
243    my $static_path      = $app->static_path;
244    my $static_file_path = $app->static_file_path;
245
246    if ( -f File::Spec->catfile( $static_file_path, "mt.js" ) ) {
247        $param->{static_file_path} = $static_file_path;
248    }
249    else {
250        return;
251    }
252
253    my $low_dir = sprintf("%03d", $user_id % 1000);
254    my $sub_dir = sprintf("%03d", $blog_id % 1000);
255    my $top_dir = $blog_id > $sub_dir ? $blog_id - $sub_dir : 0;
256    $param->{support_path} =
257      File::Spec->catdir( $static_file_path, 'support', 'dashboard', 'stats',
258        $top_dir, $sub_dir, $low_dir); 
259
260    require MT::FileMgr;
261    my $fmgr = MT::FileMgr->new('Local');
262    unless ( $fmgr->exists( $param->{support_path} ) ) {
263        $fmgr->mkpath( $param->{support_path} );
264        unless ( $fmgr->exists( $param->{support_path} ) ) {
265            return;
266        }
267    }
268
269    my $stats_static_path = $static_path . 'support/dashboard/stats/' .
270        $top_dir . '/' . $sub_dir . '/' . $low_dir;
271
272    my $tabs = $app->registry('blog_stats_tabs') or return;
273    while (my ($tab_id, $tab) = each %$tabs) {
274        my $file = "${tab_id}.xml";
275        $param->{stat_url}->{$tab_id} = $stats_static_path . '/' . $file;
276        my $path = File::Spec->catfile( $param->{support_path}, $file );
277
278        my $time = ( stat($path) )[9] if -f $path;
279
280        if ( !$time || ( time - $time > $cache_time ) ) {
281            my $gen_stats = $tab->{stats};
282            next if !$gen_stats;
283            $gen_stats = $app->handler_to_coderef($gen_stats);
284
285            my %counts = $gen_stats->($app, $tab);
286
287            unless ( create_dashboard_stats_file( $app, $path, \%counts ) ) {
288                delete $param->{stat_url}->{$tab_id};
289            }
290        }
291    }
292
293    1;
294}
295
296sub create_dashboard_stats_file {
297    my $app = shift;
298    my ( $file, $data ) = @_;
299
300    my $support_dir = File::Spec->catdir( $app->static_file_path, "support" );
301    if ( !-d $support_dir ) {
302        mkdir( $support_dir, 0777 );
303        if ($!) {
304            $app->log("Failed to create 'support' directory.");
305            return;
306        }
307    }
308
309    local *FOUT;
310    if ( !open( FOUT, ">$file" ) ) {
311        return;
312    }
313
314    print FOUT <<EOT;
315<?xml version="1.0"?>
316<rsp status_code="0" status_message="Success">
317  <daily_counts>
318EOT
319    my $now = time;
320    for ( my $i = 120 ; $i >= 1 ; $i-- ) {
321        my $ds =
322          substr( epoch2ts( $app->blog, $now - ( ( $i - 1 ) * 60 * 60 * 24 ) ),
323            0, 8 )
324          . 'T00:00:00';
325        my $count = $data->{$ds} || 0;
326        print FOUT qq{    <count date="$ds">$count</count>\n};
327    }
328    print FOUT <<EOT;
329  </daily_counts>
330</rsp>
331EOT
332    close FOUT;
333}
334
335sub generate_dashboard_stats_entry_tab {
336    my $app = shift;
337    my ($tab) = @_;
338   
339    my $blog_id = $app->blog ? $app->blog->id : 0;
340    my $user    = $app->user;
341    my $user_id = $user->id;
342
343    my $entry_class = $app->model('entry');
344    my $terms       = { status => MT::Entry::RELEASE() };
345    my $args        = {
346        group => [
347            "extract(year from authored_on)",
348            "extract(month from authored_on)",
349            "extract(day from authored_on)"
350        ],
351    };
352
353    require MT::Util;
354    my @ts = MT::Util::offset_time_list(time - (121 * 24 * 60 * 60), $blog_id);
355    my $earliest = sprintf('%04d%02d%02d%02d%02d%02d',
356        $ts[5]+1900, $ts[4]+1, @ts[3,2,1,0]);
357    $terms->{authored_on} = [ $earliest, undef ];
358    $args->{range_incl}{authored_on} = 1;
359
360    $terms->{blog_id} = $blog_id if $blog_id;
361    if ( !$user->is_superuser && !$blog_id ) {
362        $args->{join} = MT::Permission->join_on(
363            undef,
364            {
365                blog_id   => \'= entry_blog_id',
366                author_id => $user_id
367            },
368        );
369    }
370
371    my $entry_iter = $entry_class->count_group_by( $terms, $args );
372    my %counts;
373    while ( my ( $count, $y, $m, $d ) = $entry_iter->() ) {
374        my $date = sprintf( "%04d%02d%02dT00:00:00", $y, $m, $d );
375        $counts{$date} = $count;
376    }
377
378    %counts;
379}
380
381sub mt_blog_stats_tag_cloud_tab {
382    my ($app, $tmpl, $param) = @_;
383
384    my $blog = $app->blog;
385    my $blog_id = $blog->id if $blog;
386
387    my $terms = {};
388    $terms->{blog_id} = $blog_id if $blog_id;
389    $terms->{object_datasource} = 'entry';
390    my $args = {};
391    $args->{group} = [ 'tag_id' ];
392    $args->{sort} = 'count(*)';
393    $args->{direction} = 'descend';
394    $args->{limit} = 100;
395
396    my $iter = MT::ObjectTag->count_group_by($terms, $args);
397    my @tag_loop;
398    my $ntags = 0;
399    my $min = undef;
400    my $max = undef;
401    while (my ($count, $tag_id) = $iter->()) {
402        my $tag = MT::Tag->load($tag_id) or next;
403        next if $tag->is_private; # weed these from the dashboard
404        $ntags += $count;
405        $min = defined $min ? ($count < $min ? $count : $min) : $count;
406        $max = defined $max ? ($count > $max ? $count : $max) : $count;
407        push @tag_loop, { name => $tag->name, count => $count };
408    }
409
410    my $factor;
411    if ($max - $min == 0) {
412        $min -= 6;
413        $factor = 1;
414    } else {
415        $factor = 5 / log($max - $min + 1);
416    }
417    $factor *= ($ntags / 6) if $ntags < 6;
418
419    foreach my $tag (@tag_loop) {
420        # now calc rank
421        my $rank;
422        my $count = $tag->{count};
423        if ($count - $min + 1 == 0) {
424            $rank = 0;
425        } else {
426            $rank = 6 - int(log($count - $min + 1) * $factor);
427        }
428        $tag->{rank} = $rank;
429    }
430
431    @tag_loop = sort { $a->{name} cmp $b->{name} } @tag_loop;
432    $param->{tag_loop} = \@tag_loop;
433}
434
435sub mt_blog_stats_widget_comment_tab {
436    my ($app, $tmpl, $param) = @_;
437
438    my $user    = $app->user;
439    my $blog    = $app->blog;
440    my $blog_id = $blog->id if $blog;
441
442    $param->{editable} = $user->is_superuser;
443    if ( $blog && !$param->{editable} ) {
444        $param->{editable} = $user->permissions($blog_id)->can_edit_all_posts;
445        $param->{comment_editable} = $user->permissions($blog_id)->can_manage_feedback;
446    }
447
448    my $comments = sub {
449        my $args = {
450            limit     => 10,
451            sort      => 'created_on',
452            direction => 'descend',
453        };
454        if ( !$user->is_superuser && !$blog_id ) {
455            $args->{join} = MT::Permission->join_on(
456                undef,
457                {
458                    blog_id   => \'= comment_blog_id',
459                    author_id => $user->id
460                },
461            );
462        }
463        my @c = MT::Comment->load(
464            {
465                ( $blog_id ? ( blog_id => $blog_id ) : () ),
466                visible => 1,
467            },
468            $args
469        );
470        \@c;
471    };
472
473    require MT::Promise;
474    my $ctx = $tmpl->context;
475    $ctx->stash( 'comments',  MT::Promise::delay($comments) );
476}
477
478sub generate_dashboard_stats_comment_tab {
479    my $app = shift;
480    my ($tab) = @_;
481   
482    my $blog_id = $app->blog ? $app->blog->id : 0;
483    my $user    = $app->user;
484    my $user_id = $user->id;
485
486    my $cmt_class = $app->model('comment');
487    my $terms = { visible => 1 };
488    $terms->{blog_id} = $blog_id if $blog_id;
489    my $args = {
490        group => [
491            "extract(year from created_on)",
492            "extract(month from created_on)",
493            "extract(day from created_on)"
494        ],
495    };
496
497    require MT::Util;
498    my @ts = MT::Util::offset_time_list(time - (121 * 24 * 60 * 60), $blog_id);
499    my $earliest = sprintf('%04d%02d%02d%02d%02d%02d',
500        $ts[5]+1900, $ts[4]+1, @ts[3,2,1,0]);
501    $terms->{created_on} = [ $earliest, undef ];
502    $args->{range_incl}{created_on} = 1;
503
504    if ( !$user->is_superuser && !$blog_id ) {
505        $args->{join} = MT::Permission->join_on(
506            undef,
507            {
508                blog_id   => \'= comment_blog_id',
509                author_id => $user_id
510            },
511        );
512    }
513    my $cmt_iter = $cmt_class->count_group_by( $terms, $args );
514
515    my %counts;
516    while ( my ( $count, $y, $m, $d ) = $cmt_iter->() ) {
517        my $date = sprintf( "%04d%02d%02dT00:00:00", $y, $m, $d );
518        $counts{$date} = $count;
519    }
520
521    %counts;
522}
523
5241;
Note: See TracBrowser for help on using the browser.