FINAL suepr merge step : added all modules to this super repos

This commit is contained in:
Bachir Soussi Chiadmi
2015-04-19 16:46:59 +02:00
7585 changed files with 1723356 additions and 18 deletions

View File

@@ -0,0 +1,41 @@
Simplenews Scheduler 6.x-1.x-dev, 2010-06-03
--------------------------------------------
- Fixed #796160 by sgabe, sfyn, dispa: Settings not saved, multiple sends of newsletter
- Added #764122 by sgabe, catofcheshir: How to find scheduled issue?
- Additional minor bug fixes.
Simplenews Scheduler 6.x-1.x-dev, 2010-04-03
--------------------------------------------
- Major changes according to Date 2 API.
- Additional code cleanup.
- Added scheduled newsletter overview page.
- Fixed a variety of bugs:
* #713414 by sgabe: Settings are reset on node preview.
* #728994 by sgabe: Newsletter sent immediately as created.
* #738318 by sgabe: Attachments are lost in new editions.
Simplenews Scheduler 6.x-1.x-dev, 2009-10-27
--------------------------------------------
- Fixed illegal choice detected bug.
- Better cron interval.
Simplenews Scheduler 6.x-1.x-dev, 2009-10-16
--------------------------------------------
- Fixed missing update procedure.
- Simpler date selection.
Simplenews Scheduler 6.x-1.x-dev, 2009-09-16
--------------------------------------------
- Better code formatting and reviewed code with Coder module.
- Modified taxonomy term saving with taxonomy_node_save() function.
- New features: stop sending based on given date or number of editions.
- Better multilingual support with included translation template.
Simplenews Scheduler 7.x-1.x-dev, 2011-12-15
--------------------------------------------
- Code port do D7 API. I.e. DB queries were rewritten, hook_node_api replaced by different hook_node_OP hooks.
- Process to create a newsletter edition is now done by cloning the node completely
- JS file was removed since most effects can be achieved by using the #states property of the Drupal Form API
- New DB field and form widget for interval frequency. It allows to schedule all 2 weeks, all 5 days etc.
- Overview page is now a view. Views integration provides this by implementing hook_views_default_view().

View File

@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

View File

@@ -0,0 +1,52 @@
-- SUMMARY --
The Simplenews Scheduler module allows you to send a newsletter as a re-occurring item
based on a schedule. It does so by creating a new "edition" (rendered copy as HTML Format)
of a node at the time that it required to be sent again.
The editions have an extra tab (for those with permissions) for viewing all editions as well as
the original newsletter they are generated from. The original newsletter is never sent but all
editions are according to a pre-defined schedule which is triggered via cron and can be
defined when you create or edit a simplenews node.
Current options for sending are by day, week, and month.
For a full description of the module, visit the project page:
http://drupal.org/project/simplenews_scheduler
To submit bug reports and feature suggestions, or to track changes:
http://drupal.org/project/issues/simplenews_scheduler
-- REQUIREMENTS --
* Simplenews module - http://drupal.org/project/simplenews
* Date module - http://drupal.org/project/date
-- INSTALLATION --
* Install as usual, see http://drupal.org/node/70151 for further information.
-- RECOMMENDED --
* SuperCron module - http://drupal.org/project/supercron
-- CONFIGURATION --
Locate the module options under "Send newsletter" on the node edit page. When you select
"Send newsletter according to schedule" a new section titled "Schedule details" appear.
-- CONTACT --
Current maintainers:
* Leigh Morresi (dgtlmoon) - http://drupal.org/user/25027
* Gabor Seljan (sgabe) - http://drupal.org/user/232117
-- D7 RELEASE NOTES --
A field for interval frequency was integrated. At the moment it's not possible to create
a custom plaintext version of the newsletter for scheduled sending.

View File

@@ -0,0 +1,23 @@
from 6.x-2.x Simplenews Scheduler can additionally only send a newsletter if the provided PHP Eval code returns true.
Some examples to follow.
Check the current forecast using weather underground, if the weather for the next week is going to be warmer than a threshold,
then allow the newsletter to be sent. (Hey come to my place, it's going to be warm this week!, and use the insert_views module
to include a list of fun things you have planned)
$threshold=20;
$total = 0;
$i=1;
$xml = new SimpleXMLElement(file_get_contents('http://api.wunderground.com/auto/wui/geo/ForecastXML/index.xml?query=Tallinn,Estonia'));
foreach($xml->simpleforecast->forecastday as $forecast) {
$i++;
$total+=(string)$forecast->high->celsius;
}
if($total > $threshold) {
return true;
}
return false;

View File

@@ -0,0 +1,23 @@
<?php
/**
* @file
* Hooks provided by the Simplenews scheduler module.
*/
/**
* Alter the node object that was cloned from the template node before it gets saved.
*
* The node is passed as node object and therefore passed by reference. This hook
* is for example usefull if you have fields in the template node that contain
* information about data that should get rendered dynamically into the edition
* node depenedent on the current schedule date.
*
* @param $edition_node
* The cloned node object based on the scheduler node.
* @param $scheduler_node
* The original scheduler node object.
*/
function hook_simplenews_scheduler_edition_node_alter($edition_node, $scheduler_node) {
$node->title = 'Your newsletter from ' . REQUEST_TIME;
}

View File

@@ -0,0 +1,16 @@
name = "Simplenews Scheduler"
description = "Allows a schedule to be set for sending (and resending) a Simplenews item."
core = 7.x
package = Mail
php = 5.3
files[] = tests/simplenews_scheduler.test
dependencies[] = simplenews
dependencies[] = date_api
dependencies[] = token
; Information added by drupal.org packaging script on 2013-02-04
version = "7.x-1.0-beta2"
core = "7.x"
project = "simplenews_scheduler"
datestamp = "1359936620"

View File

@@ -0,0 +1,214 @@
<?php
/**
* @file
* Install and uninstall functions for the Simplenews Scheduler module.
*/
/**
* Implements hook_schema().
*/
function simplenews_scheduler_schema() {
$schema['simplenews_scheduler'] = array(
'description' => 'Scheduled newsletter data.',
'fields' => array(
'nid' => array(
'description' => 'The node id for a scheduled newsletter.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'last_run' => array(
'description' => 'The timestamp the scheduled newsletter was last sent.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'next_run' => array(
'description' => 'The future timestamp the next scheduled newsletter is due to be sent.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'activated' => array(
'description' => 'Whether the schedule is active.',
'type' => 'int',
'size' => 'tiny',
'not null' => TRUE,
'default' => 0,
),
'send_interval' => array(
'description' => 'The interval at which to send, as a text string.',
'type' => 'varchar',
'length' => 10,
),
'interval_frequency' => array(
'description' => 'The number of intervals between newsletter transmission.',
'type' => 'int',
'default' => 1,
'not null' => TRUE,
),
'start_date' => array(
'description' => 'The timestamp at which to start sending editions.',
'type' => 'int',
'not null' => TRUE,
),
'stop_type' => array(
'description' => 'How to determine when to stop sending editions.',
'type' => 'int',
'not null' => TRUE,
),
'stop_date' => array(
'description' => 'The timestamp at which to stop sending editions.',
'type' => 'int',
'not null' => TRUE,
'default' => 1388447999,
),
'stop_edition' => array(
'description' => 'The edition count at which to stop sending editions.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'php_eval' => array(
'description' => 'PHP code to evaluate to determine whether to send an edition.',
'type' => 'blob',
'not null' => TRUE,
),
'title' => array(
'description' => 'The title of new edition nodes.',
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
),
),
'primary key' => array('nid'),
);
$schema['simplenews_scheduler_editions'] = array(
'description' => 'Stores data for each edition of a scheduled newsletter.',
'fields' => array(
'eid' => array(
'description' => 'The node id for the edition.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'pid' => array(
'description' => 'The node id for the parent scheduled newsletter node.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'date_issued' => array(
'description' => 'The timestamp on which this edition was sent.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
),
'primary key' => array('eid'),
);
return $schema;
}
/**
* Implementation of hook_uninstall().
*/
function simplenews_scheduler_uninstall() {
drupal_uninstall_schema('simplenews_scheduler');
drupal_uninstall_schema('simplenews_scheduler_editions');
}
/**
* Implements hook_install().
*/
function simplenews_scheduler_install() {
// Update the module weight to run before simplenews.
db_update('system')
->condition('name', 'simplenews_scheduler')
->fields(array(
'weight' => -1,
))
->execute();
}
/*
* Implements hook_update_last_removed().
*/
function simplenews_scheduler_update_last_removed() {
return 6005;
}
/**
* Add the title field to the scheduler table.
*/
function simplenews_scheduler_update_7000() {
if (!db_field_exists('simplenews_scheduler', 'title')) {
$field = array(
'description' => 'The title of new edition nodes.',
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
'initial' => '[node:title]',
);
db_add_field('simplenews_scheduler', 'title', $field);
}
}
/**
* Add the next_run field to the scheduler table and populate it.
*/
function simplenews_scheduler_update_7001() {
// Only act if the field doesn't exist yet: this accounts for the possibility
// it's been added in a 62xx update.
if (!db_field_exists('simplenews_scheduler', 'next_run')) {
// Add the field.
$field = array(
'description' => 'The future timestamp the next scheduled newsletter is due to be sent.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'initial' => 0,
);
db_add_field('simplenews_scheduler', 'next_run', $field);
// Populate the new field with each schedule's next run time.
// Retrieve all records into an associative array keyed by nid.
$schedules = db_query("SELECT * FROM {simplenews_scheduler}")->fetchAllAssoc('nid');
foreach ($schedules as $nid => $schedule) {
// Clear last_run to force the next_run calculation to work from the
// start_date. This ensures that any error in previous edition dates due to
// bugs with month length is ignored.
// @see http://drupal.org/node/1364784
$schedule->last_run = 0;
// Get the next run time relative to the request time.
$next_run = simplenews_scheduler_calculate_next_run_time($schedule, REQUEST_TIME);
// Don't use drupal_write_record() in a hook_update_N().
db_update('simplenews_scheduler')
->fields(array('next_run' => $next_run))
->condition('nid', $nid)
->execute();
}
}
}
/**
* Update the module weight if it has not been customized.
*/
function simplenews_scheduler_update_7002() {
// Update the module weight to run before simplenews.
db_update('system')
->condition('name', 'simplenews_scheduler')
// Only change the existing value if it has not been customized.
->condition('weight', 0)
->fields(array(
'weight' => -1,
))
->execute();
}

View File

@@ -0,0 +1,728 @@
<?php
/**
* @file
* Simplenews Scheduler module allows a schedule to be set
* for sending (and resending) a Simplenews item.
*/
/**
* NEWSLETTER SEND COMMAND
*/
define('SIMPLENEWS_COMMAND_SEND_SCHEDULE', 4);
define('SIMPLENEWS_COMMAND_SEND_NONE', 5);
/**
* Implements hook_permission().
*/
function simplenews_scheduler_permission() {
return array(
'overview scheduled newsletters' => array(
'title' => t('View scheduled newsletters'),
'description' => t('Access overview page for scheduled newsletters.'),
),
'send scheduled newsletters' => array(
'title' => t('Send scheduled newsletters'),
'description' => t('Allows users to use scheduled newsletter sending option.'),
),
);
}
/**
* Implements hook_menu().
*/
function simplenews_scheduler_menu() {
$items = array();
$items["node/%node/editions"] = array(
'title' => 'Newsletter Editions',
'type' => MENU_LOCAL_TASK,
'weight' => 2,
'page callback' => 'simplenews_scheduler_node_page',
'page arguments' => array(1),
'access callback' => '_simplenews_scheduler_tab_permission',
'access arguments' => array(1),
);
return $items;
}
/**
* Implements hook_form_FORM_ID_alter().
*
* @todo replace the "This newsletter has been sent" checkbox of simplenews module
* by a message like "Last edition of this newsletter was sent at 12.12.2012"
*/
function simplenews_scheduler_form_simplenews_node_tab_send_form_alter(&$form, &$form_state) {
global $user;
// Add schedule settings to the send newsletter form.
if (user_access('send scheduled newsletters')) {
// Make sure that this is not an edition.
$node = node_load($form['nid']['#value']);
// Only add the schedule send options if the newsletter has not been sent,
// in which case there is no send form element.
if (isset($form['simplenews']['send']) && !isset($node->simplenews_scheduler_edition)) {
// Set the default values.
$form['#submit'][] = "simplenews_scheduler_submit";
$scheduler = array();
$record = db_select('simplenews_scheduler', 's')
->fields('s')
->condition('nid', $node->nid)
->execute()
->fetchAssoc();
if (!empty($record)) {
$scheduler = $record;
}
else {
$scheduler['activated'] = 0;
}
$form_state['simplenews_scheduler'] = $scheduler;
$form['simplenews']['send']['#options'] += array(
SIMPLENEWS_COMMAND_SEND_SCHEDULE => t('Send newsletter according to schedule'),
SIMPLENEWS_COMMAND_SEND_NONE => t("Stop newsletter schedule"),
);
$form['simplenews']['send']['#default_value'] = ($scheduler['activated'] == 1) ? SIMPLENEWS_COMMAND_SEND_SCHEDULE : variable_get('simplenews_send', SIMPLENEWS_COMMAND_SEND_NONE);
$form['simplenews']['scheduler'] = array(
'#type' => 'fieldset',
'#title' => t('Schedule details'),
'#attributes' => array('class' => array('schedule-info')),
'#collapsible' => TRUE,
'#collapsed' => FALSE,
'#states' => array(
'visible' => array(':input[name="simplenews[send]"]' => array('value' => (string) SIMPLENEWS_COMMAND_SEND_SCHEDULE)),
),
);
// If there is no default value, use the current time for start.
$start_date = !empty($scheduler['start_date']) ? $scheduler['start_date'] : REQUEST_TIME;
// and Today + 2 years for stop, that should be enough.
$stop_date = !empty($scheduler['stop_date']) ? $scheduler['stop_date'] : REQUEST_TIME + 2 * 365 * 24 * 60 * 60;
$form['simplenews']['scheduler']['start_date'] = array(
'#type' => 'date_select',
'#title' => t('Start sending on'),
'#default_value' => date('Y-m-d H:i', $start_date),
'#required' => TRUE,
'#date_format' => 'Y-m-d H:i',
'#date_label_position' => 'within',
'#date_year_range' => '-0:+3',
'#description' => t('Intervals work by creating a new node at the
desired time and marking this to be sent, ensure
you have your <a href="@site">site timezones</a>
configured and <a href="@user">user timezone</a>
configured.', array('@site' => url('admin/config/date-time'), '@user' => url('user/' . $user->uid . '/edit'))),
);
$intervals = array(
'hour' => t('Hour'),
'day' => t('Day'),
'week' => t('Week'),
'month' => t('Month'),
);
$form['simplenews']['scheduler']['interval'] = array(
'#type' => 'select',
'#title' => t('Sending interval'),
'#options' => $intervals,
'#description' => t('Interval to send at'),
'#default_value' => !empty($scheduler['send_interval']) ? $scheduler['send_interval'] : 'week',
);
$form['simplenews']['scheduler']['frequency'] = array(
'#type' => 'textfield',
'#title' => t('Interval frequency'),
'#size' => 5,
'#default_value' => !empty($scheduler['interval_frequency']) ? $scheduler['interval_frequency'] : 1,
'#description' => t('Set the number of Intervals between newsletter transmission.'),
);
$stoptypes = array(
t('Never'),
t('On a given date'),
t('After a maximum number of editions')
);
$form['simplenews']['scheduler']['stoptype'] = array(
'#type' => 'radios',
'#title' => t('Stop sending'),
'#options' => $stoptypes,
'#default_value' => !empty($scheduler['stop_type']) ? $scheduler['stop_type'] : 0,
'#attributes' => array('class' => array('simplenews-command-stop')),
);
$form['simplenews']['scheduler']['stop_edition'] = array(
'#type' => 'textfield',
'#default_value' => isset($scheduler['stop_edition']) ? $scheduler['stop_edition'] : 0,
'#size' => 5,
'#maxlength' => 5,
'#required' => TRUE,
'#description' => t('The maximum number of editions which should be sent.'),
'#states' => array(
'visible' => array(':input[name="simplenews[scheduler][stoptype]"]' => array('value' => (string) 2)),
),
);
$form['simplenews']['scheduler']['stop_date'] = array(
'#type' => 'date_select',
'#title' => t('Stop sending on'),
'#default_value' => date('Y-m-d H:i', $stop_date),
'#required' => TRUE,
'#date_format' => 'Y-m-d H:i',
'#date_label_position' => 'within',
'#date_year_range' => '-0:+3',
'#description' => t('The date when the last sent newsletter will be sent.'),
'#states' => array(
'visible' => array(':input[name="simplenews[scheduler][stoptype]"]' => array('value' => (string) 1)),
),
);
$form['simplenews']['scheduler']['php_eval'] = array(
'#type' => 'textarea',
'#title' => t('Additionally only create newsletter edition if the following code returns true'),
'#default_value' => isset($scheduler['php_eval']) ? $scheduler['php_eval'] : '',
'#required' => FALSE,
'#description' => t('Additionally evaluate the following PHP code and only issue the newsletter edition if it returns true. Do not include &lt;?php ?&gt; tags.'),
'#access' => user_access('use PHP for settings'),
);
$form['simplenews']['scheduler']['title'] = array(
'#type' => 'textfield',
'#title' => t('Title pattern for new edition nodes'),
'#description' => t('New edition nodes will have their title set to the above string, with tokens replaced.'),
'#required' => TRUE,
'#default_value' => isset($scheduler['title']) ? $scheduler['title'] : '[node:title]',
);
$form['simplenews']['scheduler']['token_help'] = array(
'#title' => t('Replacement patterns'),
'#type' => 'fieldset',
'#collapsible' => TRUE,
'#collapsed' => TRUE,
);
$form['simplenews']['scheduler']['token_help']['help'] = array(
'#theme' => 'token_tree',
'#token_types' => array('node'),
);
$form['simplenews']['scheduler']['activated'] = array(
'#type' => 'value',
'#value' => $scheduler['activated'],
);
}
elseif (isset($node->simplenews_scheduler_edition)) {
// This is a newsletter edition.
$form['simplenews']['none']['#title'] = t('This node is part of a scheduled newsletter configuration. View the original newsletter <a href="@parent">here</a>.', array('@parent' => url('node/' . $node->simplenews_scheduler_edition->pid)));
}
}
}
/**
* Additional submit handler for the node_tab_send_form of simplenews.
*/
function simplenews_scheduler_submit($form, &$form_state) {
$scheduler = $form_state['simplenews_scheduler'];
$nid = $form_state['values']['nid'];
$node = node_load($nid);
// Get Scheduler values from Simplenews.
$send = $form_state['values']['simplenews']['send'];
$stoptype = $form_state['values']['simplenews']['scheduler']['stoptype'];
$start_date = strtotime($form_state['values']['simplenews']['scheduler']['start_date']);
$stop_date = ($stoptype == 1) ? strtotime($form_state['values']['simplenews']['scheduler']['stop_date']) : 0;
$record = array(
'nid' => $nid,
'activated' => $send == SIMPLENEWS_COMMAND_SEND_SCHEDULE ? 1 : 0,
'send_interval' => $form_state['values']['simplenews']['scheduler']['interval'],
'interval_frequency' => $form_state['values']['simplenews']['scheduler']['frequency'],
'start_date' => $start_date,
'stop_type' => $stoptype,
'stop_date' => $stop_date,
'stop_edition' => $form_state['values']['simplenews']['scheduler']['stop_edition'],
'php_eval' => $form_state['values']['simplenews']['scheduler']['php_eval'],
'title' => $form_state['values']['simplenews']['scheduler']['title'],
);
// For a new scheduler, add the next_run time.
if (!isset($scheduler['next_run'])) {
$record['next_run'] = $start_date;
}
// Update scheduler record.
db_merge('simplenews_scheduler')
->key(array(
'nid' => $nid,
))
->fields($record)
->execute();
drupal_set_message(t('Newsletter Schedule preferences have been saved.'));
}
/**
* Implements hook_node_load().
*/
function simplenews_scheduler_node_load($nodes, $types) {
$nids = array_keys($nodes);
$result = db_select('simplenews_scheduler', 's')
->fields('s')
->condition('nid', $nids, 'IN')
->execute()
->fetchAll();
foreach ($result as $key => $record) {
$nodes[$record->nid]->simplenews_scheduler = $record;
}
$result = db_select('simplenews_scheduler_editions', 's')
->fields('s')
->condition('eid', $nids, 'IN')
->execute()
->fetchAll();
foreach ($result as $key => $record) {
$nodes[$record->eid]->simplenews_scheduler_edition = $record;
$nodes[$record->eid]->is_edition = TRUE;
$nodes[$record->eid]->simplenews_edition_parent = $record->pid;
}
}
/**
* Implements hook_node_delete().
*/
function simplenews_scheduler_node_delete($node) {
db_delete('simplenews_scheduler')
->condition('nid', $node->nid)
->execute();
}
/**
* Implements hook_node_view().
*/
function simplenews_scheduler_node_view($node) {
if (isset($node->simplenews_scheduler_edition) && user_access('send scheduled newsletters')) {
drupal_set_message(t('This is a newsletter edititon. View the the master template of this newsletter <a href="!master_url">here</a>', array('!master_url' => url('node/' . $node->simplenews_edition_parent))));
}
}
/**
* Implements hook_cron().
*
* Essentially we are just checking against a status table
* and cloning the node into edition nodes which will be sent.
*/
function simplenews_scheduler_cron() {
// Get the newsletters that need to be sent at this time.
$now_time = REQUEST_TIME;
$newsletters_to_send = simplenews_scheduler_get_newsletters_due($now_time);
foreach ($newsletters_to_send as $newsletter_parent_data) {
$edition_time = simplenews_scheduler_calculate_edition_time($newsletter_parent_data, $now_time);
// Create a new edition.
$eid = _simplenews_scheduler_new_edition($newsletter_parent_data->nid, $edition_time);
// Update the edition record.
simplenews_scheduler_scheduler_update($newsletter_parent_data, $now_time);
// Send it.
_simplenews_scheduler_send_new_edition($edition_time, $newsletter_parent_data, $eid);
}
}
/**
* Updates a scheduler record with any housekeeping changes and saves it.
*
* This should be called once a new edition has been created. This sets the
* next_run time on the scheduler.
*
* @todo: Make this a general API function for saving a new or existing scheduler?
*
* @param $newsletter_parent_data
* A row of data from {simplenews_scheduler}, as returned by
* simplenews_scheduler_get_newsletters_due().
* @param $now_time
* The time of the operation.
*/
function simplenews_scheduler_scheduler_update($newsletter_parent_data, $now_time) {
// Set the run time for the next edition.
$newsletter_parent_data->next_run = simplenews_scheduler_calculate_next_run_time($newsletter_parent_data, $now_time);
drupal_write_record('simplenews_scheduler', $newsletter_parent_data, 'nid');
}
/**
* Calculates time for the current edition about to be created.
*
* Because cron may run after the scheduled timestamp, one or more scheduled
* edition times may have been skipped. This calculates the most recent
* possible time for an edition.
*
* @param $newsletter_parent_data
* A row of data from {simplenews_scheduler}, as returned by
* simplenews_scheduler_get_newsletters_due().
* @param $now_time
* The time of the operation.
*
* @return
* The calculcated creation time of the newsletter edition.
*/
function simplenews_scheduler_calculate_edition_time($newsletter_parent_data, $now_time) {
// Make an offset string of the format '+1 month'.
$offset_string = _simplenews_scheduler_make_time_offset($newsletter_parent_data->send_interval, $newsletter_parent_data->interval_frequency);
// Make a DateInterval object that represents this.
$date_interval = DateInterval::createFromDateString($offset_string);
// Take the last run time and add as many intervals as possible without going
// past 'now'.
// Create a date object to act as a pointer we'll advance and increment.
if ($newsletter_parent_data->last_run) {
// Generate a date string to initialize a DateTime() object, otherwise the
// timezone is ignored.
$start_date = date('Y-m-d H:i:s', $newsletter_parent_data->last_run);
}
else {
$start_date = date('Y-m-d H:i:s', $newsletter_parent_data->start_date);
}
// Initialize the DateTime object using the configured ste timezone.
$pointer_date = new DateTime($start_date);
while ($pointer_date->getTimestamp() <= $now_time) {
// Get the last iteration's timestamp before we change the pointer.
$timestamp_old = $pointer_date->getTimestamp();
// Add interval to the pointer time.
$pointer_date->add($date_interval);
// Check if the pointer is now in the future.
if ($pointer_date->getTimestamp() > $now_time) {
// If so, return the last iteration timestamp as the edition time.
return $timestamp_old;
}
}
}
/**
* Calculates time for the next edition to be sent.
*
* This is set in the {simplenews_scheduler} table when a new edition is run,
* for subsequent cron runs to query against.
*
* The time is strictly in the future; that is, if the $now_time is a valid
* edition time, a schedule interval is added to it. This is to allow for cron
* runs that need to calculate the next run time at the time of the current
* edition being sent.
*
* @param $newsletter_parent_data
* A row of data from {simplenews_scheduler}, as returned by
* simplenews_scheduler_get_newsletters_due().
* @param $now_time
* The time of the operation.
*
* @return
* The calculcated run time for the next future edition.
*/
function simplenews_scheduler_calculate_next_run_time($newsletter_parent_data, $now_time) {
// Make an offset string of the format '+1 month'.
$offset_string = _simplenews_scheduler_make_time_offset($newsletter_parent_data->send_interval, $newsletter_parent_data->interval_frequency);
// Make a DateInterval object that represents this.
$date_interval = DateInterval::createFromDateString($offset_string);
// Create a date object to act as a pointer we'll advance and increment.
if ($newsletter_parent_data->last_run) {
// Generate a date string to initialize a DateTime() object, otherwise the
// timezone is ignored.
$start_date = date('Y-m-d H:i:s', $newsletter_parent_data->last_run);
}
else {
$start_date = date('Y-m-d H:i:s', $newsletter_parent_data->start_date);
}
// Initialize the DateTime object using the configured ste timezone.
$pointer_date = new DateTime($start_date);
// Add as many offsets as possible until we get into the future.
while ($pointer_date->getTimestamp() <= $now_time) {
// Add interval to the pointer time.
$pointer_date->add($date_interval);
}
return $pointer_date->getTimestamp();
}
/**
* Helper to create a PHP time offset string.
*
* @param $interval
* A time interval. One of hour, day, week, month.
* @param $frequency
* An integer that specifies how many of the $interval to create an offset for.
*
* @return
* A string representing a time offset that can be understood by strtotime(),
* eg '+1 month'.
*/
function _simplenews_scheduler_make_time_offset($interval, $frequency) {
$offset_string = "+{$frequency} {$interval}";
return $offset_string;
}
/**
* Get the newsletters that need to have new editions sent.
*
* This is a helper function for hook_cron that has the current date abstracted
* out so it can be tested.
*
* @param $timestamp
* A unix timestamp at which to determine which newsletters are due to be
* sent. In ordinary operation this should be the current time.
*
* @return
* An array of newsletter data arrays in the form of rows from the
* {simplenews_scheduler} table, keyed by newsletter nid.
*/
function simplenews_scheduler_get_newsletters_due($timestamp) {
// Get all newsletters that need to be sent.
$result = db_query("SELECT * FROM {simplenews_scheduler} WHERE activated = 1 AND next_run <= :now AND (stop_date > :now OR stop_date = 0)", array(':now' => $timestamp));
$newsletters = array();
foreach ($result as $newsletter_parent_data) {
// The node id of the parent node.
$pid = $newsletter_parent_data->nid;
// Check upon if sending should stop with a given edition number.
$stop = $newsletter_parent_data->stop_type;
$stop_edition = $newsletter_parent_data->stop_edition;
$edition_count = db_query('SELECT COUNT(*) FROM {simplenews_scheduler_editions} WHERE pid = :pid', array(':pid' => $pid))->fetchField();
// Don't create new edition if the edition number would exceed the given maximum value.
if ($stop == 2 && $edition_count >= $stop_edition) {
continue;
}
// does this newsletter have something to evaluate to check running condition?
if (strlen($newsletter_parent_data->php_eval)) {
$eval_result = eval($newsletter_parent_data->php_eval);
if (!$eval_result) {
continue;
}
}
$newsletters[$pid] = $newsletter_parent_data;
}
return $newsletters;
}
/**
* Helper for hook_cron() to send a new edition.
*
* @param $edition_time
* The time of the operation. Usually the current time unless testing.
* @param $newsletter_parent_data
* A row of data from {simplenews_scheduler}, as returned by
* simplenews_scheduler_get_newsletters_due().
* @param $eid
* The node id of the new edition to send. This should already have been
* created by _simplenews_scheduler_new_edition().
*/
function _simplenews_scheduler_send_new_edition($edition_time, $newsletter_parent_data, $eid) {
$pid = $newsletter_parent_data->nid;
// persist last_run
db_update('simplenews_scheduler')
->fields(array('last_run' => $edition_time))
->condition('nid', $pid)
->execute();
// Send the newsletter edition to each subscriber of the parent newsletter.
$node = node_load($eid);
module_load_include('inc', 'simplenews', 'includes/simplenews.mail');
simplenews_add_node_to_spool($node);
}
/**
* Function clones a node from the given template newsletter node.
*/
function simplenews_scheduler_clone_node($node) {
if (isset($node->nid)) {
$clone = clone $node;
$clone->nid = NULL;
$clone->vid = NULL;
$clone->tnid = NULL;
$clone->created = NULL;
$clone->book['mlid'] = NULL;
$clone->path = NULL;
//$clone->title = $original_node->title;
// Add an extra property as a flag.
$clone->clone_from_original_nid = $node->nid;
node_save($clone);
return $clone;
}
}
/**
* Menu callback to provide an overview page with the scheduled newsletters.
*
* @todo replace the output of this function with a default view that
* will be provided by the views integration of this module. Code below
* is ported from D6!
*/
function simplenews_scheduler_node_page($node) {
drupal_set_title(t('Scheduled newsletter editions'));
$nid = _simplenews_scheduler_get_pid($node);
$output = '';
$rows = array();
if ($nid == $node->nid) { // This is the template newsletter.
$output .= '<p>' . t('This is a newsletter template node of which all corresponding editions nodes are based on.') . '</p>';
}
else { // This is a newsletter edition.
$output .= '<p>' . t('This node is part of a scheduled newsletter configuration. View the original newsletter <a href="@parent">here</a>.', array('@parent' => url('node/' . $nid))) . '</p>';
}
// Load the corresponding editions from the database to further process.
$result = db_select('simplenews_scheduler_editions', 's')
->extend('PagerDefault')
->limit(20)
->fields('s')
->condition('s.pid', $nid)
->execute()
->fetchAll();
foreach ($result as $row) {
$node = node_load($row->eid);
$rows[] = array(l($node->title, 'node/' . $row->eid), format_date($row->date_issued, 'custom', 'Y-m-d H:i'));
}
// Display a table with all editions.
$tablecontent = array(
'header' => array(t('Edition Node'), t('Date sent')),
'rows' => $rows,
'attributes' => array('class' => array('schedule-history')),
'empty' => '<p>' . t('No scheduled newsletter editions have been sent.') . '</p>',
);
$output .= theme('table', $tablecontent);
$output .= theme('pager', array('tags' => 20));
return $output;
}
/**
* Check whether to display the Scheduled Newsletter tab.
*/
function _simplenews_scheduler_tab_permission($node) {
// Check if this is a simplenews node type and permission.
if (simplenews_check_node_types($node->type) && user_access('overview scheduled newsletters')) {
// Check if this is either a scheduler newsletter or an edition.
return !empty($node->simplenews_scheduler) || !empty($node->is_edition);
}
}
/**
* Find Full HTML input format.
*
* Use the Drupal API for finding the Full HTML input format, this is what the subsequent newsletter editions
* need to be set to.
*/
function _simplenews_scheduler_get_full_html_format() {
global $user;
$formats = filter_formats($user);
foreach ($formats as $index => $format) {
if (stristr($format->name, 'Full HTML')) {
return $index;
}
}
return false;
}
/**
* Create a new newsletter edition based on the master edition of this newsletter.
*
* This does no checking of whether a new edition should be made; it's up to
* the caller to determine this first.
*
* @param $nid
* The node id of the parent newsletter node to use as a template.
* @param $edition_time
* Desired edition creation time.
*
* @return
* The node id of the new edition node.
*/
function _simplenews_scheduler_new_edition($nid, $edition_time) {
// Load the template node and clone an edition.
$template_node = node_load($nid);
$edition_node = simplenews_scheduler_clone_node($template_node);
// Set the node's creation time as the given timestamp.
$edition_node->created = $edition_time;
// Run the title through token replacement.
// Get title pattern from the scheduler record, not newsletter node.
// $edition_node->title = token_replace($edition_node->title, array('node' => $edition_node));
$schedrecord = db_select('simplenews_scheduler', 's')
->fields('s')
->condition('nid', $template_node->nid)
->execute()
->fetchAssoc();
$edition_node->title = token_replace($schedrecord['title'], array('node' => $template_node));
// Invoke simplenews_scheduler_edition_node() to give installed modules a
// chance to modify the cloned edition node if necessary before it gets saved.
drupal_alter('simplenews_scheduler_edition_node', $edition_node, $template_node);
// Save the changes of other modules
node_save($edition_node);
// Insert edition data.
$values = array(
'eid' => $edition_node->nid,
'pid' => $template_node->nid,
'date_issued' => $edition_time,
);
db_insert('simplenews_scheduler_editions')
->fields($values)
->execute();
// Add a watchdog entry.
$variables = array('%title' => entity_label('node', $edition_node));
$uri = entity_uri('node', $edition_node);
$link = l(t('view'), $uri['path'], $uri['options']);
watchdog('simplenews_sched', 'Created a new newsletter edition %title', $variables, WATCHDOG_NOTICE, $link);
// Prepare the correct status for Simplenews to pickup.
simplenews_newsletter_update_sent_status($edition_node);
return $edition_node->nid;
}
/**
* Helper function to get the identifier of newsletter.
*
* @param $node
* The node object for the newsletter.
*
* @return
* If the node is a newsletter edition, the node id of its parent template
* newsletter; if the node is a template newsletter, its own node id; and
* FALSE if the node is not part of a scheduled newsletter set.
*/
function _simplenews_scheduler_get_pid($node) {
// First assume this is a newsletter edition,
if (isset($node->simplenews_scheduler_edition)) {
return $node->simplenews_scheduler_edition->pid;
}
// or this itself is the parent newsletter.
elseif (isset($node->simplenews_scheduler)) {
return $node->nid;
}
return FALSE;
}

View File

@@ -0,0 +1,76 @@
<?php
/**
* @file simplenews_scheduler_views.inc
* Views support for simplenews
*/
/**
* Implementation of hook_views_tables
*/
function simplenews_scheduler_views_tables() {
$tables['simplenews_scheduler'] = array(
'name' => 'simplenews_scheduler',
'filters' => array(
'node.created' => array(
'name' => t('Simplenews schedule: node created'),
'list' => 'simplenews_scheduler_handler_newsletters',
'list-type' => 'list',
'handler' => 'simplenews_scheduler_filter_value',
'operator' => 'simplenews_scheduler_handler_operator_ca',
'value-type' => 'array',
'help' => t('Select the newsletter to filter against, this will filter nodes that have been created AFTER the latest <i>edition</i> of this newsletter'),
),
),
);
return $tables;
}
function simplenews_scheduler_handler_newsletters() {
$list = array();
$result = db_query("SELECT ss.*,n.title FROM {simplenews_scheduler} ss JOIN {node} n ON n.nid = ss.snid GROUP BY snid");
while($row = db_fetch_array($result)) {
$list[$row['snid']]="Newsletter: ".$row['title'];
}
if(sizeof($list) ==0) {
drupal_set_message('No current newsletter found to filter against, you should create a simplenews newsletter first.');
}
return $list;
}
function simplenews_scheduler_filter_value($op, $filter, $filterinfo, &$query) {
// get oldest last_run time of the selected newsletter
$result=db_query("SELECT ss.*,n.created FROM {simplenews_scheduler} ss
LEFT JOIN {node} n on n.nid = ss.snid
WHERE ss.snid IN (%s)
ORDER BY ss.last_run
LIMIT 0,1",implode(',',$filter['value']));
$last_run = db_fetch_array($result);
// then it hasnt run yet, so we use the creation date of the newsletter parent instead
$trigger_time = $last_run['last_run'] > 0 ? $last_run['last_run'] : $last_run['created'];
$query->add_field('created', 'node');
$query->add_where("node.created %s %d", $filter['operator'], $trigger_time);
}
function simplenews_scheduler_handler_operator_ca() {
return array('>' => t("After Most Recent Edition Of"),'<' => t("Before Most Recent Edition Of"));
}
// any view that has this as a filter should be invalidate cache
function simplenews_scheduler_views_pre_query(&$view) {
foreach($view->filter as $i => $filter) {
if( $filter['field'] == 'simplenews_scheduler.node.created') {
$view->is_cacheable = false;
}
}
}

View File

@@ -0,0 +1,691 @@
<?php
/**
* @file
* Tests for Simplenews Scheduler.
*/
/**
* Class with common setup.
*
* Declares the module dependencies for the test.
*
* We need to use DrupalWebTestCase as our base class even for functional
* testing, as the functions that we test rely on variable_get() which requires
* a bootstrapped database.
*/
class SimpleNewsSchedulerWebTestCase extends DrupalWebTestCase {
/**
* Overrides DrupalWebTestCase::setUp().
*
* @param $modules
* Additional modules to enable for the test. simplenews_scheduler and
* the dependencies are always enabled.
*/
function setUp($modules = array()) {
// Add our dependencies to the module list.
$modules = array_merge(array('simplenews_scheduler'), $modules);
parent::setUp($modules);
// Set the site timezone to something visibly different from UTC, which
// has daylight saving changes.
variable_set('date_default_timezone', 'Europe/Kiev');
date_default_timezone_set(drupal_get_user_timezone());
}
}
/**
* Test scheduled edition creation.
*/
class SimpleNewsSchedulerNodeCreationTest extends SimpleNewsSchedulerWebTestCase {
protected $privileged_user;
/**
* Provides information about this test.
*/
public static function getInfo() {
return array(
'name' => 'Newsletter generation test',
'description' => 'Testing generation of newsletters',
'group' => 'Simplenews Scheduler',
);
}
/**
* Declares the module dependencies for the test.
*/
function setUp() {
parent::setUp();
$this->privileged_user = $this->drupalCreateUser(array(
'access content',
'administer nodes',
'create simplenews content',
'edit own simplenews content',
'send newsletter',
'send scheduled newsletters',
'overview scheduled newsletters',
));
$this->drupalLogin($this->privileged_user);
// Subscribe a user to simplenews.
$categories = simplenews_categories_load_multiple();
$this->mail = 'test@example.org';
simplenews_subscribe_user($this->mail, key($categories), FALSE, 'test');
}
/**
* Basic simplenews newsletter generation test
* create a simplenews node,
*/
function testNewsletterGeneration() {
$edit = array();
$title ="newsletter " . $this->randomName(8);
$edit = array();
$edit['title'] = $title;
$edit["body[und][0][value]"] = $this->randomName(16);
$this->drupalPost('node/add/simplenews', $edit, t('Save'));
$this->assertText($title);
preg_match('|node/(\d+)$|', $this->getUrl(), $matches);
$node = node_load($matches[1]);
// Make sure that the editions tab is not visible as long as it's not a
// scheduled newsletter.
$this->drupalGet("node/{$node->nid}/editions");
$this->assertResponse(403, t('Editions tab not accessible'));
// Now create the simplenews schedule configuration.
$this->drupalGet("node/{$node->nid}/simplenews");
$this->assertText(t("Send newsletter according to schedule"));
$edit = array();
$edit["simplenews[send]"] = '4';
$edit["simplenews[scheduler][interval]"] = "hour";
// Specify a start time 30 minutes in the past to be able to have a known
// edition creation time that can be checked.
$date = new DateTime();
$date->sub(new DateInterval('PT30M'));
$edit["simplenews[scheduler][start_date][year]"] = $date->format('Y');
$edit["simplenews[scheduler][start_date][month]"] = $date->format('n');
$edit["simplenews[scheduler][start_date][day]"] = $date->format('j');
$edit["simplenews[scheduler][start_date][hour]"] = $date->format('G');
$edit["simplenews[scheduler][start_date][minute]"] = $date->format('i');
$edit["simplenews[scheduler][title]"] = "Custom title [site:name]";
$this->drupalPost("node/{$node->nid}/simplenews", $edit, t('Submit'));
// Make sure it knows no editions created yet.
$this->drupalGet("node/{$node->nid}/editions");
$this->assertText(t("No scheduled newsletter editions have been sent."));
// Execute cron.
drupal_cron_run();
// See if it was created.
$this->drupalGet("node/{$node->nid}/editions");
$this->assertText("Custom title");
$this->assertNoText(t("No scheduled newsletter editions have been sent."));
$this->assertText($title); // original real node title
// Go to node and verify creation time and token for custom title
// @todo: make this real token integration
$this->clickLink("Custom title ". variable_get('site_name', 'Drupal'));
$this->assertText(t('This is a newsletter edititon. View the the master template of this newsletter here'));
$this->assertText(t('Submitted by @name on @date', array('@name' => format_username($this->privileged_user), '@date' => format_date($date->getTimestamp()))));
// Check sent mails.
$mails = $this->drupalGetMails();
$this->assertEqual(1, count($mails), t('Newsletter mail has been sent.'));
$this->clickLink(t('Newsletter'));
$this->assertText(t('This node is part of a scheduled newsletter configuration.'));
$this->clickLink(t('here'));
$this->assertEqual(url('node/' . $node->nid, array('absolute' => TRUE)), $this->getUrl());
// Test the tab on a sent newsletter, schedule details should not be shown.
$title ="newsletter " . $this->randomName(8);
$edit = array();
$edit['title'] = $title;
$edit["body[und][0][value]"] = $this->randomName(16);
$this->drupalPost('node/add/simplenews', $edit, t('Save'));
$this->assertText($title);
preg_match('|node/(\d+)$|', $this->getUrl(), $matches);
$node = node_load($matches[1]);
$edit = array();
$edit["simplenews[send]"] = SIMPLENEWS_COMMAND_SEND_NOW;
$this->drupalPost("node/{$node->nid}/simplenews", $edit, t('Submit'));
$this->assertNoText(t('Schedule details'));
// Check sent mails.
$mails = $this->drupalGetMails();
$this->assertEqual(1, count($mails), t('Newsletter mail has been sent.'));
}
}
/**
* Unit testing for monthly newsletter next run times.
*/
class SimpleNewsSchedulerNextRunTimeTest extends SimpleNewsSchedulerWebTestCase {
/**
* Provides information about this test.
*/
public static function getInfo() {
return array(
'name' => 'Next run time: monthly',
'description' => 'Testing edition times for newsletters due every month and every 2 months.',
'group' => 'Simplenews Scheduler',
);
}
/**
* Test a frequency of 1 month.
*/
function testNextRunTimeOneMonth() {
// The start date of the edition.
$this->edition_day = '05';
$start_date = new DateTime("2012-01-{$this->edition_day} 12:00:00");
// Fake newsletter parent data: sets the interval, start date, and frequency.
$newsletter_parent_data = (object) array(
'nid' => 1,
'last_run' => 0,
'activated' => '1',
'send_interval' => 'month',
'interval_frequency' => '1',
'start_date' => $start_date->getTimestamp(),
'next_run' => $start_date->getTimestamp(), // Needs to be set manually when creating new records programmatically.
'stop_type' => '0',
'stop_date' => '0',
'stop_edition' => '0',
'php_eval' => '',
'title' => '[node:title] for [current-date:long]',
);
// Number of days to run for.
$days = 370;
// Index of the days we've done so far.
$added_days = 0;
// Iterate over days.
$last_run_time = $start_date->getTimestamp();
while ($added_days <= $days) {
// Create today's date at noon and get the timestamp.
$date = clone($start_date);
$date->add(new DateInterval("P{$added_days}D"));
$timestamp_noon = $date->getTimestamp();
// Get the next run time from the API function we're testing.
$next_run_time = simplenews_scheduler_calculate_next_run_time($newsletter_parent_data, $timestamp_noon);
//debug($edition_time);
if ($next_run_time != $last_run_time) {
$offset = _simplenews_scheduler_make_time_offset($newsletter_parent_data->send_interval, $newsletter_parent_data->interval_frequency);
$next_run_date = date_add(date_create(date('Y-m-d H:i:s', $last_run_time)), date_interval_create_from_date_string($offset));
$this->assertEqual($next_run_date->getTimestamp(), $next_run_time, t('New next run timestamp has advanced by the expected offset of !offset.', array(
'!offset' => $offset,
)));
$last_run_time = $next_run_time;
}
$this->assertTrue($timestamp_noon < $next_run_time, t('Next run time of !next-run is in the future relative to current time of !now.', array(
'!next-run' => date("Y-n-d H:i:s", $next_run_time),
'!now' => date("Y-n-d H:i:s", $timestamp_noon),
)));
$interval = $newsletter_parent_data->interval_frequency * 31 * 24 * 60 * 60;
//$this->assertTrue($next_run_time - $timestamp_noon <= $interval, t('Next run timestamp is less than or exactly one month in the future.'));
// Create a date object from the timestamp. The '@' makes the constructor
// consider the string as a timestamp.
$next_run_date = new DateTime(date('Y-m-d H:i:s', $last_run_time));
$d = date_format($next_run_date, 'd');
$this->assertEqual($next_run_date->format('d'), $this->edition_day, t('Next run timestamp is on same day of the month as the start date.'));
$this->assertEqual($next_run_date->format('H:i:s'), '12:00:00', t('Next run timestamp is at the same time.'));
$added_days++;
} // while days
}
/**
* Test a frequency of 2 months.
*/
function testNextRunTimeTwoMonths() {
// The start date of the edition.
$this->edition_day = '05';
$start_date = new DateTime("2012-01-{$this->edition_day} 12:00:00");
// Fake newsletter parent data: sets the interval, start date, and frequency.
$newsletter_parent_data = (object) array(
'nid' => 1,
'last_run' => 0,
'activated' => '1',
'send_interval' => 'month',
'interval_frequency' => '2',
'start_date' => $start_date->getTimestamp(),
'next_run' => $start_date->getTimestamp(), // Needs to be set manually when creating new records programmatically.
'stop_type' => '0',
'stop_date' => '0',
'stop_edition' => '0',
'php_eval' => '',
'title' => '[node:title] for [current-date:long]',
);
// Number of days to run for.
$days = 370;
// Index of the days we've done so far.
$added_days = 0;
// Iterate over days.
while ($added_days <= $days) {
// Create today's date at noon and get the timestamp.
$date = clone($start_date);
$date->add(new DateInterval("P{$added_days}D"));
$timestamp_noon = $date->getTimestamp();
// Get the next run time from the API function we're testing.
$next_run_time = simplenews_scheduler_calculate_next_run_time($newsletter_parent_data, $timestamp_noon);
//debug($edition_time);
$this->assertTrue($timestamp_noon < $next_run_time, t('Next run time of !next-run is in the future relative to current time of !now.', array(
'!next-run' => date("Y-n-d H:i:s", $next_run_time),
'!now' => date("Y-n-d H:i:s", $timestamp_noon),
)));
$interval = $newsletter_parent_data->interval_frequency * 31 * 24 * 60 * 60;
$this->assertTrue($next_run_time - $timestamp_noon <= $interval, t('Next run timestamp is less than or exactly two months in the future.'));
// Create a date object from the timestamp. The '@' makes the constructor
// consider the string as a timestamp.
$next_run_date = new DateTime("@$next_run_time");
$d = date_format($next_run_date, 'd');
$this->assertEqual($next_run_date->format('d'), $this->edition_day, t('Next run timestamp is on same day of the month as the start date.'));
$added_days++;
} // while days
}
}
/**
* Unit testing for monthly newsletter edition times.
*/
class SimpleNewsSchedulerEditionTimeTest extends SimpleNewsSchedulerWebTestCase {
/**
* Provides information about this test.
*/
public static function getInfo() {
return array(
'name' => 'Edition time: monthly',
'description' => 'Testing edition times for newsletters due every month and every 2 months.',
'group' => 'Simplenews Scheduler',
);
}
/**
* Test a frequency of 1 month.
*/
function testEditionTimeOneMonth() {
// The start date of the edition.
$this->edition_day = '01';
$start_date = new DateTime("2012-01-{$this->edition_day} 12:00:00");
// Fake newsletter parent data: sets the interval, start date, and frequency.
$newsletter_parent_data = (object) array(
'nid' => 1,
'last_run' => 0,
'activated' => '1',
'send_interval' => 'month',
'interval_frequency' => '1',
'start_date' => $start_date->getTimestamp(),
'stop_type' => '0',
'stop_date' => '0',
'stop_edition' => '0',
'php_eval' => '',
'title' => '[node:title] for [current-date:long]',
);
// Number of days to run for. Go just over one year.
$days = 370;
// Index of the days we've done so far.
$added_days = 0;
// Iterate over days.
while ($added_days <= $days) {
// Create today's date at noon and get the timestamp.
$date = clone($start_date);
$date->add(new DateInterval("P{$added_days}D"));
$timestamp_noon = $date->getTimestamp();
$edition_time = simplenews_scheduler_calculate_edition_time($newsletter_parent_data, $timestamp_noon);
//debug($edition_time);
// Expected edition time is always the {$this->edition_day}th of the month
// at noon.
$edition_time_formatted = date("Y-m-d H:i:s", $edition_time);
$this_month = $date->format('Y-m');
$expected_time_formatted = "{$this_month}-{$this->edition_day} 12:00:00";
$this->assertEqual($edition_time_formatted, $expected_time_formatted, t("Edition time of !edition-time matches expected time of !edition-time-expected at time !now.", array(
'!edition-time' => $edition_time_formatted,
'!edition-time-expected' => $expected_time_formatted,
'!now' => $date->format("Y-m-d H:i:s"),
)));
$added_days++;
} // while days
}
/**
* Test a frequency of 2 months.
*/
function testEditionTimeTwoMonths() {
// The start date of the edition.
$this->edition_day = '01';
$start_date = new DateTime("2012-01-{$this->edition_day} 12:00:00");
// Fake newsletter parent data: sets the interval, start date, and frequency.
$newsletter_parent_data = (object) array(
'nid' => 1,
'last_run' => 0,
'activated' => '1',
'send_interval' => 'month',
'interval_frequency' => '2',
'start_date' => $start_date->getTimestamp(),
'stop_type' => '0',
'stop_date' => '0',
'stop_edition' => '0',
'php_eval' => '',
'title' => '[node:title] for [current-date:long]',
);
// Number of days to run for. Go just over one year.
$days = 370;
// Index of the days we've done so far.
$added_days = 0;
// Iterate over days.
while ($added_days <= $days) {
// Create today's date at noon and get the timestamp.
$date = clone($start_date);
$date->add(new DateInterval("P{$added_days}D"));
$timestamp_noon = $date->getTimestamp();
$edition_time = simplenews_scheduler_calculate_edition_time($newsletter_parent_data, $timestamp_noon);
//debug($edition_time);
// Expected edition time is always the {$this->edition_day}th of the month
// at noon.
// Note here we use 'n' for the month to avoid having to pad.
$edition_time_formatted = date("Y-n-d H:i:s", $edition_time);
$this_year = $date->format('Y');
$this_month = $date->format('n');
// We start in January and run 2-monthly.
// We want the number of elapsed months, module 2 (the frequency), to know
// the remainder to subtract.
$elapsed_mod = ($this_month - 1) % 2;
$expected_month = $this_month - $elapsed_mod;
$expected_time_formatted = "{$this_year}-{$expected_month}-{$this->edition_day} 12:00:00";
$this->assertEqual($edition_time_formatted, $expected_time_formatted, t("Edition time of !edition-time matches expected time of !edition-time-expected at time !now.", array(
'!edition-time' => $edition_time_formatted,
'!edition-time-expected' => $expected_time_formatted,
'!now' => $date->format("Y-m-d H:i:s"),
)));
$added_days++;
} // while days
}
}
/**
* Unit testing for simplenews_scheduler_get_newsletters_due().
*/
class SimpleNewsSchedulerEditionDueTest extends SimpleNewsSchedulerWebTestCase {
protected $privileged_user;
/**
* Provides information about this test.
*/
public static function getInfo() {
return array(
'name' => 'Edition due test',
'description' => 'Functional tests for simplenews_scheduler_get_newsletters_due().',
'group' => 'Simplenews Scheduler',
);
}
/**
* Declares the module dependencies and create data for the test.
*/
function setUp() {
parent::setUp();
$this->privileged_user = $this->drupalCreateUser(array(
'access content',
'administer nodes',
'create simplenews content',
'edit own simplenews content',
'send newsletter',
'send scheduled newsletters',
'overview scheduled newsletters',
));
$this->drupalLogin($this->privileged_user);
// The start date of the edition. This is on 5 January so that we get some
// days in either month, and at at noon to keep things simple.
$this->edition_day = '05';
$start_date = new DateTime("2012-01-{$this->edition_day} 12:00:00");
// Create a parent newsletter node.
$node = (object) NULL;
$node->type = 'simplenews';
$node->title = 'Parent';
$node->uid = 1;
$node->status = 1;
$node->language = 'und';
// Safe to assume there is only one taxonomy term and it's the newsletter.
$node->field_simplenews_term['und'][0]['tid'] = 1;
// Workaround for http://drupal.org/node/1480258
$node->nid = NULL;
node_save($node);
// Grumble grumble there's no node saving API in our module!
// @see http://drupal.org/node/1480328 to clean this up.
$node->simplenews_scheduler = (object) array(
'nid' => $node->nid,
'last_run' => 0,
'activated' => '1',
'send_interval' => 'month',
'interval_frequency' => '1',
'start_date' => $start_date->getTimestamp(),
'next_run' => $start_date->getTimestamp(), // Needs to be set manually when creating new records programmatically.
'stop_type' => '0',
'stop_date' => '0',
'stop_edition' => '0',
'php_eval' => '',
'title' => '[node:title] for [current-date:long]',
);
$record = (array) $node->simplenews_scheduler;
$query = db_merge('simplenews_scheduler');
$query->key(array(
'nid' => $record['nid'],
))
->fields($record)
->execute();
// Store the node ID for the test to use.
$this->parent_nid = $node->nid;
}
/**
* Test simplenews_scheduler_get_newsletters_due().
*/
function testEditionsDue() {
// Get the node id of the parent newsletter node.
$parent_nid = $this->parent_nid ;
// But just check it exists for sanity.
$this->drupalGet("node/$parent_nid");
// Simulate cron running daily at half past 12 so that an edition due at
// 12 noon should be picked up.
$start_date = new DateTime("2012-01-01 12:00:00");
$time_offsets = array(
'before' => "-1 hour",
'after' => "+1 hour",
);
// Number of days to run cron for.
$days = 150;
// Index of the days we've done so far.
$added_days = 0;
// Iterate over days.
while ($added_days <= $days) {
// Create today's date at noon and get the timestamp.
$date = clone($start_date);
$date->add(new DateInterval("P{$added_days}D"));
$timestamp_noon = $date->getTimestamp();
// We simulate running cron one hour before and one hour after noon.
foreach ($time_offsets as $offset_key => $offset) {
// Create a timestamp based on noon + the offset.
// This gives us either 11:00 or 13:00 on the current day.
$timestamp = strtotime($offset, $timestamp_noon);
// debug("base: $timestamp_noon, off: $offset, result: $timestamp");
// Get the list of newsletters due.
$due = simplenews_scheduler_get_newsletters_due($timestamp);
// An edition is due if it's 13:00 on the edition day.
$formatted = date(DATE_RFC850, $timestamp);
if ($offset_key == 'after' && date('d', $timestamp) == $this->edition_day) {
$this->assertTrue(isset($due[$parent_nid]), "Edition due at day $added_days, $formatted, $timestamp");
}
else {
$this->assertFalse(isset($due[$parent_nid]), "Edition not due at day $added_days, $formatted, $timestamp");
}
// Get some debug output to figure out what is going on in
// simplenews_scheduler_get_newsletters_due().
$intervals['hour'] = 3600;
$intervals['day'] = 86400;
$intervals['week'] = $intervals['day'] * 7;
$intervals['month'] = $intervals['day'] * date_days_in_month(date('Y', $timestamp), date('m', $timestamp));
if (isset($due[$parent_nid])) {
// Output what we got back from the function.
// debug($due);
$newsletter_parent_data = $due[$parent_nid];
$edition_time = simplenews_scheduler_calculate_edition_time($newsletter_parent_data, $timestamp);
$eid = _simplenews_scheduler_new_edition($newsletter_parent_data->nid, $timestamp);
// Output the last_run as a sanity check.
$result = db_query("SELECT last_run FROM {simplenews_scheduler} WHERE nid = :nid", array(':nid' => $parent_nid));
$last_run = $result->fetchField();
$formatted = date(DATE_RFC850, $last_run);
// debug("Last run: $formatted, $last_run");
// Output the calculated edition time.
$formatted = date(DATE_RFC850, $edition_time);
// debug("Edition time: $formatted, $edition_time");
// Check the edition time is 12:00.
$this->assertEqual(date('H:i', $edition_time), '12:00', t('Edition time is at 12:00.'));
// Fake sending it: update the 'last_run' for subsequent iterations.
db_update('simplenews_scheduler')
->fields(array('last_run' => $timestamp))
->condition('nid', $parent_nid)
->execute();
// Update the edition record.
simplenews_scheduler_scheduler_update($newsletter_parent_data, $timestamp);
// Check the node exists.
$this->drupalGet("node/$eid");
} // handling the request for a new edition
} // foreach offset timestamp
// Increment our counter.
$added_days++;
} // foreach day
}
}
/**
* Test edition time around DST changes.
*
* Test that simplenews_scheduler_calculate_edition_time() returns an edition
* timestamp whose time in the local timezone remains the same after the
* timezone changes over to Daylight Saving Time.
*/
class SimpleNewsSchedulerDaylightSavingSwitchTest extends SimpleNewsSchedulerWebTestCase {
protected $privileged_user;
/**
* Provides information about this test.
*/
public static function getInfo() {
return array(
'name' => 'Daylight Saving Time',
'description' => 'Functional tests for DST changes.',
'group' => 'Simplenews Scheduler',
);
}
/**
* Test edition time after DST changes for a monthly newsletter.
*
* @todo: generalize this for other intervals.
*/
function testDSTMonthly() {
$timezone_name = date_default_timezone();
//debug($timezone_name);
// Create a last run time before DST begins, and a now time after.
// Use date_create() rather than strtotime so that we create a date at the
// given time *in the current timezone* rather than UTC.
$last_run_date = new DateTime("01-Mar-12 12:00:00");
$now_date = date_create("05-Apr-12 12:00:00");
//debug('last run date TZ: ' . $last_run_date->getTimezone()->getName());
//debug('now date TZ: ' . $now_date->getTimezone()->getName());
// Fake up newsletter data.
$newsletter_parent_data = (object) array(
'last_run' => $last_run_date->getTimestamp(),
'send_interval' => 'month',
'interval_frequency' => 1,
);
// Get the edition time.
$edition_time = simplenews_scheduler_calculate_edition_time($newsletter_parent_data, $now_date->getTimestamp());
$edition_date = date_create('@' . $edition_time);
//debug($edition_date->format(DATE_ATOM));
// Format the edition time.
$edition_time_formatted = format_date($edition_time, 'custom', DATE_ATOM);
$edition_hour_formatted = format_date($edition_time, 'custom', 'H:i');
$this->assertEqual($edition_hour_formatted, '12:00', t('Edition time is at 12:00 in the local timezone; full edition time is %time.', array(
'%time' => $edition_time_formatted,
)));
}
}