Jelajahi Sumber

Fix #21 - self documenting whee

George S. Baugh 5 tahun lalu
induk
melakukan
f7039ac373
10 mengubah file dengan 213 tambahan dan 25 penghapusan
  1. 1 0
      .gitignore
  2. 3 2
      Makefile
  3. 10 0
      lib/Trog/Config.pm
  4. 10 0
      lib/Trog/Data.pm
  5. 25 1
      lib/Trog/Data/DUMMY.pm
  6. 98 11
      lib/Trog/Routes/HTML.pm
  7. 61 0
      lib/tCMS/Manual.pod
  8. 1 10
      www/server.psgi
  9. 2 0
      www/templates/manual.tx
  10. 2 1
      www/templates/sysbar.tx

+ 1 - 0
.gitignore

@@ -4,3 +4,4 @@ favicon.ico
 dist/
 www/assets/*.*
 data/DUMMY.json
+pod2htmd.tmp

+ 3 - 2
Makefile

@@ -1,10 +1,11 @@
 .PHONY: install
 install:
 	test -d $(HOME) || mkdir $(HOME)/.tcms
+	rm pod2htmd.tmp; /bin/true
 
 .PHONY: test
 test: reset-dummy-data
-	prove t/*.t
+	prove
 
 .PHONY: reset-dummy-data
 reset-dummy-data:
@@ -13,4 +14,4 @@ reset-dummy-data:
 .PHONY: depend
 depend:
 	sudo apt install -y sqlite3 libsqlite3-dev libdbd-sqlite3-perl cpanminus starman  libcal-dav-perl libtext-xslate-perl libserver-starter-perl liburl-encode-perl libplack-perl libcal-dav-perl libconfig-tiny-perl libdatetime-format-http-perl libjson-maybexs-perl libuuid-tiny-perl libcapture-tiny-perl libconfig-simple-perl libdbi-perl libfile-slurper-perl libfile-touch-perl libfile-copy-recursive-perl libxml-rss-perl
-	sudo cpanm Mojo::File Date::Format WWW::SitemapIndex::XML WWW::Sitemap::XML HTTP::Body
+	sudo cpanm Mojo::File Date::Format WWW::SitemapIndex::XML WWW::Sitemap::XML HTTP::Body Pod::Html

+ 10 - 0
lib/Trog/Config.pm

@@ -5,6 +5,16 @@ use warnings;
 
 use Config::Simple;
 
+=head1 Trog::Config
+
+A thin wrapper around Config::Simple which reads the configuration from the appropriate place.
+
+=head2 Trog::Config::get() = Config::Simple
+
+Returns a configuration object that will be used by server.psgi, the data model and Routing modules.
+
+=cut
+
 our $home_cfg = "$ENV{HOME}/.tcms/main.cfg";
 
 sub get {

+ 10 - 0
lib/Trog/Data.pm

@@ -8,6 +8,16 @@ use feature qw{signatures};
 
 #It's just a factory
 
+=head1 Trog::Data
+
+This is a data model factory.
+
+=head2 Trog::Data->new(Trog::Config) = $handle
+
+Returns a new Trog::Data::* class appropriate to what is configured in the Trog::Config object passed.
+
+=cut
+
 sub new( $class, $config ) {
     my $module = "Trog::Data::".$config->param('general.data_model');
     my $req = $module;

+ 25 - 1
lib/Trog/Data/DUMMY.pm

@@ -67,6 +67,8 @@ Queries the data model in the way a "real" data model module ought to.
 
     id   => Filter down to just the post by ID.  May be subsequently filtered by ACL, resulting in a 404 (which is good, as it does not disclose info).
 
+    version => if id is passed, return the provided post version rather than the most recent one
+
     tags => ARRAYREF of tags, any one of which is required to give a result.  If none are passed, no filtering is performed.
 
     acls => ARRAYREF of acl tags, any one of which is required to give result. Filter applies after tags.  'admin' ACL being present skips this filter.
@@ -77,6 +79,8 @@ Queries the data model in the way a "real" data model module ought to.
 
     like => Search query, as might be passed in the search bar.
 
+    author => filter by post author
+
 =cut
 
 sub _read {
@@ -91,7 +95,6 @@ sub _write($data) {
     close $fh;
 }
 
-# These have to be sorted as requested by the client
 sub get ($self, %request) {
 
     my $example_posts = _read();
@@ -163,6 +166,13 @@ sub _dedup_versions ($version=-1, @posts) {
     return @deduped;
 }
 
+=head2 total_posts() = INT $num
+
+Returns the total number of posts.
+Used to determine paginator parameters.
+
+=cut
+
 sub total_posts {
     my $example_posts = _read();
     return scalar(@$example_posts);
@@ -202,6 +212,13 @@ sub _add_visibility (@posts) {
     } @posts;
 }
 
+=head2 add(@posts) = BOOL $failed_or_not
+
+Add the provided posts to the datastore.
+If any post already exists with the same id, a new post with a version higher than it will be added.
+
+=cut
+
 sub add ($self, @posts) {
     require UUID::Tiny;
     my $example_posts = _read();
@@ -273,6 +290,13 @@ sub _handle_upload ($file, $uuid) {
     return "/assets/$newname";
 }
 
+=head2 delete(@posts)
+
+Delete the following posts.
+Will remove all versions of said post.
+
+=cut
+
 sub delete($self, @posts) {
     my $example_posts = _read();
     foreach my $update (@posts) {

+ 98 - 11
lib/Trog/Routes/HTML.pm

@@ -8,6 +8,7 @@ use feature qw{signatures state};
 
 use File::Touch();
 use List::Util();
+use Capture::Tiny qw{capture};
 
 use Trog::Config;
 use Trog::Data;
@@ -153,6 +154,17 @@ our %routes = (
         callback => \&Trog::Routes::HTML::users,
         captures => ['username'],
     },
+    '/manual' => {
+        method => 'GET',
+        auth   => 1,
+        callback => \&Trog::Routes::HTML::manual,
+    },
+    '/lib/(.*)' => {
+        method => 'GET',
+        auth   => 1,
+        captures => ['module'],
+        callback => \&Trog::Routes::HTML::manual,
+    },
 );
 
 # Build aliases for /posts and /post with extra data
@@ -184,12 +196,14 @@ if ($theme_dir) {
     }
 }
 
-sub robots ($query, $render_cb) {
-    my $processor = Text::Xslate->new(
-        path   => $template_dir,
-    );
-    return [200, ["Content-type:text/plain\n"],[$processor->render('robots.tx', { domain => $query->{domain} })]];
-}
+=head1 PRIMARY ROUTE
+
+=head2 index
+
+Implements the primary route used by all pages not behind auth.
+Most subsequent functions simply pass content to this function.
+
+=cut
 
 sub index ($query,$render_cb, $content = '', $i_styles = []) {
     $query->{theme_dir}  = $theme_dir || '';
@@ -280,6 +294,19 @@ sub badrequest (@args) {
 
 These are expected to either return a 200, or redirect to something which does.
 
+=head2 robots
+
+Return an appropriate robots.txt
+
+=cut
+
+sub robots ($query, $render_cb) {
+    my $processor = Text::Xslate->new(
+        path   => $template_dir,
+    );
+    return [200, ["Content-type:text/plain\n"],[$processor->render('robots.tx', { domain => $query->{domain} })]];
+}
+
 =head2 setup
 
 One time setup page; should only display to the first user to visit the site which we presume to be the administrator.
@@ -489,6 +516,12 @@ sub post ($query, $render_cb) {
     });
 }
 
+=head2 post_save
+
+Saves posts submitted via the /post pages
+
+=cut
+
 sub post_save ($query, $render_cb) {
     my $to = delete $query->{to};
 
@@ -503,6 +536,12 @@ sub post_save ($query, $render_cb) {
     return post($query, $render_cb);
 }
 
+=head2 profile
+
+Saves / updates new users.
+
+=cut
+
 sub profile ($query, $render_cb) {
     #TODO allow users to do something OTHER than be admins
     if ($query->{password}) {
@@ -516,6 +555,11 @@ sub profile ($query, $render_cb) {
     return post_save($query, $render_cb);
 }
 
+=head2 post_delete
+
+deletes posts.
+
+=cut
 
 sub post_delete ($query, $render_cb) {
     state $data = Trog::Data->new($conf);
@@ -524,6 +568,12 @@ sub post_delete ($query, $render_cb) {
     return post($query, $render_cb);
 }
 
+=head2 series
+
+Add new 'series' (ACLs) to classify content with.
+
+=cut
+
 sub series ($query, $render_cb) {
     #Grab the relevant tag (aclname), then pass that to posts
     my (undef, $posts) = _post_helper($query, [], $query->{acls});
@@ -533,6 +583,12 @@ sub series ($query, $render_cb) {
     return posts($query,$render_cb);
 }
 
+=head2 avatars
+
+Returns the avatars.css.  Limited to 1000 users.
+
+=cut
+
 sub avatars ($query, $render_cb) {
     #XXX if you have more than 1000 editors you should stop
     my $tags = _coerce_array($query->{tag});
@@ -549,6 +605,12 @@ sub avatars ($query, $render_cb) {
     return [200,["Content-type: text/css\n"],[$content]];
 }
 
+=head2 users
+
+Implements direct user profile view.
+
+=cut
+
 sub users ($query, $render_cb) {
     my (undef,$posts) = _post_helper({ limit => 10000 }, ['about'], $query->{acls});
     my @user = grep { $_->{user} eq $query->{username} } @$posts;
@@ -767,19 +829,19 @@ sub _rss ($query,$posts) {
         title          => "$query->{domain}",
         link           => "http://$query->{domain}/$query->{route}?format=rss",
         language       => 'en', #TODO localization
-        description    => 'tCMS website', #TODO make configurable
-        pubDate        => $now, #TODO format
-        lastBuildDate  => $now, #TODO format
+        description    => "$query->{domain} : $query->{route}",
+        pubDate        => $now,
+        lastBuildDate  => $now,
     );
  
     #TODO configurability
     $rss->image(
         title       => $query->{domain},
-        url         => "http://$query->{domain}/img/icon/tcms.svg",
+        url         => "/$theme_dir/img/icon/favicon.ico",
         link        => "http://$query->{domain}",
         width       => 88,
         height      => 31,
-        description => 'tCMS image'
+        description => "$query->{domain} favicon",
     );
  
     foreach my $post (@$posts) {
@@ -798,6 +860,31 @@ sub _rss ($query,$posts) {
     return [200, ["Content-type: application/rss+xml\n"], [$rss->as_string]];
 }
 
+=head2 manual
+
+Implements the /manual and /lib/* routes.
+
+Basically a thin wrapper around Pod::Html.
+
+=cut
+
+sub manual ($query, $render_cb) {
+    require Pod::Html;
+    require Capture::Tiny;
+
+    #Fix links from Pod::HTML
+    $query->{module} =~ s/\.html$//g if $query->{module};
+
+    my $infile = $query->{module} ? "$query->{module}.pm" : 'tCMS/Manual.pod';
+    return notfound($query,$render_cb) unless -f "lib/$infile";
+    my $content = capture { Pod::Html::pod2html(qw{--podpath=lib --podroot=.},"--infile=lib/$infile") };
+    return $render_cb->('manual.tx', {
+        title       => 'tCMS Manual',
+        content     => $content,
+        stylesheets => _build_themed_styles('post.css'),
+    });
+}
+
 # Deal with Params which may or may not be arrays
 sub _coerce_array ($param) {
     my $p = $param || [];

+ 61 - 0
lib/tCMS/Manual.pod

@@ -0,0 +1,61 @@
+=head1 tCMS Manual
+
+=head2 First time setup
+
+Run these makefile targets:
+
+    make depend
+    make install
+
+From there, running tCMS is pretty simple:
+
+    starman www/server.psgi
+
+The application expects to run from the repository root.
+The first time you open the application, you will be presented with a first-time page that tells you to load /login.
+
+You will note that the submission button says 'Register' rather than 'Login'.
+The first user which logs in will be set up as the administrator, and all further users must be made by them via the /post/about route.
+
+You will want to do the following to make your user public:
+
+=over 4
+
+=item Create a user via the /post/about route using the name you just registered as.  This will require you to re-set your password.
+
+=item Ensure the 'public' visibility is chosen when submitting the form.
+
+=back
+
+=head2 Application Structure
+
+server.psgi is a very straightforward application router which serves requests based on routing modules.
+There are 3 routing modules you will need to know about:
+
+=over 4
+
+=item L<Trog::Routes::HTML> - Render various pages which (mostly) output text/html
+
+=item L<Trog::Routes::JSON> - Implement various application/json output routes
+
+=item Themed B<Routes> - Inside of any given theme in themes/ there will be a Routes.pm module defining custom routes for your theme.
+
+=back
+
+From there the routes are generally going to call out to the data model, which is configured via the /config route.
+There are only two relevant things to configure, which is what Theme and Data model to use.
+
+The configuration module is L<Trog::Config>.
+The Data model modules are all subclasses of L<Trog::Data>.
+
+We include a bogus data model for testing called 'DUMMY' which should not be used for production purposes.
+It is useful as an example for developers: L<Trog::Data::DUMMY>
+
+Authentication is accomplished via a cookie (tcmslogin) which we check against an sqlite database, ~/.tcms/auth.db
+Passwords are hashed and salted, and the only other thing stored there is what ACLs users have.
+The module controlling this is L<Trog::Auth>.
+
+=head2 Theming
+
+Themes are subdirectories of /themes, which mirror the structure of www/ internally.
+Stylesheets are included after the mainline ones, so that your styles will override the default.

+ 1 - 10
www/server.psgi

@@ -14,6 +14,7 @@ use Mojo::File   ();
 use DateTime::Format::HTTP();
 use Encode qw{encode_utf8};
 use CGI::Cookie ();
+use File::Basename();
 
 #Grab our custom routes
 use lib 'lib';
@@ -36,12 +37,6 @@ my %content_types = (
     blob  => "$ct:application/octet-stream;",
 );
 
-my $cd = 'Content-disposition';
-my %content_dispositions = (
-    attachment => 'attachment; filename=',
-    inline     => 'inline; filename=',
-);
-
 my $cc = 'Cache-control';
 my %cache_control = (
     revalidate => "$cc: no-cache, max-age=0;",
@@ -149,12 +144,9 @@ sub _serve ($path, $last_fetch=0) {
 
     my @headers = ($ft);
 
-    #TODO figure out content-disposition
-
     #TODO use static Cache-Control for everything but JS/CSS?
     push(@headers,$cache_control{revalidate});
 
-
     #TODO Return 304 unchanged for files that haven't changed since the requestor reports they last fetched
     my $mt = (stat($path))[9];
     my @gm = gmtime($mt);
@@ -197,7 +189,6 @@ sub _render ($template, $vars, @headers) {
     $vars->{code} ||= 200;
 
     push(@headers, $vars->{contenttype});
-    push(@headers,$vars->{contentdisposition}) if $vars->{contentdisposition};
     push(@headers, $vars->{cachecontrol}) if $vars->{cachecontrol};
     my $h = join("\n",@headers);
 

+ 2 - 0
www/templates/manual.tx

@@ -0,0 +1,2 @@
+: include "sysbar.tx";
+<: $content | mark_raw :>

+ 2 - 1
www/templates/sysbar.tx

@@ -1,8 +1,9 @@
 <div id="topkek" style="text-align: center; vertical-align: middle;">
     <button title="Menu" id="clickme">☰</button>
     <span id="configbar">
-        <a class="topbar" title="Back home" href="/">Home</a>
+        <a href="/"            title="Back home"     class="topbar">Home</a>
         <a href="/config"      title="Configuration" class="topbar">Settings</a>
+        <a href="/manual"      title="Manual"        class="topbar">Manual</a>
         <a href="/post/news"   title="Micro Blog"    class="topbar">News</a>
         <a href="/post/blog"   title="Blog"          class="topbar">Blog</a>
         <a href="/post/image"  title="Images"        class="topbar">Images</a>