Currency conversion and formatting, a real dashboard, and statistics generation

master
Sven Slootweg 11 years ago
parent dd4c327c76
commit 59c1620e89

@ -21,17 +21,25 @@ class Campaign extends CPHPDatabaseRecordClass
public $prototype = array(
'string' => array(
'Name' => "Name",
'UrlName' => "UrlName"
'Name' => "Name",
'UrlName' => "UrlName"
),
'numeric' => array(
'OwnerId' => "UserId"
'OwnerId' => "UserId",
'DonationRate' => "DonationRate",
'SubscriberCount' => "SubscriberCount",
'MonthlyTotal' => "TotalMonthlyDonations",
'MonthlyProjection' => "ProjectedMonthlyDonations"
),
'boolean' => array(
'AllowOneTime' => "AllowOneTime"
'AllowOneTime' => "AllowOneTime",
'HaveData' => "HaveData"
),
'timestamp' => array(
'LastStatisticsUpdate' => "LastStatisticsUpdate"
),
'user' => array(
'Owner' => "Owner"
'Owner' => "Owner"
)
);
@ -52,4 +60,78 @@ class Campaign extends CPHPDatabaseRecordClass
{
return self::CreateFromQuery("SELECT * FROM campaigns WHERE `UrlName` = :UrlName", array(':UrlName' => $urlname), 0, true);
}
public function UpdateStatistics()
{
global $database;
if($this->sLastStatisticsUpdate < time() - (60 * 5))
{
/* Update subscriber count */
if($result = $database->CachedQuery("SELECT COUNT(*) FROM subscriptions WHERE `CampaignId` = :CampaignId AND `Confirmed` = 1", array(":CampaignId" => $this->sId)))
{
$this->uSubscriberCount = $result->data[0]["COUNT(*)"];
}
/* Update total monthly donations */
try
{
$sSubscriptions = Subscription::CreateFromQuery("SELECT * FROM subscriptions WHERE `CampaignId` = :CampaignId AND `Confirmed` = 1", array(":CampaignId" => $this->sId));
$sTotalDonations = 0;
foreach($sSubscriptions as $sSubscription)
{
$sTotalDonations += Currency::Convert("usd", $sSubscription->sCurrency, $sSubscription->sAmount);
}
$this->uMonthlyTotal = $sTotalDonations;
}
catch (NotFoundException $e)
{
$this->uMonthlyTotal = 0;
}
/* Update donation rate */
try
{
$sDonationsAsked = LogEntry::CreateFromQuery("SELECT * FROM log_entries WHERE `CampaignId` = :CampaignId AND `Type` = :Type AND `Date` > DATE_SUB(NOW(), INTERVAL 1 MONTH)",
array(":CampaignId" => $this->sId, ":Type" => LogEntry::DONATION_ASKED));
$have_data = true;
}
catch (NotFoundException $e)
{
/* We don't have any data to work from yet. */
$have_data = false;
}
if($have_data)
{
try
{
$sDonationsMade = LogEntry::CreateFromQuery("SELECT * FROM log_entries WHERE `CampaignId` = :CampaignId AND `Type` = :Type AND `Date` > DATE_SUB(NOW(), INTERVAL 1 MONTH)",
array(":CampaignId" => $this->sId, ":Type" => LogEntry::DONATION_MADE));
$this->uDonationRate = (count($sDonationsMade) / count($sDonationsAsked)) * 100;
$this->uHaveData = true;
}
catch (NotFoundException $e)
{
$this->uDonationRate = 0;
$this->uHaveData = false;
}
}
else
{
$this->uDonationRate = 100;
$this->uHaveData = false;
}
/* Update projected monthly donations */
$this->uMonthlyProjection = $this->uMonthlyTotal * ($this->uDonationRate / 100);
$this->uLastStatisticsUpdate = time();
$this->InsertIntoDatabase();
}
}
}

@ -0,0 +1,91 @@
<?php
/*
* ReDonate is more free software. It is licensed under the WTFPL, which
* allows you to do pretty much anything with it, without having to
* ask permission. Commercial use is allowed, and no attribution is
* required. We do politely request that you share your modifications
* to benefit other developers, but you are under no enforced
* obligation to do so :)
*
* Please read the accompanying LICENSE document for the full WTFPL
* licensing text.
*/
if(!isset($_APP)) { die("Unauthorized."); }
class CurrencyConversionException extends Exception { }
class CurrencyFormattingException extends Exception { }
class Currency
{
public static function Convert($target_currency, $original_currency, $amount)
{
$original_currency = strtoupper($original_currency);
$target_currency = strtoupper($target_currency);
try
{
$sOriginRate = ExchangeRate::CreateFromQuery("SELECT * FROM exchange_rates WHERE `Code` = :Code", array(":Code" => $original_currency), 300, true);
}
catch (NotFoundException $e)
{
throw new CurrencyConversionException("The specified origin currency is not a valid currency code.");
}
$sUsdRate = $amount * $sOriginRate->sFromRate;
if($target_currency === "USD")
{
return $sUsdRate;
}
else
{
try
{
$sTargetRate = ExchangeRate::CreateFromQuery("SELECT * FROM exchange_rates WHERE `Code` = :Code", array(":Code" => $target_currency), 300, true);
}
catch (NotFoundException $e)
{
throw new CurrencyConversionException("The specified target currency is not a valid currency code.");
}
$sResult = $sUsdRate * $sTargetRate->sToRate;
return $sResult;
}
}
public static function Format($currency, $amount, $precision = 2)
{
try
{
$sExchangeRate = ExchangeRate::CreateFromQuery("SELECT * FROM exchange_rates WHERE `Code` = :Code", array(":Code" => $currency), 30, true);
}
catch (NotFoundException $e)
{
throw new CurrencyFormattingException("The specified currency is not a valid currency code.");
}
if($sExchangeRate->sSymbol == "")
{
return "{$sExchangeRate->sCurrencyCode} " . number_format($amount, $precision);
}
else
{
return "{$sExchangeRate->sSymbol}" . number_format($amount, $precision);
}
}
public static function UpdateRates()
{
global $cphp_config;
$json = json_decode(file_get_contents("http://openexchangerates.org/api/latest.json?app_id={$cphp_config->openexchangerates->app_id}"), true);
$rates = $json["rates"];
foreach($rates as $currency => $rate)
{
ExchangeRate::Update($currency, $rate);
}
}
}

@ -0,0 +1,56 @@
<?php
/*
* projectname is more free software. It is licensed under the WTFPL, which
* allows you to do pretty much anything with it, without having to
* ask permission. Commercial use is allowed, and no attribution is
* required. We do politely request that you share your modifications
* to benefit other developers, but you are under no enforced
* obligation to do so :)
*
* Please read the accompanying LICENSE document for the full WTFPL
* licensing text.
*/
if(!isset($_APP)) { die("Unauthorized."); }
class ExchangeRate extends CPHPDatabaseRecordClass
{
public $table_name = "exchange_rates";
public $fill_query = "SELECT * FROM exchange_rates WHERE `Id` = :Id";
public $verify_query = "SELECT * FROM exchange_rates WHERE `Id` = :Id";
public $prototype = array(
'string' => array(
'Name' => "Name",
'CurrencyCode' => "Code",
'Symbol' => "Symbol"
),
'numeric' => array(
'FromRate' => "From",
'ToRate' => "To"
),
'timestamp' => array(
'UpdateDate' => "Updated"
)
);
public static function Update($code, $to)
{
$from = 1 / $to;
try
{
$sRate = ExchangeRate::CreateFromQuery("SELECT * FROM exchange_rates WHERE `Code` = :Code", array(":Code" => $code), 0, true);
}
catch(NotFoundException $e)
{
$sRate = new ExchangeRate(0);
$sRate->uCurrencyCode = $code;
}
$sRate->uToRate = $to;
$sRate->uFromRate = $from;
$sRate->uUpdateDate = time();
$sRate->InsertIntoDatabase();
}
}

@ -39,4 +39,6 @@ class LogEntry extends CPHPDatabaseRecordClass
const PAGELOAD = 1;
const SUBSCRIPTION = 2;
const DONATION_ASKED = 3;
const DONATION_MADE = 4;
}

@ -0,0 +1,53 @@
<?php
/*
* ReDonate is more free software. It is licensed under the WTFPL, which
* allows you to do pretty much anything with it, without having to
* ask permission. Commercial use is allowed, and no attribution is
* required. We do politely request that you share your modifications
* to benefit other developers, but you are under no enforced
* obligation to do so :)
*
* Please read the accompanying LICENSE document for the full WTFPL
* licensing text.
*/
if(!isset($_APP)) { die("Unauthorized."); }
class PaymentMethod extends CPHPDatabaseRecordClass
{
public $table_name = "payment_methods";
public $fill_query = "SELECT * FROM payment_methods WHERE `Id` = :Id";
public $verify_query = "SELECT * FROM payment_methods WHERE `Id` = :Id";
public $prototype = array(
'string' => array(
'Address' => "Address"
),
'numeric' => array(
'Type' => "Type",
'CampaignId' => "CampaignId"
),
'campaign' => array(
'Campaign' => "Campaign"
)
);
const PAYPAL = 1;
const BITCOIN = 2;
const IBAN = 3;
public function GetLogo()
{
switch($this->sType)
{
case PaymentMethod::PAYPAL:
return array("image" => "/static/images/paypal.png", "text" => "PayPal");
case PaymentMethod::BITCOIN:
return array("image" => "/static/images/bitcoin.png", "text" => "Bitcoin");
case PaymentMethod::IBAN:
return array("text" => "IBAN");
default:
return array("text" => "Unknown");
}
}
}

@ -15,12 +15,49 @@ if(!isset($_APP)) { die("Unauthorized."); }
$sCampaigns = array();
$sPercentages = array();
$sTotals = array();
$sProjections = array();
$sSubscribers = array();
try
{
foreach(Campaign::CreateFromQuery("SELECT * FROM campaigns WHERE `OwnerId` = :UserId", array(":UserId" => $sCurrentUser->sId)) as $sCampaign)
{
$sCampaign->UpdateStatistics();
$sPaymentMethods = array();
try
{
foreach(PaymentMethod::CreateFromQuery("SELECT * FROM payment_methods WHERE `CampaignId` = :CampaignId",
array(":CampaignId" => $sCampaign->sId)) as $sPaymentMethod)
{
$sPaymentMethods[] = $sPaymentMethod->GetLogo();
}
}
catch (NotFoundException $e)
{
/* No payment methods...? */
}
if($sCampaign->sHaveData)
{
$sPercentages[] = $sCampaign->sDonationRate;
$sTotals[] = $sCampaign->sMonthlyTotal;
$sProjections[] = $sCampaign->sMonthlyProjection;
}
$sSubscribers[] = $sCampaign->sSubscriberCount;
$sCampaigns[] = array(
"name" => $sCampaign->sName
"name" => $sCampaign->sName,
"subscribers" => number_format($sCampaign->sSubscriberCount, 0),
"rate" => number_format($sCampaign->sDonationRate, 2),
"total" => Currency::Format("usd", $sCampaign->sMonthlyTotal),
"projection" => Currency::Format("usd", $sCampaign->sMonthlyProjection),
"one-off" => $sCampaign->sAllowOneTime,
"payment-methods" => $sPaymentMethods,
"have-data" => $sCampaign->sHaveData
);
}
}
@ -29,7 +66,16 @@ catch (NotFoundException $e)
/* pass */
}
$sPercentages = (empty($sPercentages)) ? array(0) : $sPercentages;
$sTotals = (empty($sTotals)) ? array(0) : $sTotals;
$sProjections = (empty($sProjections)) ? array(0) : $sProjections;
$sSubscribers = (empty($sSubscribers)) ? array(0) : $sSubscribers;
$sPageTitle = "Dashboard";
$sPageContents = NewTemplater::Render("dashboard", $locale->strings, array(
"campaigns" => $sCampaigns
"campaigns" => $sCampaigns,
"total-rate" => number_format(array_sum($sPercentages) / count($sPercentages), 2),
"total-subscribers" => number_format(array_sum($sSubscribers), 0),
"total-total" => Currency::Format("usd", array_sum($sTotals)),
"total-projection" => Currency::Format("usd", array_sum($sProjections))
));

@ -24,6 +24,8 @@ catch (NotFoundException $e)
return;
}
$sCampaign->UpdateStatistics();
$sLogEntry = new LogEntry(0);
$sLogEntry->uType = LogEntry::PAGELOAD;
$sLogEntry->uIp = $_SERVER['REMOTE_ADDR'];

@ -269,6 +269,93 @@ form button:active
margin-bottom: 13px;
}
/**************************************
* TABLES *
**************************************/
table
{
border-collapse: collapse;
width: 100%;
}
table, td
{
border: 1px solid #688A3C;
}
th, td
{
padding: 7px 10px;
}
td
{
color: #292929;
}
th
{
text-align: left;
font-size: 17px;
background-color: #688A3C;
border: 1px solid #5A6B46;
color: #FDFFFD;
text-shadow: 0px 1px 0px #1A5200;
-webkit-text-shadow: 0px 1px 0px #1A5200;
-moz-text-shadow: 0px 1px 0px #1A5200;
-o-text-shadow: 0px 1px 0px #1A5200;
-ms-text-shadow: 0px 1px 0px #1A5200;
}
th.icon
{
padding: 4px;
text-align: center;
}
td.numeric
{
text-align: right;
}
td.name, td.total
{
font-weight: bold;
}
td.name
{
color: #203A00;
}
td.total
{
color: black;
}
tr.total td
{
border-top: 2px solid #030600;
border-right: 1px solid #D3E8B7;
border-left: 1px solid #D3E8B7;
}
tr.total td:nth-child(1)
{
border-left: 1px solid #688A3C;
}
tr.total td:nth-last-child(1)
{
border-right: 1px solid #688A3C;
}
td.meta
{
font-style: italic;
}
/**************************************
* NOTIFICATIONS *
**************************************/
@ -291,6 +378,39 @@ form button:active
background-color: #F8FFF7;
}
/**************************************
* LOGOS *
**************************************/
.logo.thumb
{
margin-right: 11px;
}
img.logo.thumb
{
vertical-align: middle;
height: 20px;
}
div.logo.thumb
{
font-size: 18px;
font-weight: bold;
font-style: italic;
color: #1F2E0B;
display: inline-block;
}
/**************************************
* DASHBOARD *
**************************************/
td.payment-methods
{
max-width: 240px;
}
/**************************************
* LANDING *
**************************************/

@ -1,3 +1,5 @@
<h2 class="spaced">Dashboard</h2>
{%if isempty|notices == false}
{%foreach notice in notices}
<div class="notices">
@ -9,14 +11,52 @@
<table>
<tr>
<th>Name</th>
<th>Campaign type</th>
<th>Type</th>
<th class="icon"><img src="/static/images/icons/subscribers.png" alt="Amount of subscribers" title="Amount of subscribers"></th>
<th class="icon"><img src="/static/images/icons/rate.png" alt="Percentage of subscribed donations actually made in the past month" title="Percentage of subscribed donations actually made in the past month"></th>
<th class="icon"><img src="/static/images/icons/total.png" alt="Total amount of subscribed donations per month" title="Total amount of subscribed donations per month"></th>
<th class="icon"><img src="/static/images/icons/projected.png" alt="Estimate of real donations per month, based on donation rate" title="Estimate of real donations per month, based on donation rate"></th>
<th>Payment methods</th>
</tr>
{%foreach campaign in campaigns}
<tr>
<td>{%?campaign[name]}</td>
<td></td>
<td></td>
<td class="name">{%?campaign[name]}</td>
<td>
{%if campaign[one-off] == false}
Recurring
{%else}
Recurring &amp; One-off
{%/if}
</td>
<td class="numeric">{%?campaign[subscribers]}</td>
<td class="numeric">
{%if campaign[have-data] == true}
{%?campaign[rate]}%
{%else}
-
{%/if}
</td>
<td class="numeric">{%?campaign[total]}</td>
<td class="numeric total">{%?campaign[projection]}</td>
<td class="payment-methods">
{%foreach method in campaign[payment-methods]}
{%if isempty|method[image] == false}
<img class="logo thumb" src="{%?method[image]}" alt="{%?method[text]}">
{%else}
<div class="logo thumb">{%?method[text]}</div>
{%/if}
{%/foreach}
</td>
</tr>
{%/foreach}
<tr class="total">
<td class="meta" colspan="2">Total</td>
<td class="numeric">{%?total-subscribers}</td>
<td class="numeric">{%?total-rate}%</td>
<td class="numeric">{%?total-total}</td>
<td class="numeric total">{%?total-projection}</td>
<td class="payment-methods">
</td>
</tr>
</table>

Loading…
Cancel
Save