Beefy Boxes and Bandwidth Generously Provided by pair Networks
Syntactic Confectionery Delight
 
PerlMonks  

AJAX popup windows - an example

by snowhare (Friar)
on Sep 16, 2007 at 01:43 UTC ( [id://639211]=perlmeditation: print w/replies, xml ) Need Help??

This morning hacker was working on some web code that needed access to information that was both remote and potentially required many seperate HTTP requests (most of which would not actually be used by the end user). My suggestion in the Chatterbox was that he use AJAX to 'late bind' those requests to limit them to only the requests he actually needed for the user.

For various reasons that suggestion didn't solve his problem, but it set me on the road to creating an example of an AJAX based script that when you hover over a link opens a popup display that is filled by an AJAX server request.

This is that example. It is a standalone script using CGI::Minimal and CGI::Ajax that displays a simple templated web page with a couple of links that when you hover your mouse over them open a popup window (that goes away shortly after you move your mouse away from the link).

I'm sure the creative types here will immediately see how it could be easily modified for a variety of other things like 'preview images' for links or 'glossary' links and other things I've never thought of (using other event triggers than 'onMouseOver' and 'onMouseOut' are an obvious place to start).

If there is enough interest, I may work this up into a full tutorial on using AJAX.

Enjoy.

Update: Added taint flag to script. Always a good habit.

Update2: Did a little cleanup on the Javascript and added a little more documentation.

#!/usr/bin/perl -wT ##################################### # This script demonstrates a AJAX based popup window use strict; use CGI::Minimal; use CGI::Ajax; # main execution block { # $output is the output of the CGI script, ready for sending # to the web browser my $output = eval { my $cgi = CGI::Minimal->new; # A dispatch table makes it easy to add new branches # to the program functionality without having to # have endless 'if..ifelse..ifelse..else' clauses my %dispatch_table = ( 'show_page' => \&show_page, 'ajax' => \&ajax_request, ); my $default_action = 'show_page'; my $action = $cgi->param('action'); $action = defined($action) ? $action : $default_acti +on; my $action_call = $dispatch_table{$action}; my $script_output = format_output(defined($action_call) ? &$ac +tion_call(cgi => $cgi) : bad_call(cgi => $cgi)); return $script_output; }; # Ordinary 'the program blew up' errors if ($@) { $output = "Status: 500 Server Error\015\012Content-Type: text/ +plain\015\012\015\012Fatal Script Error: $@\n"; # Unusual 'the program just didn't output anything' errors } elsif ((! defined $output) || ($output eq '')) { $output = "Status: 500 Server Error\015\012Content-Type: text/ +plain\015\012\015\012Script Error: No output generated by script.\n"; } print $output; } ################################################ # ajax_request( cgi => $cgi_object ); # # handle ajax requests # # We expect to find a 'content' CGI parameter that specifies # what AJAX request has been made. sub ajax_request { my %args = @_; my ($cgi) = $args{'cgi'}; my $ajax_request = $cgi->param('content'); my $default_request = 'bad_call'; $ajax_request = defined($ajax_request) ? $ajax_request : $de +fault_request; my %ajax_dispatch = ( 'bad_call' => \&bad_ajax_call, 'example1' => \&ajax_example1, 'example2' => \&ajax_example2, ); my $ajax_call = $ajax_dispatch{$ajax_request}; my $script_output = format_output(defined($ajax_call) ? &$ajax_cal +l(cgi => $cgi) : bad_ajax_call(cgi => $cgi)); return $script_output; } ################################################# # ajax_example1( cgi => $cgi ); # # Load up our #1 example AJAX response sub ajax_example1 { my %args = @_; my ($cgi) = $args{'cgi'}; # Not actually used, but here for API co +nsistency. my $response =<<"EOT"; Content-Type: text/plain; charset=utf-8 <div style="border-style: solid; background: #ccffcc; margin: 2px 2px; + padding: 5px 5px;">Example 1 Popup data</div> EOT } ################################################# # ajax_example2( cgi => $cgi ); # # Load up our #2 example AJAX response sub ajax_example2 { my %args = @_; my ($cgi) = $args{'cgi'}; # Not actually used, but here for API co +nsistency. my $response =<<"EOT"; Content-Type: text/plain; charset=utf-8 <div style="border-style: solid; background: #cccccff; margin: 2px 2px +; padding: 5px 5px;">Example 2 Popup data</div> EOT } ################################################# # bad_ajax_call( cgi => $cgi ) # # Load our 'bad' ajax response sub bad_ajax_call { my %args = @_; my ($cgi) = $args{'cgi'}; # Not actually used, but here for API co +nsistency. my $response =<<"EOT"; Content-Type: text/plain; charset=utf-8 BAD AJAX CALL. Didn't find expected CGI parameters. EOT } ################################################# # format_output($output_text) # # Processes the text we are about to send to the browser # to ensure it has a Status: header, a Content-Length: # header as necessary and uses CRLF convention for # headers EOL. # # Adds a 'Status: 200 OK' header (if there isn't a CGI # Status header already), adds a Content-Length header, # and ensures that we are compliant to the internet EOL # convention of \015\012 for the headers # # This gives us both CGI and ModPerl compatibility sub format_output { my ($source_output) = @_; my ($headers, $break, $body) = $source_output =~ m/^(.+?)(\015\012 +\015\012|\012\012|\015\015)(.*)$/s; unless (defined $break) { $headers = "Content-Type: text/plain; charset=utf-8"; $body = "Script Error: Unable to identify HTTP headers and +body of output? Something is wrong....:\n$source_output"; } my @header_lines = split(/[\015\012]+/,$headers); unless (grep(/^Status: /i, @header_lines)) { unshift(@header_lines, 'Status: 200 OK'); } my $content_length = length($body); push(@header_lines, "Content-Length: $content_length"); my $output = join("\015\012",@header_lines,'',$body); return $output; } ################################################# # show_page( cgi=> $cgi [, results => $results] [, errors => $errors ] +) # # cgi - the CGI object (CGI, CGI::Minimal or other broker with a ' +param' method) # results - a text fragment to be inserted for the [% results %] macro + (optional) # errors - a text fragment to be inserted for the [% errors %] macro +(optional) # # Shows the base HTML page (along with any errors or other messages) sub show_page { my %args = @_; my ($cgi, $results, $errors) = @args{'cgi','results','errors'}; $results = defined($results) ? $results : ''; $errors = defined($errors) ? $errors : ''; my $pajax= CGI::Ajax->new( 'load_ajax_popup_content' => script_url +()); my $ajax_js = $pajax->show_javascript(); my $substitutions = { 'errors' => $errors, 'results' => $results, 'ajax_js' => $ajax_js, 'popups_js' => popups_js(), 'ajax_popup' => ajax_popup(), 'script_name' => script_name(), }; my $output = macro_sub( 'text' => basic_page(), 'subs' => $substit +utions); return $output; } ################################################# # macro_sub(text => $text, subs => { 'key' => value, ... }) # # text - the text template # subs - an anon hash of key/values for substitution # # performs macro substitution on passed text # it looks for strings matching [% macro_name %] # and replaces them with the corresponding hash values from # the anon hash sub macro_sub { my %args = @_; my ($text, $subs) = @args{'text','subs'}; my @sub_keys = sort keys %$subs; my $sub_re = '\[\%\s*' . '(' . join('|', @sub_keys) . ')\s*\%\]' +; $text =~ s/$sub_re/defined($subs->{$1}) ? $subs->{$1} : $1/egs; return $text; } ################################################# # The code used to show and hide the hovering popup # # This javascript chunk performs the actual showing and # hiding of the popup. The AJAX portion # is handled by the Javascript generated # by CGI::Ajax sub popups_js { my $script_name = script_name(); my $js_text =<<"EOT"; <script type="text/javascript" language="Javascript"> //<![CDATA[ /** Functions for supporting a popup text box */ // Tracks whether the popup is live var textPopupInUse = 0; // This is the ID of the block we are going to use for the popup var ajaxTargetId = 'ajax_popup_placeholder_field'; /** showTextPopup( text_id, event, color, element_width ) text_id - the ID of the block we are going to put in the p +opup event - the Javascript event (onmousein in this case) color - the color to be assigned to the enclosing div element_width - the width for the enclosing div */ function showTextPopup( text_id, event, color, element_width ){ var x, y; if( event.x ){ // IE x = event.x + document.body.scrollLeft; y = event.y + document.body.scrollTop; } else { // Mozilla x = event.pageX; y = event.pageY; } var textPopup = document.getElementById( text_id ); var d_style = textPopup.style; d_style.left = x + 5; d_style.top = y + 10; d_style.width = element_width; d_style.wordWrap = "normal"; if (color != '') { d_style.background = color; } d_style.visibility = "visible"; textPopupInUse = 1; } /** closeTextPopup( delay ) delay - number of milliseconds to wait before closing the popup after the close event is requested */ function closeTextPopup(ajaxTargetId, delay ){ textPopupInUse = 0; setTimeout( 'realCloseTextPopup(ajaxTargetId)', delay ); } /** realCloseTextPopup(textPopupId) textPopupId - The ID of the popup block Really closes the popup */ function realCloseTextPopup(textPopupId){ if( textPopupInUse == 1 ) return; document.getElementById( textPopupId ).style.visibility = "hidden" +; } /** show_ajax_popup( event, ajaxParms, backgroundColor, boxWidth) event - the event object ajaxParms - CGI parameters for the AJAX call (['key1__val +ue1','key2__value2', etc]) backgroundColor - A background color for the popup block boxWidth - the width of the popup block */ function show_ajax_popup (event, ajaxParms, backgroundColor, boxWidth +) { load_ajax_popup_content(ajaxParms, ajaxTargetId, 'GET'); showTextPopup(ajaxTargetId, event, backgroundColor, boxWidth); } /** hide_ajax_popup(delay) delay - number of milliseconds to wait before actually closing + the popup block */ function hide_ajax_popup (delay) { closeTextPopup(ajaxTargetId, delay); } //]]> </script> EOT return $js_text; } ################################################# # We got a request we don't know how to handle. sub bad_call { my %args = @_; my ($cgi) = $args{'cgi'}; my $errors =<<"EOT"; <p>Something isn't right, the script was called with an 'action' it d +oes not understand.</p> EOT return show_page(cgi => $cgi, results => '', errors => $errors); } ################################################ # The HTML fragment used for the popups sub ajax_popup { my $ajax_popup_text =<<"EOT"; <div id="ajax_popup_placeholder_field" style='cursor: hand; visibility: hidden; position: absolute; z-index +: 100; text-align: left;'></div> EOT return $ajax_popup_text; } ################################################ # The page template # sub basic_page { my $template =<<"EOT"; Content-Type: text/html; charset=utf-8 <html> <head> <title>Ajaxy example</title> [% ajax_js %] [% popups_js %] </head> <body> <h1>Example Page</h1> [% errors %] [% results %] <ul> <li> <a href="http://nihongo.org/" target="_top" onMouseOver="javascript:show_ajax_popup(event, ['action +__ajax','content__example1'],'#ffcccc','40%');" onMouseOut="javascript:hide_ajax_popup(3000);">Example +1</a> </li> <li> <a href="http://devilbunnies.org/" target="_top" onMouseOver="javascript:show_ajax_popup(event, ['action +__ajax','content__example2'],'#ffcccc','40%');" onMouseOut="javascript:hide_ajax_popup(3000);">Example +2</a> </li> </ul> [% ajax_popup %] </body> </html> EOT } ################################################ # the html ready script name sub script_name { return CGI::Minimal->htmlize($ENV{'SCRIPT_NAME'}); } ################################################## # The complete script URL sub script_url { my $script_name = script_name(); my $script_host = CGI::Minimal->htmlize($ENV{'HTTP_HOST'}); my $script_url = "http://$script_host$script_name"; return $script_name; }

Replies are listed 'Best First'.
Re: AJAX popup windows - an example
by erroneousBollock (Curate) on Sep 16, 2007 at 04:29 UTC
    If you do make it up into a tutorial, perhaps you might focus a bit more on "separation of concerns".

    At the moment it's a little difficult to read the Javascript code. Maybe I'm just biased, but since CGI::Ajax can't do all the Javascript you need, I just don't see the point in using it at all. Perl handles the server side nicely, why not use one of the many excellent Javascript tooklits to handle the client side?

    There are large comment blocks over each Javascript function, perhaps you could move all the Javascript into a separate file? If you use a modern JS/Ajax toolkit such as jQuery (or Dojo or Ext) you're Javascript code will be much shorter and more concise.

    Perhaps your tutorial could deal with issues such as:

    • how to serve standard RPC wire-formats (JSON, SOAP, REST) in Perl,
    • use of unobtrusive, browser-degradable javascript
    • some of the animation effects provided by modern JS/Ajax toolkits to improve the popup/tooltip presentation.

    -David

      Interesting.

      Don't take what I'm about to write as disagreeing with you. It is more about the thought process that went into my choice to use CGI::Ajax for the example. Your points are strong, although possibly not complete: You've validly pointed out that clarity and 'non-intrusiveness' suffers in my example. What you missed is that it traded it for performance.

      I deliberately used CGI::Ajax to reduce the amount of explict Javascript I used for the example. You see, I normally just add the actual Javascript generated by CGI::Ajax as another block of output text - without actually using the CGI::Ajax module. I used the actual CGI::Ajax module here to make a clearer example without the burden of a full fledged toolkit like JQuery.

      Sound backwards?

      Not really. You see, performance and size tend to be my primary goals because I frequently need these kinds of scripts to be used on web pages that both receive many hits per day and that are already overly heavy byte-wise.

      So my goals of "runs fast, loads fast" compromised with the goals of "code clarity, full featured" to settle on CGI::Ajax for the example to get "Ajax clarity, acceptable performance".

      Code clarity would have argued for using one of the major JS toolkits. Performance would have argued for eliminating CGI::Ajax entirely and not using a toolkit library.

      As a data point to illustrate my point on performance, I modified my script to follow my normal practice of inlining the Ajax support Javascript as part of my Perl and benchmarked it for 1000 requests at a concurrency of 20 using the 'ab' Apache benchmark script (with a dry run in each case to allow the server to 'get up to speed'). I also made the minimal modifications required to make run under FastCGI as another comparision point. The tests were run using Apache2 on a hyperthreaded 3Ghz P4 machine running Fedora Core 6 Linux (with a second machine making the HTTP requests).

      CGI ModPerl2 FastCGI Original Example 24/sec 397/sec 328/sec Inlined Ajax JS 69/sec 505/sec 446/sec

      Why such a dramatic slowdown when using CGI::Ajax? Because it pulls in a lot of extra Perl code to generate that chunk of Javascript. CGI::Ajax is about 41K, and it pulls in Exporter (14.4K), Data::Dumper (38K), base (5.4K), overload (46K), vars (2.3K), warnings::register (1K), and Carp (8.8K). For a startling 156K of Perl to generate just under 7K of final Javascript using CGI::Ajax. While the ModPerl2 and FastCGI environments are not nearly as sensitive to the byte count, there is still a noticable runtime overhead to the dynamic code generation of CGI::Ajax.

      Similiar issues arise using the off-the-shelf Ajax libraries. They add dozens to hundreds of Kbytes of stuff needing to be loaded by the web browser (with dramatic performance consequences resulting just from that fact). Additionally, they tend to run slowly above and beyond that (Digg is a classic example - they use an off-the-shelf JS Ajax library that causes my machine to bog completely down on their longer pages. It is quite annoying.)

      So, in addition to your (excellent) suggestion of covering the use of off-the-shelf Ajax JS libraries, I need to cover when-and-why NOT to use them.

        More actually. On my machine:
        qwurx [shmem] ~ > perl -le 'use CGI::Ajax; print "$_ => $INC{$_}" for +sort keys %INC' CGI/Ajax.pm => /usr/lib/perl5/site_perl/5.8.8/CGI/Ajax.pm Carp.pm => /usr/lib/perl5/5.8.8/Carp.pm Class/Accessor.pm => /usr/lib/perl5/site_perl/5.8.8/Class/Accessor.pm Data/Dumper.pm => /usr/lib/perl5/5.8.8/i386-linux-thread-multi/Data/Du +mper.pm Exporter.pm => /usr/lib/perl5/5.8.8/Exporter.pm XSLoader.pm => /usr/lib/perl5/5.8.8/i386-linux-thread-multi/XSLoader.p +m base.pm => /usr/lib/perl5/5.8.8/base.pm bytes.pm => /usr/lib/perl5/5.8.8/bytes.pm overload.pm => /usr/lib/perl5/5.8.8/overload.pm strict.pm => /usr/lib/perl5/5.8.8/strict.pm vars.pm => /usr/lib/perl5/5.8.8/vars.pm warnings.pm => /usr/lib/perl5/5.8.8/warnings.pm warnings/register.pm => /usr/lib/perl5/5.8.8/warnings/register.pm

        Class::Accessor is included out of laziness - not having to code setters/getters for objects.

        As we all know, laziness is one of the virtues of a perl programmer, so that seems OK. But it isn't, because it conflicts with hubris, which beats laziness for Module authors. You don't want anybody to ever say something bad about your module, do you?

        Data::Dumper is a leftover from development. It isn't used anywhere in that module. And that one pulls in:

        qwurx [shmem] ~ > perl -le 'use Data::Dumper; print "$_ => $INC{$_}" f +or sort keys %INC' Carp.pm => /usr/lib/perl5/5.8.8/Carp.pm Data/Dumper.pm => /usr/lib/perl5/5.8.8/i386-linux-thread-multi/Data/Du +mper.pm Exporter.pm => /usr/lib/perl5/5.8.8/Exporter.pm XSLoader.pm => /usr/lib/perl5/5.8.8/i386-linux-thread-multi/XSLoader.p +m bytes.pm => /usr/lib/perl5/5.8.8/bytes.pm overload.pm => /usr/lib/perl5/5.8.8/overload.pm warnings.pm => /usr/lib/perl5/5.8.8/warnings.pm warnings/register.pm => /usr/lib/perl5/5.8.8/warnings/register.pm

        Then there's the ubiquitous 'vars' module, which does

        qwurx [shmem] ~ > perl -le 'use vars qw($foo); print "$_ => $INC{$_}" +for sort keys %INC' Carp.pm => /usr/lib/perl5/5.8.8/Carp.pm Exporter.pm => /usr/lib/perl5/5.8.8/Exporter.pm strict.pm => /usr/lib/perl5/5.8.8/strict.pm vars.pm => /usr/lib/perl5/5.8.8/vars.pm warnings.pm => /usr/lib/perl5/5.8.8/warnings.pm warnings/register.pm => /usr/lib/perl5/5.8.8/warnings/register.pm

        A little bit of (crude *) cleanup to the header of CGI::Ajax

        --- /usr/lib/perl5/site_perl/5.8.8/CGI/Ajax.pm 2007-02-01 00:35:44.00 +0000000 +0100 +++ CGI/Ajax.pm 2007-09-16 16:18:47.000000000 +0200 @@ -1,17 +1,32 @@ package CGI::Ajax; -use strict; -use Data::Dumper; -use base qw(Class::Accessor); -use overload '""' => 'show_javascript'; # for building web pages, +so - # you can just say: print +$pjx BEGIN { - use vars qw ($VERSION @ISA @METHODS); + our ($VERSION, @ISA, @METHODS); @METHODS = qw(url_list coderef_list DEBUG JSDEBUG html js_encode_function cgi_header_extra); - CGI::Ajax->mk_accessors(@METHODS); - + for my $field (@METHODS) { + *{'CGI::Ajax::'.$field} = sub { + my $self = shift @_; + if (@_) { + return $self->set($field, @_); + } else { + return $self->get($field); + } + }; + }; + for (qw(set get)) { + *{'CGI::Ajax::'.$_} = sub { + my $self = shift @_; + if (@_ == 1) { + return $$self{$_[0]}; + } elsif (@_ > 1) { + return $self->{$_[0]} = $_[1]; + } else { + $self->_croak('Wrong number of arguments received'); + } + }; + } $VERSION = .701; }
        and all dependencies are eliminated
        qwurx [shmem] ~ > perl -I. -le 'use CGI::Ajax(); print "$_ => $INC{$_} +" for sort keys %INC' CGI/Ajax.pm => CGI/Ajax.pm

        at the expense of not being able to just print $pjx but having to say print $pjx->show_javascript.

        qwurx [shmem] ~ > perl -le 'use overload "+" => \&add; print "$_ => $ +INC{$_}" for sort keys %INC' Carp.pm => /usr/lib/perl5/5.8.8/Carp.pm Exporter.pm => /usr/lib/perl5/5.8.8/Exporter.pm overload.pm => /usr/lib/perl5/5.8.8/overload.pm warnings.pm => /usr/lib/perl5/5.8.8/warnings.pm warnings/register.pm => /usr/lib/perl5/5.8.8/warnings/register.pm

        Of course the code using CGI::Ajax might pull in Exporter, scrict, warnings, vars et al anyways - but it is not CGI::Ajax's business to do so if it can do without.

        update:

        OTOH, if the code that uses CGI::Ajax uses all those modules anyways, it is better to have them imported once instead of compiling duplicated code. So the cleanest thing would be adding a conditional in a BEGIN block, and pulling in Class::Accessor et cetera only if those namespaces are already present, because the cost of linking in already loaded modules is close to nil.

        But then, this would depend on module loading order... not doing so for this particular module would just add 15 lines of code (which doesn't say much about perl's internal waste thereof... :-)

        *)I just dumped the methods mk_accessors() generates and stuck them into the BEGIN block. Works.

        update: corrected bug in set/get constructor

        --shmem

        _($_=" "x(1<<5)."?\n".q·/)Oo.  G°\        /
                                      /\_¯/(q    /
        ----------------------------  \__(m.====·.(_("always off the crowd"))."·
        ");sub _{s./.($e="'Itrs `mnsgdq Gdbj O`qkdq")=~y/"-y/#-z/;$e.e && print}
        Those are interesting points. Your example was a good, simple one; I hope you only took my concerns as input in the case that you do create a tutorial on the matter. I don't wish to rebut what you have said and don't want to prescribe solutions for anyone else, but perhaps some of the following can provide some interesting counterpoints to some issues raised in this thread.

        Speed.

        The reason that I don't have any speed issues with my HTML user interfaces is that, from perl's perspective, my user interfaces consist entirely of static HTML (which is nicely cached by the web server). perl code doesn't serve that HTML at all (with the exception of some mod_perl-based authentication handlers).

        Also, client-side caching of Javascript, HTML and images (through various mechanisms) has finally come of age in modern browsers; you can count on it not to be a problem. The user waits for say 20-60KB of javascript to come down once and that code is cached thereafter. They see a very attractive progress indicator in the mean-time, and let's face it 60K (jQuery, 4 plugins, and all my Javascript) is small these days.

        Dynamism.

        It may be heretical to some folks here, but I think templating is fine for web content, not so much for exposing web application functionality. The more complicated or multi-faceted a user interface the more convoluted templates become, often with the outcome that the templates don't serve their initial goals - to uncouple viewables from code (for clarity), and to make visual assets separately maintainable.

        I still like the idea of using templates to frame the overall look and feel of a site.

        Functionality.

        Nowadays, it's possible to have fast, semantically concise, intuitive, attractive user interfaces provided in a cross-platform manner in a variety of technologies.

        Amazing cross-platform Javascript toolkits and other technologies (such as Adobe Flex and Microsoft Silverlight) exists which can provide consistent, performant, lightweight user interfaces for web applications.

        None of those technologies requires the authors of client-side user interfaces to move to completely homogeneous implementations; the technologies can be separately embedded in HTML and combined to interesting effect.

        -David

        Updated: added a bit more detail in to clarify some points.

      Maybe I'm just biased, but since CGI::Ajax can't do all the Javascript you need, I just don't see the point in using it at all.

      No module ever claimed to Do It All For You, except perhaps some module from the Acme namespace.

      CGI::Ajax does a specific task, and it does it pretty damn well and in a KISS way. Of course it is not good-for-all and constrains you in e.g. always passing a parameter fname for the perl function callback in the XHTTP query, but then fully-fledged Ajax Toolkits aren't any different: they put you on rails.

      CGI::Ajax is a very good example of a 'module at its best': it solves a particular problem without getting too over-featured and provides a nice wheel that hasn't evolved into a monster truck. <update> Although it has some issues. See my follow-up below. </update>

      --shmem

      _($_=" "x(1<<5)."?\n".q·/)Oo.  G°\        /
                                    /\_¯/(q    /
      ----------------------------  \__(m.====·.(_("always off the crowd"))."·
      ");sub _{s./.($e="'Itrs `mnsgdq Gdbj O`qkdq")=~y/"-y/#-z/;$e.e && print}

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlmeditation [id://639211]
Approved by almut
Front-paged by shmem
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others sharing their wisdom with the Monastery: (3)
As of 2024-04-18 22:49 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found