Commit b3af3712 authored by Normann Lou's avatar Normann Lou

tag branches/0.4 at r98706 to be tags/rc/0.4.0-rc1

parent 919b7bd8
0.4
0.3
0.2
Enhancements
[70943] Newsletter module heading doesn't have current status
0.1.1
Enhancements
[64365] Pulled out Newsletter specific stuff from Member.php in sapphire core into the newsletter module. This includes Member_UnsubscribeRecord which is now just UnsubscribeRecord, and member fields and methods that can now be found in NewsletterRole
[66757] Added static properties to NewsletterType for allowing decoration
[66760] Encapsulated the NewsletterType CMS fields into NewsletterType->getCMSFields(), removing the old unused getCMSFields()
[68929] Made tree items collapsed instead of expanded by default to avoid insanity
[68996] Added ability to choose message in unsubscribe
[69336] Allow for longer FromEmail in the DB
[69898] Allow preview of a Newsletter object by going to the URL admin/newsletter/preview/(ID) where (ID) is a valid ID of a Newsletter record in the database
[69904] Added link to preview the newsletter (opens a new tab or window)
[70406] Allow manual selection of group to send newsletters to instead of hardcoded group automagically created when new newsletter type is created
[70809] Removed blacklist newsletter specific code out of core and into newsletter module
API Changes
[62309] Moved ProgressBar and support files to newsletter/trunk module, as this is the module where it's used
[65554] Tidy up NewsletterAdmin
Bugfixes
[64434] Fixing usage of deprecated APIs
[65098] Adjusted NewsletterAdmin to new CMS Menu generation (see #2872)
[65554] a lot of methods in this class now passed $params as HTTPRequest object, rather than as a array if the function is called from Ajax or top-level of front-end, some method is called in both manner, ie. called from Ajax and called internally as well, so we need to check $params type and do further process. This is a partial fix of open source ticket #3035
[66703] Fix newsletter module to work with the 2.3 URL handler
[66760] Allow loading data from the NewsletterType for all fields, not just 2
[68703] Updated newsletter admin to support HtmlEditorField changes in r68701
[68936] Fixed member search in Mailing List in CMS
[68967] Fixed resend and save buttons greyed out when viewing a draft
[68987] fixed bugs in URL for unsubscribe
[68989] updated URL handler for unsubscribe controller
[69461] Fixed TinyMCE 3.2 in newsletter
[69920] #3322: Fixed newsletter html editor saving
[70599] styling newsletter send button to match
[70668] Fixed error in preview if no template is discovered (falls back to GenericEmail)
[70672] Fixed newsletter cancel/send actions to be styled consistent - removed button and used consistent input type submit tag instead
Minor Changes
[62487] Added or updated README information (mostly with maintainer contact)
[62514] MINOR Added or edited README files, added LICENSE and CHANGELOG files
[63872] Updated entities from translate.silverstripe.com
[64367] Updated phpDoc @package comments for newsletter module
[64388] Misc deprecation fixes
[64434] Added language strings
[65042] Collecting entities for language master table with new i18nTextCollector functionality. The table is now sorted alphabetically by namespace and entity. Entities now include more translatable statics from DataObject subclasses like $db, $has_one etc.
[65084] Updated master language tables
[65496] type enviroment -> environment, post-Payment polishing, receipt, email template polishing
[65499] remove debug information
[66388] delete Debug information from the js code
[66760] Code formatting
[67267] formatting improvements
[69900] Remove unncessary Permission::check() in NewsletterAdmin->preview()
[69903] Added notes about a hack
[69998] Updated translations
[70112] Updated translations
[70420] Removed redundant code from NewsletterAdmin
[70737] Fixed sorting of groups in dropdown
[70813] Fixed calls of Requirements to use constants instead of hardcoded "cms" and "jsparty" for NewsletterAdmin
[70813] Ensure ContentCSS uses theme CSS, falling back to project if not available
[70820] Hide the UnsubscribedRecords relation field in NewsletterRole
[70821] Ensure "BlacklistedEmail" field is hidden from CMS fields
0.1.0
Initial release
trunk
* Copyright (c) 2008, Silverstripe Ltd.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the <organization> nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY Silverstripe Ltd. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL Silverstripe Ltd. BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
############################################
Newsletter Module
############################################
Maintainer Contact
-----------------------------------------------
Normann Lou (Nickname: nlou)
<normann (at) silverstripe (dot) com>
Requirements
-----------------------------------------------
SilverStripe 2.3+
userforms 0.2+
Documentation
-----------------------------------------------
http://doc.silverstripe.com/doku.php?id=modules:newsletter
Installation Instructions
-----------------------------------------------
Extract the newsletter folder into the top level of your site, and visit
http://www.mysite.com/dev/build to rebuild the database.
<?php
/**
* URL rules for the CMS module
*
* @package newsletter
*/
define('NEWSLETTER_DIR', 'newsletter');
Director::addRules(50, array(
'unsubscribe//$Action/$Email/$MailingList' => 'Unsubscribe_Controller'
));
DataObject::add_extension('Member', 'NewsletterRole');
?>
\ No newline at end of file
<?php
/**
* Create a process in session which is incremented to calls from the client
*
* @package newsletter
*/
class BatchProcess extends Object {
protected $objects;
protected $current;
protected $id;
protected $scriptOutput = true;
function __construct( $collection ) {
$this->current = 0;
if( $collection ) {
if( is_array( $collection ) )
$this->objects = $collection;
elseif( is_a( $collection, 'DataObjectSet' ) ) {
$this->objects = $collection->toArray();
} else
$this->objects = array( $collection );
}
parent::__construct();
}
function runToCompletion() {
$this->scriptOutput = false;
$this->current = 0;
$ignore = $this->next( count( $this->objects ) );
$this->complete();
}
function getID() {
return $this->id;
}
function next() {
self::addProcess( $this );
return $this->id.':'.$this->current.'/'.count( $this->objects );
}
function start() {
$this->current = 0;
$this->id = self::generateID();
if( !$this->objects || count( $this->objects ) === 0 )
return $this->complete();
return $this->next();
}
function complete() {
self::removeProcess( $this );
}
static function generateID() {
return count(Session::get('BatchProcesses')) + 1;
}
static function addProcess( $process ) {
Session::set('BatchProcesses.' . ($process->getID() - 1), serialize($process));
}
static function removeProcess( $process ) {
Session::clear('BatchProcesses.' . ($process->getID() - 1));
}
}
/**
* Controller for calling the batch processes via Ajax.
*
* @package newsletter
*/
class BatchProcess_Controller extends Controller {
function next() {
$processID = $this->urlParams['ID'];
if( !$processID ) {
return _t('BatchProcess_Controller.ERROR', 'ERROR: Could not continue process');
}
$process = unserialize(Session::get('BatchProcesses.' . ($this->urlParams['ID'] - 1)));
if( !$process ) {
return _t('BatchProcess_Controller.ERROR');
}
if( $this->urlParams['Batch'] )
return $process->next( $this->urlParams['Batch'] );
else
return $process->next();
}
}
?>
<?php
/**
* Form field showing a list of bounced addresses
*
* @package newsletter
*/
class BouncedList extends FormField {
protected $nlType;
function __construct( $name, $newsletterType ) {
parent::__construct( $name, '', null );
if( is_object( $newsletterType ) )
$this->nlType = $newsletterType;
else
$this->nlType = DataObject::get_by_id( 'NewsletterType', $newsletterType );
}
function setController($controller) {
$this->controller = $controller;
}
function FieldHolder() {
return $this->renderWith( 'NewsletterAdmin_BouncedList' );
}
function Entries() {
$id = $this->nlType->GroupID;
if(defined('DB::USE_ANSI_SQL')) {
//$bounceRecords = DataObject::get( 'Email_BounceRecord', "\"GroupID\"=\"$id\"", null, "INNER JOIN \"Group_Members\" USING(\"MemberID\")" );
$bounceRecords = DataObject::get( 'Email_BounceRecord', "\"GroupID\"='$id'", null, "INNER JOIN \"Group_Members\" ON \"Email_BounceRecord\".\"MemberID\" = \"Group_Members\".\"MemberID\"" );
} else {
$bounceRecords = DataObject::get( 'Email_BounceRecord', "`GroupID`='$id'", null, "INNER JOIN `Group_Members` USING(`MemberID`)" );
}
//user_error($id, E_USER_ERROR );
if( !$bounceRecords )
return null;
foreach( $bounceRecords as $bounceRecord ) {
if( $bounceRecord ) {
$bouncedUsers[] = new ArrayData( array(
'Record' => $bounceRecord,
'GroupID' => $id,
'Member' => DataObject::get_by_id( 'Member', $bounceRecord->MemberID )
));
}
}
return new DataObjectSet( $bouncedUsers );
}
}
?>
\ No newline at end of file
<?php
class CheckboxSetWithExtraField extends CheckboxSetField{
public $extra = array();
public $extraValue = array();
// can be two-D array eg. array("Email"=>arrray("Value","Reqired"));
public $cellDisabled = array();
public $tragable = true;
/**
* Creates a new optionset field.
* @param name The field name
* @param title The field title
* @param source An map of the dropdown items
* @param extra An map of label (DBField's name) => classname (DBField's class) for extra field
* @param value The current value
* @param extraValue The current extraValues
* @param form The parent form
*/
function __construct($name, $title = "", $source = array(), $extra=array(), $value = "", $extraValue=array(), $form = null) {
if(!empty($extra)) $this->extra = $extra;
if(!empty($extraValue)) $this->extraValue = $extraValue;
parent::__construct($name, $title, $source, $value, $form);
}
function setCellDisabled($cellDisabled){
$this->cellDisabled = $cellDisabled;
}
/**
* Sets the template to be rendered with
*/
function FieldHolder() {
Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery.js');
Requirements::javascript(THIRDPARTY_DIR . '/jquery-livequery/jquery.livequery.js');
Requirements::javascript(NEWSLETTER_DIR . '/thirdparty/jquery-tablednd/jquery.tablednd_0_5.js');
Requirements::javascript(NEWSLETTER_DIR . '/javascript/CheckboxSetWithExtraField.js');
return parent::FieldHolder();
}
/**
* @todo Explain different source data that can be used with this field,
* e.g. SQLMap, DataObjectSet or an array.
*
* @todo Should use CheckboxField FieldHolder rather than constructing own markup.
*/
function Field() {
Requirements::css(NEWSLETTER_DIR . '/css/CheckboxSetWithExtraField.css');
$source = $this->source;
$values = $this->value;
// Get values from the join, if available
if(is_object($this->form)) {
$record = $this->form->getRecord();
if(!$values && $record && $record->hasMethod($this->name)) {
$funcName = $this->name;
$join = $record->$funcName();
if($join) {
foreach($join as $joinItem) {
$values[] = $joinItem->ID;
}
}
}
}
// Source is not an array
if(!is_array($source) && !is_a($source, 'SQLMap')) {
if(is_array($values)) {
$items = $values;
} else {
// Source and values are DataObject sets.
if($values && is_a($values, 'DataObjectSet')) {
foreach($values as $object) {
if(is_a($object, 'DataObject')) {
$items[] = $object->ID;
}
}
} elseif($values && is_string($values)) {
$items = explode(',', $values);
$items = str_replace('{comma}', ',', $items);
}
}
} else {
// Sometimes we pass a singluar default value thats ! an array && !DataObjectSet
if(is_a($values, 'DataObjectSet') || is_array($values)) {
$items = $values;
} else {
$items = explode(',', $values);
$items = str_replace('{comma}', ',', $items);
}
}
if(is_array($source)) {
unset($source['']);
}
$odd = 0;
$options = '';
if ($source == null) {
$source = array();
$options = "<tr><td>No options available</td></tr>";
}else{
$header = "<thead><tr><th>Checked?</th>";
$footer = "<tfoot><tr><td>Checked?</td>";
if(!empty($this->extra)){
foreach($this->extra as $label=>$type){
$fieldLabel = FormField::name_to_label($label);
$header .= "<th>$fieldLabel</th>";
$footer .= "<td>$fieldLabel</td>";
}
}
//add a column for drag&drop icon
if($this->tragable) {
$header .= "<th></th>";
$footer .= "<td></td>";
}
$header .= "</tr></thead>";
$footer .= "</tr></tfoot>";
foreach($source as $index => $item) {
if(is_a($item, 'DataObject')) {
$key = $item->ID;
$value = $item->Title;
} else {
$key = $index;
$value = $item;
}
$odd = ($odd + 1) % 2;
$extraClass = $odd ? 'odd' : 'even';
$extraClass .= ' val' . str_replace(' ', '', $key);
$itemID = $this->id() . '_' . ereg_replace('[^a-zA-Z0-9]+', '', $key);
$checked = '';
if(isset($items)) {
$checked = (in_array($key, $items)) ? ' checked="checked"' : '';
}
$disabled = isset($this->cellDisabled[$key]) && in_array('Value', $this->cellDisabled[$key]) ? $disabled = ' disabled="disabled"' : '';
$options .= "<tr class=\"$extraClass\">
<td>
<input id=\"$itemID\" name=\"$this->name[$key][Value]\" type=\"checkbox\" value=\"$key\"$checked $disabled class=\"checkbox\" /> $value
</td>";
if(!empty($this->extraValue)){
foreach($this->extraValue as $label => $val){
if($val)
$extraValue[$label] = Convert::json2array($val);
}
}
if(!empty($this->extra)){
foreach($this->extra as $label => $fieldType){
$value = "";
if(isset($extraValue[$label][$key])){
$value = $extraValue[$label][$key];
}
$dbField = DBField::create($fieldType, $value, $this->name."[".$key."][".$label."]");
$extraField = $dbField->scaffoldFormField($this->name."[".$key."][".$label."]");
$extraField -> setValue($value);
if(isset($this->cellDisabled[$key]) && in_array($label, $this->cellDisabled[$key])) $extraField->setDisabled(true);
$options .= "<td>".$extraField->Field()."</td>";
}
}
$options .= "<td class=\"dragHandle\"></td>";
$options .= "</tr>";
}
}
return "<table id=\"{$this->id()}\" class=\"optionset checkboxsetwithextrafield{$this->extraClass()}\">".$header.$footer.$options."</table>\n";
}
/**
* Return the CheckboxSetField value as an array
* selected item keys.
*
* @return string
*/
function dataValue() {
$value = ArrayLib::invert($this->value);
if(isset($value) && is_array($value)) {
foreach($value as $key=>$items) {
foreach($items as $k=>$v){
if($v)
$filtered[$key][$k] = str_replace(", ", "{comma}", $v);
}
}
if(isset($filtered)) return Convert::array2json($filtered);
else return '';
}
return '';
}
function saveInto(DataObject $record) {
$fieldname = $this->name ;
$value = ArrayLib::invert($this->value);
if($fieldname && $record && ($record->has_many($fieldname) || $record->many_many($fieldname))) {
$idList = array();
if($value) foreach($value['Value'] as $id => $bool) {
if($bool) $idList[] = $id;
}
$record->$fieldname()->setByIDList($idList);
} elseif($fieldname && $record) {
if($value) {
if(is_array($value)) foreach($value as $k => $items){
if(is_array($items)) foreach($items as $key => $val){
if(!$val){
unset($value[$k][$key]);
}else{
if($k == 'Value'){
$value[$k][$key] = str_replace(", ", "{comma}", $val);
}
}
}
}
foreach($value as $k => $v){
if($k == 'Value'){
$record->$fieldname = implode(",", $v);
}else{
$record->$k = Convert::array2json($v);
}
}
} else {
$record->$fieldname = '';
}
}
}
function setValue($val, $data){
if(is_string($val)) {
$val = explode(",", $val);
}
$this->value = $val;
// We need to sort the fields according to the $val, so that the list of
// fields apparing in the right order.
$sortedSource = array();
$sourceKeys = array_keys($this->source);
foreach($this->value as $item){
if(in_array($item, $sourceKeys)){
$sortedSource[$item] = $this->source[$item];
}
}
$this->source = array_merge($sortedSource, $this->source);
if(count($this->extra)){
foreach($this->extra as $field => $type){
if($data && isset($data->$field)){
$this->extraValue[$field] = $data->$field;
}
}
}
}
}
?>
\ No newline at end of file
<?php
/**
* Single newsletter instance. Each Newsletter belongs to a NewsletterType.
*
* @package newsletter
*/
class Newsletter extends DataObject {
static $db = array(
"Status" => "Enum('Draft, Send', 'Draft')",
"Content" => "HTMLText",
"Subject" => "Varchar(255)",
"SentDate" => "Datetime"
);
static $has_one = array(