<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[6662] sites/trunk/wordcamp.org/public_html/wp-content/plugins: WordCamp Reports: Initial Commit.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="http://meta.trac.wordpress.org/changeset/6662">6662</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"http://meta.trac.wordpress.org/changeset/6662","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>iandunn</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2018-02-16 16:49:05 +0000 (Fri, 16 Feb 2018)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>WordCamp Reports: Initial Commit.

Merged from https://github.com/coreymckrill/wordcamp-reports/ @ `594f180`.

Props coreymckrill</pre>

<h3>Added Paths</h3>
<ul>
<li>sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/</li>
<li>sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/assets/</li>
<li>sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/assets/css/</li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsassetscssadmincommoncss">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/assets/css/admin-common.css</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsassetscsswordcampstatuscss">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/assets/css/wordcamp-status.css</a></li>
<li>sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/assets/js/</li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsassetsjswordcampstatusjs">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/assets/js/wordcamp-status.js</a></li>
<li>sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/classes/</li>
<li>sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/classes/report/</li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsclassesreportclassbasephp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/classes/report/class-base.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsclassesreportclassdaterangephp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/classes/report/class-date-range.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsclassesreportclassmeetupgroupsphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/classes/report/class-meetup-groups.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsclassesreportclasspaymentactivityphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/classes/report/class-payment-activity.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsclassesreportclasssponsorinvoicesphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/classes/report/class-sponsor-invoices.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsclassesreportclasssponsorshipgrantsphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/classes/report/class-sponsorship-grants.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsclassesreportclassticketrevenuephp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/classes/report/class-ticket-revenue.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsclassesreportclasswordcampstatusphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/classes/report/class-wordcamp-status.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsindexphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/index.php</a></li>
<li>sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/</li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewsadminphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/admin.php</a></li>
<li>sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/html/</li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewshtmlmeetupgroupsphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/html/meetup-groups.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewshtmlpaymentactivityphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/html/payment-activity.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewshtmlsponsorinvoicesphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/html/sponsor-invoices.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewshtmlsponsorshipgrantsphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/html/sponsorship-grants.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewshtmlticketrevenuephp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/html/ticket-revenue.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewshtmlwordcampstatusphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/html/wordcamp-status.php</a></li>
<li>sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/public/</li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewspublicmeetupgroupsphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/public/meetup-groups.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewspublicpaymentactivityphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/public/payment-activity.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewspublicsponsorinvoicesphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/public/sponsor-invoices.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewspublicsponsorshipgrantsphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/public/sponsorship-grants.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewspublicticketrevenuephp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/public/ticket-revenue.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewspublicwordcampstatusphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/public/wordcamp-status.php</a></li>
<li>sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/report/</li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewsreportmeetupgroupsphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/report/meetup-groups.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewsreportpaymentactivityphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/report/payment-activity.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewsreportsponsorinvoicesphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/report/sponsor-invoices.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewsreportsponsorshipgrantsphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/report/sponsorship-grants.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewsreportticketrevenuephp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/report/ticket-revenue.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewsreportwordcampstatusphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/report/wordcamp-status.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsassetscssadmincommoncss"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/assets/css/admin-common.css</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/assets/css/admin-common.css                              (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/assets/css/admin-common.css        2018-02-16 16:49:05 UTC (rev 6662)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,13 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+/** Tables **/
+.widefat.but-not-too-wide {
+       width: auto;
+}
+
+td.number {
+       text-align: right;
+}
+
+td.total {
+       font-weight: bold;
+}
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of file
</span></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsassetscsswordcampstatuscss"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/assets/css/wordcamp-status.css</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/assets/css/wordcamp-status.css                           (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/assets/css/wordcamp-status.css     2018-02-16 16:49:05 UTC (rev 6662)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,12 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+.status-log-toggle {
+       background: transparent;
+       border: 0;
+       cursor: pointer;
+       display: inline-block;
+       vertical-align: middle;
+}
+
+#status-log-bulk-bar .status-log-bulk-toggle {
+       margin-right: 5px;
+}
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsassetsjswordcampstatusjs"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/assets/js/wordcamp-status.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/assets/js/wordcamp-status.js                             (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/assets/js/wordcamp-status.js       2018-02-16 16:49:05 UTC (rev 6662)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,156 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+( function( window, $ ) {
+
+       'use strict';
+
+       var WordCampStatus = window.WordCampStatus || {};
+
+       $.extend( WordCampStatus, {
+               /**
+                * Initialize the script.
+                */
+               init: function() {
+                       var self = this;
+
+                       this.cache = {
+                               $logs: $( '.status-log' ),
+                               toggles: []
+                       };
+
+                       this.setupSingleToggles();
+                       this.setupBulkToggles();
+
+                       $( document ).ready( function() {
+                               self.cache.$hideAll.trigger( 'click' );
+                       } );
+               },
+
+               /**
+                * Set up status log toggles for individual camps.
+                */
+               setupSingleToggles: function() {
+                       var self = this,
+                               $logs = this.cache.$logs;
+
+                       if ( $logs.length ) {
+                               $logs.each( function() {
+                                       var $button = $( '<button>' )
+                                                       .addClass( 'report-button status-log-toggle' )
+                                                       .data( {
+                                                               status: 'visible',
+                                                               showLabel: 'Show Details',
+                                                               hideLabel: 'Hide details'
+                                                       } )
+                                                       .on( 'click', self.toggleDetails ),
+                                               $icon = $( '<span>' )
+                                                       .addClass( 'status-log-toggle-icon dashicons dashicons-arrow-down' )
+                                                       .attr( 'aria-hidden', true ),
+                                               $label = $( '<span>' )
+                                                       .addClass( 'status-log-toggle-label screen-reader-text' )
+                                                       .text( $button.data( 'hideLabel' ) )
+                                       ;
+
+                                       $button
+                                               .append( $icon )
+                                               .append( $label )
+                                               .appendTo( $( this ).prev( 'p' ) )
+                                       ;
+
+                                       self.cache.toggles.push( $button );
+                               } );
+                       }
+               },
+
+               /**
+                * Toggle the visibility of a status log.
+                *
+                * @param e
+                */
+               toggleDetails: function( e ) {
+                       e.preventDefault();
+
+                       var $button = $( this ),
+                               $icon = $button.find( '.status-log-toggle-icon' ),
+                               $label = $button.find( '.status-log-toggle-label' ),
+                               $log = $button.parent().next( '.status-log' );
+
+                       if ( $log.is( ':visible' ) ) {
+                               // Currently visible. Hide.
+                               $log.hide();
+                               $icon.removeClass( 'dashicons-arrow-up' ).addClass( 'dashicons-arrow-down' );
+                               $label.text( $button.data( 'showLabel' ) );
+                               $button.data( 'status', 'hidden' );
+                       } else {
+                               // Currently hidden. Show.
+                               $log.show();
+                               $icon.removeClass( 'dashicons-arrow-down' ).addClass( 'dashicons-arrow-up' );
+                               $label.text( $button.data( 'hideLabel' ) );
+                               $button.data( 'status', 'visible' );
+                       }
+               },
+
+               /**
+                * Set up buttons to toggle every status log at once.
+                */
+               setupBulkToggles: function () {
+                       var self = this,
+                               $activeheading = $( '#active-heading' ),
+                               $bar;
+
+                       if ( $activeheading.length ) {
+                               $bar = $( '<div>' )
+                                       .attr( 'id', 'status-log-bulk-bar' )
+                                       .addClass( 'report-results-control-bar' )
+                                       .insertAfter( $activeheading )
+                               ;
+
+                               self.cache.$showAll = $( '<button>' )
+                                       .addClass( 'button report-button status-log-bulk-toggle-show' )
+                                       .text( 'Show all details' )
+                                       .on( 'click', self.showAll )
+                                       .appendTo( $bar )
+                               ;
+
+                               self.cache.$hideAll = $( '<button>' )
+                                       .addClass( 'button report-button status-log-bulk-toggle-hide' )
+                                       .text( 'Hide all details' )
+                                       .on( 'click', self.hideAll )
+                                       .appendTo( $bar )
+                               ;
+                       }
+               },
+
+               /**
+                * Show all status logs.
+                *
+                * @param e
+                */
+               showAll: function( e ) {
+                       e.preventDefault();
+
+                       $.each( WordCampStatus.cache.toggles, function( index, $toggle ) {
+                               if ( 'hidden' === $toggle.data( 'status' ) ) {
+                                       $toggle.trigger( 'click' );
+                               }
+                       } );
+               },
+
+               /**
+                * Hide all status logs.
+                *
+                * @param e
+                */
+               hideAll: function( e ) {
+                       e.preventDefault();
+
+                       $.each( WordCampStatus.cache.toggles, function( index, $toggle ) {
+                               if ( 'visible' === $toggle.data( 'status' ) ) {
+                                       $toggle.trigger( 'click' );
+                               }
+                       } );
+               }
+       } );
+
+       WordCampStatus.init();
+
+} )( window, jQuery );
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsclassesreportclassbasephp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/classes/report/class-base.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/classes/report/class-base.php                            (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/classes/report/class-base.php      2018-02-16 16:49:05 UTC (rev 6662)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,306 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * @package WordCamp\Reports
+ */
+
+namespace WordCamp\Reports\Report;
+use WP_Post;
+
+defined( 'WPINC' ) || die();
+
+/**
+ * Class Base
+ *
+ * A base report class with methods for filtering and caching data, and handling errors, plus some other helper methods.
+ *
+ * Things to keep in mind when writing a new report
+ *  - Currently all reports extend `Date_Range` range than extending this directly. If your new report isn't based
+ *    on pulling data between a date range, then you might want to write a new abstract class that's similar to
+ *    `Date_Range`, or just extend this directly, depending on the likelihood of that new abstract class being
+ *    reused in the future.
+ *  - Most reports are displayed to both public and private audiences, so be careful that private data is never
+ *    exposed to the public. See `$private_data_fields`, `$public_data_fields`, and `filter_data_fields()`.
+ *  - You'll probably need to update https://central.wordcamp.org/reports/ with a link to the new reports. In the
+ *    future, this could be automated.
+ *  - `get_data()` typically returns the raw data (like for a CSV export), while `compile_report_data()` creates
+ *    user-facing summaries and tables based on the raw data.
+ *  - In wp-admin, the UI is generated by combining `views/report` with `views/html`; the former is the header and
+ *    wrapper markup, while the latter is the markup for the actual data.
+ *  - On the front end, the public reports are rendered by combining `views/public` with `views/html`.
+ *  - REST API endpoints are created by some of the classes, but not actually used by this plugin yet.
+ *
+ * @package WordCamp\Reports\Report
+ */
+abstract class Base {
+       /**
+        * Report name.
+        *
+        * @var string
+        */
+       public static $name = '';
+
+       /**
+        * Report slug.
+        *
+        * @var string
+        */
+       public static $slug = '';
+
+       /**
+        * Report description.
+        *
+        * @var string
+        */
+       public static $description = '';
+
+       /**
+        * Report methodology.
+        *
+        * @var string
+        */
+       public static $methodology = '';
+
+       /**
+        * Report group.
+        *
+        * @var string
+        */
+       public static $group = '';
+
+       /**
+        * Additional report parameters.
+        *
+        * @var array
+        */
+       public $options = array();
+
+       /**
+        * A container object to hold error messages.
+        *
+        * @var \WP_Error
+        */
+       public $error = null;
+
+       /**
+        * Data fields that can be visible in a public context.
+        *
+        * @var array An associative array of key/default value pairs.
+        */
+       protected $public_data_fields = array();
+
+       /**
+        * Data fields that should only be visible in a private context.
+        *
+        * @var array An associative array of key/default value pairs.
+        */
+       protected $private_data_fields = array();
+
+       /**
+        * Base constructor.
+        *
+        * @param array $options    {
+        *     Optional. Additional report parameters.
+        *
+        *     @type bool $cache_data  True to look for cached data and cache the generated data set. Default true.
+        *     @type bool $flush_cache True to delete any cached data generated with the current report parameters. Default false.
+        *     @type bool $public      True if the report data is for public consumption. Reports can use this value to determine
+        *                             whether to redact or remove some fields if necessary. Default true.
+        * }
+        */
+       public function __construct( array $options = array() ) {
+               $this->options = wp_parse_args( $options, array(
+                       'cache_data'  => true,
+                       'flush_cache' => false,
+                       'public'      => true,
+               ) );
+
+               $this->error = new \WP_Error();
+       }
+
+       /**
+        * Query and parse the data for the report.
+        *
+        * @return array
+        */
+       public abstract function get_data();
+
+       /**
+        * Compile the report data into results.
+        *
+        * @param array $data The data to compile.
+        *
+        * @return array
+        */
+       public abstract function compile_report_data( array $data );
+
+       /**
+        * Filter the report data prior to caching and compiling.
+        *
+        * @param array $data The data to filter.
+        *
+        * @return array
+        */
+       protected function filter_data_fields( array $data ) {
+               $safelist = $this->public_data_fields;
+
+               if ( false === $this->options['public'] ) {
+                       $safelist = array_merge( $safelist, $this->private_data_fields );
+               }
+
+               array_walk( $data, function ( &$row ) use ( $safelist ) {
+                       $row = shortcode_atts( $safelist, $row );
+               } );
+
+               return $data;
+       }
+
+       /**
+        * Generate a cache key.
+        *
+        * @return string
+        */
+       protected function get_cache_key() {
+               $context   = ( false === $this->options['public'] ) ? '_private' : '_public';
+               $cache_key = 'report_' . static::$slug . $context;
+
+               return $cache_key;
+       }
+
+       /**
+        * Generate a cache expiration interval.
+        *
+        * @return int A time interval in seconds.
+        */
+       protected function get_cache_expiration() {
+               return WEEK_IN_SECONDS;
+       }
+
+       /**
+        * If this instance has caching enabled, retrieve cached data.
+        *
+        * @return mixed|null Null if caching is disabled. Otherwise a cached value, or false if none is available.
+        */
+       protected function maybe_get_cached_data() {
+               if ( true === $this->options['flush_cache'] ) {
+                       $this->flush_cache();
+                       return false;
+               } elseif ( false !== $this->options['cache_data'] ) {
+                       return get_transient( $this->get_cache_key() );
+               }
+
+               return null;
+       }
+
+       /**
+        * If this instance has caching enabled, cache the supplied data.
+        *
+        * @param mixed $data The data to cache.
+        *
+        * @return bool True if the data was successfully cached. Otherwise false.
+        */
+       protected function maybe_cache_data( $data ) {
+               if ( false !== $this->options['cache_data'] ) {
+                       $cache_key  = $this->get_cache_key();
+                       $expiration = $this->get_cache_expiration();
+
+                       return set_transient( $cache_key, $data, $expiration );
+               }
+
+               return false;
+       }
+
+       /**
+        * Delete the cached data for this report instance, if it exists.
+        *
+        * @return bool
+        */
+       protected function flush_cache() {
+               return delete_transient( $this->get_cache_key() );
+       }
+
+       /**
+        * Merge two error objects into one, new error object.
+        *
+        * @param \WP_Error $error1 An error object.
+        * @param \WP_Error $error2 An error object.
+        *
+        * @return \WP_Error The combined errors of the two parameters.
+        */
+       protected function merge_errors( \WP_Error $error1, \WP_Error $error2 ) {
+               $codes = $error2->get_error_codes();
+
+               foreach ( $codes as $code ) {
+                       $messages = $error2->get_error_messages( $code );
+
+                       foreach ( $messages as $message ) {
+                               $error1->add( $code, $message );
+                       }
+               }
+
+               return $error1;
+       }
+
+       /**
+        * Validate a given WordCamp post ID.
+        *
+        * @param int $wordcamp_id The ID of a WordCamp post.
+        *
+        * @return bool True if the WordCamp ID is valid. Otherwise false.
+        */
+       protected function validate_wordcamp_id( $wordcamp_id ) {
+               $wordcamp = get_post( $wordcamp_id );
+
+               if ( ! $wordcamp instanceof WP_Post || WCPT_POST_TYPE_ID !== get_post_type( $wordcamp ) ) {
+                       $this->error->add( 'invalid_wordcamp_id', 'Please enter a valid WordCamp ID.' );
+
+                       return false;
+               }
+
+               $wordcamp_site_id = get_wordcamp_site_id( $wordcamp );
+
+               if ( ! $wordcamp_site_id ) {
+                       $this->error->add( 'wordcamp_without_site', 'The specified WordCamp does not have a site yet.' );
+
+                       return false;
+               }
+
+               return true;
+       }
+
+       /**
+        * Render an HTML notice containing error messages.
+        *
+        * @return void
+        */
+       protected function render_error_html() {
+               ?>
+               <div class="notice notice-error">
+                       <?php foreach ( $this->error->get_error_messages() as $message ) : ?>
+                               <?php echo wpautop( wp_kses_post( $message ) ); ?>
+                       <?php endforeach; ?>
+               </div>
+               <?php
+       }
+
+       /**
+        * Prepare report data for a REST response.
+        *
+        * This takes an arbitrary data value and wraps it in a WP REST Response object along with additional
+        * information about the report.
+        *
+        * @param mixed $data                       The data that will go in the `data` parameter of the response.
+        * @param array $additional_response_params Additional top-level parameters to add to the response.
+        *
+        * @return \WP_REST_Response
+        */
+       protected static function prepare_rest_response( $data, array $additional_response_params = array() ) {
+               $response_data = array_merge( array(
+                       'report_name'        => static::$name,
+                       'report_description' => static::$description,
+               ), $additional_response_params );
+
+               $response_data['data'] = $data;
+
+               return new \WP_REST_Response( $response_data );
+       }
+}
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsclassesreportclassdaterangephp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/classes/report/class-date-range.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/classes/report/class-date-range.php                              (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/classes/report/class-date-range.php        2018-02-16 16:49:05 UTC (rev 6662)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,274 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * @package WordCamp\Reports
+ */
+
+namespace WordCamp\Reports\Report;
+defined( 'WPINC' ) || die();
+
+/**
+ * Class Date_Range
+ *
+ * A base report class designed to generate a data set based on a specified date range. See `Base` for some developer notes.
+ *
+ * @package WordCamp\Reports\Report
+ */
+abstract class Date_Range extends Base {
+       /**
+        * The start of the date range for the report.
+        *
+        * @var \DateTime|null
+        */
+       public $start_date = null;
+
+       /**
+        * The end of the date range for the report.
+        *
+        * @var \DateTime|null
+        */
+       public $end_date = null;
+
+       /**
+        * Date_Range constructor.
+        *
+        * @param string $start_date The start of the date range for the report.
+        * @param string $end_date   The end of the date range for the report.
+        * @param array  $options    {
+        *     Optional. Additional report parameters.
+        *     See Base::__construct for additional parameters.
+        *
+        *     @type \DateTime     $earliest_start The earliest date that can be used for the start of the date range.
+        *     @type \DateInterval $max_interval   The max interval of time between the start and end dates.
+        * }
+        */
+       public function __construct( $start_date, $end_date, array $options = array() ) {
+               // Date Range specific options.
+               $options = wp_parse_args( $options, array(
+                       'earliest_start' => null,
+                       'max_interval'   => null,
+               ) );
+
+               parent::__construct( $options );
+
+               if ( $this->validate_date_range_inputs( $start_date, $end_date ) ) {
+                       $this->start_date = new \DateTime( $start_date );
+                       $this->end_date   = new \DateTime( $end_date );
+                       $now              = new \DateTimeImmutable( 'now' );
+
+                       // If the end date is more than a month in the future, limit it to the end of the current year.
+                       // This allows for a date range spanning Dec/Jan, but not an arbitrary date far in the future.
+                       if ( $this->end_date > $now &&
+                            $now->diff( $this->end_date, true )->days > 31 &&
+                            $this->end_date->format( 'Y' ) !== $now->format( 'Y' ) ) {
+                               $this->end_date->setDate( intval( $now->format( 'Y' ) ), 12, 31 );
+                       }
+
+                       // If the end date doesn't have a specific time, make sure
+                       // the entire day is included.
+                       if ( '00:00:00' === $this->end_date->format( 'H:i:s' ) ) {
+                               $this->end_date->setTime( 23, 59, 59 );
+                       }
+               }
+       }
+
+       /**
+        * Validate the given strings for the start and end dates.
+        *
+        * @param string $start_date The start of the date range for the report.
+        * @param string $end_date   The end of the date range for the report.
+        *
+        * @return bool True if the parameters are valid. Otherwise false.
+        */
+       protected function validate_date_range_inputs( $start_date, $end_date ) {
+               if ( ! $start_date || ! $end_date ) {
+                       $this->error->add( 'invalid_date', 'Please enter a valid start and end date.' );
+
+                       return false;
+               }
+
+               try {
+                       $start_date = new \DateTimeImmutable( $start_date ); // Immutable so methods don't modify the original object.
+               } catch ( \Exception $e ) {
+                       $this->error->add( 'invalid_date', 'Please enter a valid start date.' );
+
+                       return false;
+               }
+
+               // No future start dates.
+               if ( $start_date > date_create( 'now' ) ) {
+                       $this->error->add( 'future_start_date', 'Please enter a start date that is the same as or before today\'s date.' );
+               }
+
+               // Check for start date boundary.
+               if ( $this->options['earliest_start'] instanceof \DateTime && $start_date < $this->options['earliest_start'] ) {
+                       $this->error->add( 'start_date_too_old', sprintf(
+                               'Please enter a start date of %s or later.',
+                               $this->options['earliest_start']->format( 'Y-m-d' )
+                       ) );
+               }
+
+               try {
+                       $end_date = new \DateTimeImmutable( $end_date ); // Immutable so methods don't modify the original object.
+               } catch ( \Exception $e ) {
+                       $this->error->add( 'invalid_date', 'Please enter a valid end date.' );
+
+                       return false;
+               }
+
+               // No negative date intervals.
+               if ( $start_date > $end_date ) {
+                       $this->error->add( 'negative_date_interval', 'Please enter an end date that is the same as or after the start date.' );
+               }
+
+               // Check for date interval boundary.
+               if ( $this->options['max_interval'] instanceof \DateInterval ) {
+                       $max_end_date = $start_date->add( $this->options['max_interval'] );
+
+                       if ( $end_date > $max_end_date ) {
+                               $this->error->add( 'exceeds_max_date_interval', sprintf(
+                                       'Please enter an end date that is no more than %s days after the start date.',
+                                       $start_date->diff( $max_end_date )->format( '%a' )
+                               ) );
+                       }
+               }
+
+               if ( ! empty( $this->error->get_error_messages() ) ) {
+                       return false;
+               }
+
+               return true;
+       }
+
+       /**
+        * Generate a cache key.
+        *
+        * @return string
+        */
+       protected function get_cache_key() {
+               $cache_key = parent::get_cache_key() . '_' . $this->start_date->getTimestamp() . '-' . $this->end_date->getTimestamp();
+
+               return $cache_key;
+       }
+
+       /**
+        * Generate a cache expiration interval.
+        *
+        * @return int A time interval in seconds.
+        */
+       protected function get_cache_expiration() {
+               $expiration = parent::get_cache_expiration();
+
+               $now = new \DateTimeImmutable( 'now' );
+               $now->setTime( 0, 0, 0 ); // Beginning of the current day.
+
+               if ( $this->end_date >= $now ) {
+                       // Expire the cache sooner if the data includes the current day.
+                       $expiration = HOUR_IN_SECONDS;
+               } elseif ( $this->end_date->diff( $now )->y > 0 ) {
+                       // Keep the cache longer if the end of the date range is over a year ago.
+                       $expiration = MONTH_IN_SECONDS;
+               }
+
+               return $expiration;
+       }
+
+       /**
+        * Generate a simple array of years.
+        *
+        * @param int $start_year The first year in the array.
+        * @param int $end_year   The last year in the array.
+        *
+        * @return array
+        */
+       protected static function year_array( int $start_year, int $end_year ) {
+               return range( $start_year, $end_year, 1 );
+       }
+
+       /**
+        * Generate an associative array of quarters, with abbreviation keys and label values.
+        *
+        * @return array
+        */
+       protected static function quarter_array() {
+               return array(
+                       'q1' => '1st quarter',
+                       'q2' => '2nd quarter',
+                       'q3' => '3rd quarter',
+                       'q4' => '4th quarter',
+               );
+       }
+
+       /**
+        * Generate an associative array of months, with numerical keys and string values.
+        *
+        * @return array
+        */
+       protected static function month_array() {
+               $months = array();
+
+               foreach ( range( 1, 12 ) as $number ) {
+                       $months[ $number ] = date( 'F', mktime( 0, 0, 0, $number, 10 ) );
+               }
+
+               return $months;
+       }
+
+       /**
+        * Convert a time period within a given year into specific start and end dates.
+        *
+        * @param int        $year   The year containing the time period.
+        * @param string|int $period The time period to convert. E.g. 2, 'February', 'q1'.
+        *
+        * @return array An associative array containing 'start_date' and 'end_date' keys with string values.
+        */
+       protected static function convert_time_period_to_date_range( $year, $period = '' ) {
+               $range = array(
+                       'start_date' => '',
+                       'end_date'   => '',
+               );
+
+               if ( ! is_int( $year ) ) {
+                       return $range;
+               }
+
+               $months = static::month_array();
+
+               if ( ! $period || 'all' === $period ) {
+                       // Period is the entire year.
+                       $range['start_date'] = "$year-01-01";
+                       $range['end_date']   = "$year-12-31";
+               } elseif ( in_array( $period, array( 'q1', 'q2', 'q3', 'q4' ), true ) ) {
+                       // Period is a quarter.
+                       switch ( $period ) {
+                               case 'q1' :
+                                       $range['start_date'] = "$year-01-01";
+                                       break;
+
+                               case 'q2' :
+                                       $range['start_date'] = "$year-04-01";
+                                       break;
+
+                               case 'q3' :
+                                       $range['start_date'] = "$year-07-01";
+                                       break;
+
+                               case 'q4' :
+                                       $range['start_date'] = "$year-10-01";
+                                       break;
+                       }
+
+                       $range['end_date'] = date( 'Y-m-d', strtotime( '+ 3 months - 1 second', strtotime( $range['start_date'] ) ) );
+               } elseif ( array_key_exists( $period, $months ) || in_array( $period, $months, true ) ) {
+                       // Period is a specific month.
+                       if ( in_array( $period, $months, true ) ) {
+                               // Month name given. Convert it to a number.
+                               $period = array_search( $period, $months, true );
+                       }
+
+                       $range['start_date'] = "$year-$period-01";
+                       $range['end_date']   = date( 'Y-m-d', strtotime( '+ 1 month - 1 second', strtotime( $range['start_date'] ) ) );
+               }
+
+               return $range;
+       }
+}
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsclassesreportclassmeetupgroupsphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/classes/report/class-meetup-groups.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/classes/report/class-meetup-groups.php                           (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/classes/report/class-meetup-groups.php     2018-02-16 16:49:05 UTC (rev 6662)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,387 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Meetup Groups.
+ *
+ * @package WordCamp\Reports
+ */
+
+namespace WordCamp\Reports\Report;
+defined( 'WPINC' ) || die();
+
+use WordCamp\Reports;
+use WordCamp\Utilities;
+
+/**
+ * Class Meetup_Groups
+ *
+ * @package WordCamp\Reports\Report
+ */
+class Meetup_Groups extends Date_Range {
+       /**
+        * Report name.
+        *
+        * @var string
+        */
+       public static $name = 'Meetup Groups';
+
+       /**
+        * Report slug.
+        *
+        * @var string
+        */
+       public static $slug = 'meetup-groups';
+
+       /**
+        * Report description.
+        *
+        * @var string
+        */
+       public static $description = 'The number of meetup groups in the Chapter program on a given date and the number of groups that joined during a given time period.';
+
+       /**
+        * Report methodology.
+        *
+        * @var string
+        */
+       public static $methodology = "
+               Retrieve data about groups in the Chapter program from the Meetup.com API. Only groups who joined the Chapter program before the specified end date will be included.
+       ";
+
+       /**
+        * Report group.
+        *
+        * @var string
+        */
+       public static $group = 'meetup';
+
+       /**
+        * Shortcode tag for outputting the public report form.
+        *
+        * @var string
+        */
+       public static $shortcode_tag = 'meetup_groups_report';
+
+       /**
+        * Data fields that can be visible in a public context.
+        *
+        * @var array An associative array of key/default value pairs.
+        */
+       protected $public_data_fields = array(
+               'name'          => '',
+               'urlname'       => '',
+               'city'          => '',
+               'state'         => '',
+               'country'       => '',
+               'lat'           => 0,
+               'lon'           => 0,
+               'member_count'  => 0,
+               'founded_date'  => 0,
+               'pro_join_date' => 0,
+       );
+
+       /**
+        * Query and parse the data for the report.
+        *
+        * @return array
+        */
+       public function get_data() {
+               // Bail if there are errors.
+               if ( ! empty( $this->error->get_error_messages() ) ) {
+                       return array();
+               }
+
+               // Maybe use cached data.
+               $data = $this->maybe_get_cached_data();
+               if ( is_array( $data ) ) {
+                       return $data;
+               }
+
+               $meetup = new Utilities\Meetup_Client();
+
+               $data = $meetup->get_groups( array(
+                       'pro_join_date_max' => $this->end_date->getTimestamp() * 1000, // Meetup API uses milliseconds.
+               ) );
+
+               if ( is_wp_error( $data ) ) {
+                       $this->error = $this->merge_errors( $this->error, $data );
+
+                       return array();
+               }
+
+               $data = $this->filter_data_fields( $data );
+               $this->maybe_cache_data( $data );
+
+               return $data;
+       }
+
+       /**
+        * Compile the report data into results.
+        *
+        * @param array $data The data to compile.
+        *
+        * @return array
+        */
+       public function compile_report_data( array $data ) {
+               $joined_groups = array_filter( $data, function( $group ) {
+                       $join_date = new \DateTime();
+                       $join_date->setTimestamp( intval( $group['pro_join_date'] / 1000 ) ); // Meetup API uses milliseconds.
+
+                       if ( $join_date >= $this->start_date && $join_date <= $this->end_date ) {
+                               return true;
+                       }
+
+                       return false;
+               } );
+
+               $compiled_data = array(
+                       'total_groups'              => count( $data ),
+                       'total_groups_by_country'   => $this->count_groups_by_country( $data ),
+                       'total_members'             => $this->count_members( $data ),
+                       'total_members_by_country'  => $this->count_group_members_by_country( $data ),
+                       'joined_groups'             => count( $joined_groups ),
+                       'joined_groups_by_country'  => $this->count_groups_by_country( $joined_groups ),
+                       'joined_members'            => $this->count_members( $joined_groups ),
+                       'joined_members_by_country' => $this->count_group_members_by_country( $joined_groups ),
+               );
+
+               return $compiled_data;
+       }
+
+       /**
+        * From a list of groups, count how many total members there are.
+        *
+        * @param array $groups Meetup groups.
+        *
+        * @return int The number of total members.
+        */
+       protected function count_members( $groups ) {
+               return array_reduce( $groups, function( $carry, $item ) {
+                       $carry += absint( $item['member_count'] );
+
+                       return $carry;
+               }, 0 );
+       }
+
+       /**
+        * From a list of groups, count how many there are in each country.
+        *
+        * @param array $groups Meetup groups.
+        *
+        * @return array An associative array of country keys and group count values, sorted high to low.
+        */
+       protected function count_groups_by_country( $groups ) {
+               $counts = array_reduce( $groups, function( $carry, $item ) {
+                       $country = $item['country'];
+
+                       if ( ! isset( $carry[ $country ] ) ) {
+                               $carry[ $country ] = 0;
+                       }
+
+                       $carry[ $country ] ++;
+
+                       return $carry;
+               }, array() );
+
+               arsort( $counts );
+
+               return $counts;
+       }
+
+       /**
+        * From a list of groups, count how many total group members there are in each country.
+        *
+        * @param array $groups Meetup groups.
+        *
+        * @return array An associative array of country keys and group member count values, sorted high to low.
+        */
+       protected function count_group_members_by_country( $groups ) {
+               $counts = array_reduce( $groups, function( $carry, $item ) {
+                       $country = $item['country'];
+
+                       if ( ! isset( $carry[ $country ] ) ) {
+                               $carry[ $country ] = 0;
+                       }
+
+                       $carry[ $country ] += absint( $item['member_count'] );
+
+                       return $carry;
+               }, array() );
+
+               arsort( $counts );
+
+               return $counts;
+       }
+
+       /**
+        * Render an HTML version of the report output.
+        *
+        * @return void
+        */
+       public function render_html() {
+               $data       = $this->compile_report_data( $this->get_data() );
+               $start_date = $this->start_date;
+               $end_date   = $this->end_date;
+
+               if ( ! empty( $this->error->get_error_messages() ) ) {
+                       $this->render_error_html();
+               } else {
+                       include Reports\get_views_dir_path() . 'html/meetup-groups.php';
+               }
+       }
+
+       /**
+        * Render the page for this report in the WP Admin.
+        *
+        * @return void
+        */
+       public static function render_admin_page() {
+               $start_date  = filter_input( INPUT_POST, 'start-date' );
+               $end_date    = filter_input( INPUT_POST, 'end-date' );
+               $refresh     = filter_input( INPUT_POST, 'refresh', FILTER_VALIDATE_BOOLEAN );
+               $action      = filter_input( INPUT_POST, 'action' );
+               $nonce       = filter_input( INPUT_POST, self::$slug . '-nonce' );
+
+               $report = null;
+
+               if ( 'Show results' === $action
+                    && wp_verify_nonce( $nonce, 'run-report' )
+                    && current_user_can( 'manage_network' )
+               ) {
+                       $options = array(
+                               'earliest_start' => new \DateTime( '2015-01-01' ), // Chapter program started in 2015.
+                       );
+
+                       if ( $refresh ) {
+                               $options['flush_cache'] = true;
+                       }
+
+                       $report = new self( $start_date, $end_date, $options );
+
+                       // The report adjusts the end date in some circumstances.
+                       if ( empty( $report->error->get_error_messages() ) ) {
+                               $end_date = $report->end_date->format( 'Y-m-d' );
+                       }
+               }
+
+               include Reports\get_views_dir_path() . 'report/meetup-groups.php';
+       }
+
+       /**
+        * Export the report data to a file.
+        *
+        * @return void
+        */
+       public static function export_to_file() {
+               $start_date  = filter_input( INPUT_POST, 'start-date' );
+               $end_date    = filter_input( INPUT_POST, 'end-date' );
+               $refresh     = filter_input( INPUT_POST, 'refresh', FILTER_VALIDATE_BOOLEAN );
+               $action      = filter_input( INPUT_POST, 'action' );
+               $nonce       = filter_input( INPUT_POST, self::$slug . '-nonce' );
+
+               $report = null;
+
+               if ( 'Export CSV' !== $action ) {
+                       return;
+               }
+
+               if ( wp_verify_nonce( $nonce, 'run-report' ) && current_user_can( 'manage_network' ) ) {
+                       $options = array(
+                               'earliest_start' => new \DateTime( '2015-01-01' ), // Chapter program started in 2015.
+                       );
+
+                       if ( $refresh ) {
+                               $options['flush_cache'] = true;
+                       }
+
+                       $report = new self( $start_date, $end_date, $options );
+
+                       // The report adjusts the end date in some circumstances.
+                       if ( empty( $report->error->get_error_messages() ) ) {
+                               $end_date = $report->end_date->format( 'Y-m-d' );
+                       }
+
+                       $filename   = array( $report::$name );
+                       $filename[] = $report->start_date->format( 'Y-m-d' );
+                       $filename[] = $report->end_date->format( 'Y-m-d' );
+
+                       $headers = array( 'Name', 'URL', 'City', 'State', 'Country', 'Latitude', 'Longitude', 'Member Count', 'Date Founded', 'Date Joined' );
+
+                       $data = $report->get_data();
+
+                       array_walk( $data, function( &$group ) {
+                               $group['urlname']       = ( $group['urlname'] ) ? esc_url( 'https://www.meetup.com/' . $group['urlname'] . '/' ) : '';
+                               $group['founded_date']  = ( $group['founded_date'] ) ? date( 'Y-m-d', $group['founded_date'] / 1000 ) : '';
+                               $group['pro_join_date'] = ( $group['pro_join_date'] ) ? date( 'Y-m-d', $group['pro_join_date'] / 1000 ) : '';
+                       } );
+
+                       $exporter = new Utilities\Export_CSV( array(
+                               'filename' => $filename,
+                               'headers'  => $headers,
+                               'data'     => $data,
+                       ) );
+
+                       if ( ! empty( $report->error->get_error_messages() ) ) {
+                               $exporter->error = $report->merge_errors( $report->error, $exporter->error );
+                       }
+
+                       $exporter->emit_file();
+               } // End if().
+       }
+
+       /**
+        * Determine whether to render the public report form.
+        *
+        * This shortcode is limited to use on pages.
+        *
+        * @return string HTML content to display shortcode.
+        */
+       public static function handle_shortcode() {
+               $html = '';
+
+               if ( 'page' === get_post_type() ) {
+                       ob_start();
+                       self::render_public_page();
+                       $html = ob_get_clean();
+               }
+
+               return $html;
+       }
+
+       /**
+        * Render the page for this report on the front end.
+        *
+        * @return void
+        */
+       public static function render_public_page() {
+               // Apparently 'year' is a reserved URL parameter on the front end, so we prepend 'report-'.
+               $year   = filter_input( INPUT_GET, 'report-year', FILTER_VALIDATE_INT );
+               $period = filter_input( INPUT_GET, 'period' );
+               $action = filter_input( INPUT_GET, 'action' );
+
+               $years    = self::year_array( absint( date( 'Y' ) ), 2015 );
+               $quarters = self::quarter_array();
+               $months   = self::month_array();
+
+               if ( ! $year ) {
+                       $year = absint( date( 'Y' ) );
+               }
+
+               if ( ! $period ) {
+                       $period = absint( date( 'm' ) );
+               }
+
+               $report = null;
+
+               if ( 'Show results' === $action ) {
+                       $range = self::convert_time_period_to_date_range( $year, $period );
+
+                       $options = array(
+                               'earliest_start' => new \DateTime( '2015-01-01' ), // Chapter program started in 2015.
+                       );
+
+                       $report = new self( $range['start_date'], $range['end_date'], $options );
+               }
+
+               include Reports\get_views_dir_path() . 'public/meetup-groups.php';
+       }
+}
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsclassesreportclasspaymentactivityphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/classes/report/class-payment-activity.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/classes/report/class-payment-activity.php                                (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/classes/report/class-payment-activity.php  2018-02-16 16:49:05 UTC (rev 6662)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,690 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Payment Activity.
+ *
+ * @package WordCamp\Reports
+ */
+
+namespace WordCamp\Reports\Report;
+defined( 'WPINC' ) || die();
+
+use WordCamp\Reports;
+use WordCamp\Utilities;
+use WordCamp\Budgets_Dashboard\Reimbursement_Requests;
+
+/**
+ * Class Payment_Activity
+ *
+ * @package WordCamp\Reports\Report
+ */
+class Payment_Activity extends Date_Range {
+       /**
+        * Report name.
+        *
+        * @var string
+        */
+       public static $name = 'Payment Activity';
+
+       /**
+        * Report slug.
+        *
+        * @var string
+        */
+       public static $slug = 'payment-activity';
+
+       /**
+        * Report description.
+        *
+        * @var string
+        */
+       public static $description = 'Vendor payments and reimbursement requests.';
+
+       /**
+        * Report methodology.
+        *
+        * @var string
+        */
+       public static $methodology = "
+               <ol>
+                       <li>Retrieve index entries for vendor payments and reimbursement requests that have a created and/or paid date that fall within the specified date range.</li>
+                       <li>Query each WordCamp site from the index results and retrieve additional data for each matched payment.</li>
+                       <li>Parse the activity log for each payment and determine (or guess) if/when the payment was approved, if/when it was paid, and if/when it was cancelled or it failed.</li>
+                       <li>Filter out payments don't have an approved, paid, or failed date within the specified date range.</li>
+               </ol>
+       ";
+
+       /**
+        * Report group.
+        *
+        * @var string
+        */
+       public static $group = 'finance';
+
+       /**
+        * Shortcode tag for outputting the public report form.
+        *
+        * @var string
+        */
+       public static $shortcode_tag = 'payment_activity_report';
+
+       /**
+        * WordCamp post ID.
+        *
+        * @var int The ID of the WordCamp post for this report.
+        */
+       public $wordcamp_id = 0;
+
+       /**
+        * WordCamp site ID.
+        *
+        * @var int The ID of the WordCamp site where the invoices are located.
+        */
+       public $wordcamp_site_id = 0;
+
+       /**
+        * Currency exchange rate client.
+        *
+        * @var Utilities\Currency_XRT_Client Utility to handle currency conversion.
+        */
+       protected $xrt = null;
+
+       /**
+        * Data fields that can be visible in a public context.
+        *
+        * @var array An associative array of key/default value pairs.
+        */
+       protected $public_data_fields = array(
+               'blog_id'            => 0,
+               'post_id'            => 0,
+               'post_type'          => '',
+               'currency'           => '',
+               'amount'             => 0,
+               'status'             => '',
+               'timestamp_approved' => 0,
+               'timestamp_paid'     => 0,
+               'timestamp_failed'   => 0,
+       );
+
+       /**
+        * Payment_Activity constructor.
+        *
+        * @param string $start_date  The start of the date range for the report.
+        * @param string $end_date    The end of the date range for the report.
+        * @param int    $wordcamp_id Optional. The ID of a WordCamp post to limit this report to.
+        * @param array  $options     {
+        *     Optional. Additional report parameters.
+        *     See Base::__construct and Date_Range::__construct for additional parameters.
+        * }
+        */
+       public function __construct( $start_date, $end_date, $wordcamp_id = 0, array $options = array() ) {
+               parent::__construct( $start_date, $end_date, $options );
+
+               $this->xrt = new Utilities\Currency_XRT_Client();
+
+               if ( $wordcamp_id && $this->validate_wordcamp_id( $wordcamp_id ) ) {
+                       $this->wordcamp_id      = $wordcamp_id;
+                       $this->wordcamp_site_id = get_wordcamp_site_id( get_post( $wordcamp_id ) );
+               }
+       }
+
+       /**
+        * Generate a cache key.
+        *
+        * @return string
+        */
+       protected function get_cache_key() {
+               $cache_key = parent::get_cache_key();
+
+               if ( $this->wordcamp_id ) {
+                       $cache_key .= '_' . $this->wordcamp_id;
+               }
+
+               return $cache_key;
+       }
+
+       /**
+        * Query and parse the data for the report.
+        *
+        * @return array
+        */
+       public function get_data() {
+               // Bail if there are errors.
+               if ( ! empty( $this->error->get_error_messages() ) ) {
+                       return array();
+               }
+
+               // Maybe use cached data.
+               $data = $this->maybe_get_cached_data();
+               if ( is_array( $data ) ) {
+                       return $data;
+               }
+
+               $indexed_payments = $this->get_indexed_payments();
+               $payments_by_site = array();
+
+               foreach ( $indexed_payments as $index ) {
+                       if ( ! isset( $payments_by_site[ $index['blog_id'] ] ) ) {
+                               $payments_by_site[ $index['blog_id'] ] = array();
+                       }
+
+                       $payments_by_site[ $index['blog_id'] ][] = $index['post_id'];
+               }
+
+               $payment_posts = array();
+
+               foreach ( $payments_by_site as $blog_id => $post_ids ) {
+                       $payment_posts = array_merge( $payment_posts, $this->get_payment_posts( $blog_id, $post_ids ) );
+               }
+
+               $payment_posts = array_map( array( $this, 'parse_payment_post_log' ), $payment_posts );
+
+               $data = array_filter( $payment_posts, function( $payment ) {
+                       if ( ! $this->timestamp_within_date_range( $payment['timestamp_approved'] )
+                            && ! $this->timestamp_within_date_range( $payment['timestamp_paid'] )
+                            && ! $this->timestamp_within_date_range( $payment['timestamp_failed'] )
+                       ) {
+                               return false;
+                       }
+
+                       return true;
+               } );
+
+               $data = $this->filter_data_fields( $data );
+               $this->maybe_cache_data( $data );
+
+               return $data;
+       }
+
+       /**
+        * Compile the report data into results.
+        *
+        * @param array $data The data to compile.
+        *
+        * @return array
+        */
+       public function compile_report_data( array $data ) {
+               $compiled_data = $this->derive_totals_from_payment_events( $data );
+
+               return $compiled_data;
+       }
+
+       /**
+        * Retrieve Vendor Payments and Reimbursement Requests from their respective index database tables.
+        *
+        * @return array
+        */
+       protected function get_indexed_payments() {
+               // Ensure all the needed files are loaded.
+               $wordcamp_payments_network_path = trailingslashit( str_replace( 'wordcamp-reports', 'wordcamp-payments-network', Reports\PLUGIN_DIR ) );
+               require_once $wordcamp_payments_network_path . 'includes/payment-requests-dashboard.php';
+               require_once $wordcamp_payments_network_path . 'includes/reimbursement-requests-dashboard.php';
+
+               /** @global \wpdb $wpdb */
+               global $wpdb;
+
+               $payments_table       = \Payment_Requests_Dashboard::get_table_name();
+               $reimbursements_table = Reimbursement_Requests\get_index_table_name();
+
+               $extra_where = ( $this->wordcamp_site_id ) ? ' AND blog_id = ' . (int) $this->wordcamp_site_id : '';
+
+               $index_query = $wpdb->prepare( "
+                       (
+                               SELECT blog_id, post_id
+                               FROM $payments_table
+                               WHERE created <= %d
+                                       AND ( paid = 0 OR paid >= %d )
+                                       $extra_where
+                       ) UNION (
+                               SELECT blog_id, request_id AS post_id
+                               FROM $reimbursements_table
+                               WHERE date_requested <= %d
+                                       AND ( date_paid = 0 OR date_paid >= %d )
+                                       $extra_where
+                       )",
+                       $this->end_date->getTimestamp(),
+                       $this->start_date->getTimestamp(),
+                       $this->end_date->getTimestamp(),
+                       $this->start_date->getTimestamp()
+               );
+
+               return $wpdb->get_results( $index_query, ARRAY_A );
+       }
+
+       /**
+        * Get payment posts from a particular site.
+        *
+        * @param int   $blog_id  The ID of the site.
+        * @param array $post_ids The list of post IDs to get.
+        *
+        * @return array
+        */
+       protected function get_payment_posts( $blog_id, array $post_ids ) {
+               $payment_posts = array();
+               $post_types    = array( 'wcp_payment_request', 'wcb_reimbursement' );
+
+               switch_to_blog( $blog_id );
+
+               $query_args = array(
+                       'post_type'           => $post_types,
+                       'post_status'         => 'all',
+                       'post__in'            => $post_ids,
+                       'nopaging'            => true,
+               );
+
+               $raw_posts = get_posts( $query_args );
+
+               foreach ( $raw_posts as $raw_post ) {
+                       switch ( $raw_post->post_type ) {
+                               case 'wcp_payment_request' :
+                                       $currency = $raw_post->_camppayments_currency;
+                                       $amount   = $raw_post->_camppayments_payment_amount;
+                                       break;
+
+                               case 'wcb_reimbursement' :
+                                       $currency = get_post_meta( $raw_post->ID, '_wcbrr_currency', true );
+                                       $amount   = Reimbursement_Requests\get_amount( $raw_post->ID );
+                                       break;
+
+                               default :
+                                       $currency = '';
+                                       $amount   = '';
+                                       break;
+                       }
+
+                       $payment_posts[] = array(
+                               'blog_id'   => $blog_id,
+                               'post_id'   => $raw_post->ID,
+                               'post_type' => $raw_post->post_type,
+                               'currency'  => $currency,
+                               'amount'    => $amount,
+                               'status'    => $raw_post->post_status,
+                               'log'       => json_decode( $raw_post->_wcp_log, true ),
+                       );
+
+                       clean_post_cache( $raw_post );
+               }
+
+               restore_current_blog();
+
+               return $payment_posts;
+       }
+
+       /**
+        * Determine the timestamps for particular payment post events from the post's log.
+        *
+        * This walks through the log array looking for specific events. If it finds them, it adds the event
+        * timestamp to a new key in the payment post array. At the end, it removes the log from the array.
+        *
+        * @param array $payment_post The array of data for a payment post.
+        *
+        * @return array
+        */
+       protected function parse_payment_post_log( array $payment_post ) {
+               $parsed_post = wp_parse_args( array(
+                       'timestamp_approved' => 0,
+                       'timestamp_paid'     => 0,
+                       'timestamp_failed'   => 0,
+               ), $payment_post );
+
+               if ( ! isset( $parsed_post['log'] ) ) {
+                       return $parsed_post;
+               }
+
+               usort( $parsed_post['log'], function( $a, $b ) {
+                       // Sort log entries in chronological order.
+                       if ( $a['timestamp'] === $b['timestamp'] ) {
+                               return 0;
+                       }
+
+                       return ( $a['timestamp'] > $b['timestamp'] ) ? 1 : -1;
+               } );
+
+               foreach ( $parsed_post['log'] as $index => $entry ) {
+                       if ( \BLOG_ID_CURRENT_SITE === $parsed_post['blog_id'] && 0 === $index ) {
+                               // Payments on central.wordcamp.org have a different workflow.
+                               $parsed_post['timestamp_approved'] = $entry['timestamp'];
+                       } elseif ( false !== stripos( $entry['message'], 'Request approved' ) ) {
+                               $parsed_post['timestamp_approved'] = $entry['timestamp'];
+                       } elseif ( false !== stripos( $entry['message'], 'Pending Payment' ) ) {
+                               $parsed_post['timestamp_paid'] = $entry['timestamp'];
+                       } elseif ( false !== stripos( $entry['message'], 'Marked as paid' ) && ! $parsed_post['timestamp_paid'] ) {
+                               $parsed_post['timestamp_paid'] = $entry['timestamp'];
+                       }
+               }
+
+               if ( $parsed_post['timestamp_paid'] && ! $parsed_post['timestamp_approved'] ) {
+                       // If we didn't find an approved timestamp, but we did find a paid timestamp, use the same for both.
+                       $parsed_post['timestamp_approved'] = $parsed_post['timestamp_paid'];
+               }
+
+               // There isn't an explicit log entry for failed or cancelled payments, so we have to look at the post status.
+               if ( in_array( $parsed_post['status'], array( 'wcb-failed', 'wcb-cancelled' ), true ) ) {
+                       $parsed_post['timestamp_paid'] = 0;
+
+                       // Assume the last log entry is when the payment was marked failed/cancelled.
+                       $last_log = array_slice( $parsed_post['log'], -1 )[0];
+                       $parsed_post['timestamp_failed'] = $last_log['timestamp'];
+               }
+
+               unset( $parsed_post['log'] );
+
+               return $parsed_post;
+       }
+
+       /**
+        * Aggregate the number and payment amounts of a group of Vendor Payments and Reimbursement Requests.
+        *
+        * @param array $payment_posts The group of posts to aggregate.
+        *
+        * @return array
+        */
+       protected function derive_totals_from_payment_events( array $payment_posts ) {
+               $data = array(
+                       'vendor_payment_count'              => 0,
+                       'reimbursement_count'               => 0,
+                       'vendor_payment_amount_by_currency' => array(),
+                       'reimbursement_amount_by_currency'  => array(),
+                       'total_amount_by_currency'          => array(),
+                       'converted_amounts'                 => array(),
+                       'total_amount_converted'            => 0,
+               );
+
+               $data_groups = array(
+                       'requests' => $data,
+                       'payments' => $data,
+                       'failures' => $data,
+               );
+
+               $currencies      = array();
+               $failed_statuses = array( 'wcb-failed', 'wcb-cancelled' );
+
+               foreach ( $payment_posts as $payment ) {
+                       if ( ! isset( $payment['currency'] ) || ! $payment['currency'] ) {
+                               continue;
+                       }
+
+                       if ( ! in_array( $payment['currency'], $currencies, true ) ) {
+                               $data_groups['requests']['vendor_payment_amount_by_currency'][ $payment['currency'] ] = 0;
+                               $data_groups['requests']['reimbursement_amount_by_currency'][ $payment['currency'] ]  = 0;
+                               $data_groups['requests']['total_amount_by_currency'][ $payment['currency'] ]          = 0;
+                               $data_groups['payments']['vendor_payment_amount_by_currency'][ $payment['currency'] ] = 0;
+                               $data_groups['payments']['reimbursement_amount_by_currency'][ $payment['currency'] ]  = 0;
+                               $data_groups['payments']['total_amount_by_currency'][ $payment['currency'] ]          = 0;
+                               $data_groups['failures']['vendor_payment_amount_by_currency'][ $payment['currency'] ] = 0;
+                               $data_groups['failures']['reimbursement_amount_by_currency'][ $payment['currency'] ]  = 0;
+                               $data_groups['failures']['total_amount_by_currency'][ $payment['currency'] ]          = 0;
+                               $currencies[]                                                                         = $payment['currency'];
+                       }
+
+                       switch ( $payment['post_type'] ) {
+                               case 'wcp_payment_request' :
+                                       if ( $this->timestamp_within_date_range( $payment['timestamp_approved'] ) ) {
+                                               $data_groups['requests']['vendor_payment_count'] ++;
+                                               $data_groups['requests']['vendor_payment_amount_by_currency'][ $payment['currency'] ] += floatval( $payment['amount'] );
+                                               $data_groups['requests']['total_amount_by_currency'][ $payment['currency'] ]          += floatval( $payment['amount'] );
+                                       }
+                                       if ( $this->timestamp_within_date_range( $payment['timestamp_paid'] ) ) {
+                                               $data_groups['payments']['vendor_payment_count'] ++;
+                                               $data_groups['payments']['vendor_payment_amount_by_currency'][ $payment['currency'] ] += floatval( $payment['amount'] );
+                                               $data_groups['payments']['total_amount_by_currency'][ $payment['currency'] ]          += floatval( $payment['amount'] );
+                                       } elseif ( $this->timestamp_within_date_range( $payment['timestamp_failed'] ) ) {
+                                               $data_groups['failures']['vendor_payment_count'] ++;
+                                               $data_groups['failures']['vendor_payment_amount_by_currency'][ $payment['currency'] ] += floatval( $payment['amount'] );
+                                               $data_groups['failures']['total_amount_by_currency'][ $payment['currency'] ]          += floatval( $payment['amount'] );
+                                       }
+                                       break;
+
+                               case 'wcb_reimbursement' :
+                                       if ( $this->timestamp_within_date_range( $payment['timestamp_approved'] ) ) {
+                                               $data_groups['requests']['reimbursement_count'] ++;
+                                               $data_groups['requests']['reimbursement_amount_by_currency'][ $payment['currency'] ] += floatval( $payment['amount'] );
+                                               $data_groups['requests']['total_amount_by_currency'][ $payment['currency'] ]         += floatval( $payment['amount'] );
+                                       }
+                                       if ( $this->timestamp_within_date_range( $payment['timestamp_paid'] ) ) {
+                                               $data_groups['payments']['reimbursement_count'] ++;
+                                               $data_groups['payments']['reimbursement_amount_by_currency'][ $payment['currency'] ] += floatval( $payment['amount'] );
+                                               $data_groups['payments']['total_amount_by_currency'][ $payment['currency'] ]         += floatval( $payment['amount'] );
+                                       } elseif ( $this->timestamp_within_date_range( $payment['timestamp_failed'] ) ) {
+                                               $data_groups['failures']['reimbursement_count'] ++;
+                                               $data_groups['failures']['reimbursement_amount_by_currency'][ $payment['currency'] ] += floatval( $payment['amount'] );
+                                               $data_groups['failures']['total_amount_by_currency'][ $payment['currency'] ]         += floatval( $payment['amount'] );
+                                       }
+                                       break;
+                       }
+               } // End foreach().
+
+               foreach ( $data_groups as &$group ) {
+                       ksort( $group['vendor_payment_amount_by_currency'] );
+                       ksort( $group['reimbursement_amount_by_currency'] );
+                       ksort( $group['total_amount_by_currency'] );
+
+                       foreach ( $group['total_amount_by_currency'] as $currency => $amount ) {
+                               if ( 'USD' === $currency ) {
+                                       $group['converted_amounts'][ $currency ] = $amount;
+                               } else {
+                                       $group['converted_amounts'][ $currency ] = 0;
+
+                                       $conversion = $this->xrt->convert( $amount, $currency, $this->end_date->format( 'Y-m-d' ) );
+
+                                       if ( is_wp_error( $conversion ) ) {
+                                               // Unsupported currencies are ok, but other errors should be surfaced.
+                                               if ( 'unknown_currency' !== $conversion->get_error_code() ) {
+                                                       $this->merge_errors( $this->error, $conversion );
+                                               }
+                                       } else {
+                                               $group['converted_amounts'][ $currency ] = $conversion->USD;
+                                       }
+                               }
+                       }
+
+                       $group['total_amount_converted'] = array_reduce( $group['converted_amounts'], function( $carry, $item ) {
+                               return $carry + floatval( $item );
+                       }, 0 );
+               }
+
+               return $data_groups;
+       }
+
+       /**
+        * Check if a given Unix timestamp is within the date range set in the report.
+        *
+        * @param int $timestamp The Unix timestamp to test.
+        *
+        * @return bool True if within the date range.
+        */
+       protected function timestamp_within_date_range( $timestamp ) {
+               $date = new \DateTime();
+               $date->setTimestamp( $timestamp );
+
+               if ( $date >= $this->start_date && $date <= $this->end_date ) {
+                       return true;
+               }
+
+               return false;
+       }
+
+       /**
+        * Render an HTML version of the report output.
+        *
+        * @return void
+        */
+       public function render_html() {
+               $data       = $this->compile_report_data( $this->get_data() );
+               $start_date = $this->start_date;
+               $end_date   = $this->end_date;
+
+               $wordcamp_name = ( $this->wordcamp_site_id ) ? get_wordcamp_name( $this->wordcamp_site_id ) : '';
+               $requests      = $data['requests'];
+               $payments      = $data['payments'];
+               $failures      = $data['failures'];
+
+               if ( ! empty( $this->error->get_error_messages() ) ) {
+                       $this->render_error_html();
+               } else {
+                       include Reports\get_views_dir_path() . 'html/payment-activity.php';
+               }
+       }
+
+       /**
+        * Render the page for this report in the WP Admin.
+        *
+        * @return void
+        */
+       public static function render_admin_page() {
+               $start_date  = filter_input( INPUT_POST, 'start-date' );
+               $end_date    = filter_input( INPUT_POST, 'end-date' );
+               $wordcamp_id = filter_input( INPUT_POST, 'wordcamp-id' );
+               $refresh     = filter_input( INPUT_POST, 'refresh', FILTER_VALIDATE_BOOLEAN );
+               $action      = filter_input( INPUT_POST, 'action' );
+               $nonce       = filter_input( INPUT_POST, self::$slug . '-nonce' );
+
+               $report = null;
+
+               if ( 'Show results' === $action
+                    && wp_verify_nonce( $nonce, 'run-report' )
+                    && current_user_can( 'manage_network' )
+               ) {
+                       $options = array(
+                               'earliest_start' => new \DateTime( '2015-01-01' ), // No indexed payment data before 2015.
+                       );
+
+                       if ( $refresh ) {
+                               $options['flush_cache'] = true;
+                       }
+
+                       $report = new self( $start_date, $end_date, $wordcamp_id, $options );
+
+                       // The report adjusts the end date in some circumstances.
+                       if ( empty( $report->error->get_error_messages() ) ) {
+                               $end_date = $report->end_date->format( 'Y-m-d' );
+                       }
+               }
+
+               include Reports\get_views_dir_path() . 'report/payment-activity.php';
+       }
+
+       /**
+        * Export the report data to a file.
+        *
+        * @return void
+        */
+       public static function export_to_file() {
+               $start_date  = filter_input( INPUT_POST, 'start-date' );
+               $end_date    = filter_input( INPUT_POST, 'end-date' );
+               $wordcamp_id = filter_input( INPUT_POST, 'wordcamp-id' );
+               $refresh     = filter_input( INPUT_POST, 'refresh', FILTER_VALIDATE_BOOLEAN );
+               $action      = filter_input( INPUT_POST, 'action' );
+               $nonce       = filter_input( INPUT_POST, self::$slug . '-nonce' );
+
+               $report = null;
+
+               if ( 'Export CSV' !== $action ) {
+                       return;
+               }
+
+               if ( wp_verify_nonce( $nonce, 'run-report' ) && current_user_can( 'manage_network' ) ) {
+                       $options = array(
+                               'earliest_start' => new \DateTime( '2015-01-01' ), // No indexed payment data before 2015.
+                       );
+
+                       if ( $refresh ) {
+                               $options['flush_cache'] = true;
+                       }
+
+                       $report = new self( $start_date, $end_date, $wordcamp_id, $options );
+
+                       // The report adjusts the end date in some circumstances.
+                       if ( empty( $report->error->get_error_messages() ) ) {
+                               $end_date = $report->end_date->format( 'Y-m-d' );
+                       }
+
+                       $filename = array( $report::$name );
+                       if ( $report->wordcamp_site_id ) {
+                               $filename[] = get_wordcamp_name( $report->wordcamp_site_id );
+                       }
+                       $filename[] = $report->start_date->format( 'Y-m-d' );
+                       $filename[] = $report->end_date->format( 'Y-m-d' );
+
+                       $headers = array( 'Blog ID', 'Payment ID', 'Payment Type', 'Currency', 'Amount', 'Status', 'Date Approved', 'Date Paid', 'Date Failed/Cancelled' );
+
+                       $data = $report->get_data();
+
+                       array_walk( $data, function( &$payment ) {
+                               $payment['post_type']          = get_post_type_labels( get_post_type_object( $payment['post_type'] ) )->singular_name;
+                               $payment['timestamp_approved'] = ( $payment['timestamp_approved'] > 0 ) ? date( 'Y-m-d', $payment['timestamp_approved'] ) : '';
+                               $payment['timestamp_paid']     = ( $payment['timestamp_paid'] > 0 ) ? date( 'Y-m-d', $payment['timestamp_paid'] ) : '';
+                               $payment['timestamp_failed']   = ( $payment['timestamp_failed'] > 0 ) ? date( 'Y-m-d', $payment['timestamp_failed'] ) : '';
+                       } );
+
+                       $exporter = new Utilities\Export_CSV( array(
+                               'filename' => $filename,
+                               'headers'  => $headers,
+                               'data'     => $data,
+                       ) );
+
+                       if ( ! empty( $report->error->get_error_messages() ) ) {
+                               $exporter->error = $report->merge_errors( $report->error, $exporter->error );
+                       }
+
+                       $exporter->emit_file();
+               } // End if().
+       }
+
+       /**
+        * Determine whether to render the public report form.
+        *
+        * This shortcode is limited to use on pages.
+        *
+        * @return string HTML content to display shortcode.
+        */
+       public static function handle_shortcode() {
+               $html = '';
+
+               if ( 'page' === get_post_type() ) {
+                       ob_start();
+                       self::render_public_page();
+                       $html = ob_get_clean();
+               }
+
+               return $html;
+       }
+
+       /**
+        * Render the page for this report on the front end.
+        *
+        * @return void
+        */
+       public static function render_public_page() {
+               // Apparently 'year' is a reserved URL parameter on the front end, so we prepend 'report-'.
+               $year        = filter_input( INPUT_GET, 'report-year', FILTER_VALIDATE_INT );
+               $period      = filter_input( INPUT_GET, 'period' );
+               $wordcamp_id = filter_input( INPUT_GET, 'wordcamp-id' );
+               $action      = filter_input( INPUT_GET, 'action' );
+
+               $years    = self::year_array( absint( date( 'Y' ) ), 2015 );
+               $quarters = self::quarter_array();
+               $months   = self::month_array();
+
+               if ( ! $year ) {
+                       $year = absint( date( 'Y' ) );
+               }
+
+               if ( ! $period ) {
+                       $period = absint( date( 'm' ) );
+               }
+
+               $report = null;
+
+               if ( 'Show results' === $action ) {
+                       $range = self::convert_time_period_to_date_range( $year, $period );
+
+                       $options = array(
+                               'earliest_start' => new \DateTime( '2015-01-01' ), // No indexed payment data before 2015.
+                       );
+
+                       $report = new self( $range['start_date'], $range['end_date'], $wordcamp_id, $options );
+               }
+
+               include Reports\get_views_dir_path() . 'public/payment-activity.php';
+       }
+}
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsclassesreportclasssponsorinvoicesphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/classes/report/class-sponsor-invoices.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/classes/report/class-sponsor-invoices.php                                (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/classes/report/class-sponsor-invoices.php  2018-02-16 16:49:05 UTC (rev 6662)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,643 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Sponsor Invoices.
+ *
+ * @package WordCamp\Reports
+ */
+
+namespace WordCamp\Reports\Report;
+defined( 'WPINC' ) || die();
+
+use WordCamp\Reports;
+use WordCamp\Utilities;
+use WordCamp\Budgets_Dashboard\Sponsor_Invoices as WCBD_Sponsor_Invoices;
+
+/**
+ * Class Sponsor_Invoices
+ *
+ * @package WordCamp\Reports\Report
+ */
+class Sponsor_Invoices extends Date_Range {
+       /**
+        * Report name.
+        *
+        * @var string
+        */
+       public static $name = 'Sponsor Invoices';
+
+       /**
+        * Report slug.
+        *
+        * @var string
+        */
+       public static $slug = 'sponsor-invoices';
+
+       /**
+        * Report description.
+        *
+        * @var string
+        */
+       public static $description = 'Sponsor invoices sent and paid.';
+
+       /**
+        * Report methodology.
+        *
+        * @var string
+        */
+       public static $methodology = "
+               <ol>
+                       <li>Retrieve data from QuickBooks Online via their API for invoices sent during the specified date range.</li>
+                       <li>Match the invoice data against indexed invoices in the WordCamp database.</li>
+                       <li>Also via the QuickBooks Online API, retrieve data for payments made during the date range.</li>
+                       <li>Filter out payments that aren't related to invoices (but keep payments made to invoices not sent within the date range).</li>
+               </ol>
+       ";
+
+       /**
+        * Report group.
+        *
+        * @var string
+        */
+       public static $group = 'finance';
+
+       /**
+        * Shortcode tag for outputting the public report form.
+        *
+        * @var string
+        */
+       public static $shortcode_tag = 'sponsor_invoices_report';
+
+       /**
+        * WordCamp post ID.
+        *
+        * @var int The ID of the WordCamp post for this report.
+        */
+       public $wordcamp_id = 0;
+
+       /**
+        * WordCamp site ID.
+        *
+        * @var int The ID of the WordCamp site where the invoices are located.
+        */
+       public $wordcamp_site_id = 0;
+
+       /**
+        * Currency exchange rate client.
+        *
+        * @var Utilities\Currency_XRT_Client Utility to handle currency conversion.
+        */
+       protected $xrt = null;
+
+       /**
+        * Data fields that can be visible in a public context.
+        *
+        * @var array An associative array of key/default value pairs.
+        */
+       protected $public_data_fields = array(
+               'date'          => '',
+               'type'          => '',
+               'invoice_id'    => 0,
+               'wordcamp_name' => '',
+               'sponsor_name'  => '',
+               'invoice_title' => '',
+               'currency'      => '',
+               'amount'        => 0,
+       );
+
+       /**
+        * Sponsor_Invoices constructor.
+        *
+        * @param string $start_date  The start of the date range for the report.
+        * @param string $end_date    The end of the date range for the report.
+        * @param int    $wordcamp_id Optional. The ID of a WordCamp post to limit this report to.
+        * @param array  $options     {
+        *     Optional. Additional report parameters.
+        *     See Base::__construct and Date_Range::__construct for additional parameters.
+        * }
+        */
+       public function __construct( $start_date, $end_date, $wordcamp_id = 0, array $options = array() ) {
+               parent::__construct( $start_date, $end_date, $options );
+
+               $this->xrt = new Utilities\Currency_XRT_Client();
+
+               if ( $wordcamp_id && $this->validate_wordcamp_id( $wordcamp_id ) ) {
+                       $this->wordcamp_id      = $wordcamp_id;
+                       $this->wordcamp_site_id = get_wordcamp_site_id( get_post( $wordcamp_id ) );
+               }
+       }
+
+       /**
+        * Generate a cache key.
+        *
+        * @return string
+        */
+       protected function get_cache_key() {
+               $cache_key = parent::get_cache_key();
+
+               if ( $this->wordcamp_id ) {
+                       $cache_key .= '_' . $this->wordcamp_id;
+               }
+
+               return $cache_key;
+       }
+
+       /**
+        * Query and parse the data for the report.
+        *
+        * @todo Take into account refunded invoice payments.
+        *
+        * @return array
+        */
+       public function get_data() {
+               // Bail if there are errors.
+               if ( ! empty( $this->error->get_error_messages() ) ) {
+                       return array();
+               }
+
+               // Maybe use cached data.
+               $data = $this->maybe_get_cached_data();
+               if ( is_array( $data ) ) {
+                       return $data;
+               }
+
+               $data = array();
+
+               $indexed_invoices = $this->get_indexed_invoices();
+
+               if ( ! empty( $indexed_invoices ) ) {
+                       $qbo_invoices = $this->get_qbo_invoices( $indexed_invoices );
+
+                       if ( is_wp_error( $qbo_invoices ) ) {
+                               $this->error = $this->merge_errors( $this->error, $qbo_invoices );
+
+                               return array();
+                       }
+
+                       $qbo_payments = $this->get_qbo_payments( $indexed_invoices );
+
+                       if ( is_wp_error( $qbo_payments ) ) {
+                               $this->error = $this->merge_errors( $this->error, $qbo_payments );
+
+                               return array();
+                       }
+
+                       $data = array_merge( $qbo_invoices, $qbo_payments );
+               }
+
+               $data = $this->filter_data_fields( $data );
+               $this->maybe_cache_data( $data );
+
+               return $data;
+       }
+
+       /**
+        * Compile the report data into results.
+        *
+        * @param array $data The data to compile.
+        *
+        * @return array
+        */
+       public function compile_report_data( array $data ) {
+               $invoices = $this->filter_transactions_by_type( $data, 'Invoice' );
+               $payments = $this->filter_transactions_by_type( $data, 'Payment' );
+
+               $compiled_data = array(
+                       'invoices' => $this->parse_transaction_stats( $invoices ),
+                       'payments' => $this->parse_transaction_stats( $payments ),
+               );
+
+               return $compiled_data;
+       }
+
+       /**
+        * Get invoices from the WordCamp database that that have a corresponding ID in QBO.
+        *
+        * Limit the returned invoices to a specific WordCamp if the `wordcamp_id` property has been set.
+        *
+        * @return array
+        */
+       protected function get_indexed_invoices( array $ids = array() ) {
+               /** @var \wpdb $wpdb */
+               global $wpdb;
+
+               $table_name = self::get_index_table_name();
+
+               $where_clause = array();
+               $where_values = array();
+               $where        = '';
+
+               // Invoices that don't have a corresponding entity in QBO yet have a `qbo_invoice_id` value of 0.
+               $where_clause[]  = "qbo_invoice_id != 0";
+
+               if ( $this->wordcamp_site_id ) {
+                       $where_clause[] = "blog_id = %d";
+                       $where_values[] = $this->wordcamp_site_id;
+               }
+
+               if ( ! empty( $where_clause ) ) {
+                       $where = 'WHERE ' . implode( ' AND ', $where_clause );
+               }
+
+               $sql = "
+                       SELECT qbo_invoice_id, blog_id, invoice_id, wordcamp_name, invoice_title, sponsor_name
+                       FROM $table_name
+               " . $where;
+
+               $query   = $wpdb->prepare( $sql, $where_values );
+               $results = $wpdb->get_results( $query, ARRAY_A );
+
+               if ( ! empty( $results ) ) {
+                       // Key the invoices array with the `qbo_invoice_id` field.
+                       $results = array_combine(
+                               wp_list_pluck( $results, 'qbo_invoice_id' ),
+                               $results
+                       );
+               }
+
+               return $results;
+       }
+
+       /**
+        * Get all the invoices created in QBO within the given date range.
+        *
+        * @param array $indexed_invoices Relevant invoices that are indexed in the WordCamp system.
+        *
+        * @return array|\WP_Error An array of invoices or an error object.
+        */
+       protected function get_qbo_invoices( array $indexed_invoices ) {
+               $qbo = new Utilities\QBO_Client();
+
+               $invoices = $qbo->get_transactions_by_date( 'Invoice', $this->start_date, $this->end_date );
+
+               if ( is_wp_error( $invoices ) ) {
+                       return $invoices;
+               }
+
+               $indexed_invoice_ids = array_keys( $indexed_invoices );
+
+               // Filter out invoices that aren't in the index, or aren't for the specified WordCamp.
+               $invoices = array_filter( $invoices, function( $invoice ) use ( $indexed_invoice_ids ) {
+                       if ( in_array( absint( $invoice['Id'] ), $indexed_invoice_ids, true ) ) {
+                               return true;
+                       }
+
+                       return false;
+               } );
+
+               // Normalize data keys.
+               $normalized_invoices = array();
+
+               foreach ( $invoices as $invoice ) {
+                       $normalized_invoices[] = array(
+                               'date'          => $invoice['TxnDate'],
+                               'type'          => 'Invoice',
+                               'invoice_id'    => $invoice['Id'],
+                               'wordcamp_name' => $indexed_invoices[ $invoice['Id'] ]['wordcamp_name'],
+                               'sponsor_name'  => $indexed_invoices[ $invoice['Id'] ]['sponsor_name'],
+                               'invoice_title' => $indexed_invoices[ $invoice['Id'] ]['invoice_title'],
+                               'currency'      => $invoice['CurrencyRef']['value'],
+                               'amount'        => $invoice['TotalAmt'],
+                       );
+               }
+
+               return $normalized_invoices;
+       }
+
+       /**
+        * Get all the payment transactions created in QBO within the given date range.
+        *
+        * @param array $indexed_invoices Relevant invoices that are indexed in the WordCamp system.
+        *
+        * @return array|\WP_Error An array of payments or an error object.
+        */
+       protected function get_qbo_payments( array $indexed_invoices ) {
+               $qbo = new Utilities\QBO_Client();
+
+               $payments = $qbo->get_transactions_by_date( 'Payment', $this->start_date, $this->end_date );
+
+               if ( is_wp_error( $payments ) ) {
+                       return $payments;
+               }
+
+               $indexed_invoice_ids = array_keys( $indexed_invoices );
+
+               // Isolate the ID of the invoice each payment is for.
+               array_walk( $payments, function( &$payment ) use ( $indexed_invoice_ids ) {
+                       $payment['invoice_id'] = 0;
+
+                       if ( isset( $payment['Line'] ) ) {
+                               foreach ( $payment['Line'] as $line ) {
+                                       if ( ! isset( $line['LinkedTxn'] ) ) {
+                                               continue;
+                                       }
+
+                                       foreach ( $line['LinkedTxn'] as $txn ) {
+                                               if ( 'Invoice' === $txn['TxnType'] && in_array( absint( $txn['TxnId'] ), $indexed_invoice_ids, true ) ) {
+                                                       $payment['invoice_id'] = absint( $txn['TxnId'] );
+                                                       break 2;
+                                               }
+                                       }
+                               }
+
+                               unset( $payment['Line'] );
+                       }
+               } );
+
+               // Filter out payments that aren't for relevant invoices.
+               $payments = array_filter( $payments, function ( $payment ) {
+                       if ( 0 !== $payment['invoice_id'] ) {
+                               return true;
+                       }
+
+                       return false;
+               } );
+
+               // Normalize data keys.
+               $normalized_payments = array();
+
+               foreach ( $payments as $payment ) {
+                       $normalized_payments[] = array(
+                               'date'          => $payment['TxnDate'],
+                               'type'          => 'Payment',
+                               'invoice_id'    => $payment['invoice_id'],
+                               'wordcamp_name' => $indexed_invoices[ $payment['invoice_id'] ]['wordcamp_name'],
+                               'sponsor_name'  => $indexed_invoices[ $payment['invoice_id'] ]['sponsor_name'],
+                               'invoice_title' => $indexed_invoices[ $payment['invoice_id'] ]['invoice_title'],
+                               'currency'      => $payment['CurrencyRef']['value'],
+                               'amount'        => $payment['TotalAmt'],
+                       );
+               }
+
+               return $normalized_payments;
+       }
+
+       /**
+        * Out of an array of transactions, generate an array of only one type of transaction.
+        *
+        * @param array  $transactions The transactions to filter.
+        * @param string $type         The type to filter for.
+        *
+        * @return array
+        */
+       protected function filter_transactions_by_type( array $transactions, $type ) {
+               return array_filter( $transactions, function( $transaction ) use ( $type ) {
+                       if ( $type === $transaction['type'] ) {
+                               return true;
+                       }
+
+                       return false;
+               } );
+       }
+
+       /**
+        * Gather statistics about a given collection of transactions.
+        *
+        * @param array $transactions A list of invoice or payment entities from QBO.
+        *
+        * @return array
+        */
+       protected function parse_transaction_stats( array $transactions ) {
+               $total_count = count( $transactions );
+
+               $amount_by_currency = array();
+
+               foreach ( $transactions as $transaction ) {
+                       $currency = $transaction['currency'];
+                       $amount   = $transaction['amount'];
+
+                       if ( ! isset( $amount_by_currency[ $currency ] ) ) {
+                               $amount_by_currency[ $currency ] = 0;
+                       }
+
+                       $amount_by_currency[ $currency ] += $amount;
+               }
+
+               ksort( $amount_by_currency );
+
+               $converted_amounts = array();
+
+               foreach ( $amount_by_currency as $currency => $amount ) {
+                       if ( 'USD' === $currency ) {
+                               $converted_amounts[ $currency ] = $amount;
+                       } else {
+                               $converted_amounts[ $currency ] = 0;
+
+                               $conversion = $this->xrt->convert( $amount, $currency, $this->end_date->format( 'Y-m-d' ) );
+
+                               if ( is_wp_error( $conversion ) ) {
+                                       // Unsupported currencies are ok, but other errors should be surfaced.
+                                       if ( 'unknown_currency' !== $conversion->get_error_code() ) {
+                                               $this->merge_errors( $this->error, $conversion );
+                                       }
+                               } else {
+                                       $converted_amounts[ $currency ] = $conversion->USD;
+                               }
+                       }
+               }
+
+               $total_amount_converted = array_reduce( $converted_amounts, function( $carry, $item ) {
+                       return $carry + floatval( $item );
+               }, 0 );
+
+               return array(
+                       'total_count'            => $total_count,
+                       'amount_by_currency'     => $amount_by_currency,
+                       'converted_amounts'      => $converted_amounts,
+                       'total_amount_converted' => $total_amount_converted,
+               );
+       }
+
+       /**
+        * The name of the table containing an index of all sponsor invoices in the network.
+        *
+        * Wrapper method to help minimize coupling with the WordCamp Payments Network plugin.
+        *
+        * If this needs to be used outside of this class, move it to utilities.php.
+        *
+        * @return string
+        */
+       protected static function get_index_table_name() {
+               // Ensure the needed file is loaded.
+               $wordcamp_payments_network_path = trailingslashit( str_replace( 'wordcamp-reports', 'wordcamp-payments-network', Reports\PLUGIN_DIR ) );
+               require_once $wordcamp_payments_network_path . 'includes/sponsor-invoices-dashboard.php';
+
+               return WCBD_Sponsor_Invoices\get_index_table_name();
+       }
+
+       /**
+        * Render an HTML version of the report output.
+        *
+        * @return void
+        */
+       public function render_html() {
+               $data       = $this->compile_report_data( $this->get_data() );
+               $start_date = $this->start_date;
+               $end_date   = $this->end_date;
+
+               $wordcamp_name = ( $this->wordcamp_site_id ) ? get_wordcamp_name( $this->wordcamp_site_id ) : '';
+               $invoices      = $data['invoices'];
+               $payments      = $data['payments'];
+
+               if ( ! empty( $this->error->get_error_messages() ) ) {
+                       $this->render_error_html();
+               } else {
+                       include Reports\get_views_dir_path() . 'html/sponsor-invoices.php';
+               }
+       }
+
+       /**
+        * Render the page for this report in the WP Admin.
+        *
+        * @return void
+        */
+       public static function render_admin_page() {
+               $start_date  = filter_input( INPUT_POST, 'start-date' );
+               $end_date    = filter_input( INPUT_POST, 'end-date' );
+               $wordcamp_id = filter_input( INPUT_POST, 'wordcamp-id' );
+               $refresh     = filter_input( INPUT_POST, 'refresh', FILTER_VALIDATE_BOOLEAN );
+               $action      = filter_input( INPUT_POST, 'action' );
+               $nonce       = filter_input( INPUT_POST, self::$slug . '-nonce' );
+
+               $report = null;
+
+               if ( 'Show results' === $action
+                    && wp_verify_nonce( $nonce, 'run-report' )
+                    && current_user_can( 'manage_network' )
+               ) {
+                       $options = array(
+                               'earliest_start' => new \DateTime( '2016-01-01' ), // No invoices in QBO before 2016.
+                       );
+
+                       if ( $refresh ) {
+                               $options['flush_cache'] = true;
+                       }
+
+                       $report = new self( $start_date, $end_date, $wordcamp_id, $options );
+
+                       // The report adjusts the end date in some circumstances.
+                       if ( empty( $report->error->get_error_messages() ) ) {
+                               $end_date = $report->end_date->format( 'Y-m-d' );
+                       }
+               }
+
+               include Reports\get_views_dir_path() . 'report/sponsor-invoices.php';
+       }
+
+       /**
+        * Export the report data to a file.
+        *
+        * @return void
+        */
+       public static function export_to_file() {
+               $start_date  = filter_input( INPUT_POST, 'start-date' );
+               $end_date    = filter_input( INPUT_POST, 'end-date' );
+               $wordcamp_id = filter_input( INPUT_POST, 'wordcamp-id' );
+               $refresh     = filter_input( INPUT_POST, 'refresh', FILTER_VALIDATE_BOOLEAN );
+               $action      = filter_input( INPUT_POST, 'action' );
+               $nonce       = filter_input( INPUT_POST, self::$slug . '-nonce' );
+
+               $report = null;
+
+               if ( 'Export CSV' !== $action ) {
+                       return;
+               }
+
+               if ( wp_verify_nonce( $nonce, 'run-report' ) && current_user_can( 'manage_network' ) ) {
+                       $options = array(
+                               'earliest_start' => new \DateTime( '2016-01-01' ), // No invoices in QBO before 2016.
+                       );
+
+                       if ( $refresh ) {
+                               $options['flush_cache'] = true;
+                       }
+
+                       $report = new self( $start_date, $end_date, $wordcamp_id, $options );
+
+                       // The report adjusts the end date in some circumstances.
+                       if ( empty( $report->error->get_error_messages() ) ) {
+                               $end_date = $report->end_date->format( 'Y-m-d' );
+                       }
+
+                       $filename = array( $report::$name );
+                       if ( $report->wordcamp_site_id ) {
+                               $filename[] = get_wordcamp_name( $report->wordcamp_site_id );
+                       }
+                       $filename[] = $report->start_date->format( 'Y-m-d' );
+                       $filename[] = $report->end_date->format( 'Y-m-d' );
+
+                       $headers = array( 'Date', 'Type', 'QBO Invoice ID', 'WordCamp', 'Sponsor', 'Invoice Title', 'Currency', 'Amount' );
+
+                       $data = $report->get_data();
+
+                       $exporter = new Reports\Export_CSV( array(
+                               'filename' => $filename,
+                               'headers'  => $headers,
+                               'data'     => $data,
+                       ) );
+
+                       if ( ! empty( $report->error->get_error_messages() ) ) {
+                               $exporter->error = $report->merge_errors( $report->error, $exporter->error );
+                       }
+
+                       $exporter->emit_file();
+               } // End if().
+       }
+
+       /**
+        * Determine whether to render the public report form.
+        *
+        * This shortcode is limited to use on pages.
+        *
+        * @return string HTML content to display shortcode.
+        */
+       public static function handle_shortcode() {
+               $html = '';
+
+               if ( 'page' === get_post_type() ) {
+                       ob_start();
+                       self::render_public_page();
+                       $html = ob_get_clean();
+               }
+
+               return $html;
+       }
+
+       /**
+        * Render the page for this report on the front end.
+        *
+        * @return void
+        */
+       public static function render_public_page() {
+               // Apparently 'year' is a reserved URL parameter on the front end, so we prepend 'report-'.
+               $year        = filter_input( INPUT_GET, 'report-year', FILTER_VALIDATE_INT );
+               $period      = filter_input( INPUT_GET, 'period' );
+               $wordcamp_id = filter_input( INPUT_GET, 'wordcamp-id' );
+               $action      = filter_input( INPUT_GET, 'action' );
+
+               $years    = self::year_array( absint( date( 'Y' ) ), 2016 );
+               $quarters = self::quarter_array();
+               $months   = self::month_array();
+
+               if ( ! $year ) {
+                       $year = absint( date( 'Y' ) );
+               }
+
+               if ( ! $period ) {
+                       $period = absint( date( 'm' ) );
+               }
+
+               $report = null;
+
+               if ( 'Show results' === $action ) {
+                       $range = self::convert_time_period_to_date_range( $year, $period );
+
+                       $options = array(
+                               'earliest_start' => new \DateTime( '2016-01-01' ), // No invoices in QBO before 2016.
+                       );
+
+                       $report = new self( $range['start_date'], $range['end_date'], $wordcamp_id, $options );
+               }
+
+               include Reports\get_views_dir_path() . 'public/sponsor-invoices.php';
+       }
+}
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsclassesreportclasssponsorshipgrantsphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/classes/report/class-sponsorship-grants.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/classes/report/class-sponsorship-grants.php                              (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/classes/report/class-sponsorship-grants.php        2018-02-16 16:49:05 UTC (rev 6662)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,491 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Sponsorship Grants.
+ *
+ * @package WordCamp\Reports
+ */
+
+namespace WordCamp\Reports\Report;
+defined( 'WPINC' ) || die();
+
+use WordCamp\Reports;
+use WordCamp\Reports\Report;
+use WordCamp\Utilities;
+
+/**
+ * Class Sponsorship_Grants
+ *
+ * @package WordCamp\Reports\Report
+ */
+class Sponsorship_Grants extends Date_Range {
+       /**
+        * Report name.
+        *
+        * @var string
+        */
+       public static $name = 'Global Sponsorship Grants';
+
+       /**
+        * Report slug.
+        *
+        * @var string
+        */
+       public static $slug = 'sponsorship-grants';
+
+       /**
+        * Report description.
+        *
+        * @var string
+        */
+       public static $description = 'Global Sponsorship Grant amounts and a list of recipients.';
+
+       /**
+        * Report methodology.
+        *
+        * @var string
+        */
+       public static $methodology = "
+               <ol>
+                       <li>Use the WordCamp Status report to pull a list of WordCamps that received the status of \"Needs Contract to be Signed\" sometime during the specified date range.</li>
+                       <li>Parse the status log of each matched WordCamp to determine when the sponsorship grant was approved.</li>
+               </ol>
+       ";
+
+       /**
+        * Report group.
+        *
+        * @var string
+        */
+       public static $group = 'finance';
+
+       /**
+        * Shortcode tag for outputting the public report form.
+        *
+        * @var string
+        */
+       public static $shortcode_tag = 'sponsorship_grants_report';
+
+       /**
+        * WordCamp post ID.
+        *
+        * @var int The ID of the WordCamp post for this report.
+        */
+       public $wordcamp_id = 0;
+
+       /**
+        * WordCamp site ID.
+        *
+        * @var int The ID of the WordCamp site where the invoices are located.
+        */
+       public $wordcamp_site_id = 0;
+
+       /**
+        * Currency exchange rate client.
+        *
+        * @var Utilities\Currency_XRT_Client Utility to handle currency conversion.
+        */
+       protected $xrt = null;
+
+       /**
+        * Data fields that can be visible in a public context.
+        *
+        * @var array An associative array of key/default value pairs.
+        */
+       protected $public_data_fields = array(
+               'timestamp' => 0,
+               'id'        => 0,
+               'name'      => '',
+               'currency'  => '',
+               'amount'    => 0,
+       );
+
+       /**
+        * Sponsorship_Grants constructor.
+        *
+        * @param string $start_date  The start of the date range for the report.
+        * @param string $end_date    The end of the date range for the report.
+        * @param int    $wordcamp_id Optional. The ID of a WordCamp post to limit this report to.
+        * @param array  $options     {
+        *     Optional. Additional report parameters.
+        *     See Base::__construct and Date_Range::__construct for additional parameters.
+        * }
+        */
+       public function __construct( $start_date, $end_date, $wordcamp_id = 0, array $options = array() ) {
+               parent::__construct( $start_date, $end_date, $options );
+
+               $this->xrt = new Utilities\Currency_XRT_Client();
+
+               if ( $wordcamp_id && $this->validate_wordcamp_id( $wordcamp_id ) ) {
+                       $this->wordcamp_id      = $wordcamp_id;
+                       $this->wordcamp_site_id = get_wordcamp_site_id( get_post( $wordcamp_id ) );
+               }
+       }
+
+       /**
+        * Generate a cache key.
+        *
+        * @return string
+        */
+       protected function get_cache_key() {
+               $cache_key = parent::get_cache_key();
+
+               if ( $this->wordcamp_id ) {
+                       $cache_key .= '_' . $this->wordcamp_id;
+               }
+
+               return $cache_key;
+       }
+
+       /**
+        * Query and parse the data for the report.
+        *
+        * @return array
+        */
+       public function get_data() {
+               // Bail if there are errors.
+               if ( ! empty( $this->error->get_error_messages() ) ) {
+                       return array();
+               }
+
+               // Maybe use cached data.
+               $data = $this->maybe_get_cached_data();
+               if ( is_array( $data ) ) {
+                       return $data;
+               }
+
+               $wordcamps = $this->get_wordcamps();
+               $data      = array();
+
+               foreach ( $wordcamps as $wordcamp_id => $wordcamp ) {
+                       $timestamp = $this->get_grant_timestamp( $wordcamp['logs'] );
+                       $currency  = get_post_meta( $wordcamp_id, 'Global Sponsorship Grant Currency', true );
+                       $amount    = get_post_meta( $wordcamp_id, 'Global Sponsorship Grant Amount', true );
+
+                       if ( $timestamp && $currency && $amount ) {
+                               $data[] = array(
+                                       'timestamp' => $timestamp,
+                                       'id'        => $wordcamp_id,
+                                       'name'      => $wordcamp['name'],
+                                       'currency'  => $currency,
+                                       'amount'    => $amount,
+                               );
+                       }
+               }
+
+               // Sort grants in chronological order.
+               usort( $data, function( $a, $b ) {
+                       if ( $a['timestamp'] === $b['timestamp'] ) {
+                               return 0;
+                       }
+
+                       return ( $a['timestamp'] > $b['timestamp'] ) ? 1 : -1;
+               } );
+
+               $data = $this->filter_data_fields( $data );
+               $this->maybe_cache_data( $data );
+
+               return $data;
+       }
+
+       /**
+        * Compile the report data into results.
+        *
+        * @param array $data The data to compile.
+        *
+        * @return array
+        */
+       public function compile_report_data( array $data ) {
+               $compiled_data = $this->derive_totals_from_grant_amounts( $data );
+
+               return $compiled_data;
+       }
+
+       /**
+        * Get a list of WordCamps that might have received a grant during the given date range.
+        *
+        * Camps are considered to have officially received their grants when their status changes to
+        * "Needs Contract to be Signed".
+        *
+        * Uses the WordCamp Status report to find camps that were set to the relevant status during the
+        * given date range.
+        *
+        * @return array
+        */
+       protected function get_wordcamps() {
+               $status_report = new Report\WordCamp_Status(
+                       $this->start_date->format( 'Y-m-d' ),
+                       $this->end_date->format( 'Y-m-d' ),
+                       'wcpt-needs-contract'
+               );
+
+               $data = $status_report->get_data();
+
+               if ( $this->wordcamp_id ) {
+                       if ( array_key_exists( $this->wordcamp_id, $data ) ) {
+                               return array( $this->wordcamp_id => $data[ $this->wordcamp_id ] );
+                       } else {
+                               return array();
+                       }
+               }
+
+               return $data;
+       }
+
+       /**
+        * Get the timestamp when a camp officially received its grant.
+        *
+        * @param array $logs A WordCamp's status logs.
+        *
+        * @return int
+        */
+       protected function get_grant_timestamp( array $logs ) {
+               $timestamp = 0;
+
+               $filtered_logs = array_filter( $logs, function( $entry ) {
+                       return preg_match( '/Needs Contract to be Signed$/', $entry['message'] );
+               } );
+
+               if ( ! empty( $filtered_logs ) ) {
+                       $log = array_shift( $filtered_logs );
+
+                       if ( isset( $log['timestamp'] ) ) {
+                               $timestamp = $log['timestamp'];
+                       }
+               }
+
+               return $timestamp;
+       }
+
+       /**
+        * Aggregate the number and amounts of Global Sponsorship Grants.
+        *
+        * @param array $grants The grants to aggregate.
+        *
+        * @return array
+        */
+       protected function derive_totals_from_grant_amounts( $grants ) {
+               $data = array(
+                       'grant_count'              => 0,
+                       'total_amount_by_currency' => array(),
+                       'converted_amounts'        => array(),
+                       'total_amount_converted'   => 0,
+               );
+
+               $currencies = array();
+
+               foreach ( $grants as $grant ) {
+                       if ( ! in_array( $grant['currency'], $currencies, true ) ) {
+                               $data['total_amount_by_currency'][ $grant['currency'] ] = 0;
+                               $currencies[]                                           = $grant['currency'];
+                       }
+
+                       $data['grant_count'] ++;
+                       $data['total_amount_by_currency'][ $grant['currency'] ] += floatval( $grant['amount'] );
+               }
+
+               foreach ( $data['total_amount_by_currency'] as $currency => $amount ) {
+                       if ( 'USD' === $currency ) {
+                               $data['converted_amounts'][ $currency ] = $amount;
+                       } else {
+                               $data['converted_amounts'][ $currency ] = 0;
+
+                               $conversion = $this->xrt->convert( $amount, $currency, $this->end_date->format( 'Y-m-d' ) );
+
+                               if ( is_wp_error( $conversion ) ) {
+                                       // Unsupported currencies are ok, but other errors should be surfaced.
+                                       if ( 'unknown_currency' !== $conversion->get_error_code() ) {
+                                               $this->merge_errors( $this->error, $conversion );
+                                       }
+                               } else {
+                                       $data['converted_amounts'][ $currency ] = $conversion->USD;
+                               }
+                       }
+               }
+
+               $data['total_amount_converted'] = array_reduce( $data['converted_amounts'], function( $carry, $item ) {
+                       return $carry + floatval( $item );
+               }, 0 );
+
+               return $data;
+       }
+
+       /**
+        * Render an HTML version of the report output.
+        *
+        * @return void
+        */
+       public function render_html() {
+               $data          = $this->get_data();
+               $compiled_data = $this->compile_report_data( $data );
+               $start_date    = $this->start_date;
+               $end_date      = $this->end_date;
+
+               $wordcamp_name = ( $this->wordcamp_site_id ) ? get_wordcamp_name( $this->wordcamp_site_id ) : '';
+
+               if ( ! empty( $this->error->get_error_messages() ) ) {
+                       $this->render_error_html();
+               } else {
+                       include Reports\get_views_dir_path() . 'html/sponsorship-grants.php';
+               }
+       }
+
+       /**
+        * Render the page for this report in the WP Admin.
+        *
+        * @return void
+        */
+       public static function render_admin_page() {
+               $start_date  = filter_input( INPUT_POST, 'start-date' );
+               $end_date    = filter_input( INPUT_POST, 'end-date' );
+               $wordcamp_id = filter_input( INPUT_POST, 'wordcamp-id' );
+               $refresh     = filter_input( INPUT_POST, 'refresh', FILTER_VALIDATE_BOOLEAN );
+               $action      = filter_input( INPUT_POST, 'action' );
+               $nonce       = filter_input( INPUT_POST, self::$slug . '-nonce' );
+
+               $report = null;
+
+               if ( 'Show results' === $action
+                    && wp_verify_nonce( $nonce, 'run-report' )
+                    && current_user_can( 'manage_network' )
+               ) {
+                       $options = array(
+                               'earliest_start' => new \DateTime( '2017-01-01' ), // Currently no sponsorship grant data before 2017.
+                       );
+
+                       if ( $refresh ) {
+                               $options['flush_cache'] = true;
+                       }
+
+                       $report = new self( $start_date, $end_date, $wordcamp_id, $options );
+
+                       // The report adjusts the end date in some circumstances.
+                       if ( empty( $report->error->get_error_messages() ) ) {
+                               $end_date = $report->end_date->format( 'Y-m-d' );
+                       }
+               }
+
+               include Reports\get_views_dir_path() . 'report/sponsorship-grants.php';
+       }
+
+       /**
+        * Export the report data to a file.
+        *
+        * @return void
+        */
+       public static function export_to_file() {
+               $start_date  = filter_input( INPUT_POST, 'start-date' );
+               $end_date    = filter_input( INPUT_POST, 'end-date' );
+               $wordcamp_id = filter_input( INPUT_POST, 'wordcamp-id' );
+               $refresh     = filter_input( INPUT_POST, 'refresh', FILTER_VALIDATE_BOOLEAN );
+               $action      = filter_input( INPUT_POST, 'action' );
+               $nonce       = filter_input( INPUT_POST, self::$slug . '-nonce' );
+
+               $report = null;
+
+               if ( 'Export CSV' !== $action ) {
+                       return;
+               }
+
+               if ( wp_verify_nonce( $nonce, 'run-report' ) && current_user_can( 'manage_network' ) ) {
+                       $options = array(
+                               'earliest_start' => new \DateTime( '2017-01-01' ), // Currently no sponsorship grant data before 2017.
+                       );
+
+                       if ( $refresh ) {
+                               $options['flush_cache'] = true;
+                       }
+
+                       $report = new self( $start_date, $end_date, $wordcamp_id, $options );
+
+                       // The report adjusts the end date in some circumstances.
+                       if ( empty( $report->error->get_error_messages() ) ) {
+                               $end_date = $report->end_date->format( 'Y-m-d' );
+                       }
+
+                       $filename = array( $report::$name );
+                       if ( $report->wordcamp_site_id ) {
+                               $filename[] = get_wordcamp_name( $report->wordcamp_site_id );
+                       }
+                       $filename[] = $report->start_date->format( 'Y-m-d' );
+                       $filename[] = $report->end_date->format( 'Y-m-d' );
+
+                       $headers = array( 'Date', 'WordCamp ID', 'WordCamp Name', 'Currency', 'Amount' );
+
+                       $data = $report->get_data();
+
+                       array_walk( $data, function( &$grant ) {
+                               $grant['timestamp'] = date( 'Y-m-d', $grant['timestamp'] );
+                       } );
+
+                       $exporter = new Utilities\Export_CSV( array(
+                               'filename' => $filename,
+                               'headers'  => $headers,
+                               'data'     => $data,
+                       ) );
+
+                       if ( ! empty( $report->error->get_error_messages() ) ) {
+                               $exporter->error = $report->merge_errors( $report->error, $exporter->error );
+                       }
+
+                       $exporter->emit_file();
+               } // End if().
+       }
+
+       /**
+        * Determine whether to render the public report form.
+        *
+        * This shortcode is limited to use on pages.
+        *
+        * @return string HTML content to display shortcode.
+        */
+       public static function handle_shortcode() {
+               $html = '';
+
+               if ( 'page' === get_post_type() ) {
+                       ob_start();
+                       self::render_public_page();
+                       $html = ob_get_clean();
+               }
+
+               return $html;
+       }
+
+       /**
+        * Render the page for this report on the front end.
+        *
+        * @return void
+        */
+       public static function render_public_page() {
+               // Apparently 'year' is a reserved URL parameter on the front end, so we prepend 'report-'.
+               $year        = filter_input( INPUT_GET, 'report-year', FILTER_VALIDATE_INT );
+               $period      = filter_input( INPUT_GET, 'period' );
+               $wordcamp_id = filter_input( INPUT_GET, 'wordcamp-id' );
+               $action      = filter_input( INPUT_GET, 'action' );
+
+               $years    = self::year_array( absint( date( 'Y' ) ), 2017 );
+               $quarters = self::quarter_array();
+               $months   = self::month_array();
+
+               if ( ! $year ) {
+                       $year = absint( date( 'Y' ) );
+               }
+
+               if ( ! $period ) {
+                       $period = absint( date( 'm' ) );
+               }
+
+               $report = null;
+
+               if ( 'Show results' === $action ) {
+                       $range = self::convert_time_period_to_date_range( $year, $period );
+
+                       $options = array(
+                               'earliest_start' => new \DateTime( '2017-01-01' ), // Currently no sponsorship grant data before 2017.
+                       );
+
+                       $report = new self( $range['start_date'], $range['end_date'], $wordcamp_id, $options );
+               }
+
+               include Reports\get_views_dir_path() . 'public/sponsorship-grants.php';
+       }
+}
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsclassesreportclassticketrevenuephp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/classes/report/class-ticket-revenue.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/classes/report/class-ticket-revenue.php                          (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/classes/report/class-ticket-revenue.php    2018-02-16 16:49:05 UTC (rev 6662)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,668 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Ticket Revenue.
+ *
+ * @package WordCamp\Reports
+ */
+
+namespace WordCamp\Reports\Report;
+defined( 'WPINC' ) || die();
+
+use WordCamp\Reports;
+use WordCamp\Utilities;
+
+/**
+ * Class Ticket_Revenue
+ *
+ * @package WordCamp\Reports\Report
+ */
+class Ticket_Revenue extends Date_Range {
+       /**
+        * Report name.
+        *
+        * @var string
+        */
+       public static $name = 'Ticket Revenue';
+
+       /**
+        * Report slug.
+        *
+        * @var string
+        */
+       public static $slug = 'ticket-revenue';
+
+       /**
+        * Report description.
+        *
+        * @var string
+        */
+       public static $description = 'A breakdown of WordCamp ticket revenue during a given time period.';
+
+       /**
+        * Report methodology.
+        *
+        * @var string
+        */
+       public static $methodology = "
+               <ol>
+                       <li>Query the CampTix events log for attendee status changes to \"publish\" or \"refund\" during the specified date range.</li>
+                       <li>Query each WordCamp site with matched events and retrieve ticket data related to each event.</li>
+                       <li>Append the ticket data to the event data.</li>
+                       <li>Group the events based on whether the transaction was handled by WPCS. Assume all transactions in a currency supported by PayPal were handled by WPCS.</li>
+               </ol>
+       ";
+
+       /**
+        * Report group.
+        *
+        * @var string
+        */
+       public static $group = 'finance';
+
+       /**
+        * Shortcode tag for outputting the public report form.
+        *
+        * @var string
+        */
+       public static $shortcode_tag = 'ticket_revenue_report';
+
+       /**
+        * REST route for this report.
+        *
+        * @var string
+        */
+       //public static $rest_base = 'ticket-revenue';
+
+       /**
+        * WordCamp post ID.
+        *
+        * @var int The ID of the WordCamp post for this report.
+        */
+       public $wordcamp_id = 0;
+
+       /**
+        * WordCamp site ID.
+        *
+        * @var int The ID of the WordCamp site where the invoices are located.
+        */
+       public $wordcamp_site_id = 0;
+
+       /**
+        * Currency exchange rate client.
+        *
+        * @var Utilities\Currency_XRT_Client Utility to handle currency conversion.
+        */
+       protected $xrt = null;
+
+       /**
+        * Data fields that can be visible in a public context.
+        *
+        * @var array An associative array of key/default value pairs.
+        */
+       protected $public_data_fields = array(
+               'timestamp'        => '',
+               'blog_id'          => 0,
+               'object_id'        => 0,
+               'type'             => '',
+               'method'           => '',
+               'currency'         => '',
+               'full_price'       => 0,
+               'discounted_price' => 0,
+       );
+
+       /**
+        * Ticket_Revenue constructor.
+        *
+        * @param string $start_date  The start of the date range for the report.
+        * @param string $end_date    The end of the date range for the report.
+        * @param int    $wordcamp_id Optional. The ID of a WordCamp post to limit this report to.
+        * @param array  $options     {
+        *     Optional. Additional report parameters.
+        *     See Base::__construct and Date_Range::__construct for additional parameters.
+        * }
+        */
+       public function __construct( $start_date, $end_date, $wordcamp_id = 0, array $options = array() ) {
+               parent::__construct( $start_date, $end_date, $options );
+
+               $this->xrt = new Utilities\Currency_XRT_Client();
+
+               if ( $wordcamp_id && $this->validate_wordcamp_id( $wordcamp_id ) ) {
+                       $this->wordcamp_id      = $wordcamp_id;
+                       $this->wordcamp_site_id = get_wordcamp_site_id( get_post( $wordcamp_id ) );
+               }
+       }
+
+       /**
+        * Generate a cache key.
+        *
+        * @return string
+        */
+       protected function get_cache_key() {
+               $cache_key = parent::get_cache_key();
+
+               if ( $this->wordcamp_id ) {
+                       $cache_key .= '_' . $this->wordcamp_id;
+               }
+
+               return $cache_key;
+       }
+
+       /**
+        * Query and parse the data for the report.
+        *
+        * @return array
+        */
+       public function get_data() {
+               // Bail if there are errors.
+               if ( ! empty( $this->error->get_error_messages() ) ) {
+                       return array();
+               }
+
+               // Maybe use cached data.
+               $data = $this->maybe_get_cached_data();
+               if ( is_array( $data ) ) {
+                       return $data;
+               }
+
+               // This script is a memory hog for date intervals larger than ~2 months.
+               // @todo Maybe find a way to run this without having to hack the memory limit.
+               ini_set( 'memory_limit', '512M' );
+
+               $data = $this->get_indexed_camptix_events( array(
+                       'Attendee status has been changed to publish',
+                       'Attendee status has been changed to refund',
+               ) );
+
+               $tickets_by_site = $this->sort_indexed_ticket_ids_by_site( $data );
+               $ticket_details  = array();
+
+               foreach ( $tickets_by_site as $blog_id => $ticket_ids ) {
+                       $ticket_details = array_merge( $ticket_details, $this->get_ticket_details( $blog_id, $ticket_ids ) );
+               }
+
+               array_walk( $data, function( &$event ) use ( $ticket_details ) {
+                       if ( false !== strpos( $event['message'], 'publish' ) ) {
+                               $event['type'] = 'Purchase';
+                               unset( $event['message'] );
+                       } elseif ( false !== strpos( $event['message'], 'refund' ) ) {
+                               $event['type'] = 'Refund';
+                               unset( $event['message'] );
+                       }
+
+                       $details_key = $event['blog_id'] . '_' . $event['object_id'];
+
+                       if ( isset( $ticket_details[ $details_key ] ) ) {
+                               $event['method']           = $ticket_details[ $details_key ]['method'];
+                               $event['currency']         = $ticket_details[ $details_key ]['currency'];
+                               $event['full_price']       = $ticket_details[ $details_key ]['full_price'];
+                               $event['discounted_price'] = $ticket_details[ $details_key ]['discounted_price'];
+                       }
+               } );
+
+               $data = $this->filter_data_fields( $data );
+               $this->maybe_cache_data( $data );
+
+               return $data;
+       }
+
+       /**
+        * Compile the report data into results.
+        *
+        * @param array $data The data to compile.
+        *
+        * @return array
+        */
+       public function compile_report_data( array $data ) {
+               $compiled_data = $this->derive_revenue_from_ticket_events( $data );
+
+               return $compiled_data;
+       }
+
+       /**
+        * Retrieve events from the CampTix log database table.
+        *
+        * @param array $message_filter Array of strings to search for in the event message field, using the OR operator.
+        *
+        * @return array
+        */
+       protected function get_indexed_camptix_events( array $message_filter = array() ) {
+               /** @var \wpdb $wpdb */
+               global $wpdb;
+
+               $table_name = $wpdb->base_prefix . 'camptix_log';
+
+               $where_clause = array();
+               $where_values = array();
+               $where        = '';
+
+               $where_clause[] = 'UNIX_TIMESTAMP( timestamp ) BETWEEN ' .
+                                 $this->start_date->getTimestamp() .
+                                 ' AND ' .
+                                 $this->end_date->getTimestamp();
+
+               if ( ! empty( $message_filter ) ) {
+                       $like_clause = array();
+
+                       foreach ( $message_filter as $string ) {
+                               $like_clause[] = 'message LIKE \'%%%s%%\'';
+                               $where_values[] = $string;
+                       }
+
+                       $where_clause[] = '( ' . implode( ' OR ', $like_clause ) . ' )';
+               }
+
+               if ( $this->wordcamp_site_id ) {
+                       $where_clause[] = 'blog_id = %d';
+                       $where_values[] = $this->wordcamp_site_id;
+               }
+
+               if ( ! empty( $where_clause ) ) {
+                       $where = 'WHERE ' . implode( ' AND ', $where_clause );
+               }
+
+               $sql = "
+                       SELECT timestamp, blog_id, object_id, message
+                       FROM $table_name
+               " . $where;
+
+               $query  = $wpdb->prepare( $sql, $where_values );
+               $events = $wpdb->get_results( $query, ARRAY_A );
+
+               return $events;
+       }
+
+       /**
+        * Group log event ticket IDs by their blog ID.
+        *
+        * @param array $events An array of CampTix log events/tickets.
+        *
+        * @return array
+        */
+       protected function sort_indexed_ticket_ids_by_site( $events ) {
+               $sorted = array();
+
+               foreach ( $events as $event ) {
+                       if ( ! isset( $sorted[ $event['blog_id'] ] ) ) {
+                               $sorted[ $event['blog_id'] ] = array();
+                       }
+
+                       $sorted[ $event['blog_id'] ][] = $event['object_id'];
+               }
+
+               $sorted = array_map( 'array_unique', $sorted );
+
+               return $sorted;
+       }
+
+       /**
+        * Get relevant details for a given list of tickets for a particular site.
+        *
+        * @param int   $blog_id    The ID of the site that the tickets are associated with.
+        * @param array $ticket_ids The IDs of specific tickets to get details for.
+        *
+        * @return array
+        */
+       protected function get_ticket_details( $blog_id, array $ticket_ids ) {
+               $ticket_details = array();
+               $currency       = '';
+
+               switch_to_blog( $blog_id );
+
+               $options = get_option( 'camptix_options', array() );
+
+               if ( isset( $options['currency'] ) ) {
+                       $currency = $options['currency'];
+               }
+
+               foreach ( $ticket_ids as $ticket_id ) {
+                       $ticket_details[ $blog_id . '_' . $ticket_id ] = array(
+                               'method'           => get_post_meta( $ticket_id, 'tix_payment_method', true ),
+                               'currency'         => $currency,
+                               'full_price'       => floatval( get_post_meta( $ticket_id, 'tix_ticket_price', true ) ),
+                               'discounted_price' => floatval( get_post_meta( $ticket_id, 'tix_ticket_discounted_price', true ) ),
+                       );
+
+                       clean_post_cache( $ticket_id );
+               }
+
+               restore_current_blog();
+
+               return $ticket_details;
+       }
+
+       /**
+        * Aggregate revenue totals from a list of ticket events.
+        *
+        * @param array $events The ticket events.
+        *
+        * @return array
+        */
+       protected function derive_revenue_from_ticket_events( array $events ) {
+               $initial_data = array(
+                       'tickets_sold'                => 0,
+                       'gross_revenue_by_currency'   => array(),
+                       'discounts_by_currency'       => array(),
+                       'tickets_refunded'            => 0,
+                       'amount_refunded_by_currency' => array(),
+                       'net_revenue_by_currency'     => array(),
+                       'converted_net_revenue'       => array(),
+                       'total_converted_revenue'     => 0,
+               );
+
+               $data_groups = array(
+                       'wpcs'     => array_merge( $initial_data, array(
+                               'label'       => 'WPCS ticket revenue',
+                               'description' => 'Transactions using a payment method for which WPCS has an established account.',
+                       ) ),
+                       'non_wpcs' => array_merge( $initial_data, array(
+                               'label'       => 'Non-WPCS ticket revenue',
+                               'description' => 'Transactions using a payment method for which WPCS does not have an established account.',
+                       ) ),
+                       'none'     => array_merge( $initial_data, array(
+                               'label'       => 'Ticket transactions with no payment',
+                               'description' => 'Transactions for which no payment method was recorded.',
+                       ) ),
+                       'total'    => array_merge( $initial_data, array(
+                               'label'       => 'Total ticket revenue',
+                               'description' => '',
+                       ) ),
+               );
+
+               // Assume that all transactions through a gateway for which WPCS has an account, used the WPCS account.
+               $wpcs_payment_methods = array( 'paypal', 'stripe' );
+               $currencies           = array();
+
+               foreach ( $events as $event ) {
+                       $currency = $event['currency'];
+
+                       if ( ! $event['method'] ) {
+                               $group = 'none';
+                       } elseif ( in_array( $event['method'], $wpcs_payment_methods, true ) ) {
+                               $group = 'wpcs';
+                       } else {
+                               $group = 'non_wpcs';
+                       }
+
+                       if ( ! in_array( $currency, $currencies, true ) ) {
+                               $data_groups[ $group ]['gross_revenue_by_currency'][ $currency ]   = 0;
+                               $data_groups[ $group ]['discounts_by_currency'][ $currency ]       = 0;
+                               $data_groups[ $group ]['amount_refunded_by_currency'][ $currency ] = 0;
+                               $data_groups[ $group ]['net_revenue_by_currency'][ $currency ]     = 0;
+                               $data_groups['total']['gross_revenue_by_currency'][ $currency ]    = 0;
+                               $data_groups['total']['discounts_by_currency'][ $currency ]        = 0;
+                               $data_groups['total']['amount_refunded_by_currency'][ $currency ]  = 0;
+                               $data_groups['total']['net_revenue_by_currency'][ $currency ]      = 0;
+                               $currencies[]                                                      = $currency;
+                       }
+
+                       switch ( $event['type'] ) {
+                               case 'Purchase' :
+                                       $data_groups[ $group ]['tickets_sold'] ++;
+                                       $data_groups[ $group ]['gross_revenue_by_currency'][ $currency ] += $event['full_price'];
+                                       $data_groups[ $group ]['discounts_by_currency'][ $currency ]     += $event['full_price'] - $event['discounted_price'];
+                                       $data_groups[ $group ]['net_revenue_by_currency'][ $currency ]   += $event['discounted_price'];
+                                       $data_groups['total']['tickets_sold']  ++;
+                                       $data_groups['total']['gross_revenue_by_currency'][ $currency ]  += $event['full_price'];
+                                       $data_groups['total']['discounts_by_currency'][ $currency ]      += $event['full_price'] - $event['discounted_price'];
+                                       $data_groups['total']['net_revenue_by_currency'][ $currency ]    += $event['discounted_price'];
+                                       break;
+
+                               case 'Refund' :
+                                       $data_groups[ $group ]['tickets_refunded'] ++;
+                                       $data_groups[ $group ]['amount_refunded_by_currency'][ $currency ] += $event['discounted_price'];
+                                       $data_groups[ $group ]['net_revenue_by_currency'][ $currency ]     -= $event['discounted_price'];
+                                       $data_groups['total']['tickets_refunded']  ++;
+                                       $data_groups['total']['amount_refunded_by_currency'][ $currency ]  += $event['discounted_price'];
+                                       $data_groups['total']['net_revenue_by_currency'][ $currency ]      -= $event['discounted_price'];
+                                       break;
+                       }
+               } // End foreach().
+
+               foreach ( $data_groups as &$group ) {
+                       ksort( $group['gross_revenue_by_currency'] );
+                       ksort( $group['discounts_by_currency'] );
+                       ksort( $group['amount_refunded_by_currency'] );
+                       ksort( $group['net_revenue_by_currency'] );
+
+                       foreach ( $group['net_revenue_by_currency'] as $currency => $amount ) {
+                               if ( 'USD' === $currency ) {
+                                       $group['converted_net_revenue'][ $currency ] = $amount;
+                               } else {
+                                       $group['converted_net_revenue'][ $currency ] = 0;
+
+                                       $conversion = $this->xrt->convert( $amount, $currency, $this->end_date->format( 'Y-m-d' ) );
+
+                                       if ( is_wp_error( $conversion ) ) {
+                                               // Unsupported currencies are ok, but other errors should be surfaced.
+                                               if ( 'unknown_currency' !== $conversion->get_error_code() ) {
+                                                       $this->merge_errors( $this->error, $conversion );
+                                               }
+                                       } else {
+                                               $group['converted_net_revenue'][ $currency ] = $conversion->USD;
+                                       }
+                               }
+                       }
+
+                       $group['total_converted_revenue'] = array_reduce( $group['converted_net_revenue'], function( $carry, $item ) {
+                               return $carry + floatval( $item );
+                       }, 0 );
+               }
+
+               return $data_groups;
+       }
+
+       /**
+        * Render an HTML version of the report output.
+        *
+        * @return void
+        */
+       public function render_html() {
+               $data       = $this->compile_report_data( $this->get_data() );
+               $start_date = $this->start_date;
+               $end_date   = $this->end_date;
+
+               $wordcamp_name = ( $this->wordcamp_site_id ) ? get_wordcamp_name( $this->wordcamp_site_id ) : '';
+               $wpcs          = $data['wpcs'];
+               $non_wpcs      = $data['non_wpcs'];
+               $none          = $data['none'];
+               $total         = $data['total'];
+
+               if ( ! empty( $this->error->get_error_messages() ) ) {
+                       $this->render_error_html();
+               } else {
+                       include Reports\get_views_dir_path() . 'html/ticket-revenue.php';
+               }
+       }
+
+       /**
+        * Render the page for this report in the WP Admin.
+        *
+        * @return void
+        */
+       public static function render_admin_page() {
+               $start_date  = filter_input( INPUT_POST, 'start-date' );
+               $end_date    = filter_input( INPUT_POST, 'end-date' );
+               $wordcamp_id = filter_input( INPUT_POST, 'wordcamp-id' );
+               $refresh     = filter_input( INPUT_POST, 'refresh', FILTER_VALIDATE_BOOLEAN );
+               $action      = filter_input( INPUT_POST, 'action' );
+               $nonce       = filter_input( INPUT_POST, self::$slug . '-nonce' );
+
+               $report = null;
+
+               if ( 'Show results' === $action
+                    && wp_verify_nonce( $nonce, 'run-report' )
+                    && current_user_can( 'manage_network' )
+               ) {
+                       $options = array(
+                               'earliest_start' => new \DateTime( '2015-01-01' ), // No indexed CampTix events before 2015.
+                               'max_interval'   => new \DateInterval( 'P1Y' ), // 1 year. See http://php.net/manual/en/dateinterval.construct.php.
+                       );
+
+                       if ( $refresh ) {
+                               $options['flush_cache'] = true;
+                       }
+
+                       $report = new self( $start_date, $end_date, $wordcamp_id, $options );
+
+                       // The report adjusts the end date in some circumstances.
+                       if ( empty( $report->error->get_error_messages() ) ) {
+                               $end_date = $report->end_date->format( 'Y-m-d' );
+                       }
+               }
+
+               include Reports\get_views_dir_path() . 'report/ticket-revenue.php';
+       }
+
+       /**
+        * Export the report data to a file.
+        *
+        * @return void
+        */
+       public static function export_to_file() {
+               $start_date  = filter_input( INPUT_POST, 'start-date' );
+               $end_date    = filter_input( INPUT_POST, 'end-date' );
+               $wordcamp_id = filter_input( INPUT_POST, 'wordcamp-id' );
+               $refresh     = filter_input( INPUT_POST, 'refresh', FILTER_VALIDATE_BOOLEAN );
+               $action      = filter_input( INPUT_POST, 'action' );
+               $nonce       = filter_input( INPUT_POST, self::$slug . '-nonce' );
+
+               $report = null;
+
+               if ( 'Export CSV' !== $action ) {
+                       return;
+               }
+
+               if ( wp_verify_nonce( $nonce, 'run-report' ) && current_user_can( 'manage_network' ) ) {
+                       $options = array(
+                               'earliest_start' => new \DateTime( '2015-01-01' ), // No indexed CampTix events before 2015.
+                               'max_interval'   => new \DateInterval( 'P1Y' ), // 1 year. See http://php.net/manual/en/dateinterval.construct.php.
+                       );
+
+                       if ( $refresh ) {
+                               $options['flush_cache'] = true;
+                       }
+
+                       $report = new self( $start_date, $end_date, $wordcamp_id, $options );
+
+                       // The report adjusts the end date in some circumstances.
+                       if ( empty( $report->error->get_error_messages() ) ) {
+                               $end_date = $report->end_date->format( 'Y-m-d' );
+                       }
+
+                       $filename = array( $report::$name );
+                       if ( $report->wordcamp_site_id ) {
+                               $filename[] = get_wordcamp_name( $report->wordcamp_site_id );
+                       }
+                       $filename[] = $report->start_date->format( 'Y-m-d' );
+                       $filename[] = $report->end_date->format( 'Y-m-d' );
+
+                       $headers = array( 'Date', 'Blog ID', 'Attendee ID', 'Type', 'Payment Method', 'Currency', 'Full Price', 'Discounted Price' );
+
+                       $data = $report->get_data();
+
+                       $exporter = new Utilities\Export_CSV( array(
+                               'filename' => $filename,
+                               'headers'  => $headers,
+                               'data'     => $data,
+                       ) );
+
+                       if ( ! empty( $report->error->get_error_messages() ) ) {
+                               $exporter->error = $report->merge_errors( $report->error, $exporter->error );
+                       }
+
+                       $exporter->emit_file();
+               } // End if().
+       }
+
+       /**
+        * Determine whether to render the public report form.
+        *
+        * This shortcode is limited to use on pages.
+        *
+        * @return string HTML content to display shortcode.
+        */
+       public static function handle_shortcode() {
+               $html = '';
+
+               if ( 'page' === get_post_type() ) {
+                       ob_start();
+                       self::render_public_page();
+                       $html = ob_get_clean();
+               }
+
+               return $html;
+       }
+
+       /**
+        * Render the page for this report on the front end.
+        *
+        * @return void
+        */
+       public static function render_public_page() {
+               // Apparently 'year' is a reserved URL parameter on the front end, so we prepend 'report-'.
+               $year        = filter_input( INPUT_GET, 'report-year', FILTER_VALIDATE_INT );
+               $period      = filter_input( INPUT_GET, 'period' );
+               $wordcamp_id = filter_input( INPUT_GET, 'wordcamp-id' );
+               $action      = filter_input( INPUT_GET, 'action' );
+
+               $years    = self::year_array( absint( date( 'Y' ) ), 2015 );
+               $quarters = self::quarter_array();
+               $months   = self::month_array();
+
+               if ( ! $year ) {
+                       $year = absint( date( 'Y' ) );
+               }
+
+               if ( ! $period ) {
+                       $period = absint( date( 'm' ) );
+               }
+
+               $report = null;
+
+               if ( 'Show results' === $action ) {
+                       $range = self::convert_time_period_to_date_range( $year, $period );
+
+                       $options = array(
+                               'earliest_start' => new \DateTime( '2015-01-01' ), // No indexed CampTix events before 2015.
+                               'max_interval'   => new \DateInterval( 'P1Y' ), // 1 year. See http://php.net/manual/en/dateinterval.construct.php.
+                       );
+
+                       $report = new self( $range['start_date'], $range['end_date'], $wordcamp_id, $options );
+               }
+
+               include Reports\get_views_dir_path() . 'public/ticket-revenue.php';
+       }
+
+       /**
+        * Prepare a REST response version of the report output.
+        *
+        * @todo Make the params here match the public page.
+        *
+        * @param \WP_REST_Request $request The REST request.
+        *
+        * @return \WP_REST_Response
+        */
+       public static function rest_callback( \WP_REST_Request $request ) {
+               $params = wp_parse_args( $request->get_params(), array(
+                       'start_date'  => '',
+                       'end_date'    => '',
+                       'wordcamp_id' => 0,
+               ) );
+
+               $options = array(
+                       'earliest_start' => new \DateTime( '2015-01-01' ), // No indexed CampTix events before 2015.
+                       'max_interval'   => new \DateInterval( 'P1Y' ), // 1 year. See http://php.net/manual/en/dateinterval.construct.php.
+               );
+
+               $report = new self( $params['start_date'], $params['end_date'], $params['wordcamp_id'], $options );
+
+               if ( $report->error->get_error_messages() ) {
+                       $response = self::prepare_rest_response( $report->error->errors );
+                       $response->set_status( 400 );
+               } else {
+                       $response = self::prepare_rest_response( $report->compile_report_data( $report->get_data() ) );
+               }
+
+               return $response;
+       }
+}
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsclassesreportclasswordcampstatusphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/classes/report/class-wordcamp-status.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/classes/report/class-wordcamp-status.php                         (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/classes/report/class-wordcamp-status.php   2018-02-16 16:49:05 UTC (rev 6662)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,575 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * @package WordCamp\Reports
+ */
+
+namespace WordCamp\Reports\Report;
+defined( 'WPINC' ) || die();
+
+use WordCamp_Loader;
+use WordCamp\Reports;
+
+/**
+ * Class WordCamp_Status
+ *
+ * A report class for generating a snapshot of WordCamp status activity during a specified date range.
+ *
+ * @package WordCamp\Reports\Report
+ */
+class WordCamp_Status extends Date_Range {
+       /**
+        * Report name.
+        *
+        * @var string
+        */
+       public static $name = 'WordCamp Status';
+
+       /**
+        * Report slug.
+        *
+        * @var string
+        */
+       public static $slug = 'wordcamp-status';
+
+       /**
+        * Report description.
+        *
+        * @var string
+        */
+       public static $description = 'WordCamp application status changes during a given time period.';
+
+       /**
+        * Report methodology.
+        *
+        * @var string
+        */
+       public static $methodology = "
+               <ol>
+                       <li>Retrieve all WordCamp posts that either don't have an event date yet or the event date isn't more than three months prior to the specified date range.</li>
+                       <li>Parse the status log for each WordCamp and filter out log entries that aren't within the date range.</li>
+                       <li>Filter out WordCamps that don't have any log entries within the date range and have an inactive status (rejected, cancelled, scheduled, or closed).</li>
+               </ol>
+       ";
+
+       /**
+        * Report group.
+        *
+        * @var string
+        */
+       public static $group = 'wordcamp';
+
+       /**
+        * Shortcode tag for outputting the public report form.
+        *
+        * @var string
+        */
+       public static $shortcode_tag = 'wordcamp_status_report';
+
+       /**
+        * The status to filter for in the report.
+        *
+        * @var string
+        */
+       public $status = '';
+
+       /**
+        * Data fields that can be visible in a public context.
+        *
+        * @var array An associative array of key/default value pairs.
+        */
+       protected $public_data_fields = array(
+               'name'          => '',
+               'logs'          => array(),
+               'latest_log'    => '',
+               'latest_status' => '',
+       );
+
+       /**
+        * WordCamp_Status constructor.
+        *
+        * @param string $start_date The start of the date range for the report.
+        * @param string $end_date   The end of the date range for the report.
+        * @param string $status     Optional. The status ID to filter for in the report.
+        * @param array  $options    {
+        *     Optional. Additional report parameters.
+        *     See Base::__construct and Date_Range::__construct for additional parameters.
+        *
+        *     @type array $status_subset A list of valid status IDs.
+        * }
+        */
+       public function __construct( $start_date, $end_date, $status = '', array $options = array() ) {
+               // Report-specific options.
+               $options = wp_parse_args( $options, array(
+                       'status_subset' => array(),
+               ) );
+
+               parent::__construct( $start_date, $end_date, $options );
+
+               if ( 'any' === $status ) {
+                       $status = '';
+               }
+
+               if ( $status && $this->validate_status_input( $status ) ) {
+                       $this->status = $status;
+               }
+       }
+
+       /**
+        * Validate the given status ID string.
+        *
+        * @param string $status The status ID to filter for in the report.
+        *
+        * @return bool True if the status ID is valid. Otherwise false.
+        */
+       protected function validate_status_input( $status ) {
+               if ( is_array( $this->options['status_subset'] ) && ! empty( $this->options['status_subset'] ) ) {
+                       if ( ! in_array( $status, $this->options['status_subset'], true ) ) {
+                               $this->error->add( 'invalid_status', 'Please enter a valid status ID.' );
+
+                               return false;
+                       }
+
+                       return true;
+               }
+
+               if ( ! in_array( $status, array_keys( WordCamp_Loader::get_post_statuses() ), true ) ) {
+                       $this->error->add( 'invalid_status', 'Please enter a valid status ID.' );
+
+                       return false;
+               }
+
+               return true;
+       }
+
+       /**
+        * Filter: Set the locale to en_US.
+        *
+        * Some translated strings in the wcpt plugin are used here for comparison and matching. To ensure
+        * that the matching happens correctly, we need need to prevent these strings from being converted
+        * to a different locale.
+        *
+        * @return string
+        */
+       public function set_locale_to_en_US() {
+               return 'en_US';
+       }
+
+       /**
+        * Generate a cache key.
+        *
+        * @return string
+        */
+       protected function get_cache_key() {
+               $cache_key = parent::get_cache_key();
+
+               if ( $this->status ) {
+                       $cache_key .= '_' . $this->status;
+               }
+
+               return $cache_key;
+       }
+
+       /**
+        * Query and parse the data for the report.
+        *
+        * @return array
+        */
+       public function get_data() {
+               // Bail if there are errors.
+               if ( ! empty( $this->error->get_error_messages() ) ) {
+                       return array();
+               }
+
+               // Maybe use cached data.
+               $data = $this->maybe_get_cached_data();
+               if ( is_array( $data ) ) {
+                       return $data;
+               }
+
+               // Ensure status labels can match status log messages.
+               add_filter( 'locale', array( $this, 'set_locale_to_en_US' ) );
+
+               $wordcamp_posts = $this->get_wordcamp_posts();
+               $statuses       = WordCamp_Loader::get_post_statuses();
+               $data           = array();
+
+               foreach ( $wordcamp_posts as $wordcamp ) {
+                       $logs = $this->get_wordcamp_status_logs( $wordcamp );
+
+                       // Trim log entries occurring after the date range.
+                       $logs = array_filter( $logs, function( $entry ) {
+                               if ( $entry['timestamp'] > $this->end_date->getTimestamp() ) {
+                                       return false;
+                               }
+
+                               return true;
+                       } );
+
+                       // Skip if there is no log activity before the end of the date range.
+                       if ( empty( $logs ) ) {
+                               continue;
+                       }
+
+                       $latest_log    = end( $logs );
+                       $latest_status = $this->get_log_status_result( $latest_log );
+                       reset( $logs );
+
+                       // Trim log entries occurring before the date range.
+                       $logs = array_filter( $logs, function( $entry ) {
+                               if ( $entry['timestamp'] < $this->start_date->getTimestamp() ) {
+                                       return false;
+                               }
+
+                               return true;
+                       } );
+
+                       // Skip if there is no log activity in the date range and the camp has an inactive status.
+                       if ( empty( $logs ) && ( in_array( $latest_status, self::get_inactive_statuses(), true ) || ! $latest_status ) ) {
+                               continue;
+                       }
+
+                       // Skip if there is no log entry with a resulting status that matches the status filter.
+                       if ( $this->status && $latest_status !== $this->status ) {
+                               $filtered = array_filter( $logs, function( $entry ) use ( $statuses ) {
+                                       return preg_match( '/' . preg_quote( $statuses[ $this->status ], '/' ) . '$/', $entry['message'] );
+                               } );
+
+                               if ( empty( $filtered ) ) {
+                                       continue;
+                               }
+                       }
+
+                       if ( $site_id = get_wordcamp_site_id( $wordcamp ) ) {
+                               $name = get_wordcamp_name( $site_id );
+                       } else {
+                               $name = get_the_title( $wordcamp );
+                       }
+
+                       $data[ $wordcamp->ID ] = array(
+                               'name'          => $name,
+                               'logs'          => $logs,
+                               'latest_log'    => $latest_log,
+                               'latest_status' => $latest_status,
+                       );
+               }
+
+               // Remove the temporary locale change.
+               remove_filter( 'locale', array( $this, 'set_locale_to_en_US' ) );
+
+               $data = $this->filter_data_fields( $data );
+               $this->maybe_cache_data( $data );
+
+               return $data;
+       }
+
+       /**
+        * Compile the report data into results.
+        *
+        * @param array $data The data to compile.
+        *
+        * @return array
+        */
+       public function compile_report_data( array $data ) {
+               $compiled_data = array();
+
+               $compiled_data['active_camps'] = array_filter( $data, function( $wordcamp ) {
+                       if ( ! empty( $wordcamp['logs'] ) ) {
+                               return true;
+                       }
+
+                       return false;
+               } );
+
+               $compiled_data['inactive_camps'] = array_filter( $data, function( $wordcamp ) {
+                       if ( empty( $wordcamp['logs'] ) ) {
+                               return true;
+                       }
+
+                       return false;
+               } );
+
+               return $compiled_data;
+       }
+
+       /**
+        * Get all current WordCamp posts.
+        *
+        * @return array
+        */
+       protected function get_wordcamp_posts() {
+               $post_args = array(
+                       'post_type'           => WCPT_POST_TYPE_ID,
+                       'post_status'         => 'any',
+                       'posts_per_page'      => 9999,
+                       'nopaging'            => true,
+                       'no_found_rows'       => false,
+                       'ignore_sticky_posts' => true,
+                       'orderby'             => 'date',
+                       'order'               => 'ASC',
+                       'meta_query'          => array(
+                               'relation' => 'OR',
+                               array(
+                                       'key'     => 'Start Date (YYYY-mm-dd)',
+                                       'compare' => 'NOT EXISTS',
+                               ),
+                               array(
+                                       'key'     => 'Start Date (YYYY-mm-dd)',
+                                       'compare' => '=',
+                                       'value'   => '',
+                               ),
+                               array(
+                                       // Don't include WordCamps that happened more than 3 months before the start date.
+                                       'key'     => 'Start Date (YYYY-mm-dd)',
+                                       'compare' => '>=',
+                                       'value'   => strtotime( '-3 months', $this->start_date->getTimestamp() ),
+                                       'type'    => 'NUMERIC',
+                               ),
+                       ),
+               );
+
+               return get_posts( $post_args );
+       }
+
+       /**
+        * Retrieve the log of status changes for a particular WordCamp.
+        *
+        * @param \WP_Post $wordcamp A WordCamp post.
+        *
+        * @return array
+        */
+       protected function get_wordcamp_status_logs( \WP_Post $wordcamp ) {
+               $log_entries = get_post_meta( $wordcamp->ID, '_status_change' );
+
+               if ( ! empty( $log_entries ) ) {
+                       // Sort log entries in chronological order.
+                       usort( $log_entries, function( $a, $b ) {
+                               if ( $a['timestamp'] === $b['timestamp'] ) {
+                                       return 0;
+                               }
+
+                               return ( $a['timestamp'] > $b['timestamp'] ) ? 1 : -1;
+                       } );
+
+                       return $log_entries;
+               }
+
+               return array();
+       }
+
+       /**
+        * Determine the ending status of a particular status change event.
+        *
+        * E.g. for this event:
+        *
+        *     Needs Vetting â†’ More Info Requested
+        *
+        * The ending status would be "More Info Requested".
+        *
+        * @param array $log_entry A status change log entry.
+        *
+        * @return string
+        */
+       protected function get_log_status_result( $log_entry ) {
+               if ( isset( $log_entry['message'] ) ) {
+                       $pieces = explode( ' &rarr; ', $log_entry['message'] );
+
+                       if ( isset( $pieces[1] ) ) {
+                               return $this->get_status_id_from_name( $pieces[1] );
+                       }
+               }
+
+               return '';
+       }
+
+       /**
+        * Given the ID of a WordCamp status, determine the ID string.
+        *
+        * @param string $status_name A WordCamp status name.
+        *
+        * @return string
+        */
+       protected function get_status_id_from_name( $status_name ) {
+               $statuses = array_flip( WordCamp_Loader::get_post_statuses() );
+
+               if ( isset( $statuses[ $status_name ] ) ) {
+                       return $statuses[ $status_name ];
+               }
+
+               return '';
+       }
+
+       /**
+        * A list of status IDs for statuses that indicate a camp is not active.
+        *
+        * @return array
+        */
+       protected static function get_inactive_statuses() {
+               return array(
+                       'wcpt-rejected',
+                       'wcpt-cancelled',
+                       'wcpt-scheduled',
+                       'wcpt-closed',
+               );
+       }
+
+       /**
+        * Render an HTML version of the report output.
+        *
+        * @return void
+        */
+       public function render_html() {
+               $data       = $this->compile_report_data( $this->get_data() );
+               $start_date = $this->start_date;
+               $end_date   = $this->end_date;
+               $status     = $this->status;
+
+               $active_camps   = $data['active_camps'];
+               $inactive_camps = $data['inactive_camps'];
+               $statuses       = WordCamp_Loader::get_post_statuses();
+
+               if ( ! empty( $this->error->get_error_messages() ) ) {
+                       ?>
+                       <div class="notice notice-error">
+                               <?php foreach ( $this->error->get_error_messages() as $message ) : ?>
+                                       <?php echo wpautop( wp_kses_post( $message ) ); ?>
+                               <?php endforeach; ?>
+                       </div>
+               <?php
+               } else {
+                       include Reports\get_views_dir_path() . 'html/wordcamp-status.php';
+               }
+       }
+
+       /**
+        * Register all assets used by this report.
+        *
+        * @return void
+        */
+       protected static function register_assets() {
+               wp_register_script(
+                       self::$slug,
+                       Reports\get_assets_url() . 'js/' . self::$slug . '.js',
+                       array( 'jquery' ),
+                       Reports\JS_VERSION,
+                       true
+               );
+
+               wp_register_style(
+                       self::$slug,
+                       Reports\get_assets_url() . 'css/' . self::$slug . '.css',
+                       array(),
+                       Reports\CSS_VERSION,
+                       'screen'
+               );
+       }
+
+       /**
+        * Enqueue JS and CSS assets for this report's admin interface.
+        *
+        * @return void
+        */
+       public static function enqueue_admin_assets() {
+               self::register_assets();
+
+               wp_enqueue_script( self::$slug );
+               wp_enqueue_style( self::$slug );
+       }
+
+       /**
+        * Render the page for this report in the WP Admin.
+        *
+        * @return void
+        */
+       public static function render_admin_page() {
+               $start_date = filter_input( INPUT_POST, 'start-date' );
+               $end_date   = filter_input( INPUT_POST, 'end-date' );
+               $status     = filter_input( INPUT_POST, 'status' );
+               $refresh    = filter_input( INPUT_POST, 'refresh', FILTER_VALIDATE_BOOLEAN );
+               $action     = filter_input( INPUT_POST, 'action' );
+               $nonce      = filter_input( INPUT_POST, self::$slug . '-nonce' );
+               $statuses   = WordCamp_Loader::get_post_statuses();
+
+               $report = null;
+
+               if ( 'run-report' === $action && wp_verify_nonce( $nonce, 'run-report' ) ) {
+                       $options = array(
+                               'earliest_start' => new \DateTime( '2015-01-01' ), // No status log data before 2015.
+                       );
+
+                       if ( $refresh ) {
+                               $options['flush_cache'] = true;
+                       }
+
+                       $report = new self( $start_date, $end_date, $status, $options );
+
+                       // The report adjusts the end date in some circumstances.
+                       if ( empty( $report->error->get_error_messages() ) ) {
+                               $end_date = $report->end_date->format( 'Y-m-d' );
+                       }
+               }
+
+               include Reports\get_views_dir_path() . 'report/wordcamp-status.php';
+       }
+
+       /**
+        * Determine whether to render the public report form.
+        *
+        * This shortcode is limited to use on pages.
+        *
+        * @return string HTML content to display shortcode.
+        */
+       public static function handle_shortcode() {
+               $html = '';
+
+               if ( 'page' === get_post_type() ) {
+                       self::register_assets();
+
+                       wp_enqueue_script( self::$slug );
+
+                       ob_start();
+                       self::render_public_page();
+                       $html = ob_get_clean();
+               }
+
+               return $html;
+       }
+
+       /**
+        * Render the page for this report on the front end.
+        *
+        * @return void
+        */
+       public static function render_public_page() {
+               // Apparently 'year' is a reserved URL parameter on the front end, so we prepend 'report-'.
+               $year   = filter_input( INPUT_GET, 'report-year', FILTER_VALIDATE_INT );
+               $period = filter_input( INPUT_GET, 'period' );
+               $status = filter_input( INPUT_GET, 'status' );
+               $action = filter_input( INPUT_GET, 'action' );
+
+               $years    = self::year_array( absint( date( 'Y' ) ), 2015 );
+               $months   = self::month_array();
+               $statuses = WordCamp_Loader::get_post_statuses();
+
+               if ( ! $year ) {
+                       $year = absint( date( 'Y' ) );
+               }
+
+               if ( ! $period ) {
+                       $period = absint( date( 'm' ) );
+               }
+
+               $report = null;
+
+               if ( 'Show results' === $action ) {
+                       $range = self::convert_time_period_to_date_range( $year, $period );
+
+                       $options = array(
+                               'earliest_start' => new \DateTime( '2015-01-01' ), // No status log data before 2015.
+                       );
+
+                       $report = new self( $range['start_date'], $range['end_date'], $status, $options );
+               }
+
+               include Reports\get_views_dir_path() . 'public/wordcamp-status.php';
+       }
+}
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsindexphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/index.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/index.php                                (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/index.php  2018-02-16 16:49:05 UTC (rev 6662)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,312 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Plugin Name:     WordCamp Reports
+ * Plugin URI:      https://wordcamp.org
+ * Description:     Automated reports for WordCamp.org.
+ * Author:          WordCamp.org
+ * Author URI:      https://wordcamp.org
+ * Version:         1
+ *
+ * @package         WordCamp\Reports
+ */
+
+namespace WordCamp\Reports;
+defined( 'WPINC' ) || die();
+
+use WordCamp\Reports\Report;
+
+const JS_VERSION  = 1;
+const CSS_VERSION = 1;
+
+define( __NAMESPACE__ . '\PLUGIN_DIR', \plugin_dir_path( __FILE__ ) );
+define( __NAMESPACE__ . '\PLUGIN_URL', \plugins_url( '/', __FILE__ ) );
+
+/**
+ * Get the path for the includes directory.
+ *
+ * @return string Path with trailing slash
+ */
+function get_classes_dir_path() {
+       return trailingslashit( PLUGIN_DIR ) . 'classes/';
+}
+
+/**
+ * Get the path for the views directory.
+ *
+ * @return string Path with trailing slash
+ */
+function get_views_dir_path() {
+       return trailingslashit( PLUGIN_DIR ) . 'views/';
+}
+
+/**
+ * Get the URL for the assets directory.
+ *
+ * @return string URL with trailing slash.
+ */
+function get_assets_url() {
+       return trailingslashit( PLUGIN_URL ) . 'assets/';
+}
+
+/**
+ * Autoloader for plugin classes.
+ *
+ * @param string $class The fully-qualified class name.
+ *
+ * @return void
+ */
+spl_autoload_register( function( $class ) {
+       // Project-specific namespace prefix.
+       $prefix = 'WordCamp\\Reports\\';
+
+       // Base directory for the namespace prefix.
+       $base_dir = get_classes_dir_path();
+
+       // Does the class use the namespace prefix?
+       $len = strlen( $prefix );
+       if ( strncmp( $prefix, $class, $len ) !== 0 ) {
+               // No, move to the next registered autoloader.
+               return;
+       }
+
+       // Get the relative class name.
+       $relative_class = substr( $class, $len );
+
+       // Convert the relative class name to a relative path.
+       $relative_path_parts = explode( '\\', $relative_class );
+       $filename = 'class-' . array_pop( $relative_path_parts );
+       $relative_path = implode( '/', $relative_path_parts ) . "/$filename.php";
+       $relative_path = strtolower( $relative_path );
+       $relative_path = str_replace( '_', '-', $relative_path );
+
+       $file = $base_dir . $relative_path;
+
+       // If the file exists, require it.
+       if ( file_exists( $file ) ) {
+               require $file;
+       }
+} );
+
+/**
+ * A list of available report classes.
+ *
+ * @todo Maybe parse the classes/report directory and generate this dynamically?
+ *
+ * @return array
+ */
+function get_report_classes() {
+       return array(
+               __NAMESPACE__ . '\Report\Ticket_Revenue',
+               __NAMESPACE__ . '\Report\Sponsor_Invoices',
+               __NAMESPACE__ . '\Report\Payment_Activity',
+               __NAMESPACE__ . '\Report\Sponsorship_Grants',
+               __NAMESPACE__ . '\Report\WordCamp_Status',
+               __NAMESPACE__ . '\Report\Meetup_Groups',
+       );
+}
+
+
+function get_report_groups( $classes = array() ) {
+       $groups = [
+               'finance'  => [
+                       'label'   => 'Finances',
+                       'classes' => [],
+               ],
+               'wordcamp'  => [
+                       'label'   => 'WordCamps',
+                       'classes' => [],
+               ],
+               'meetup'  => [
+                       'label'   => 'Meetups',
+                       'classes' => [],
+               ],
+               'misc'  => [
+                       'label'   => 'Miscellaneous',
+                       'classes' => [],
+               ],
+       ];
+
+       if ( empty( $classes ) ) {
+               $classes = get_report_classes();
+       }
+
+       foreach ( $classes as $class ) {
+               if ( property_exists( $class, 'group' ) && array_key_exists( $class::$group, $groups ) ) {
+                       $groups[ $class::$group ]['classes'][] = $class;
+               } else {
+                       $groups['misc']['classes'][] = $class;
+               }
+       }
+
+       return $groups;
+}
+
+/**
+ * Register the Reports page in the WP Admin.
+ *
+ * @hook action admin_menu
+ *
+ * @return void
+ */
+function add_reports_page() {
+       \add_submenu_page(
+               'index.php',
+               __( 'Reports', 'wordcamporg' ),
+               __( 'Reports', 'wordcamporg' ),
+               'manage_network',
+               'wordcamp-reports',
+               __NAMESPACE__ . '\render_page'
+       );
+}
+
+add_action( 'admin_menu', __NAMESPACE__ . '\add_reports_page' );
+
+/**
+ * Render the main Reports page or use an appropriate class method to
+ * render a particular child report page.
+ *
+ * @return void
+ */
+function render_page() {
+       $report       = filter_input( INPUT_GET, 'report', FILTER_SANITIZE_STRING );
+       $report_class = get_report_class_by_slug( $report );
+
+       $reports_with_admin = array_filter( get_report_classes(), function( $class ) {
+               if ( ! method_exists( $class, 'render_admin_page' ) ) {
+                       return false;
+               }
+
+               return true;
+       } );
+
+       if ( $report_class && in_array( $report_class, $reports_with_admin, true ) ) {
+               $report_class::render_admin_page();
+       } else {
+               $report_groups = get_report_groups( $reports_with_admin );
+
+               include get_views_dir_path() . 'admin.php';
+       }
+}
+
+/**
+ * Enqueue JS and CSS assets for a particular report's admin interface, if it has any.
+ *
+ * @param string $hook_suffix The ID of the current admin page.
+ */
+function enqueue_admin_assets( $hook_suffix ) {
+       if ( 'dashboard_page_wordcamp-reports' !== $hook_suffix ) {
+               return;
+       }
+
+       wp_enqueue_style(
+               'admin-common',
+               get_assets_url() . 'css/admin-common.css',
+               array(),
+               CSS_VERSION
+       );
+
+       $report       = filter_input( INPUT_GET, 'report', FILTER_SANITIZE_STRING );
+       $report_class = get_report_class_by_slug( $report );
+
+       if ( ! is_null( $report_class ) && method_exists( $report_class, 'enqueue_admin_assets' ) ) {
+               $report_class::enqueue_admin_assets();
+       }
+}
+
+add_action( 'admin_enqueue_scripts', __NAMESPACE__ . '\enqueue_admin_assets' );
+
+/**
+ * Determine the class used for a report based on a given string ID.
+ *
+ * @param string $report_slug String identifying a particular report class.
+ *
+ * @return Report\Base|null
+ */
+function get_report_class_by_slug( $report_slug ) {
+       $report_classes = get_report_classes();
+
+       $report_slugs = array_map( function( $class ) {
+               return $class::$slug;
+       }, $report_classes );
+
+       $reports = array_combine( $report_slugs, $report_classes );
+
+       if ( isset( $reports[ $report_slug ] ) ) {
+               return $reports[ $report_slug ];
+       }
+
+       return null;
+}
+
+/**
+ * Get the URL for a Reports-related page.
+ *
+ * @param string $report_slug The slug string for a particular report.
+ *
+ * @return string
+ */
+function get_page_url( $report_slug = '' ) {
+       $url = add_query_arg( array( 'page' => 'wordcamp-reports' ), admin_url( 'index.php' ) );
+
+       if ( $report_slug ) {
+               $url = add_query_arg( array( 'report' => sanitize_key( $report_slug ) ), $url );
+       }
+
+       return $url;
+}
+
+/**
+ * Register shortcodes for reports that have a public interface.
+ *
+ * @return void
+ */
+function register_shortcodes() {
+       $report_classes = get_report_classes();
+
+       foreach ( $report_classes as $class ) {
+               if ( property_exists( $class, 'shortcode_tag' ) && method_exists( $class, 'handle_shortcode' ) ) {
+                       add_shortcode( $class::$shortcode_tag, array( $class, 'handle_shortcode' ) );
+               }
+       }
+}
+
+add_action( 'plugins_loaded', __NAMESPACE__ . '\register_shortcodes' );
+
+/**
+ * Register endpoints for reports that have a REST API interface.
+ *
+ * @return void
+ */
+function register_rest_endpoints() {
+       $namespace = 'wordcamp-reports/v1';
+
+       $report_classes = get_report_classes();
+
+       foreach ( $report_classes as $class ) {
+               if ( property_exists( $class, 'rest_base' ) && method_exists( $class, 'rest_callback' ) ) {
+                       register_rest_route( $namespace, '/' . $class::$rest_base, array(
+                               'methods'  => array( 'GET' ),
+                               'callback' => array( $class, 'rest_callback' ),
+                       ) );
+               }
+       }
+}
+
+add_action( 'rest_api_init', __NAMESPACE__ . '\register_rest_endpoints' );
+
+/**
+ * Add action hooks for methods that emit data files.
+ *
+ * @return void
+ */
+function register_file_exports() {
+       $report_classes = get_report_classes();
+
+       foreach ( $report_classes as $class ) {
+               if ( method_exists( $class, 'export_to_file' ) ) {
+                       add_action( 'admin_init', array( $class, 'export_to_file' ) );
+               }
+       }
+}
+
+add_action( 'plugins_loaded', __NAMESPACE__ . '\register_file_exports' );
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewsadminphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/admin.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/admin.php                          (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/admin.php    2018-02-16 16:49:05 UTC (rev 6662)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,34 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * @package WordCamp\Reports
+ */
+
+namespace WordCamp\Reports\Views\Admin;
+defined( 'WPINC' ) || die();
+
+use WordCamp\Reports;
+
+/** @var array $report_groups */
+/** @var array $reports_with_admin */
+?>
+
+<div class="wrap">
+       <h1>WordCamp Reports</h1>
+
+       <p>Choose a report:</p>
+
+       <?php foreach ( $report_groups as $group_id => $group ) : ?>
+               <?php if ( ! empty( $group['classes'] ) ) : ?>
+                       <h2><?php echo esc_html( $group['label'] ); ?></h2>
+                       <ul class="ul-disc">
+                               <?php foreach ( $group['classes'] as $class ) : ?>
+                                       <li>
+                                               <a href="<?php echo esc_attr( Reports\get_page_url( $class::$slug ) ); ?>"><?php echo esc_html( $class::$name ); ?></a>
+                                               &ndash;
+                                               <em><?php echo esc_html( $class::$description ); ?></em>
+                                       </li>
+                               <?php endforeach; ?>
+                       </ul>
+               <?php endif; ?>
+       <?php endforeach; ?>
+</div>
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewshtmlmeetupgroupsphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/html/meetup-groups.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/html/meetup-groups.php                             (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/html/meetup-groups.php       2018-02-16 16:49:05 UTC (rev 6662)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,109 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * @package WordCamp\Reports
+ */
+
+namespace WordCamp\Reports\Views\HTML\Ticket_Revenue;
+defined( 'WPINC' ) || die();
+
+/** @var \DateTime $start_date */
+/** @var \DateTime $end_date */
+/** @var array $data */
+?>
+
+<?php if ( $data['total_groups'] ) : ?>
+       <h3>Meetup groups in the chapter program as of <?php echo esc_html( $end_date->format( 'M jS, Y' ) ); ?></h3>
+
+       <h4>Total groups: <?php echo number_format_i18n( $data['total_groups'] ); ?></h4>
+       <h4>Total groups by country:</h4>
+
+       <table class="striped widefat but-not-too-wide">
+               <thead>
+               <tr>
+                       <td>Country</td>
+                       <td># of Groups</td>
+               </tr>
+               </thead>
+               <tbody>
+               <?php foreach ( array_keys( $data['total_groups_by_country'] ) as $country ) : ?>
+                       <tr>
+                               <td><?php echo esc_html( $country ); ?></td>
+                               <td class="number"><?php echo number_format_i18n( $data['total_groups_by_country'][ $country ] ); ?></td>
+                       </tr>
+               <?php endforeach; ?>
+               </tbody>
+       </table>
+
+       <h4>Total group members (non-unique): <?php echo number_format_i18n( $data['total_members'] ); ?></h4>
+       <h4>Total group members by country:</h4>
+
+       <table class="striped widefat but-not-too-wide">
+               <thead>
+               <tr>
+                       <td>Country</td>
+                       <td># of Members</td>
+               </tr>
+               </thead>
+               <tbody>
+               <?php foreach ( array_keys( $data['total_members_by_country'] ) as $country ) : ?>
+                       <tr>
+                               <td><?php echo esc_html( $country ); ?></td>
+                               <td class="number"><?php echo number_format_i18n( $data['total_members_by_country'][ $country ] ); ?></td>
+                       </tr>
+               <?php endforeach; ?>
+               </tbody>
+       </table>
+
+       <?php if ( $data['joined_groups'] ) : ?>
+               <h3>Meetup groups that joined the chapter program between <?php echo esc_html( $start_date->format( 'M jS, Y' ) ); ?> and <?php echo esc_html( $end_date->format( 'M jS, Y' ) ); ?></h3>
+
+               <h4>Total groups that joined: <?php echo number_format_i18n( $data['joined_groups'] ); ?></h4>
+               <h4>Total groups that joined by country:</h4>
+
+               <table class="striped widefat but-not-too-wide">
+                       <thead>
+                       <tr>
+                               <td>Country</td>
+                               <td># of Groups</td>
+                       </tr>
+                       </thead>
+                       <tbody>
+                       <?php foreach ( array_keys( $data['joined_groups_by_country'] ) as $country ) : ?>
+                               <tr>
+                                       <td><?php echo esc_html( $country ); ?></td>
+                                       <td class="number"><?php echo number_format_i18n( $data['joined_groups_by_country'][ $country ] ); ?></td>
+                               </tr>
+                       <?php endforeach; ?>
+                       </tbody>
+               </table>
+
+               <h4>Total group members that joined (non-unique): <?php echo number_format_i18n( $data['joined_members'] ); ?></h4>
+               <h4>Total group members that joined by country:</h4>
+
+               <table class="striped widefat but-not-too-wide">
+                       <thead>
+                       <tr>
+                               <td>Country</td>
+                               <td># of Members</td>
+                       </tr>
+                       </thead>
+                       <tbody>
+                       <?php foreach ( array_keys( $data['joined_members_by_country'] ) as $country ) : ?>
+                               <tr>
+                                       <td><?php echo esc_html( $country ); ?></td>
+                                       <td class="number"><?php echo number_format_i18n( $data['joined_members_by_country'][ $country ] ); ?></td>
+                               </tr>
+                       <?php endforeach; ?>
+                       </tbody>
+               </table>
+       <?php endif; ?>
+<?php else : ?>
+       <p>
+               No data
+               <?php if ( $start_date->format( 'Y-m-d' ) === $end_date->format( 'Y-m-d' ) ) : ?>
+                       on <?php echo esc_html( $start_date->format( 'M jS, Y' ) ); ?>
+               <?php else : ?>
+                       between <?php echo esc_html( $start_date->format( 'M jS, Y' ) ); ?> and <?php echo esc_html( $end_date->format( 'M jS, Y' ) ); ?>
+               <?php endif; ?>
+       </p>
+<?php endif; ?>
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewshtmlpaymentactivityphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/html/payment-activity.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/html/payment-activity.php                          (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/html/payment-activity.php    2018-02-16 16:49:05 UTC (rev 6662)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,192 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * @package WordCamp\Reports
+ */
+
+namespace WordCamp\Reports\Views\HTML\Payment_Activity;
+defined( 'WPINC' ) || die();
+
+/** @var \DateTime $start_date */
+/** @var \DateTime $end_date */
+/** @var string $wordcamp_name */
+/** @var array $requests */
+/** @var array $payments */
+/** @var array $failures */
+
+$asterisk2 = false;
+?>
+
+<?php if ( $requests['vendor_payment_count'] || $requests['reimbursement_count'] ) : ?>
+       <h3>
+               Requested Payments
+               <?php if ( $wordcamp_name ) : ?>
+                       for <?php echo esc_html( $wordcamp_name ); ?>
+               <?php endif; ?>
+               <?php if ( $start_date->format( 'Y-m-d' ) === $end_date->format( 'Y-m-d' ) ) : ?>
+                       on <?php echo esc_html( $start_date->format( 'M jS, Y' ) ); ?>
+               <?php else : ?>
+                       between <?php echo esc_html( $start_date->format( 'M jS, Y' ) ); ?> and <?php echo esc_html( $end_date->format( 'M jS, Y' ) ); ?>
+               <?php endif; ?>
+       </h3>
+
+       <ul>
+               <?php if ( $requests['vendor_payment_count'] ) : ?>
+                       <li>Vendor payments: <?php echo number_format_i18n( $requests['vendor_payment_count'] ) ?></li>
+               <?php endif; ?>
+               <?php if ( $requests['reimbursement_count'] ) : ?>
+                       <li>Reimbursements: <?php echo number_format_i18n( $requests['reimbursement_count'] ) ?></li>
+               <?php endif; ?>
+       </ul>
+
+       <table class="striped widefat but-not-too-wide">
+               <thead>
+               <tr>
+                       <td>Currency</td>
+                       <td>Total Amount Requested</td>
+                       <td>Estimated Value in USD *</td>
+               </tr>
+               </thead>
+               <tbody>
+               <?php foreach ( array_keys( $requests['total_amount_by_currency'] ) as $currency ) : ?>
+                       <tr>
+                               <td><?php echo esc_html( $currency ); ?></td>
+                               <td class="number"><?php echo number_format_i18n( $requests['total_amount_by_currency'][ $currency ] ); ?></td>
+                               <td class="number">
+                                       <?php echo number_format_i18n( $requests['converted_amounts'][ $currency ] ); ?>
+                                       <?php if ( $requests['total_amount_by_currency'][ $currency ] > 0 && $requests['converted_amounts'][ $currency ] === 0 ) : $asterisk2 = true; ?>
+                                               **
+                                       <?php endif; ?>
+                               </td>
+                       </tr>
+               <?php endforeach; ?>
+               <tr>
+                       <td></td>
+                       <td>Total: </td>
+                       <td class="number total"><?php echo number_format_i18n( $requests['total_amount_converted'] ); ?></td>
+               </tr>
+               </tbody>
+       </table>
+<?php endif; ?>
+
+<?php if ( $payments['vendor_payment_count'] || $payments['reimbursement_count'] ) : ?>
+       <h3>
+               Completed Payments
+               <?php if ( $wordcamp_name ) : ?>
+                       for <?php echo esc_html( $wordcamp_name ); ?>
+               <?php endif; ?>
+               <?php if ( $start_date->format( 'Y-m-d' ) === $end_date->format( 'Y-m-d' ) ) : ?>
+                       on <?php echo esc_html( $start_date->format( 'M jS, Y' ) ); ?>
+               <?php else : ?>
+                       between <?php echo esc_html( $start_date->format( 'M jS, Y' ) ); ?> and <?php echo esc_html( $end_date->format( 'M jS, Y' ) ); ?>
+               <?php endif; ?>
+       </h3>
+
+       <ul>
+               <?php if ( $payments['vendor_payment_count'] ) : ?>
+                       <li>Vendor payments: <?php echo number_format_i18n( $payments['vendor_payment_count'] ) ?></li>
+               <?php endif; ?>
+               <?php if ( $payments['reimbursement_count'] ) : ?>
+                       <li>Reimbursements: <?php echo number_format_i18n( $payments['reimbursement_count'] ) ?></li>
+               <?php endif; ?>
+       </ul>
+
+       <table class="striped widefat but-not-too-wide">
+               <thead>
+               <tr>
+                       <td>Currency</td>
+                       <td>Total Amount Requested</td>
+                       <td>Estimated Value in USD *</td>
+               </tr>
+               </thead>
+               <tbody>
+               <?php foreach ( array_keys( $payments['total_amount_by_currency'] ) as $currency ) : ?>
+                       <tr>
+                               <td><?php echo esc_html( $currency ); ?></td>
+                               <td class="number"><?php echo number_format_i18n( $payments['total_amount_by_currency'][ $currency ] ); ?></td>
+                               <td class="number">
+                                       <?php echo number_format_i18n( $payments['converted_amounts'][ $currency ] ); ?>
+                                       <?php if ( $payments['total_amount_by_currency'][ $currency ] > 0 && $payments['converted_amounts'][ $currency ] === 0 ) : $asterisk2 = true; ?>
+                                               **
+                                       <?php endif; ?>
+                               </td>
+                       </tr>
+               <?php endforeach; ?>
+               <tr>
+                       <td></td>
+                       <td>Total: </td>
+                       <td class="number total"><?php echo number_format_i18n( $payments['total_amount_converted'] ); ?></td>
+               </tr>
+               </tbody>
+       </table>
+<?php endif; ?>
+
+<?php if ( $failures['vendor_payment_count'] || $failures['reimbursement_count'] ) : ?>
+       <h3>
+               Failed/Cancelled Payments
+               <?php if ( $wordcamp_name ) : ?>
+                       for <?php echo esc_html( $wordcamp_name ); ?>
+               <?php endif; ?>
+               <?php if ( $start_date->format( 'Y-m-d' ) === $end_date->format( 'Y-m-d' ) ) : ?>
+                       on <?php echo esc_html( $start_date->format( 'M jS, Y' ) ); ?>
+               <?php else : ?>
+                       between <?php echo esc_html( $start_date->format( 'M jS, Y' ) ); ?> and <?php echo esc_html( $end_date->format( 'M jS, Y' ) ); ?>
+               <?php endif; ?>
+       </h3>
+
+       <ul>
+               <?php if ( $failures['vendor_payment_count'] ) : ?>
+                       <li>Vendor payments: <?php echo number_format_i18n( $failures['vendor_payment_count'] ) ?></li>
+               <?php endif; ?>
+               <?php if ( $failures['reimbursement_count'] ) : ?>
+                       <li>Reimbursements: <?php echo number_format_i18n( $failures['reimbursement_count'] ) ?></li>
+               <?php endif; ?>
+       </ul>
+
+       <table class="striped widefat but-not-too-wide">
+               <thead>
+               <tr>
+                       <td>Currency</td>
+                       <td>Total Amount Requested</td>
+                       <td>Estimated Value in USD *</td>
+               </tr>
+               </thead>
+               <tbody>
+               <?php foreach ( array_keys( $failures['total_amount_by_currency'] ) as $currency ) : ?>
+                       <tr>
+                               <td><?php echo esc_html( $currency ); ?></td>
+                               <td class="number"><?php echo number_format_i18n( $failures['total_amount_by_currency'][ $currency ] ); ?></td>
+                               <td class="number">
+                                       <?php echo number_format_i18n( $failures['converted_amounts'][ $currency ] ); ?>
+                                       <?php if ( $failures['total_amount_by_currency'][ $currency ] > 0 && $failures['converted_amounts'][ $currency ] === 0 ) : $asterisk2 = true; ?>
+                                               **
+                                       <?php endif; ?>
+                               </td>
+                       </tr>
+               <?php endforeach; ?>
+               <tr>
+                       <td></td>
+                       <td>Total: </td>
+                       <td class="number total"><?php echo number_format_i18n( $failures['total_amount_converted'] ); ?></td>
+               </tr>
+               </tbody>
+       </table>
+<?php endif; ?>
+
+<?php if ( $requests['vendor_payment_count'] || $requests['reimbursement_count'] || $payments['vendor_payment_count'] || $payments['reimbursement_count'] || $failures['vendor_payment_count'] || $failures['reimbursement_count'] ) : ?>
+       <p class="description">* Estimate based on exchange rates for <?php echo esc_html( $end_date->format( 'M jS, Y' ) ); ?></p>
+       <?php if ( $asterisk2 ) : ?>
+               <p class="description">** Currency exchange rate not available.</p>
+       <?php endif; ?>
+<?php else : ?>
+       <p>
+               No data
+               <?php if ( $wordcamp_name ) : ?>
+                       for <?php echo esc_html( $wordcamp_name ); ?>
+               <?php endif; ?>
+               <?php if ( $start_date->format( 'Y-m-d' ) === $end_date->format( 'Y-m-d' ) ) : ?>
+                       on <?php echo esc_html( $start_date->format( 'M jS, Y' ) ); ?>
+               <?php else : ?>
+                       between <?php echo esc_html( $start_date->format( 'M jS, Y' ) ); ?> and <?php echo esc_html( $end_date->format( 'M jS, Y' ) ); ?>
+               <?php endif; ?>
+       </p>
+<?php endif; ?>
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewshtmlsponsorinvoicesphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/html/sponsor-invoices.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/html/sponsor-invoices.php                          (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/html/sponsor-invoices.php    2018-02-16 16:49:05 UTC (rev 6662)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,129 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * @package WordCamp\Reports
+ */
+
+namespace WordCamp\Reports\Views\HTML\Sponsor_Invoices;
+defined( 'WPINC' ) || die();
+
+/** @var \DateTime $start_date */
+/** @var \DateTime $end_date */
+/** @var string $wordcamp_name */
+/** @var array $invoices */
+/** @var array $payments */
+
+$asterisk2 = false;
+?>
+
+<?php if ( $invoices['total_count'] > 0 ) : ?>
+       <h3>
+               Sponsor Invoices Sent
+               <?php if ( $wordcamp_name ) : ?>
+                       for <?php echo esc_html( $wordcamp_name ); ?>
+               <?php endif; ?>
+               <?php if ( $start_date->format( 'Y-m-d' ) === $end_date->format( 'Y-m-d' ) ) : ?>
+                       on <?php echo esc_html( $start_date->format( 'M jS, Y' ) ); ?>
+               <?php else : ?>
+                       between <?php echo esc_html( $start_date->format( 'M jS, Y' ) ); ?> and <?php echo esc_html( $end_date->format( 'M jS, Y' ) ); ?>
+               <?php endif; ?>
+       </h3>
+
+       <ul>
+               <li>Invoices sent: <?php echo number_format_i18n( $invoices['total_count'] ); ?></li>
+       </ul>
+
+       <table class="striped widefat but-not-too-wide">
+               <thead>
+               <tr>
+                       <td>Currency</td>
+                       <td>Amount</td>
+                       <td>Estimated Value in USD *</td>
+               </tr>
+               </thead>
+               <tbody>
+               <?php foreach ( array_keys( $invoices['amount_by_currency'] ) as $currency ) : ?>
+                       <tr>
+                               <td><?php echo esc_html( $currency ); ?></td>
+                               <td class="number"><?php echo number_format_i18n( $invoices['amount_by_currency'][ $currency ] ); ?></td>
+                               <td class="number">
+                                       <?php echo number_format_i18n( $invoices['converted_amounts'][ $currency ] ); ?>
+                                       <?php if ( $invoices['amount_by_currency'][ $currency ] > 0 && $invoices['converted_amounts'][ $currency ] === 0 ) : $asterisk2 = true; ?>
+                                               **
+                                       <?php endif; ?>
+                               </td>
+                       </tr>
+               <?php endforeach; ?>
+               <tr>
+                       <td></td>
+                       <td>Total: </td>
+                       <td class="number total"><?php echo number_format_i18n( $invoices['total_amount_converted'] ); ?></td>
+               </tr>
+               </tbody>
+       </table>
+<?php endif; ?>
+
+<?php if ( $payments['total_count'] > 0 ) : ?>
+       <h3>
+               Sponsor Invoice Payments Received
+               <?php if ( $wordcamp_name ) : ?>
+                       for <?php echo esc_html( $wordcamp_name ); ?>
+               <?php endif; ?>
+               <?php if ( $start_date->format( 'Y-m-d' ) === $end_date->format( 'Y-m-d' ) ) : ?>
+                       on <?php echo esc_html( $start_date->format( 'M jS, Y' ) ); ?>
+               <?php else : ?>
+                       between <?php echo esc_html( $start_date->format( 'M jS, Y' ) ); ?> and <?php echo esc_html( $end_date->format( 'M jS, Y' ) ); ?>
+               <?php endif; ?>
+       </h3>
+
+       <ul>
+               <li>Payments received: <?php echo number_format_i18n( $payments['total_count'] ); ?></li>
+       </ul>
+
+       <table class="striped widefat but-not-too-wide">
+               <thead>
+               <tr>
+                       <td>Currency</td>
+                       <td>Amount</td>
+                       <td>Estimated Value in USD *</td>
+               </tr>
+               </thead>
+               <tbody>
+               <?php foreach ( array_keys( $payments['amount_by_currency'] ) as $currency ) : ?>
+                       <tr>
+                               <td><?php echo esc_html( $currency ); ?></td>
+                               <td class="number"><?php echo number_format_i18n( $payments['amount_by_currency'][ $currency ] ); ?></td>
+                               <td class="number">
+                                       <?php echo number_format_i18n( $payments['converted_amounts'][ $currency ] ); ?>
+                                       <?php if ( $invoices['amount_by_currency'][ $currency ] > 0 && $invoices['converted_amounts'][ $currency ] === 0 ) : $asterisk2 = true; ?>
+                                               **
+                                       <?php endif; ?>
+                               </td>
+                       </tr>
+               <?php endforeach; ?>
+               <tr>
+                       <td></td>
+                       <td>Total: </td>
+                       <td class="number total"><?php echo number_format_i18n( $payments['total_amount_converted'] ); ?></td>
+               </tr>
+               </tbody>
+       </table>
+<?php endif; ?>
+
+<?php if ( $invoices['total_count'] > 0 || $payments['total_count'] > 0 ) : ?>
+       <p class="description">* Estimate based on exchange rates for <?php echo esc_html( $end_date->format( 'M jS, Y' ) ); ?></p>
+       <?php if ( $asterisk2 ) : ?>
+               <p class="description">** Currency exchange rate not available.</p>
+       <?php endif; ?>
+<?php else : ?>
+       <p>
+               No data
+               <?php if ( $wordcamp_name ) : ?>
+                       for <?php echo esc_html( $wordcamp_name ); ?>
+               <?php endif; ?>
+               <?php if ( $start_date->format( 'Y-m-d' ) === $end_date->format( 'Y-m-d' ) ) : ?>
+                       on <?php echo esc_html( $start_date->format( 'M jS, Y' ) ); ?>
+               <?php else : ?>
+                       between <?php echo esc_html( $start_date->format( 'M jS, Y' ) ); ?> and <?php echo esc_html( $end_date->format( 'M jS, Y' ) ); ?>
+               <?php endif; ?>
+       </p>
+<?php endif; ?>
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewshtmlsponsorshipgrantsphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/html/sponsorship-grants.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/html/sponsorship-grants.php                                (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/html/sponsorship-grants.php  2018-02-16 16:49:05 UTC (rev 6662)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,101 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * @package WordCamp\Reports
+ */
+
+namespace WordCamp\Reports\Views\HTML\Sponsorship_Grants;
+defined( 'WPINC' ) || die();
+
+/** @var \DateTime $start_date */
+/** @var \DateTime $end_date */
+/** @var string $wordcamp_name */
+/** @var array $data */
+/** @var array $compiled_data */
+
+$asterisk2 = false;
+?>
+
+<?php if ( $compiled_data['grant_count'] ) : ?>
+       <h3>
+               Global Sponsorship Grants
+               <?php if ( $wordcamp_name ) : ?>
+                       for <?php echo esc_html( $wordcamp_name ); ?>
+               <?php endif; ?>
+               <?php if ( $start_date->format( 'Y-m-d' ) === $end_date->format( 'Y-m-d' ) ) : ?>
+                       on <?php echo esc_html( $start_date->format( 'M jS, Y' ) ); ?>
+               <?php else : ?>
+                       between <?php echo esc_html( $start_date->format( 'M jS, Y' ) ); ?> and <?php echo esc_html( $end_date->format( 'M jS, Y' ) ); ?>
+               <?php endif; ?>
+       </h3>
+
+       <h4>Grants awarded: <?php echo number_format_i18n( $compiled_data['grant_count'] ) ?></h4>
+
+       <table class="striped widefat but-not-too-wide">
+               <thead>
+               <tr>
+                       <td>Currency</td>
+                       <td>Total Amount Awarded</td>
+                       <td>Estimated Value in USD *</td>
+               </tr>
+               </thead>
+               <tbody>
+               <?php foreach ( array_keys( $compiled_data['total_amount_by_currency'] ) as $currency ) : ?>
+                       <tr>
+                               <td><?php echo esc_html( $currency ); ?></td>
+                               <td class="number"><?php echo number_format_i18n( $compiled_data['total_amount_by_currency'][ $currency ] ); ?></td>
+                               <td class="number">
+                                       <?php echo number_format_i18n( $compiled_data['converted_amounts'][ $currency ] ); ?>
+                                       <?php if ( $compiled_data['total_amount_by_currency'][ $currency ] > 0 && $compiled_data['converted_amounts'][ $currency ] === 0 ) : $asterisk2 = true; ?>
+                                               **
+                                       <?php endif; ?>
+                               </td>
+                       </tr>
+               <?php endforeach; ?>
+               <tr>
+                       <td></td>
+                       <td>Total: </td>
+                       <td class="number total"><?php echo number_format_i18n( $compiled_data['total_amount_converted'] ); ?></td>
+               </tr>
+               </tbody>
+       </table>
+
+       <p class="description">* Estimate based on exchange rates for <?php echo esc_html( $end_date->format( 'M jS, Y' ) ); ?></p>
+       <?php if ( $asterisk2 ) : ?>
+               <p class="description">** Currency exchange rate not available.</p>
+       <?php endif; ?>
+
+       <h4>Grant details:</h4>
+
+       <table class="striped widefat but-not-too-wide">
+               <thead>
+               <tr>
+                       <td>Date</td>
+                       <td>WordCamp</td>
+                       <td>Currency</td>
+                       <td>Amount</td>
+               </tr>
+               </thead>
+               <tbody>
+               <?php foreach ( $data as $grant ) : ?>
+                       <tr>
+                               <td><?php echo date( 'Y-m-d', $grant['timestamp'] ); ?></td>
+                               <td><?php echo esc_html( $grant['name'] ); ?></td>
+                               <td><?php echo esc_html( $grant['currency'] ); ?></td>
+                               <td class="number"><?php echo number_format_i18n( $grant['amount'] ); ?></td>
+                       </tr>
+               <?php endforeach; ?>
+               </tbody>
+       </table>
+<?php else : ?>
+       <p>
+               No data
+               <?php if ( $wordcamp_name ) : ?>
+                       for <?php echo esc_html( $wordcamp_name ); ?>
+               <?php endif; ?>
+               <?php if ( $start_date->format( 'Y-m-d' ) === $end_date->format( 'Y-m-d' ) ) : ?>
+                       on <?php echo esc_html( $start_date->format( 'M jS, Y' ) ); ?>
+               <?php else : ?>
+                       between <?php echo esc_html( $start_date->format( 'M jS, Y' ) ); ?> and <?php echo esc_html( $end_date->format( 'M jS, Y' ) ); ?>
+               <?php endif; ?>
+       </p>
+<?php endif; ?>
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewshtmlticketrevenuephp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/html/ticket-revenue.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/html/ticket-revenue.php                            (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/html/ticket-revenue.php      2018-02-16 16:49:05 UTC (rev 6662)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,107 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * @package WordCamp\Reports
+ */
+
+namespace WordCamp\Reports\Views\HTML\Ticket_Revenue;
+defined( 'WPINC' ) || die();
+
+/** @var \DateTime $start_date */
+/** @var \DateTime $end_date */
+/** @var string $wordcamp_name */
+/** @var array $data */
+
+$asterisk2 = false;
+$groups    = 0;
+?>
+
+<?php foreach ( $data as $key => $group ) : ?>
+       <?php if ( empty( $group['gross_revenue_by_currency'] ) ) continue; ?>
+       <?php if ( 'total' === $key && $groups < 2 ) continue; ?>
+
+       <h3>
+               <?php echo esc_html( $group['label'] ); ?>
+               <?php if ( $wordcamp_name ) : ?>
+                       for <?php echo esc_html( $wordcamp_name ); ?>
+               <?php endif; ?>
+               <?php if ( $start_date->format( 'Y-m-d' ) === $end_date->format( 'Y-m-d' ) ) : ?>
+                       on <?php echo esc_html( $start_date->format( 'M jS, Y' ) ); ?>
+               <?php else : ?>
+                       between <?php echo esc_html( $start_date->format( 'M jS, Y' ) ); ?> and <?php echo esc_html( $end_date->format( 'M jS, Y' ) ); ?>
+               <?php endif; ?>
+       </h3>
+
+       <?php if ( $group['description'] ) : ?>
+               <p class="description"><?php echo wp_kses_post( $group['description'] ); ?></p>
+       <?php endif; ?>
+
+       <ul>
+               <li>Tickets sold: <?php echo number_format_i18n( $group['tickets_sold'] ); ?></li>
+               <li>Tickets refunded: <?php echo number_format_i18n( $group['tickets_refunded'] ); ?></li>
+       </ul>
+
+       <table class="striped widefat but-not-too-wide">
+               <thead>
+               <tr>
+                       <td>Currency</td>
+                       <td>Gross Revenue</td>
+                       <td>Discounts</td>
+                       <td>Refunds</td>
+                       <td>Net Revenue</td>
+                       <td>Estimated Value in USD *</td>
+               </tr>
+               </thead>
+               <tbody>
+               <?php foreach ( array_keys( $group['net_revenue_by_currency'] ) as $currency ) : ?>
+                       <tr>
+                               <td><?php echo esc_html( $currency ); ?></td>
+                               <td class="number"><?php echo number_format_i18n( $group['gross_revenue_by_currency'][ $currency ] ); ?></td>
+                               <td class="number"><?php echo number_format_i18n( $group['discounts_by_currency'][ $currency ] ); ?></td>
+                               <td class="number"><?php echo number_format_i18n( $group['amount_refunded_by_currency'][ $currency ] ); ?></td>
+                               <td class="number"><?php echo number_format_i18n( $group['net_revenue_by_currency'][ $currency ] ); ?></td>
+                               <td class="number">
+                                       <?php echo number_format_i18n( $group['converted_net_revenue'][ $currency ] ); ?>
+                                       <?php if ( $group['net_revenue_by_currency'][ $currency ] > 0 && $group['converted_net_revenue'][ $currency ] === 0 ) : $group['asterisk2'] = $asterisk2 = true; ?>
+                                               **
+                                       <?php endif; ?>
+                               </td>
+                       </tr>
+               <?php endforeach; ?>
+               <tr>
+                       <td></td>
+                       <td></td>
+                       <td></td>
+                       <td></td>
+                       <td>Total: </td>
+                       <td class="number total">
+                               <?php echo number_format_i18n( $group['total_converted_revenue'] ); ?>
+                               <?php if ( isset( $group['asterisk2'] ) ) : ?>
+                                       **
+                               <?php endif; ?>
+                       </td>
+               </tr>
+               </tbody>
+       </table>
+       <?php $groups ++; ?>
+<?php endforeach; ?>
+
+<?php if ( ! empty( $total['net_revenue_by_currency'] ) ) : ?>
+       <p class="description">
+               * Estimate based on exchange rates for <?php echo esc_html( $end_date->format( 'M jS, Y' ) ); ?>
+               <?php if ( $asterisk2 ) : ?>
+                       <br />** Currency exchange rate not available.
+               <?php endif; ?>
+       </p>
+<?php else : ?>
+       <p>
+               No data
+               <?php if ( $wordcamp_name ) : ?>
+                       for <?php echo esc_html( $wordcamp_name ); ?>
+               <?php endif; ?>
+               <?php if ( $start_date->format( 'Y-m-d' ) === $end_date->format( 'Y-m-d' ) ) : ?>
+                       on <?php echo esc_html( $start_date->format( 'M jS, Y' ) ); ?>
+               <?php else : ?>
+                       between <?php echo esc_html( $start_date->format( 'M jS, Y' ) ); ?> and <?php echo esc_html( $end_date->format( 'M jS, Y' ) ); ?>
+               <?php endif; ?>
+       </p>
+<?php endif; ?>
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewshtmlwordcampstatusphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/html/wordcamp-status.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/html/wordcamp-status.php                           (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/html/wordcamp-status.php     2018-02-16 16:49:05 UTC (rev 6662)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,82 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * @package WordCamp\Reports
+ */
+
+namespace WordCamp\Reports\Views\HTML\WordCamp_Status;
+defined( 'WPINC' ) || die();
+
+/** @var \DateTime $start_date */
+/** @var \DateTime $end_date */
+/** @var string $status */
+/** @var array $active_camps */
+/** @var array $inactive_camps */
+/** @var array $statuses */
+?>
+
+<?php if ( ! empty( $active_camps ) ) : ?>
+       <h3 id="active-heading">
+               <?php if ( $status ) : ?>
+                       WordCamps set to &ldquo;<?php echo esc_html( $statuses[ $status ] ); ?>&rdquo;
+               <?php else : ?>
+                       WordCamp activity
+               <?php endif; ?>
+               <?php if ( $start_date->format( 'Y-m-d' ) === $end_date->format( 'Y-m-d' ) ) : ?>
+                       on <?php echo esc_html( $start_date->format( 'M jS, Y' ) ); ?>
+               <?php else : ?>
+                       between <?php echo esc_html( $start_date->format( 'M jS, Y' ) ); ?> and <?php echo esc_html( $end_date->format( 'M jS, Y' ) ); ?>
+               <?php endif; ?>
+       </h3>
+
+       <?php foreach ( $active_camps as $active_camp ) : ?>
+               <p><strong class="active-camp"><?php echo esc_html( $active_camp['name'] ); ?></strong> &ndash; <?php echo esc_html( $statuses[ $active_camp['latest_status'] ] ); ?></p>
+               <ul class="status-log ul-disc">
+                       <?php foreach ( $active_camp['logs'] as $log ) : ?>
+                               <li><?php
+                               echo date( 'Y-m-d', $log['timestamp'] );
+                               echo ': ';
+                               echo esc_html( $log['message'] );
+                               ?></li>
+                       <?php endforeach; ?>
+               </ul>
+       <?php endforeach; ?>
+<?php endif; ?>
+
+<?php if ( ! empty( $inactive_camps ) ) : ?>
+       <h3 id="inactive-heading">
+               WordCamps
+               <?php if ( $status ) : ?>
+                       set to &ldquo;<?php echo esc_html( $statuses[ $status ] ); ?>&rdquo;
+               <?php endif; ?>
+               with no activity
+               <?php if ( $start_date->format( 'Y-m-d' ) === $end_date->format( 'Y-m-d' ) ) : ?>
+                       on <?php echo esc_html( $start_date->format( 'M jS, Y' ) ); ?>
+               <?php else : ?>
+                       between <?php echo esc_html( $start_date->format( 'M jS, Y' ) ); ?> and <?php echo esc_html( $end_date->format( 'M jS, Y' ) ); ?>
+               <?php endif; ?>
+       </h3>
+
+       <ul>
+       <?php foreach ( $inactive_camps as $inactive_camp ) : ?>
+               <li>
+                       <strong class="inactive-camp"><?php echo esc_html( $inactive_camp['name'] ); ?></strong> &ndash;
+                       <?php echo esc_html( $statuses[ $inactive_camp['latest_status'] ] ); ?> &ndash;
+                       <em>Last activity before date range: <?php echo date( 'Y-m-d', $inactive_camp['latest_log']['timestamp'] ); ?></em>
+               </li>
+       <?php endforeach; ?>
+       </ul>
+<?php endif; ?>
+
+<?php if ( empty( $active_camps ) && empty( $inactive_camps ) ) : ?>
+       <h3 id="no-data-heading">
+               No data
+               <?php if ( $status ) : ?>
+                       involving &ldquo;<?php echo esc_html( $statuses[ $status ] ); ?>&rdquo;
+               <?php endif; ?>
+               <?php if ( $start_date->format( 'Y-m-d' ) === $end_date->format( 'Y-m-d' ) ) : ?>
+                       on <?php echo esc_html( $start_date->format( 'M jS, Y' ) ); ?>
+               <?php else : ?>
+                       between <?php echo esc_html( $start_date->format( 'M jS, Y' ) ); ?> and <?php echo esc_html( $end_date->format( 'M jS, Y' ) ); ?>
+               <?php endif; ?>
+       </h3>
+<?php endif; ?>
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewspublicmeetupgroupsphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/public/meetup-groups.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/public/meetup-groups.php                           (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/public/meetup-groups.php     2018-02-16 16:49:05 UTC (rev 6662)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,57 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * @package WordCamp\Reports
+ */
+
+namespace WordCamp\Reports\Views\Report\Meetup_Groups;
+defined( 'WPINC' ) || die();
+
+use WordCamp\Reports\Report;
+
+/** @var string $year */
+/** @var string $period */
+/** @var array  $years */
+/** @var array  $quarters */
+/** @var array  $months */
+/** @var Report\Meetup_Groups|null $report */
+?>
+
+<div id="<?php echo esc_attr( Report\Meetup_Groups::$slug ); ?>-report" class="report-container">
+       <p class="report-description">
+               <?php echo wp_kses_post( Report\Meetup_Groups::$description ); ?>
+       </p>
+
+       <form method="get" action="" class="report-form">
+               <div class="field_report-year">
+                       <label for="report-year">Year</label>
+                       <select id="report-year" name="report-year">
+                               <?php foreach ( $years as $year_value ) : ?>
+                                       <option value="<?php echo esc_attr( $year_value ); ?>"<?php selected( $year_value, $year ); ?>><?php echo esc_html( $year_value ); ?></option>
+                               <?php endforeach; ?>
+                       </select>
+               </div>
+
+               <div class="field_period">
+                       <label for="period">Time Period</label>
+                       <select id="period" name="period">
+                               <option value="all"<?php selected( 'all' === $period ); ?>>Entire year</option>
+                               <?php foreach ( $quarters as $quarter_value => $quarter_label ) : ?>
+                                       <option value="<?php echo esc_attr( $quarter_value ); ?>"<?php selected( $quarter_value, $period ); ?>><?php echo esc_html( $quarter_label ); ?></option>
+                               <?php endforeach; ?>
+                               <?php foreach ( $months as $month_value => $month_label ) : ?>
+                                       <option value="<?php echo esc_attr( $month_value ); ?>"<?php selected( $month_value, $period ); ?>><?php echo esc_html( $month_label ); ?></option>
+                               <?php endforeach; ?>
+                       </select>
+               </div>
+
+               <div class="submit_show-results">
+                       <?php submit_button( 'Show results', 'primary', 'action', false ); ?>
+               </div>
+       </form>
+
+       <?php if ( $report instanceof Report\Meetup_Groups ) : ?>
+               <div class="report-results">
+                       <?php $report->render_html(); ?>
+               </div>
+       <?php endif; ?>
+</div>
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewspublicpaymentactivityphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/public/payment-activity.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/public/payment-activity.php                                (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/public/payment-activity.php  2018-02-16 16:49:05 UTC (rev 6662)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,63 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * @package WordCamp\Reports
+ */
+
+namespace WordCamp\Reports\Views\Report\Payment_Activity;
+defined( 'WPINC' ) || die();
+
+use WordCamp\Reports\Report;
+
+/** @var string $year */
+/** @var string $period */
+/** @var int    $wordcamp_id */
+/** @var array  $years */
+/** @var array  $quarters */
+/** @var array  $months */
+/** @var Report\Payment_Activity|null $report */
+?>
+
+<div id="<?php echo esc_attr( Report\Payment_Activity::$slug ); ?>-report" class="report-container">
+       <p class="report-description">
+               <?php echo wp_kses_post( Report\Payment_Activity::$description ); ?>
+       </p>
+
+       <form method="get" action="" class="report-form">
+               <div class="field_report-year">
+                       <label for="report-year">Year</label>
+                       <select id="report-year" name="report-year">
+                               <?php foreach ( $years as $year_value ) : ?>
+                                       <option value="<?php echo esc_attr( $year_value ); ?>"<?php selected( $year_value, $year ); ?>><?php echo esc_html( $year_value ); ?></option>
+                               <?php endforeach; ?>
+                       </select>
+               </div>
+
+               <div class="field_period">
+                       <label for="period">Time Period</label>
+                       <select id="period" name="period">
+                               <option value="all"<?php selected( 'all' === $period ); ?>>Entire year</option>
+                               <?php foreach ( $quarters as $quarter_value => $quarter_label ) : ?>
+                                       <option value="<?php echo esc_attr( $quarter_value ); ?>"<?php selected( $quarter_value, $period ); ?>><?php echo esc_html( $quarter_label ); ?></option>
+                               <?php endforeach; ?>
+                               <?php foreach ( $months as $month_value => $month_label ) : ?>
+                                       <option value="<?php echo esc_attr( $month_value ); ?>"<?php selected( $month_value, $period ); ?>><?php echo esc_html( $month_label ); ?></option>
+                               <?php endforeach; ?>
+                       </select>
+               </div>
+
+               <div class="field_wordcamp-id">
+                       <label for="wordcamp-id">WordCamp <span>(optional)</span></label>
+                       <?php echo get_wordcamp_dropdown( 'wordcamp-id', array(), $wordcamp_id ); ?>
+               </div>
+
+               <div class="submit_show-results">
+                       <?php submit_button( 'Show results', 'primary', 'action', false ); ?>
+               </div>
+       </form>
+
+       <?php if ( $report instanceof Report\Payment_Activity ) : ?>
+               <div class="report-results">
+                       <?php $report->render_html(); ?>
+               </div>
+       <?php endif; ?>
+</div>
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewspublicsponsorinvoicesphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/public/sponsor-invoices.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/public/sponsor-invoices.php                                (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/public/sponsor-invoices.php  2018-02-16 16:49:05 UTC (rev 6662)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,63 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * @package WordCamp\Reports
+ */
+
+namespace WordCamp\Reports\Views\Report\Sponsor_Invoices;
+defined( 'WPINC' ) || die();
+
+use WordCamp\Reports\Report;
+
+/** @var string $year */
+/** @var string $period */
+/** @var int    $wordcamp_id */
+/** @var array  $years */
+/** @var array  $quarters */
+/** @var array  $months */
+/** @var Report\Sponsor_Invoices|null $report */
+?>
+
+<div id="<?php echo esc_attr( Report\Sponsor_Invoices::$slug ); ?>-report" class="report-container">
+       <p class="report-description">
+               <?php echo wp_kses_post( Report\Sponsor_Invoices::$description ); ?>
+       </p>
+
+       <form method="get" action="" class="report-form">
+               <div class="field_report-year">
+                       <label for="report-year">Year</label>
+                       <select id="report-year" name="report-year">
+                               <?php foreach ( $years as $year_value ) : ?>
+                                       <option value="<?php echo esc_attr( $year_value ); ?>"<?php selected( $year_value, $year ); ?>><?php echo esc_html( $year_value ); ?></option>
+                               <?php endforeach; ?>
+                       </select>
+               </div>
+
+               <div class="field_period">
+                       <label for="period">Time Period</label>
+                       <select id="period" name="period">
+                               <option value="all"<?php selected( 'all' === $period ); ?>>Entire year</option>
+                               <?php foreach ( $quarters as $quarter_value => $quarter_label ) : ?>
+                                       <option value="<?php echo esc_attr( $quarter_value ); ?>"<?php selected( $quarter_value, $period ); ?>><?php echo esc_html( $quarter_label ); ?></option>
+                               <?php endforeach; ?>
+                               <?php foreach ( $months as $month_value => $month_label ) : ?>
+                                       <option value="<?php echo esc_attr( $month_value ); ?>"<?php selected( $month_value, $period ); ?>><?php echo esc_html( $month_label ); ?></option>
+                               <?php endforeach; ?>
+                       </select>
+               </div>
+
+               <div class="field_wordcamp-id">
+                       <label for="wordcamp-id">WordCamp <span>(optional)</span></label>
+                       <?php echo get_wordcamp_dropdown( 'wordcamp-id', array(), $wordcamp_id ); ?>
+               </div>
+
+               <div class="submit_show-results">
+                       <?php submit_button( 'Show results', 'primary', 'action', false ); ?>
+               </div>
+       </form>
+
+       <?php if ( $report instanceof Report\Sponsor_Invoices ) : ?>
+               <div class="report-results">
+                       <?php $report->render_html(); ?>
+               </div>
+       <?php endif; ?>
+</div>
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewspublicsponsorshipgrantsphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/public/sponsorship-grants.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/public/sponsorship-grants.php                              (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/public/sponsorship-grants.php        2018-02-16 16:49:05 UTC (rev 6662)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,63 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * @package WordCamp\Reports
+ */
+
+namespace WordCamp\Reports\Views\Report\Sponsorship_Grants;
+defined( 'WPINC' ) || die();
+
+use WordCamp\Reports\Report;
+
+/** @var string $year */
+/** @var string $period */
+/** @var int    $wordcamp_id */
+/** @var array  $years */
+/** @var array  $quarters */
+/** @var array  $months */
+/** @var Report\Sponsorship_Grants|null $report */
+?>
+
+<div id="<?php echo esc_attr( Report\Sponsorship_Grants::$slug ); ?>-report" class="report-container">
+       <p class="report-description">
+               <?php echo wp_kses_post( Report\Sponsorship_Grants::$description ); ?>
+       </p>
+
+       <form method="get" action="" class="report-form">
+               <div class="field_report-year">
+                       <label for="report-year">Year</label>
+                       <select id="report-year" name="report-year">
+                               <?php foreach ( $years as $year_value ) : ?>
+                                       <option value="<?php echo esc_attr( $year_value ); ?>"<?php selected( $year_value, $year ); ?>><?php echo esc_html( $year_value ); ?></option>
+                               <?php endforeach; ?>
+                       </select>
+               </div>
+
+               <div class="field_period">
+                       <label for="period">Time Period</label>
+                       <select id="period" name="period">
+                               <option value="all"<?php selected( 'all' === $period ); ?>>Entire year</option>
+                               <?php foreach ( $quarters as $quarter_value => $quarter_label ) : ?>
+                                       <option value="<?php echo esc_attr( $quarter_value ); ?>"<?php selected( $quarter_value, $period ); ?>><?php echo esc_html( $quarter_label ); ?></option>
+                               <?php endforeach; ?>
+                               <?php foreach ( $months as $month_value => $month_label ) : ?>
+                                       <option value="<?php echo esc_attr( $month_value ); ?>"<?php selected( $month_value, $period ); ?>><?php echo esc_html( $month_label ); ?></option>
+                               <?php endforeach; ?>
+                       </select>
+               </div>
+
+               <div class="field_wordcamp-id">
+                       <label for="wordcamp-id">WordCamp <span>(optional)</span></label>
+                       <?php echo get_wordcamp_dropdown( 'wordcamp-id', array(), $wordcamp_id ); ?>
+               </div>
+
+               <div class="submit_show-results">
+                       <?php submit_button( 'Show results', 'primary', 'action', false ); ?>
+               </div>
+       </form>
+
+       <?php if ( $report instanceof Report\Sponsorship_Grants ) : ?>
+               <div class="report-results">
+                       <?php $report->render_html(); ?>
+               </div>
+       <?php endif; ?>
+</div>
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewspublicticketrevenuephp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/public/ticket-revenue.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/public/ticket-revenue.php                          (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/public/ticket-revenue.php    2018-02-16 16:49:05 UTC (rev 6662)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,63 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * @package WordCamp\Reports
+ */
+
+namespace WordCamp\Reports\Views\Report\Ticket_Revenue;
+defined( 'WPINC' ) || die();
+
+use WordCamp\Reports\Report;
+
+/** @var string $year */
+/** @var string $period */
+/** @var int    $wordcamp_id */
+/** @var array  $years */
+/** @var array  $quarters */
+/** @var array  $months */
+/** @var Report\Ticket_Revenue|null $report */
+?>
+
+<div id="<?php echo esc_attr( Report\Ticket_Revenue::$slug ); ?>-report" class="report-container">
+       <p class="report-description">
+               <?php echo wp_kses_post( Report\Ticket_Revenue::$description ); ?>
+       </p>
+
+       <form method="get" action="" class="report-form">
+               <div class="field_report-year">
+                       <label for="report-year">Year</label>
+                       <select id="report-year" name="report-year">
+                               <?php foreach ( $years as $year_value ) : ?>
+                                       <option value="<?php echo esc_attr( $year_value ); ?>"<?php selected( $year_value, $year ); ?>><?php echo esc_html( $year_value ); ?></option>
+                               <?php endforeach; ?>
+                       </select>
+               </div>
+
+               <div class="field_period">
+                       <label for="period">Time Period</label>
+                       <select id="period" name="period">
+                               <option value="all"<?php selected( 'all' === $period ); ?>>Entire year</option>
+                               <?php foreach ( $quarters as $quarter_value => $quarter_label ) : ?>
+                                       <option value="<?php echo esc_attr( $quarter_value ); ?>"<?php selected( $quarter_value, $period ); ?>><?php echo esc_html( $quarter_label ); ?></option>
+                               <?php endforeach; ?>
+                               <?php foreach ( $months as $month_value => $month_label ) : ?>
+                                       <option value="<?php echo esc_attr( $month_value ); ?>"<?php selected( $month_value, $period ); ?>><?php echo esc_html( $month_label ); ?></option>
+                               <?php endforeach; ?>
+                       </select>
+               </div>
+
+               <div class="field_wordcamp-id">
+                       <label for="wordcamp-id">WordCamp <span>(optional)</span></label>
+                       <?php echo get_wordcamp_dropdown( 'wordcamp-id', array(), $wordcamp_id ); ?>
+               </div>
+
+               <div class="submit_show-results">
+                       <?php submit_button( 'Show results', 'primary', 'action', false ); ?>
+               </div>
+       </form>
+
+       <?php if ( $report instanceof Report\Ticket_Revenue ) : ?>
+               <div class="report-results">
+                       <?php $report->render_html(); ?>
+               </div>
+       <?php endif; ?>
+</div>
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewspublicwordcampstatusphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/public/wordcamp-status.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/public/wordcamp-status.php                         (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/public/wordcamp-status.php   2018-02-16 16:49:05 UTC (rev 6662)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,69 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * @package WordCamp\Reports
+ */
+
+namespace WordCamp\Reports\Views\Report\WordCamp_Status;
+defined( 'WPINC' ) || die();
+
+use WordCamp\Reports\Report;
+
+/** @var string $year */
+/** @var string $period */
+/** @var string $status */
+/** @var array  $years */
+/** @var array  $months */
+/** @var array  $statuses */
+/** @var Report\WordCamp_Status|null $report */
+?>
+
+<div id="<?php echo esc_attr( Report\WordCamp_Status::$slug ); ?>-report" class="report-container">
+       <p class="report-description">
+               <?php echo wp_kses_post( Report\WordCamp_Status::$description ); ?>
+       </p>
+
+       <form method="get" action="" class="report-form">
+               <div class="field_report-year">
+                       <label for="report-year">Year</label>
+                       <select id="report-year" name="report-year">
+                               <?php foreach ( $years as $year_value ) : ?>
+                                       <option value="<?php echo esc_attr( $year_value ); ?>"<?php selected( $year_value, $year ); ?>><?php echo esc_html( $year_value ); ?></option>
+                               <?php endforeach; ?>
+                       </select>
+               </div>
+
+               <div class="field_period">
+                       <label for="period">Time Period</label>
+                       <select id="period" name="period">
+                               <option value="all"<?php selected( 'all' === $period ); ?>>Entire year</option>
+                               <option value="q1"<?php selected( 'q1' === $period ); ?>>1st quarter</option>
+                               <option value="q2"<?php selected( 'q2' === $period ); ?>>2nd quarter</option>
+                               <option value="q3"<?php selected( 'q3' === $period ); ?>>3rd quarter</option>
+                               <option value="q4"<?php selected( 'q4' === $period ); ?>>4th quarter</option>
+                               <?php foreach ( $months as $month_value => $month_label ) : ?>
+                                       <option value="<?php echo esc_attr( $month_value ); ?>"<?php selected( $month_value, $period ); ?>><?php echo esc_html( $month_label ); ?></option>
+                               <?php endforeach; ?>
+                       </select>
+               </div>
+
+               <div class="field_status">
+                       <label for="status">Status</label>
+                       <select id="status" name="status">
+                               <option value="any"<?php selected( ( ! $status || 'any' === $status ) ); ?>>Any</option>
+                               <?php foreach ( $statuses as $value => $label ) : ?>
+                                       <option value="<?php echo esc_attr( $value ); ?>"<?php selected( $value, $status ); ?>><?php echo esc_html( $label ); ?></option>
+                               <?php endforeach; ?>
+                       </select>
+               </div>
+
+               <div class="submit_show-results">
+                       <?php submit_button( 'Show results', 'primary', 'action', false ); ?>
+               </div>
+       </form>
+
+       <?php if ( $report instanceof Report\WordCamp_Status ) : ?>
+               <div class="report-results">
+                       <?php $report->render_html(); ?>
+               </div>
+       <?php endif; ?>
+</div>
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewsreportmeetupgroupsphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/report/meetup-groups.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/report/meetup-groups.php                           (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/report/meetup-groups.php     2018-02-16 16:49:05 UTC (rev 6662)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,57 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * @package WordCamp\Reports
+ */
+
+namespace WordCamp\Reports\Views\Report\Meetup_Groups;
+defined( 'WPINC' ) || die();
+
+use WordCamp\Reports;
+use WordCamp\Reports\Report;
+
+/** @var string $start_date */
+/** @var string $end_date */
+/** @var Report\Meetup_Groups|null $report */
+?>
+
+<div class="wrap">
+       <h1>
+               <a href="<?php echo esc_attr( Reports\get_page_url() ); ?>">WordCamp Reports</a>
+               &raquo;
+               <?php echo esc_html( Report\Meetup_Groups::$name ); ?>
+       </h1>
+
+       <?php echo wpautop( wp_kses_post( Report\Meetup_Groups::$description ) ); ?>
+
+       <h4>Methodology</h4>
+
+       <?php echo wpautop( wp_kses_post( Report\Meetup_Groups::$methodology ) ); ?>
+
+       <form method="post" action="">
+               <?php wp_nonce_field( 'run-report', Report\Meetup_Groups::$slug . '-nonce' ); ?>
+
+               <table class="form-table">
+                       <tbody>
+                       <tr>
+                               <th scope="row"><label for="start-date">Start Date</label></th>
+                               <td><input type="date" id="start-date" name="start-date" value="<?php echo esc_attr( $start_date ) ?>" /></td>
+                       </tr>
+                       <tr>
+                               <th scope="row"><label for="end-date">End Date</label></th>
+                               <td><input type="date" id="end-date" name="end-date" value="<?php echo esc_attr( $end_date ) ?>" /></td>
+                       </tr>
+                       <tr>
+                               <th scope="row"><label for="refresh">Refresh results</label></th>
+                               <td><input type="checkbox" id="refresh" name="refresh" /></td>
+                       </tr>
+                       </tbody>
+               </table>
+
+               <?php submit_button( 'Show results', 'primary', 'action', false ); ?>
+               <?php submit_button( 'Export CSV', 'secondary', 'action', false ); ?>
+       </form>
+
+       <?php if ( $report instanceof Report\Meetup_Groups ) : ?>
+               <?php $report->render_html(); ?>
+       <?php endif; ?>
+</div>
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewsreportpaymentactivityphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/report/payment-activity.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/report/payment-activity.php                                (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/report/payment-activity.php  2018-02-16 16:49:05 UTC (rev 6662)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,62 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * @package WordCamp\Reports
+ */
+
+namespace WordCamp\Reports\Views\Report\Payment_Activity;
+defined( 'WPINC' ) || die();
+
+use WordCamp\Reports;
+use WordCamp\Reports\Report;
+
+/** @var string $start_date */
+/** @var string $end_date */
+/** @var int $wordcamp_id */
+/** @var Report\Payment_Activity|null $report */
+?>
+
+<div class="wrap">
+       <h1>
+               <a href="<?php echo esc_attr( Reports\get_page_url() ); ?>">WordCamp Reports</a>
+               &raquo;
+               <?php echo esc_html( Report\Payment_Activity::$name ); ?>
+       </h1>
+
+       <?php echo wpautop( wp_kses_post( Report\Payment_Activity::$description ) ); ?>
+
+       <h4>Methodology</h4>
+
+       <?php echo wpautop( wp_kses_post( Report\Payment_Activity::$methodology ) ); ?>
+
+       <form method="post" action="">
+               <?php wp_nonce_field( 'run-report', Report\Payment_Activity::$slug . '-nonce' ); ?>
+
+               <table class="form-table">
+                       <tbody>
+                       <tr>
+                               <th scope="row"><label for="start-date">Start Date</label></th>
+                               <td><input type="date" id="start-date" name="start-date" value="<?php echo esc_attr( $start_date ) ?>" /></td>
+                       </tr>
+                       <tr>
+                               <th scope="row"><label for="end-date">End Date</label></th>
+                               <td><input type="date" id="end-date" name="end-date" value="<?php echo esc_attr( $end_date ) ?>" /></td>
+                       </tr>
+                       <tr>
+                               <th scope="row"><label for="wordcamp-id">WordCamp (optional)</label></th>
+                               <td><?php echo get_wordcamp_dropdown( 'wordcamp-id', array(), $wordcamp_id ); ?></td>
+                       </tr>
+                       <tr>
+                               <th scope="row"><label for="refresh">Refresh results</label></th>
+                               <td><input type="checkbox" id="refresh" name="refresh" /></td>
+                       </tr>
+                       </tbody>
+               </table>
+
+               <?php submit_button( 'Show results', 'primary', 'action', false ); ?>
+               <?php submit_button( 'Export CSV', 'secondary', 'action', false ); ?>
+       </form>
+
+       <?php if ( $report instanceof Report\Payment_Activity ) : ?>
+               <?php $report->render_html(); ?>
+       <?php endif; ?>
+</div>
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewsreportsponsorinvoicesphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/report/sponsor-invoices.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/report/sponsor-invoices.php                                (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/report/sponsor-invoices.php  2018-02-16 16:49:05 UTC (rev 6662)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,62 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * @package WordCamp\Reports
+ */
+
+namespace WordCamp\Reports\Views\Report\Sponsor_Invoices;
+defined( 'WPINC' ) || die();
+
+use WordCamp\Reports;
+use WordCamp\Reports\Report;
+
+/** @var string $start_date */
+/** @var string $end_date */
+/** @var int $wordcamp_id */
+/** @var Report\Sponsor_Invoices|null $report */
+?>
+
+<div class="wrap">
+       <h1>
+               <a href="<?php echo esc_attr( Reports\get_page_url() ); ?>">WordCamp Reports</a>
+               &raquo;
+               <?php echo esc_html( Report\Sponsor_Invoices::$name ); ?>
+       </h1>
+
+       <?php echo wpautop( wp_kses_post( Report\Sponsor_Invoices::$description ) ); ?>
+
+       <h4>Methodology</h4>
+
+       <?php echo wpautop( wp_kses_post( Report\Sponsor_Invoices::$methodology ) ); ?>
+
+       <form method="post" action="">
+               <?php wp_nonce_field( 'run-report', Report\Sponsor_Invoices::$slug . '-nonce' ); ?>
+
+               <table class="form-table">
+                       <tbody>
+                       <tr>
+                               <th scope="row"><label for="start-date">Start Date</label></th>
+                               <td><input type="date" id="start-date" name="start-date" value="<?php echo esc_attr( $start_date ) ?>" /></td>
+                       </tr>
+                       <tr>
+                               <th scope="row"><label for="end-date">End Date</label></th>
+                               <td><input type="date" id="end-date" name="end-date" value="<?php echo esc_attr( $end_date ) ?>" /></td>
+                       </tr>
+                       <tr>
+                               <th scope="row"><label for="wordcamp-id">WordCamp (optional)</label></th>
+                               <td><?php echo get_wordcamp_dropdown( 'wordcamp-id', array(), $wordcamp_id ); ?></td>
+                       </tr>
+                       <tr>
+                               <th scope="row"><label for="refresh">Refresh results</label></th>
+                               <td><input type="checkbox" id="refresh" name="refresh" /></td>
+                       </tr>
+                       </tbody>
+               </table>
+
+               <?php submit_button( 'Show results', 'primary', 'action', false ); ?>
+               <?php submit_button( 'Export CSV', 'secondary', 'action', false ); ?>
+       </form>
+
+       <?php if ( $report instanceof Report\Sponsor_Invoices ) : ?>
+               <?php $report->render_html(); ?>
+       <?php endif; ?>
+</div>
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewsreportsponsorshipgrantsphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/report/sponsorship-grants.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/report/sponsorship-grants.php                              (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/report/sponsorship-grants.php        2018-02-16 16:49:05 UTC (rev 6662)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,62 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * @package WordCamp\Reports
+ */
+
+namespace WordCamp\Reports\Views\Report\Sponsorship_Grants;
+defined( 'WPINC' ) || die();
+
+use WordCamp\Reports;
+use WordCamp\Reports\Report;
+
+/** @var string $start_date */
+/** @var string $end_date */
+/** @var int $wordcamp_id */
+/** @var Report\Sponsorship_Grants|null $report */
+?>
+
+<div class="wrap">
+       <h1>
+               <a href="<?php echo esc_attr( Reports\get_page_url() ); ?>">WordCamp Reports</a>
+               &raquo;
+               <?php echo esc_html( Report\Sponsorship_Grants::$name ); ?>
+       </h1>
+
+       <?php echo wpautop( wp_kses_post( Report\Sponsorship_Grants::$description ) ); ?>
+
+       <h4>Methodology</h4>
+
+       <?php echo wpautop( wp_kses_post( Report\Sponsorship_Grants::$methodology ) ); ?>
+
+       <form method="post" action="">
+               <?php wp_nonce_field( 'run-report', Report\Sponsorship_Grants::$slug . '-nonce' ); ?>
+
+               <table class="form-table">
+                       <tbody>
+                       <tr>
+                               <th scope="row"><label for="start-date">Start Date</label></th>
+                               <td><input type="date" id="start-date" name="start-date" value="<?php echo esc_attr( $start_date ) ?>" /></td>
+                       </tr>
+                       <tr>
+                               <th scope="row"><label for="end-date">End Date</label></th>
+                               <td><input type="date" id="end-date" name="end-date" value="<?php echo esc_attr( $end_date ) ?>" /></td>
+                       </tr>
+                       <tr>
+                               <th scope="row"><label for="wordcamp-id">WordCamp (optional)</label></th>
+                               <td><?php echo get_wordcamp_dropdown( 'wordcamp-id', array(), $wordcamp_id ); ?></td>
+                       </tr>
+                       <tr>
+                               <th scope="row"><label for="refresh">Refresh results</label></th>
+                               <td><input type="checkbox" id="refresh" name="refresh" /></td>
+                       </tr>
+                       </tbody>
+               </table>
+
+               <?php submit_button( 'Show results', 'primary', 'action', false ); ?>
+               <?php submit_button( 'Export CSV', 'secondary', 'action', false ); ?>
+       </form>
+
+       <?php if ( $report instanceof Report\Sponsorship_Grants ) : ?>
+               <?php $report->render_html(); ?>
+       <?php endif; ?>
+</div>
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewsreportticketrevenuephp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/report/ticket-revenue.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/report/ticket-revenue.php                          (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/report/ticket-revenue.php    2018-02-16 16:49:05 UTC (rev 6662)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,62 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * @package WordCamp\Reports
+ */
+
+namespace WordCamp\Reports\Views\Report\Ticket_Revenue;
+defined( 'WPINC' ) || die();
+
+use WordCamp\Reports;
+use WordCamp\Reports\Report;
+
+/** @var string $start_date */
+/** @var string $end_date */
+/** @var int $wordcamp_id */
+/** @var Report\Ticket_Revenue|null $report */
+?>
+
+<div class="wrap">
+       <h1>
+               <a href="<?php echo esc_attr( Reports\get_page_url() ); ?>">WordCamp Reports</a>
+               &raquo;
+               <?php echo esc_html( Report\Ticket_Revenue::$name ); ?>
+       </h1>
+
+       <?php echo wpautop( wp_kses_post( Report\Ticket_Revenue::$description ) ); ?>
+
+       <h4>Methodology</h4>
+
+       <?php echo wpautop( wp_kses_post( Report\Ticket_Revenue::$methodology ) ); ?>
+
+       <form method="post" action="">
+               <?php wp_nonce_field( 'run-report', Report\Ticket_Revenue::$slug . '-nonce' ); ?>
+
+               <table class="form-table">
+                       <tbody>
+                       <tr>
+                               <th scope="row"><label for="start-date">Start Date</label></th>
+                               <td><input type="date" id="start-date" name="start-date" value="<?php echo esc_attr( $start_date ) ?>" /></td>
+                       </tr>
+                       <tr>
+                               <th scope="row"><label for="end-date">End Date</label></th>
+                               <td><input type="date" id="end-date" name="end-date" value="<?php echo esc_attr( $end_date ) ?>" /></td>
+                       </tr>
+                       <tr>
+                               <th scope="row"><label for="wordcamp-id">WordCamp (optional)</label></th>
+                               <td><?php echo get_wordcamp_dropdown( 'wordcamp-id', array(), $wordcamp_id ); ?></td>
+                       </tr>
+                       <tr>
+                               <th scope="row"><label for="refresh">Refresh results</label></th>
+                               <td><input type="checkbox" id="refresh" name="refresh" /></td>
+                       </tr>
+                       </tbody>
+               </table>
+
+               <?php submit_button( 'Show results', 'primary', 'action', false ); ?>
+               <?php submit_button( 'Export CSV', 'secondary', 'action', false ); ?>
+       </form>
+
+       <?php if ( $report instanceof Report\Ticket_Revenue ) : ?>
+               <?php $report->render_html(); ?>
+       <?php endif; ?>
+</div>
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampreportsviewsreportwordcampstatusphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/report/wordcamp-status.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/report/wordcamp-status.php                         (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-reports/views/report/wordcamp-status.php   2018-02-16 16:49:05 UTC (rev 6662)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,70 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * @package WordCamp\Reports
+ */
+
+namespace WordCamp\Reports\Views\Report\WordCamp_Status;
+defined( 'WPINC' ) || die();
+
+use WordCamp\Reports;
+use WordCamp\Reports\Report;
+
+/** @var string $start_date */
+/** @var string $end_date */
+/** @var string $status */
+/** @var array  $statuses */
+/** @var Report\WordCamp_Status|null $report */
+?>
+
+<div class="wrap">
+       <h1>
+               <a href="<?php echo esc_attr( Reports\get_page_url() ); ?>">WordCamp Reports</a>
+               &raquo;
+               <?php echo esc_html( Report\WordCamp_Status::$name ); ?>
+       </h1>
+
+       <?php echo wpautop( wp_kses_post( Report\WordCamp_Status::$description ) ); ?>
+
+       <h4>Methodology</h4>
+
+       <?php echo wpautop( wp_kses_post( Report\WordCamp_Status::$methodology ) ); ?>
+
+       <form method="post" action="">
+               <input type="hidden" name="action" value="run-report" />
+               <?php wp_nonce_field( 'run-report', Report\WordCamp_Status::$slug . '-nonce' ); ?>
+
+               <table class="form-table">
+                       <tbody>
+                               <tr>
+                                       <th scope="row"><label for="start-date">Start Date</label></th>
+                                       <td><input type="date" id="start-date" name="start-date" value="<?php echo esc_attr( $start_date ) ?>" /></td>
+                               </tr>
+                               <tr>
+                                       <th scope="row"><label for="end-date">End Date</label></th>
+                                       <td><input type="date" id="end-date" name="end-date" value="<?php echo esc_attr( $end_date ) ?>" /></td>
+                               </tr>
+                               <tr>
+                                       <th scope="row"><label for="status">Status (optional)</label></th>
+                                       <td>
+                                               <select id="status" name="status">
+                                                       <option value="any"<?php selected( ( ! $status || 'any' === $status ) ); ?>>Any</option>
+                                                       <?php foreach ( $statuses as $value => $label ) : ?>
+                                                               <option value="<?php echo esc_attr( $value ); ?>"<?php selected( $value, $status ); ?>><?php echo esc_attr( $label ); ?></option>
+                                                       <?php endforeach; ?>
+                                               </select>
+                                       </td>
+                               </tr>
+                               <tr>
+                                       <th scope="row"><label for="refresh">Refresh results</label></th>
+                                       <td><input type="checkbox" id="refresh" name="refresh" /></td>
+                               </tr>
+                       </tbody>
+               </table>
+
+               <?php submit_button( 'Show results', 'primary', '' ); ?>
+       </form>
+
+       <?php if ( $report instanceof Report\WordCamp_Status ) : ?>
+               <?php $report->render_html(); ?>
+       <?php endif; ?>
+</div>
</ins></span></pre>
</div>
</div>

</body>
</html>