001package votorola.s.wic.count; // Copyright 2008-2010, 2012-2013, Michael Allan.  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Votorola Software"), to deal in the Votorola Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Votorola Software, and to permit persons to whom the Votorola Software is furnished to do so, subject to the following conditions: The preceding copyright notice and this permission notice shall be included in all copies or substantial portions of the Votorola Software. THE VOTOROLA SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE VOTOROLA SOFTWARE OR THE USE OR OTHER DEALINGS IN THE VOTOROLA SOFTWARE.
002
003import java.io.*;
004import java.sql.SQLException;
005import java.util.*;
006import javax.xml.stream.XMLStreamException;
007import org.apache.wicket.*;
008import org.apache.wicket.markup.html.basic.*;
009import org.apache.wicket.markup.html.link.*;
010import org.apache.wicket.markup.html.panel.*;
011import org.apache.wicket.markup.repeater.Item;
012import org.apache.wicket.markup.repeater.data.*;
013import org.apache.wicket.model.*;
014import org.apache.wicket.request.mapper.parameter.PageParameters;
015import votorola.a.*;
016import votorola.a.count.*;
017import votorola.a.voter.*;
018import votorola.a.web.wic.*;
019import votorola.g.lang.*;
020import votorola.g.locale.*;
021import votorola.g.web.wic.*;
022
023import static votorola.a.voter.IDPair.NOBODY;
024
025
026/** A view of the tallied results for a poll.  A live {@linkplain
027  * votorola.s.gwt.stage.StageV stage view} of Crossforum Theatre tops the page.  The bulk
028  * of the page is static HTML, including a summary of turnout and a ranked list of
029  * candidates. For example:
030  *
031  * <blockquote><code><a href='http://reluk.ca:8080/v/w/Rank?8&amp;p=G!p!sandbox' target='_top'>http://reluk.ca:8080/v/w/Rank?8&amp;p=G!p!sandbox</a></code></blockquote>
032  *
033  * <p>The particular poll is specified by query parameter 'p'. Query parameters for this
034  * page are:</p>
035  *
036  * <table class='definition' style='margin-left:1em'>
037  *     <tr>
038  *         <th class='key'>Key</th>
039  *         <th>Value</th>
040  *         <th>Default</th>
041  *         </tr>
042  *     <tr><td class='key'>p</td>
043  *
044  *         <td>The name of the <a href='http://reluk.ca/w/Category:Poll' target='_top'>poll</a>.  Slash characters (/) are technically not allowed here
045  *         and may therefore be encoded as exclamation marks (!).</td>
046  *
047  *         <td>Null, resulting in a 303 (see other) redirect that fills in the name of
048  *         the {@linkplain Poll#TEST_POLL_NAME test poll}.</td>
049  *
050  *         </tr>
051  *     <tr><td class='key'>u</td>
052  *
053  *         <td>The {@linkplain IDPair#username() username} of a candidate.  The list of
054  *         candidates is paged to ensure that the specified candidate is visible.  If the
055  *         candidate is not listed, the list is paged to the very end.  Incompatible with
056  *         parameter 'u'; specify one or the other.</td>
057  *
058  *         <td>Null, specifying no particular candidate.</td>
059  *
060  *         </tr>
061  *     <tr><td class='key'>v</td>
062  *
063  *         <td>The {@linkplain IDPair#email() email address} of a candidate.  The list of
064  *         candidates is paged to ensure that the specified candidate is visible.  If the
065  *         candidate is not listed, the list is paged to the very end.  Incompatible with
066  *         parameter 'u'; specify one or the other.</td>
067  *
068  *         <td>Null, specifying no particular candidate.</td>
069  *
070  *         </tr>
071  *     </table>
072  *
073  *     @see <a href='../../../../../../s/wic/count/WP_Rank.html' target='_top'>WP_Rank.html</a>
074  */
075  @ThreadRestricted("wicket")
076public final class WP_Rank extends VPageHTML implements TabbedPage, VoterPage
077{
078
079
080    /** Constructs a WP_Rank.
081      */
082    public WP_Rank( final PageParameters pP ) // bookmarkable page iff constructor public & (default|PageParameter)
083    {
084        super( pP );
085        final VRequestCycle cycle = VRequestCycle.get();
086        final String pollName = Poll.U.toName( WP_Poll.maybeRedirect_P( WP_Rank.class, pP, cycle ));
087
088        pFet = new WP_Poll.PollFetcher( pollName, cycle );
089        final VSession session = VSession.get();
090        session.scopePoll().setLastName( pollName );
091        voterIDPair = VoterPage.U.idPairOrNobodyFor( pP );
092        session.scopeVoterPage().setLastIDPair( voterIDPair );
093
094      // Write glue for the GWT stage module of Crossforum Theatre
095      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
096        final VoteServer vS = VOWicket.get().vsRun().voteServer();
097        {
098            final StringBuilder b = WC_Stage.appendLeader( session.user(), vS, cycle );
099
100          // s_gwt_stage_Stage_init, per WC_Stage below
101          // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
102            b.append(
103               "voGWTConfig.s_gwt_stage_Stage_init = function()"
104             + "{" );
105            if( !NOBODY.equals( voterIDPair ))
106            {
107                b.append(
108                  "s_gwt_stage_Stage_setActorName( '" );
109                    // rather than setDefaultActorName, per WP_Votespace
110                b.append( voterIDPair.username() ).append( "' );" );
111            }
112            b.append(
113                  "s_gwt_stage_Stage_setDefaultPollName( '" );
114            b.append( pollName ).append( "' );"
115             + "};" );
116
117          // ` ` `
118            add( new WC_Stage( "stage", "votorola.s.gwt.wic.CountIn", b, cycle ));
119        }
120
121      // Render view
122      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
123        add( new WC_NavigationHead( "navHead", WP_Rank.this, cycle ));
124        add( new WC_WGLogo( "wgLogo", pFet.poll().wgLogoImageLocation(),
125          pFet.poll().wgLogoLinkTarget(), cycle ));
126        {
127            final String mapPageName = pFet.poll().divisionSmallMapPageName();
128            add( mapPageName == null? newNullComponent( "divisionSmallMap" ):
129              new WC_DivisionSmallMap( "divisionSmallMap", pFet.poll().divisionPageName(),
130                mapPageName, cycle ));
131        }
132        add( new WC_NavPile( "navPile", navTab(cycle), cycle ));
133        init_content( vS, cycle );
134    }
135
136
137
138    private void init_content( final VoteServer vS, final VRequestCycle cycle )
139    {
140        final BundleFormatter bunW = cycle.bunW();
141
142      // Title
143      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
144        final Model<String> titleModel = new Model<String>( bunW.l( "s.wic.count.WP_Rank.title" ));
145        add( new Label( "title", titleModel ));
146        if( pFet.poll() == null )
147        {
148            assert false: "poll is never null"; // FIX clean up
149            add( newNullComponent( "content" ));
150            return;
151        }
152
153        titleModel.setObject( titleModel.getObject() + " - " + pFet.poll().name() );
154
155        final Fragment content = new Fragment( "content", "singlefrag-content", WP_Rank.this );
156        add( content );
157
158        content.add( new Label( "hName", pFet.poll().name() ));
159        {
160            final String displayTitle = pFet.poll().displayTitle();
161            if( displayTitle == null ) content.add( newNullComponent( "hDisplayTitle" ));
162            else content.add( new Label( "hDisplayTitle", ": " + displayTitle ));
163        }
164
165      // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
166        final BundleFormatter bunA = cycle.bunA();
167        final Fragment main;
168        if( pFet.countToReport() == null )
169        {
170            main = new Fragment( "main-content", "frag-main-content-no-count", WP_Rank.this );
171            main.add( new Label( "explanation", bunA.l( "a.count.noResultsToReport" )));
172            content.add( main );
173            return;
174        }
175
176        main = new Fragment( "main-content", "singlefrag-main-content-normal", WP_Rank.this );
177        content.add( main );
178
179      // Summary table
180      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
181        {
182            final long populationSize = pFet.poll().populationSize();
183            main.add( new Label( "populationSize-index",
184              bunA.l( "a.count.Count.summaryData.populationSize-index" )));
185            main.add( new Label( "populationSize-datum", populationSize > 0?
186              bunA.format( "%,d", populationSize ):
187              bunA.l( "a.quantityUnknown" )));
188            main.add( new Label( "populationSize-note",
189              pFet.poll().populationSizeExplanation() ));
190
191            main.add( new Label( "castVolume-index",
192              bunA.l( "a.count.Count.summaryData.singleCastCount-index" )));
193            main.add( new Label( "castVolume-datum",
194              bunA.format( "%,d", pFet.countToReport().castVolume() )));
195            main.add( new Label( "castVolume-note",
196              bunA.l( "a.count.Count.summaryData.singleCastCount-note" )));
197
198            main.add( new Label( "turnoutPercent-index",
199              bunA.l( "a.count.Count.summaryData.turnoutPercent-index" )));
200            main.add( new Label( "turnoutPercent-datum", populationSize > 0?
201              bunA.format( "%.3f",
202                pFet.countToReport().castVolume() * 100d / populationSize ):
203              bunA.l( "a.quantityUnknown" )));
204            main.add( new Label( "turnoutPercent-note",
205              bunA.l( "a.count.Count.summaryData.turnoutPercent-note" )));
206        }
207
208      // Rank table
209      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
210        main.add( new Label( "rankHeader-rank",
211          bunA.l( "a.count.Count.rankHeader-rank" )));
212        main.add( new Label( "rankHeader-voterEmail",
213          bunA.l( "a.count.Count.rankHeader-voterEmail" )));
214        main.add( new Label( "rankHeader-receiveVolume",
215          bunA.l( "a.count.Count.rankHeader-receiveCount" )));
216        main.add( new Label( "rankHeader-receivePercent",
217          bunA.l( "a.count.Count.rankHeader-receivePercent" )));
218        main.add( new Label( "note-receive", bunA.l( "a.count.Count.note-receive",
219          pFet.countToReport().castVolume() )));
220
221        final RankDataProvider dataProvider = new RankDataProvider();
222        final int preScrollIndex; // -1 for none; -2 for none, and scroll to last page
223        try
224        {
225            int index = -1; // till proven otherwise
226            if( !NOBODY.equalsEmail( voterIDPair ))
227            {
228                index = -2; // till proven otherwise
229                final CountNodeW node = pFet.countToReport().countTablePV().get(
230                  voterIDPair.email() );
231                if( node != null && node.getRankIndex() < dataProvider.size() )
232                {
233                    assert MAX_RANK_INDEX <= (long)Integer.MAX_VALUE: "safe to convert rank index to int";
234                    index = (int)node.getRankIndex();
235                }
236                titleModel.setObject( titleModel.getObject() + "/" +  voterIDPair.username() );
237            }
238            preScrollIndex = index;
239        }
240        catch( SQLException|XMLStreamException x ) { throw new RuntimeException( x ); }
241
242        final DataView<CountNodeW> dataView = new DataView<CountNodeW>( "rankData", dataProvider )
243        {
244            public @Override void populateItem( final Item<CountNodeW> item )
245            {
246                final BundleFormatter bunA = VRequestCycle.get().bunA();
247                final CountNodeW node = item.getModelObject();
248                item.add( new Label( "rankData-rank",
249                  bunA.format( "%,d", node.getRank() )));
250
251                final PageParameters pP = new PageParameters( getPageParameters() );
252                pP.remove( "v" );
253                final String nodeEmail = node.email();
254                final String nodeUsername = node.person().username();
255                if( nodeEmail.equals( voterIDPair.email() )) // already selected
256                {
257                    pP.remove( "u" ); // de-select
258                    appendStyleClass( item, "voterHighlight" );
259                }
260                else pP.set( "u", nodeUsername ); // select
261
262                final BookmarkablePageLinkX td = new BookmarkablePageLinkX( // JavaScript link
263                  "voterEmail", WP_Rank.class, pP );
264                item.add( td );
265
266                final BookmarkablePageLinkX link = new BookmarkablePageLinkX( // ordinary link, nested in JavaScript link
267                  "a", WP_Rank.class, pP );
268                link.setBody( nodeUsername );
269                td.add( link );
270
271                item.add( new Label( "rankData-receiveVolume",
272                  bunA.format( "%,d", node.receiveVolume() )));
273                item.add( new Label( "rankData-receivePercent", bunA.format( "%.2f",
274                  node.receiveVolume() * 100d / pFet.countToReport().castVolume() )));
275            }
276        };
277        main.add( dataView );
278
279        final int itemsPerPage = 10;
280        dataView.setItemsPerPage( itemsPerPage );
281        if( preScrollIndex == -2 )
282        {
283            final int nP = dataView.getPageCount();
284            if( nP > 0 ) dataView.setCurrentPage( nP - 1 );
285        }
286        else if( preScrollIndex > 0 ) dataView.setCurrentPage( preScrollIndex / itemsPerPage );
287
288        main.add( new Label( "rank-pageNav-prefix", bunW.l( "a.pageNav-prefix" )));
289        main.add( newPagingNavigator( "rank-pageNav", dataView ));
290        main.add( new Label( "rank-pageNav-suffix", bunW.l(
291          "a.pageNav-suffix", dataView.getPageCount() )));
292
293      // Count identifier
294      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
295        final ReadyDirectory ready = pFet.countToReport().readyDirectory();
296        final File snap = ready.snapDirectory();
297        main.add( new ExternalLink( "countID",
298          /*href*/vS.votorolaURI().toASCIIString() + "/out/vocount/" + snap.getName() + "/"
299            + ready.getName() + "/",
300          /*body*/bunA.l( "a.OutputStore.setNominalDate",
301            OutputStore.setNominalDate(new GregorianCalendar(),snap) )));
302    }
303
304
305
306   // ------------------------------------------------------------------------------------
307
308
309    /** The maximum index viewable in the table of ranked candidates.
310      */
311    public static final int MAX_RANK_INDEX = Integer.MAX_VALUE;
312
313
314
315   // - T a b b e d - P a g e ------------------------------------------------------------
316
317
318    /** @see #NAV_TAB
319      */
320    public NavTab navTab( VRequestCycle cycle ) { return NAV_TAB; }
321
322
323
324    /** The navigation tab that fetches the ranking page, an instance of WP_Rank.
325      */
326    static final NavTab NAV_TAB = new WP_Votespace.VotespaceTab( WP_Rank.class )
327    {
328        public @Override String shortTitle( VRequestCycle cycle )
329        {
330            return cycle.bunW().l( "s.wic.count.WP_Rank.tab.shortTitle" );
331        }
332    };
333
334
335
336   // - V o t e r - P a g e --------------------------------------------------------------
337
338
339    public String voterEmail() { return voterIDPair.email(); }
340
341
342
343    public IDPair voterIDPair() { return voterIDPair; }
344
345
346        private final IDPair voterIDPair;
347
348
349
350    public String voterUsername() { return voterIDPair.username(); }
351
352
353
354//// P r i v a t e ///////////////////////////////////////////////////////////////////////
355
356
357    private final WP_Poll.PollFetcher pFet;
358
359
360
361   // ====================================================================================
362
363
364    private final class RankDataProvider implements IDataProvider<CountNodeW>
365    {
366
367
368       // - I - D a t a - P r o v i d e r ------------------------------------------------
369
370
371        public Iterator<CountNodeW> iterator( final int first, final int count )
372        {
373            try
374            {
375                return pFet.countToReport().countTablePV().listByRankIndeces(
376                  first, first + count ).iterator();
377            }
378            catch( SQLException|XMLStreamException x ) { throw new RuntimeException( x ); }
379        }
380
381
382        public IModel<CountNodeW> model( final CountNodeW node )
383        {
384            return new Model<CountNodeW>( node );
385        }
386
387
388        public int size() { return size; }
389
390
391            private final int size;
392            {
393                final long s = pFet.countToReport().candidateCount();
394             // if( s > MAX_RANK_INDEX ) throw new VotorolaRuntimeException( "too many voters" );
395             /// no harm in silently displaying a trucated view
396                size = s > MAX_RANK_INDEX? MAX_RANK_INDEX: (int)s;
397            }
398
399
400       // - I - D e t a c h a b l e ------------------------------------------------------
401
402
403        /** Does nothing.
404          */
405        public void detach() {}
406
407
408    }
409
410
411
412}