~rgrjr/bookworm

0fbf1ab8a3d74153d205e0c4965ff5cfb521da2d — Bob Rogers 3 years ago 596d390 master
Make book and location content sortable

* Bookworm/Author.pm:
   + (post_web_update):  Use present_sorted_content for the books.
* Bookworm/Location.pm:
   + (post_web_update):  Present locations and books sortably (and
     independently) using the Modframe present_sorted_content method.
   + (ajax_sort_content):  Support for AJAX content sorting.
* Bookworm/Book.pm:
   + (compare_authors_arrays):  Pass this as the comparator for the
     authors field for locations, so that they sort properly.
* cgi/ajax-author-sort.cgi (added), cgi/web-files.tbl:
   + Page for AJAX update of an author's books.
* cgi/ajax-location-sort.cgi (added):
   + Page for AJAX content sorting.
M Bookworm/Author.pm => Bookworm/Author.pm +48 -8
@@ 136,18 136,53 @@ my $book_columns
	  return_address => 'book.cgi' },
	qw(publication_year category notes date_read location_id) ];

sub ajax_sort_content {
    # Handle AJAX requests to sort our books.
    my ($self, $q) = @_;

    my $prefix = $q->param('prefix') || '';
    my $messages = { debug => '' };
    if ($prefix eq 'book') {
	my $books = $self->books;
	my $book_presenter = @$books ? $books->[0] : $self;
	$messages->{book_content}
	    = $book_presenter->present_sorted_content
		($q, $self->html_link(undef) . ' books',
		 $book_columns, $books, prefix => 'book');
    }
    else {
	$messages->{debug} = "Unknown prefix '$prefix'.";
    }
    $q->send_encoded_xml($messages);
}

sub web_update {
    # Add an onSubmit trigger that supports AJAX book sorting.
    my ($self, $q, @options) = @_;

    $q->include_javascript('update-content.js');
    my $a1 = $q->oligo_query('ajax-author-sort.cgi', prefix => 'book');
    my $on_submit
	= qq{return maybe_update_sort(event, 'book', 'update', '$a1', '&');};
    $self->SUPER::web_update
	($q, @options, onsubmit => $on_submit);
}

sub post_web_update {
    my ($self, $q) = @_;

    my @stuff;
    my $author_book_link
	= $q->oligo_query('book.cgi', author_id => $self->author_id);
    push(@stuff,
	 $q->a({ href => $author_book_link }, '[Add book]'));
    my @links;
    my $book_link = $q->oligo_query('book.cgi', author_id => $self->author_id);
    push(@links, $q->a({ href => $book_link }, '[Add book]'));
    my $books = $self->books;
    join("\n", $q->ul(map { $q->li($_); } @stuff),
	 $self->present_object_content($q, 'Books by this author',
				       $book_columns, $books));
    my $presenter = @$books ? $books->[0] : $self;
    join("\n",
	 $q->ul(map { $q->li($_); } @links),
	 $q->div({ id => 'book_content' },
		 $presenter->present_sorted_content
		     ($q, 'Books by this author',
		      $book_columns, $books,
		      prefix => 'book', default_sort => 'title')));
}

1;


@@ 162,6 197,11 @@ interesting in terms of the books they have written.

=head2 Accessors and methods

=head3 ajax_sort_content

Support AJAX sorting of our books.  This is the implementation of the
C<ajax-author-sort.cgi> page using C<present_sorted_content>.

=head3 author_id

Returns or sets the primary key for the author in the database.

M Bookworm/Book.pm => Bookworm/Book.pm +47 -0
@@ 73,6 73,40 @@ sub validate {
	unless $self->location_id;
}

sub compare_authors_arrays {
    # $authors1 and $authors2 are arrayrefs of Bookworm::Author instances.  For
    # sorting in the Bookworm::Location books table.
    my ($authors1, $authors2) = @_;

    my $i = 0;
    while (1) {
	if ($i < @$authors1) {
	    if ($i < @$authors2) {
		# Compare the $i^{th} authors.
		my ($a1, $a2) = ($authors1->[$i], $authors2->[$i]);
		if ($a1->author_id != $a2->author_id) {
		    my $cmp = ($a1->last_name cmp $a2->last_name
			       || $a1->first_name cmp $a2->first_name
			       || $a1->mid_name cmp $a2->mid_name);
		    return $cmp
			if $cmp;
		}
		# Same authors.
		$i++;
	    }
	    else {
		# Ran out of $authors2, which is therefore a prefix, so
		# $authors1 is greater than $authors2.
		return 1;
	    }
	}
	else {
	    # Ran out of $authors1.
	    return $i < @$authors2 ? -1 : 0;
	}
    }
}

sub format_authors_field {
    # This is only used for display, so it is always read-only.
    my ($self, $q, $descriptor, $cgi_param, $read_only_p, $value) = @_;


@@ 178,6 212,7 @@ my @field_descriptors
	 type => 'string', size => 50 },
       { accessor => 'authors', pretty_name => 'Authors',
	 type => 'authors', order_by => '_sortable_authors',
	 comparator => \&compare_authors_arrays,
	 verbosity => 2 },
       { accessor => 'authorships', pretty_name => 'Authors',
	 type => 'authorship', order_by => '_sortable_authors' },


@@ 404,6 439,12 @@ Synonym for the L</title> slot, to avoid ambiguity in C<web_update>.
Returns or sets the book category, e.g. fiction, biography.  This is
implemented as an enumeration in the schema and the user interface.

=head3 compare_authors_arrays

Given two arrayrefs of C<Bookworm::Author> instances, return -1, 0, or
1 to reflect their proper sort ort.  For sorting in the
C<Bookworm::Location> books table.

=head3 contained_item_class

Returns the string 'Bookworm::Authorship', which enables books to act


@@ 522,6 563,12 @@ Returns or sets the ID of the C<Bookworm::Publisher>.  Note that there
is no fetch accessor for the publisher, because we don't do much with
publishers.

=head3 search_date_string_field

Synonym for C<search_integer_field>.  This allows us to specify "At
least" and "At most" bounds for a string-valued field that happends to
represent a date.

=head3 title

Returns or sets the string that is the primary title of the book,

M Bookworm/Location.pm => Bookworm/Location.pm +94 -19
@@ 149,7 149,8 @@ my @local_display_fields
       { accessor => 'bg_color', pretty_name => 'Background',
	 type => 'enumeration',
	 values => \@background_colors },
       { accessor => 'n_total_books', pretty_name => 'Books' },
       { accessor => 'n_total_books', pretty_name => 'Books',
	 type => 'integer', read_only_p => 1 },
       { accessor => 'parent_location_id', pretty_name => 'Parent location',
	 edit_p => 'find-location.cgi',
	 type => 'location_chain',


@@ 242,6 243,42 @@ sub root_location_p {
    return ($self->location_id // 0) == 1;
}

sub ajax_sort_content {
    # Handle AJAX requests to sort book or location content.
    my ($self, $q) = @_;

    my $prefix = $q->param('prefix') || '';
    my $messages = { debug => '' };
    my $unlink = $self->html_link(undef);
    if ($prefix eq 'locations') {
	my $child_locations = $self->location_children;
	$messages->{locations_content}
	    = $self->present_sorted_content
	        ($q, "$unlink locations",
		 [ qw(name n_total_books description) ],
		 $child_locations,
		 prefix => 'locations');
    }
    elsif ($prefix eq 'book') {
	my $books = $self->book_children;
	my $book_presenter = @$books ? $books->[0] : $self;
	$messages->{book_content}
	    = $book_presenter->present_sorted_content
		($q, "$unlink books",
		 [ { accessor => 'book_id', label => ' ',
		     pretty_name => 'Select?',
		     type => 'checkbox', checked_p => 0 },
		   { accessor => 'title', pretty_name => 'Title',
		     type => 'self_link' },
		   qw(publication_year authors category notes) ],
		 $books, prefix => 'book');
    }
    else {
	$messages->{debug} = "Unknown prefix '$prefix'.";
    }
    $q->send_encoded_xml($messages);
}

sub post_web_update {
    my ($self, $q) = @_;
    require ModGen::CGI::make_selection_op_buttons;


@@ 260,7 297,6 @@ sub post_web_update {
	    push(@links, $q->a({ href => $url }, '[Delete location]'));
	}
    }
    $q->include_javascript('selection.js');
    my $unlink = $self->html_link(undef);
    my $child_locations = $self->location_children;
    my $books = $self->book_children;


@@ 271,23 307,28 @@ sub post_web_update {
    my $book_presenter = @$books ? $books->[0] : $self;
    return join("\n",
		$q->ul(map { $q->li($_); } @links),
		$q->div({ id => 'debug' }, ''),
		$q->h3("$unlink contents"),
		(@$child_locations
		 ? ($self->present_object_content
		       ($q, "$unlink locations",
			[ qw(name n_total_books description) ],
			$child_locations)
		    . "<br>\n")
		 : ''),
		$book_presenter->present_object_content
		    ($q, "$unlink books",
		     [ { accessor => 'book_id', pretty_name => 'Select?',
			 type => 'checkbox', checked_p => 0, label => ' ' },
		       { accessor => 'title', pretty_name => 'Title',
			 type => 'self_link' },
		       qw(publication_year authors category notes) ],
		     $books,
		     sort_p => 1),
		$q->div({ id => 'locations_content' },
			(@$child_locations
			 ? ($self->present_sorted_content
			    ($q, "$unlink locations",
			     [ qw(name n_total_books description) ],
			     $child_locations,
			     prefix => 'locations', default_sort => 'name')
			    . "<br>\n")
			 : '')),
		$q->div({ id => 'book_content' },
			$book_presenter->present_sorted_content
			    ($q, "$unlink books",
			     [ { accessor => 'book_id', label => ' ',
				 pretty_name => 'Select?',
				 type => 'checkbox', checked_p => 0 },
			       { accessor => 'title', pretty_name => 'Title',
				 type => 'self_link' },
			       qw(publication_year authors category notes) ],
			     $books,
			     prefix => 'book', default_sort => 'title:up')),
		$selection_buttons, "\n");
}



@@ 333,11 374,26 @@ sub web_update {
	if $message;
    my $name = $q->escapeHTML($self->pretty_name);
    my $heading = join(' ', 'Location', $self->_backgroundify($q, $name));

    # Create an onSubmit trigger that supports AJAX book and location content
    # sorting, as well as AJAX container operations on books.
    $q->include_javascript('update-content.js');
    $q->include_javascript('selection.js');
    # This is what book sorting needs . . .
    my $a1 = $q->oligo_query('ajax-location-sort.cgi', prefix => 'book');
    my $on_submit
	= qq{return maybe_update_sort(event, 'book', 'update', '$a1', '&')};
    # . . . this is what location sorting needs . . .
    my $a2 = $q->oligo_query('ajax-location-sort.cgi', prefix => 'locations');
    $on_submit
	.= qq{ && maybe_update_sort(event, 'locations', 'update', '$a2', '&')};
    # . . . and this is what container operations for books need.
    $on_submit .= q{ && submit_or_operate_on_selected(event)};
    $self->SUPER::web_update
	($q, @options,
	 interface => $interface,
	 heading => $heading,
	 onsubmit => 'return submit_or_operate_on_selected(event)');
	 onsubmit => $on_submit);
}

sub web_move_books {


@@ 498,12 554,26 @@ books).

=head2 Accessors and methods

=head3 ajax_sort_content

Given a C<Bookworm::Location> and a C<ModGen::CGI> object, handles
AJAX requests to sort book or location content.

=head3 ancestor_of

Given another C<Bookworm::Location> object, returns true iff self
contains the other location.  This is used to prevent cycles by the
user interface that moves locations.

=head3 bg_color

Returns or sets a string that determines the background color of the
location name in most display contexts.  See the C<@background_colors>
array for allowed variables.  The special value "inherit" means to use
the location of our L</parent_location>, or no special background if
no ancestor specifies a color.  The L</backgroundify> method
implements the search.

=head3 book_children

Set fetch accessor that retrieves an arrayref of C<Bookworm::Book>


@@ 538,6 608,11 @@ usually used to describe the purpose of the location, since I tend to
create locations that are fine-grained enough to be self-describing,
e.g. "Somewhere >> home >> Bedroom >> BR Bookshelf >> BR BS #3".

=head3 display_info

Add the number of books (if we have any) to what the superclass method
provides, used as extra information on the Location Tree page.

=head3 fetch_root

Class method that fetches the "Somewhere" location.  Hierarchy browser

A cgi/ajax-author-sort.cgi => cgi/ajax-author-sort.cgi +27 -0
@@ 0,0 1,27 @@
#!/usr/bin/perl -T
#
# Sort an author's books.
#
# [created.  -- rgr, 4-Apr-21.]
#

use strict;
use warnings;

use lib '.'; ### debug ###
use lib '.'; ### hack ###

use ModGen::CGI;

my $q = ModGen::CGI->new();
$q->generate_object_page(ajax_sort_content => 'Bookworm::Author');

__END__

=head1 DESCRIPTION

Sort an author's books.  This is an internal AJAX page, so the user
would only see it in case of an unusual error.

=cut


A cgi/ajax-location-sort.cgi => cgi/ajax-location-sort.cgi +28 -0
@@ 0,0 1,28 @@
#!/usr/bin/perl -T
#
# Search for storage locations.
#
# [created.  -- rgr, 2-Apr-21.]
#

use strict;
use warnings;

use lib '.'; ### debug ###
use lib '.'; ### hack ###

use ModGen::CGI;
use Bookworm::Location;

my $q = ModGen::CGI->new();
$q->generate_object_page(ajax_sort_content => 'Bookworm::Location');

__END__

=head1 DESCRIPTION

Sort location content.  This is an internal AJAX page, so the user
would only see it in case of an unusual error.

=cut


M cgi/web-files.tbl => cgi/web-files.tbl +2 -0
@@ 4,9 4,11 @@ book.cgi	prod	cgi	Add Book	Add
add-book-author.cgi	prod	cgi	Add Book Author
publisher.cgi	prod	cgi	Add Publisher	Add
author.cgi	prod	cgi	Add Author	Add
ajax-author-sort.cgi	prod	cgi	Ajax Author Sort
book-authorship.cgi	prod	cgi	Book Authorship
update-authorship.cgi	prod	cgi	Update Authorship
location.cgi	prod	cgi	Add Location	Add
ajax-location-sort.cgi	prod	cgi	Ajax Location Sort
move-books.cgi	prod	cgi	Move Book(s)
find-book.cgi	prod	cgi	Find Book	Search
find-author.cgi	prod	cgi	Find Author	Search