File Coverage

blib/lib/Rapi/Blog.pm
Criterion Covered Total %
statement 36 70 51.4
branch 0 26 0.0
condition 0 7 0.0
subroutine 12 22 54.5
pod 0 1 0.0
total 48 126 38.1


line stmt bran cond sub pod time code
1             package Rapi::Blog;
2              
3 1     1   653 use strict;
  1         2  
  1         24  
4 1     1   4 use warnings;
  1         1  
  1         24  
5              
6             # ABSTRACT: RapidApp-powered blog
7              
8 1     1   383 use RapidApp 1.3105;
  1         22852  
  1         28  
9              
10 1     1   1046 use Moose;
  1         407446  
  1         7  
11             extends 'RapidApp::Builder';
12              
13 1     1   7321 use Types::Standard qw(:all);
  1         57223  
  1         9  
14              
15 1     1   39562 use RapidApp::Util ':all';
  1         1099027  
  1         504  
16 1     1   9 use File::ShareDir qw(dist_dir);
  1         2  
  1         46  
17 1     1   579 use FindBin;
  1         909  
  1         45  
18             require Module::Locate;
19 1     1   6 use Path::Class qw/file dir/;
  1         2  
  1         48  
20 1     1   370 use YAML::XS 0.64 'LoadFile';
  1         2390  
  1         63  
21              
22 1     1   401 use Rapi::Blog::Scaffold;
  1         4  
  1         33  
23 1     1   388 use Rapi::Blog::Scaffold::Set;
  1         3  
  1         1960  
24              
25             our $VERSION = '1.1001';
26             our $TITLE = "Rapi::Blog v" . $VERSION;
27              
28             has 'site_path', is => 'ro', required => 1;
29             has 'scaffold_path', is => 'ro', isa => Maybe[Str], default => sub { undef };
30             has 'builtin_scaffold', is => 'ro', isa => Maybe[Str], default => sub { undef };
31             has 'scaffold_config', is => 'ro', isa => HashRef, default => sub {{}};
32             has 'fallback_builtin_scaffold', is => 'ro', isa => Bool, default => sub {0};
33              
34             has 'enable_password_reset', is => 'ro', isa => Bool, default => sub {1};
35             has 'enable_user_sign_up', is => 'ro', isa => Bool, default => sub {1};
36             has 'enable_email_login', is => 'ro', isa => Bool, default => sub {1};
37              
38             has 'underlay_scaffolds', is => 'ro', isa => ArrayRef[Str], default => sub {[]};
39              
40             has 'smtp_config', is => 'ro', isa => Maybe[HashRef], default => sub { undef };
41             has 'override_email_recipient', is => 'ro', isa => Maybe[Str], default => sub { undef };
42              
43             has '+base_appname', default => sub { 'Rapi::Blog::App' };
44             has '+debug', default => sub {1};
45              
46             sub BUILD {
47 0     0 0   my $self = shift;
48 0 0         print STDERR join('',' -- ',(blessed $self),' v',$self->VERSION,' -- ',"\n") if ($self->debug);
49             }
50              
51             has 'share_dir', is => 'ro', isa => Str, lazy => 1, default => sub {
52             my $self = shift;
53             $self->_get_share_dir;
54             };
55              
56             sub _get_share_dir {
57 0   0 0     my $self = shift || __PACKAGE__;
58             $ENV{RAPI_BLOG_SHARE_DIR} || (
59 0     0     try{dist_dir('Rapi-Blog')} || (
60 0 0 0       -d "$FindBin::Bin/share" ? "$FindBin::Bin/share" :
    0          
    0          
61             -d "$FindBin::Bin/../share" ? "$FindBin::Bin/../share" :
62             join('',$self->_module_locate_dir,'/../../share')
63             )
64             )
65             }
66              
67             sub _module_locate_dir {
68 0     0     my $self = shift;
69 0 0         my $pm_path = Module::Locate::locate('Rapi::Blog') or die "Failed to locate Rapi::Blog?!";
70 0           file($pm_path)->parent->stringify
71             }
72              
73             has '+inject_components', default => sub {
74             my $self = shift;
75             my $model = 'Rapi::Blog::Model::DB';
76            
77             my $db = $self->site_dir->file('rapi_blog.db');
78            
79             Module::Runtime::require_module($model);
80             $model->config->{connect_info}{dsn} = "dbi:SQLite:$db";
81              
82             return [
83             [ $model => 'Model::DB' ],
84             [ 'Rapi::Blog::Model::Mailer' => 'Model::Mailer' ],
85             [ 'Rapi::Blog::Controller::Remote' => 'Controller::Remote' ],
86             [ 'Rapi::Blog::Controller::Remote::PreauthAction' => 'Controller::Remote::PreauthAction' ]
87             ]
88             };
89              
90              
91              
92             has 'site_dir', is => 'ro', init_arg => undef, lazy => 1, default => sub {
93             my $self = shift;
94            
95             my $Dir = dir( $self->site_path )->absolute;
96             -d $Dir or die "Scaffold directory '$Dir' not found.\n";
97            
98             return $Dir
99             }, isa => InstanceOf['Path::Class::Dir'];
100              
101              
102             has 'scaffolds', is => 'ro', lazy => 1, default => sub { undef };
103              
104             has 'ScaffoldSet', is => 'ro', init_arg => undef, lazy => 1, default => sub {
105             my $self = shift;
106            
107             my $scafs = $self->scaffolds || [];
108             $scafs = [ $scafs ] unless (ref($scafs)||'' eq 'ARRAY');
109             $scafs = [ $self->scaffold_dir ] unless (scalar(@$scafs) > 0);
110            
111             my @list = map {
112             Rapi::Blog::Scaffold->factory( $_ )
113             } @$scafs, @{ $self->_get_underlay_scaffold_dirs };
114            
115             my $Set = Rapi::Blog::Scaffold::Set->new( Scaffolds => \@list );
116            
117             # Apply any custom configs to the *first* scaffold:
118             $self->scaffold_config and $Set->first->config->_apply_params( $self->scaffold_config );
119            
120             $Set
121              
122             }, isa => InstanceOf['Rapi::Blog::Scaffold::Set'];
123              
124              
125             # This exists to be able to provide access to the running Blog config, including within
126             # templates, without the risk associated with providing direct access to the Rapi::Blog
127             # instance outright:
128             has 'BlogCfg', is => 'ro', init_arg => undef, lazy => 1, default => sub {
129             my $self = shift;
130            
131             my @keys = grep {
132             my $Attr = $self->meta->get_attribute($_);
133            
134             !($_ =~ /^_/) # ignore attrs with private names (i.e. start with "_")
135             && ($Attr->reader||'') eq $_ # only consider attributes with normal accessor names
136             && $Attr->has_value($self) # and already have a value
137              
138             } $self->meta->get_attribute_list;
139              
140             # If any normal methods are desired in the future, add them to keys here
141            
142             my $cfg = { map { $_ => $self->$_ } @keys };
143              
144             $cfg
145             }, isa => HashRef;
146              
147              
148              
149             # Single merged config object which considers, prioritizes and flattens the configs of all scaffolds
150             has 'scaffold_cfg', is => 'ro', init_arg => undef, lazy => 1, default => sub {
151             my $self = shift;
152            
153             my %merged = (
154             map {
155             %{ $_->config->_all_as_hash }
156             } reverse ($self->ScaffoldSet->all)
157             );
158            
159             Rapi::Blog::Scaffold::Config->new( %merged )
160              
161             }, isa => InstanceOf['Rapi::Blog::Scaffold::Config'];
162              
163              
164              
165             has 'scaffold_dir', is => 'ro', init_arg => undef, lazy => 1, default => sub {
166             my $self = shift;
167            
168             my $path;
169            
170             if(my $scaffold_name = $self->builtin_scaffold) {
171             die join('',
172             " Error: don't use both 'builtin_scaffold' and 'scaffold_path' options"
173             ) if ($self->scaffold_path);
174             my $Dir = $self->_get_builtin_scaffold_dir($scaffold_name)->absolute;
175             -d $Dir or die "builtin scaffold '$scaffold_name' not found\n";
176            
177             $path = $Dir->stringify;
178             }
179             else {
180             $path = $self->scaffold_path || $self->site_dir->subdir('scaffold');
181             }
182            
183             my $Dir = dir( $path );
184             if(! -d $Dir) {
185             if($self->fallback_builtin_scaffold) {
186             my $scaffold_name = 'bootstrap-blog';
187             warn join('',
188             "\n ** WARNING: local scaffold directory not found;\n --> using builtin ",
189             "scaffold '$scaffold_name' (fallback_builtin_scaffold is set to true)\n\n"
190             );
191             $Dir = $self->_get_builtin_scaffold_dir($scaffold_name);
192             -d $Dir or die join('',
193             " Fatal error: fallback scaffold not found (this could indicate a ",
194             "problem with your Rapi::Blog installation)\n\n"
195             );
196             }
197             else {
198             die "Scaffold directory '$Dir' not found.\n";
199             }
200             }
201             return $Dir
202             }, isa => InstanceOf['Path::Class::Dir'];
203              
204             sub _get_builtin_scaffold_dir {
205 0     0     my ($self, $scaffold_name) = @_;
206 0   0       $scaffold_name ||= 'bootstrap-blog';
207            
208 0           my $Scaffolds = dir( $self->share_dir )->subdir('scaffolds')->absolute;
209 0 0         -d $Scaffolds or die join('',
210             " Fatal error: Unable to locate scaffold share dir (this could indicate a ",
211             "problem with your Rapi::Blog installation)\n\n"
212             );
213            
214 0           $Scaffolds->subdir($scaffold_name)
215             }
216              
217              
218             after 'bootstrap' => sub {
219             my $self = shift;
220            
221             my $c = $self->appname;
222             $c->setup_plugins(['+Rapi::Blog::CatalystApp']);
223            
224             };
225              
226              
227             sub _get_underlay_scaffold_dirs {
228 0     0     my $self = shift;
229              
230 0           my $CommonUnderlay = dir( $self->share_dir )->subdir('common_underlay')->absolute;
231 0 0         -d $CommonUnderlay or die join('',
232             " Fatal error: Unable to locate common underlay scaffold dir (this could ",
233             "indicate a problem with your Rapi::Blog installation)\n\n"
234             );
235            
236             return [
237 0           @{$self->underlay_scaffolds},
  0            
238             $CommonUnderlay
239             ]
240             }
241              
242              
243              
244 0     0     sub _build_version { $VERSION }
245 0     0     sub _build_plugins { [qw/
246             RapidApp::RapidDbic
247             RapidApp::AuthCore
248             RapidApp::NavCore
249             RapidApp::CoreSchemaAdmin
250             /]}
251              
252             sub _build_base_config {
253 0     0     my $self = shift;
254            
255 0           my $tpl_dir = join('/',$self->share_dir,'templates');
256 0 0         -d $tpl_dir or die join('',
257             "template dir ($tpl_dir) not found; ",
258             __PACKAGE__, " may not be installed properly.\n"
259             );
260            
261 0           my $loc_assets_dir = join('/',$self->share_dir,'assets');
262 0 0         -d $loc_assets_dir or die join('',
263             "assets dir ($loc_assets_dir) not found; ",
264             __PACKAGE__, " may not be installed properly.\n"
265             );
266            
267             my $config = {
268            
269             'RapidApp' => {
270             module_root_namespace => 'adm',
271             local_assets_dir => $loc_assets_dir,
272            
273             load_modules => {
274             sections => {
275             class => 'Rapi::Blog::Module::SectionTree',
276             params => {}
277             }
278             },
279            
280             },
281            
282             'Plugin::RapidApp::NavCore' => {
283             custom_navtree_nodes => [
284             {
285             text => 'Taxonomies',
286             iconCls => 'icon-fa-cogs',
287             cls => 'pad-top-4px',
288             expand => \1,
289             children => [
290             {
291             text => 'Tags',
292             iconCls => 'icon-tags-blue',
293             url => '/adm/main/db/db_tag'
294             },
295             {
296             text => 'Categories',
297             iconCls => 'icon-images',
298             url => '/adm/main/db/db_category'
299             },
300             {
301             text => 'Sections (Tree)',
302             iconCls => 'icon-sitemap-color',
303             url => '/adm/sections'
304             },
305             {
306             text => 'Sections (Grid)',
307             iconCls => 'icon-chart-organisation',
308             url => '/adm/main/db/db_section'
309             },
310              
311             ]
312             },
313             {
314             text => 'Content',
315             iconCls => 'icon-folder-table',
316             children => [
317             {
318             text => 'Posts',
319             iconCls => 'icon-posts',
320             url => '/adm/main/db/db_post'
321             },
322             {
323             text => 'Comments',
324             iconCls => 'icon-comments',
325             url => '/adm/main/db/db_comment'
326             },
327             ]
328             },
329            
330             {
331             text => 'Index & tracking tables',
332             iconCls => 'icon-database-gear',
333             children => [
334             {
335             text => 'Post-Category Links',
336             iconCls => 'icon-logic-and',
337             url => '/adm/main/db/db_postcategory'
338             },
339             {
340             text => 'Post-Tag Links',
341             iconCls => 'icon-logic-and-blue',
342             url => '/adm/main/db/db_posttag'
343             },
344             {
345             text => 'Track Section-Posts',
346             iconCls => 'icon-table-relationship',
347             url => '/adm/main/db/db_trksectionpost'
348             },
349             {
350             text => 'Track Section-Sections',
351             iconCls => 'icon-table-relationship',
352             url => '/adm/main/db/db_trksectionsection'
353             },
354             ]
355             },
356            
357             {
358             text => 'Stats & settings',
359             iconCls => 'icon-group-gear',
360             children => [
361             {
362             text => 'Users',
363             iconCls => 'icon-users',
364             url => '/adm/main/db/db_user'
365             },
366             {
367             text => 'Roles',
368             iconCls => 'ra-icon-user-prefs',
369             url => '/adm/main/db/rapidapp_coreschema_role'
370             },
371             {
372             text => 'Hits',
373             iconCls => 'icon-world-gos',
374             url => '/adm/main/db/db_hit'
375             },
376             {
377             text => 'Sessions',
378             iconCls => 'ra-icon-environment-network',
379             url => '/adm/main/db/db_session'
380             },
381            
382             {
383             text => 'All Saved Views',
384             iconCls => 'ra-icon-data-views',
385             url => '/adm/main/db/rapidapp_coreschema_savedstate'
386             },
387            
388             {
389             text => 'Default Views by Source',
390             iconCls => 'ra-icon-data-preferences',
391             url => '/adm/main/db/rapidapp_coreschema_defaultview'
392             },
393             ]
394             },
395            
396             ]
397             },
398            
399             'Model::RapidApp::CoreSchema' => {
400             sqlite_file => $self->site_dir->file('rapidapp_coreschema.db')->stringify
401             },
402            
403             'Plugin::RapidApp::AuthCore' => {
404             linked_user_model => 'DB::User'
405             },
406            
407             'Controller::SimpleCAS' => {
408             store_path => $self->site_dir->subdir('cas_store')->stringify
409             },
410            
411             'Plugin::RapidApp::TabGui' => {
412             title => $TITLE,
413             nav_title => 'Administration',
414             banner_template => file($tpl_dir,'banner.html')->stringify,
415             dashboard_url => '/tpl/dashboard.md',
416             navtree_init_width => 190,
417             },
418            
419             'Controller::RapidApp::Template' => {
420             root_template_prefix => '/',
421             root_template => $self->scaffold_cfg->landing_page,
422             read_alias_path => '/tpl', #<-- already the default
423             edit_alias_path => '/tple', #<-- already the default
424             default_template_extension => undef,
425             include_paths => [ $tpl_dir ],
426             access_class => 'Rapi::Blog::Template::AccessStore',
427             access_params => {
428            
429             BlogCfg => $self->BlogCfg,
430             ScaffoldSet => $self->ScaffoldSet,
431             scaffold_cfg => $self->scaffold_cfg,
432            
433             #internal_post_path => $self->scaffold_cfg->internal_post_path,
434             #default_view_path => $self->scaffold_cfg->default_view_path,
435            
436            
437             #scaffold_dir => $self->scaffold_dir,
438             #scaffold_cnf => $self->scaffold_cnf,
439             #static_paths => $self->scaffold_cnf->{static_paths},
440             #private_paths => $self->scaffold_cnf->{private_paths},
441             #default_ext => $self->scaffold_cnf->{default_ext},
442             #
443             #internal_post_path => $self->scaffold_cnf->{internal_post_path},
444             #view_wrappers => $self->scaffold_cnf->{view_wrappers},
445             #default_view_path => $self->default_view_path,
446             #preview_path => $self->preview_path,
447             #
448             #underlay_scaffold_dirs => $self->_get_underlay_scaffold_dirs,
449              
450 0     0     get_Model => sub { $self->base_appname->model('DB') }
451             }
452             },
453            
454 0 0         'Model::Mailer' => {
455             smtp_config => $self->smtp_config,
456             ( $self->override_email_recipient ? (envelope_to => $self->override_email_recipient) : () )
457             }
458            
459             };
460            
461 0 0         if(my $faviconPath = $self->ScaffoldSet->first_config_value_filepath('favicon')) {
462 0           $config->{RapidApp}{default_favicon_url} = $faviconPath;
463             }
464            
465 0 0         if(my $loginTpl = $self->ScaffoldSet->first_config_value_file('login')) {
466 0           $config->{'Plugin::RapidApp::AuthCore'}{login_template} = $loginTpl;
467             }
468            
469 0 0         if(my $errorTpl = $self->ScaffoldSet->first_config_value_file('error')) {
470 0           $config->{'RapidApp'}{error_template} = $errorTpl;
471             }
472            
473 0           return $config
474             }
475              
476             1;
477              
478             __END__
479              
480             =head1 NAME
481              
482             Rapi::Blog - Plack-compatible, RapidApp-based blog engine
483              
484             =head1 SYNOPSIS
485              
486             use Rapi::Blog;
487            
488             my $app = Rapi::Blog->new({
489             site_path => '/path/to/some-site',
490             scaffold_path => '/path/to/some-site/scaffold', # default
491             });
492              
493             # Plack/PSGI app:
494             $app->to_app
495              
496             Create a new site from scratch using the L<rabl.pl> utility script:
497              
498             rabl.pl create /path/to/some-site
499             cd /path/to/some-site && plackup
500              
501             =head1 DESCRIPTION
502              
503             This is a L<Plack>-compatible blogging platform written using L<RapidApp>. This module was first
504             released during The Perl Conference 2017 in Washington D.C. where a talk/demo was given on the
505             platform:
506              
507             =begin HTML
508              
509             <p><a href="http://rapi.io/tpc2017"><img
510             src="https://raw.githubusercontent.com/vanstyn/Rapi-Blog/master/share/tpc2017-video-preview.png"
511             width="800"
512             alt="Rapi::Blog talk/video"
513             title="Rapi::Blog talk/video"
514             /></a></p>
515              
516             =end HTML
517              
518             L<rapi.io/tpc2017|http://rapi.io/tpc2017>
519              
520             See L<Rapi::Blog::Manual> for more information and usage.
521              
522             =head1 CONFIGURATION
523              
524             C<Rapi::Blog> extends L<RapidApp::Builder> and supports all of its options, as well as the following
525             params specific to this module:
526              
527             =head2 site_path
528              
529             Only required param - path to the directory containing the site.
530              
531             =head2 scaffold_path
532              
533             Path to the directory containing the "scaffold" of the site. This is like a document root with
534             some extra functionality.
535              
536             If not supplied, defaults to C<'scaffold/'> within the C<site_path> directory.
537              
538             =head2 builtin_scaffold
539              
540             Alternative to C<scaffold_path>, the name of one of the builtin skeleton scaffolds to use as the
541             live scaffold. This is mainly useful for dev and content-only testing. As of version C<1.0000>) there
542             are two built-in scaffolds:
543              
544             =head3 bootstrap-blog
545              
546             This is the default out-of-the-box scaffold which is based on the "Blog" example from the Twitter
547             Bootstrap HTML/CSS framework (v3.3.7): L<http://getbootstrap.com/examples/blog/>. This mainly exists
548             to serve as a useful reference implementation of the basic features/directives provided by the
549             Template API.
550              
551             =head3 keep-it-simple
552              
553             Based on the "Keep It Simple" website template by L<http://www.Styleshout.com>
554              
555             =head2 fallback_builtin_scaffold
556              
557             If set to true and the local scaffold directory doesn't exist, the default builtin skeleton scaffold
558             'bootstrap-blog' will be used instead. Useful for testing and content-only scenarios.
559              
560             Defaults to false.
561              
562             =head2 smtp_config
563              
564             Optional HashRef of L<Email::Sender::Transport::SMTP> params which will be used by the app for
565             sending E-Mails, such as password resets and other notifications. The options are passed directly
566             to C<Email::Sender::Transport::SMTP->new()>. If the special param C<transport_class> is included,
567             it will be used as the transport class instead of C<Email::Sender::Transport::SMTP>. If this is
568             supplied, it should still be a valid L<Email::Sender::Transport> class.
569              
570             If this option is not supplied, E-Mails will be sent via the localhost using C<sendmail> via
571             the default L<Email::Sender::Transport::Sendmail> options.
572              
573             =head2 override_email_recipient
574              
575             If set, all e-mails generated by the system will be sent to the specified address instead of normal
576             recipients.
577              
578              
579             =head1 METHODS
580              
581             =head2 to_app
582              
583             PSGI C<$app> CodeRef. Derives from L<Plack::Component>
584              
585             =head1 SEE ALSO
586              
587             =over
588              
589             =item *
590              
591             L<rabl.pl>
592              
593             =item *
594              
595             L<Rapi::Blog::Manual>
596              
597             =item *
598              
599             L<RapidApp>
600              
601             =item *
602              
603             L<RapidApp::Builder>
604              
605             =item *
606              
607             L<Plack>
608              
609             =item *
610              
611             L<http://rapi.io/blog>
612              
613             =back
614              
615              
616             =head1 AUTHOR
617              
618             Henry Van Styn <vanstyn@cpan.org>
619              
620             =head1 COPYRIGHT AND LICENSE
621              
622             This software is copyright (c) 2017 by IntelliTree Solutions llc.
623              
624             This is free software; you can redistribute it and/or modify it under
625             the same terms as the Perl 5 programming language system itself.
626              
627             =cut
628              
629