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&p=G!p!sandbox' target='_top'>http://reluk.ca:8080/v/w/Rank?8&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}