<!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( ' → ', $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>
+ –
+ <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 “<?php echo esc_html( $statuses[ $status ] ); ?>”
+ <?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> – <?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 “<?php echo esc_html( $statuses[ $status ] ); ?>”
+ <?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> –
+ <?php echo esc_html( $statuses[ $inactive_camp['latest_status'] ] ); ?> –
+ <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 “<?php echo esc_html( $statuses[ $status ] ); ?>”
+ <?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>
+ »
+ <?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>
+ »
+ <?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>
+ »
+ <?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>
+ »
+ <?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>
+ »
+ <?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>
+ »
+ <?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>