Update/merge Newsletter-related code. (Closes #1226, #858, #1047)
authorRaphaël Barrois <raphael.barrois@polytechnique.org>
Mon, 24 Jan 2011 00:23:54 +0000 (01:23 +0100)
committerRaphaël Barrois <raphael.barrois@polytechnique.org>
Sun, 30 Jan 2011 00:05:10 +0000 (01:05 +0100)
Signed-off-by: Raphaël Barrois <raphael.barrois@polytechnique.org>
28 files changed:
bin/cron/newsletters.send.php [moved from bin/cron/axletter.send.php with 89% similarity]
classes/xnet.php
configs/platal.cron.in
htdocs/css/nl.AX.css [moved from htdocs/css/ax.css with 100% similarity]
htdocs/css/nl.Polytechnique.org.css [moved from htdocs/css/nl.css with 100% similarity]
include/massmailer.inc.php [deleted file]
include/newsletter.inc.php
include/validations/nl.inc.php
modules/admin.php
modules/axletter.php
modules/axletter/axletter.inc.php [deleted file]
modules/newsletter.php
modules/xnetnl.php [new file with mode: 0644]
templates/admin/index.tpl
templates/axletter/admin.tpl [deleted file]
templates/axletter/edit.tpl [deleted file]
templates/axletter/index.tpl [deleted file]
templates/axletter/show.tpl [deleted file]
templates/axletter/unsubscribe.tpl [deleted file]
templates/include/form.valid.edit-nl.tpl
templates/include/massmailer-nav.tpl
templates/newsletter/admin.tpl
templates/newsletter/edit.tpl
templates/newsletter/index.tpl
templates/newsletter/nl.AX.mail.tpl [moved from templates/axletter/letter.mail.tpl with 89% similarity]
templates/newsletter/nl.Polytechnique.org.mail.tpl [moved from templates/newsletter/nl.mail.tpl with 85% similarity]
templates/newsletter/show.tpl
upgrade/1.1.0/20_newsletter.sql [new file with mode: 0644]

similarity index 89%
rename from bin/cron/axletter.send.php
rename to bin/cron/newsletters.send.php
index 5fa8827..e9e4ab5 100755 (executable)
  ***************************************************************************/
 
 require_once './connect.db.inc.php';
-require_once '../../modules/axletter/axletter.inc.php';
+require_once 'newsletter.inc.php';
 ini_set('memory_limit', '128M');
 
-$al = AXLetter::toSend();
-if ($al) {
-    echo "Envoi de la lettre \"{$al->title()}\"\n\n";
+$nls = NewsLetter::getIssuesToSend();
+foreach ($nls as $nl) {
+    echo "Envoi de la lettre \"{$nl->title()}\" (Groupe {$nl->group})\n\n";
     echo ' ' . date("H:i:s") . " -> début de l'envoi\n";
-    $emailsCount = $al->sendToAll();
+    $emailsCount = $nl->sendToAll();
     echo ' ' . date("H:i:s") . " -> fin de l'envoi\n\n";
-    echo $emailsCount . " emails ont été envoyés lors de cet envoi.\n";
+    echo $emailsCount . " emails ont été envoyés lors de cet envoi.\n\n";
 }
 
 // vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8:
index 5b42fa1..fe5f421 100644 (file)
@@ -24,7 +24,7 @@ class Xnet extends Platal
     public function __construct()
     {
         parent::__construct('xnet', 'xnetgrp', 'xnetlists', 'xnetevents',
-                            'payment', 'bandeau');
+                            'payment', 'bandeau', 'xnetnl');
     }
 
     public function hook_map($name)
index bb88941..f1e5936 100644 (file)
@@ -26,8 +26,8 @@ WD=/home/web/prod/platal/bin/cron
 # flux rss de banana
 */5 * * * *     www-data     cd $WD; ./banana.feedgen.php > /dev/null
 
-# AX spammer
-15 * * * *      web     cd $WD; ./axletter.send.php | mail -e -s "Envoi d'un email de l'AX" br@staff.m4x.org
+# Send group Newsletters
+15 * * * *      web     cd $WD; ./newsletters.send.php | mail -e -s "Envoi des NLs des groupes" br@staff.m4x.org
 
 # homonymes
 0 0 4 * *      web     cd $WD; ./homonyms.php
similarity index 100%
rename from htdocs/css/ax.css
rename to htdocs/css/nl.AX.css
diff --git a/include/massmailer.inc.php b/include/massmailer.inc.php
deleted file mode 100644 (file)
index b38c961..0000000
+++ /dev/null
@@ -1,253 +0,0 @@
-<?php
-/***************************************************************************
- *  Copyright (C) 2003-2011 Polytechnique.org                              *
- *  http://opensource.polytechnique.org/                                   *
- *                                                                         *
- *  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.,                                                      *
- *  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA                *
- ***************************************************************************/
-
-// {{{ class MailNotFound
-
-class MailNotFound extends Exception {
-}
-
-// }}}
-
-// {{{ class MassMailer
-
-abstract class MassMailer
-{
-    private $_tpl;
-    private $_css;
-    private $_prefix;
-
-    public $_id;
-    public $_shortname;
-    public $_title;
-    public $_title_mail;
-
-    public $_head;
-
-    protected $_table;
-    protected $_subscriptionTable;
-
-    function __construct($tpl, $css, $prefix, $tbl, $stbl)
-    {
-        $this->_tpl    = $tpl;
-        $this->_css    = $css;
-        $this->_prefix = $prefix;
-        $this->_table  = $tbl;
-        $this->_subscriptionTable = $stbl;
-    }
-
-    public function id()
-    {
-        return is_null($this->_shortname) ? $this->_id : $this->_shortname;
-    }
-
-    private function selectId($where)
-    {
-        $res = XDB::query("SELECT  IF (n.short_name IS NULL, n.id, n.short_name)
-                             FROM  {$this->_table} AS n
-                            WHERE  n.bits != 'new' AND {$where}
-                            LIMIT  1");
-        if ($res->numRows() != 1) {
-            return null;
-        }
-        return $res->fetchOneCell();
-    }
-
-    public function prev()
-    {
-        static $val;
-        if (!isset($val)) {
-            $val = $this->selectId("n.id < {$this->_id} ORDER BY n.id DESC");
-        }
-        return $val;
-    }
-
-    public function next()
-    {
-        static $val;
-        if (!isset($val)) {
-            $val = $this->selectId("n.id > {$this->_id} ORDER BY n.id");
-        }
-        return $val;
-    }
-
-    public function last()
-    {
-        static $val;
-        if (!isset($val)) {
-            $res = XDB::query("SELECT  MAX(n.id)
-                                 FROM  {$this->_table} AS n
-                                WHERE  n.bits != 'new' AND n.id > {?}",
-                              $this->_id);
-            if ($res->numRows() != 1) {
-                $val = null;
-            } else {
-                $val = $res->fetchOneCell();
-            }
-        }
-        return $val;
-    }
-
-    public function title($mail = false)
-    {
-        return $mail ? $this->_title_mail : $this->_title;
-    }
-
-    public function head($user = null, $type = 'text')
-    {
-        if (is_null($user)) {
-            return $this->_head;
-        } else {
-            $head = $this->_head;
-            $head = str_replace('<cher>',   $user->isFemale() ? 'Chère' : 'Cher', $head);
-            $head = str_replace('<prenom>', $user->displayName(), $head);
-            $head = str_replace('<nom>', '', $head);
-            return format_text($head, $type, 2, 64);
-        }
-    }
-
-    public function css(&$page = null)
-    {
-        if (!is_null($page)) {
-            $page->addCssLink($this->_css);
-            return true;
-        } else {
-            $css = file_get_contents(dirname(__FILE__) . '/../htdocs/css/' . $this->_css);
-            return preg_replace('@/\*.*?\*/@us', '', $css);
-        }
-    }
-
-    public function toText(&$page, $user)
-    {
-        $this->css($page);
-        $page->assign('is_mail', false);
-        $page->assign('mail_part', 'text');
-        $page->assign('user', $user);
-        $this->assignData($page);
-    }
-
-    public function toHtml(&$page, $user)
-    {
-        $this->css($page);
-        $page->assign('prefix', $this->_prefix . '/' . $this->id());
-        $page->assign('is_mail', false);
-        $page->assign('mail_part', 'html');
-        $page->assign('user', $user);
-        $this->assignData($page);
-    }
-
-    private function createHash($line, $key = null)
-    {
-        $hash = implode(time(), $line) . rand();
-        $hash = md5($hash);
-        return $hash;
-    }
-
-    public function sendTo($user, $hash = null)
-    {
-        if (is_null($hash)) {
-            $hash = XDB::fetchOneCell("SELECT  hash
-                                         FROM  {$this->_subscriptionTable}
-                                        WHERE  uid = {?}", $user->id());
-        }
-        if (is_null($hash)) {
-            $hash = $this->createHash(array($user->displayName(), $user->fullName(),
-                                       $user->isFemale(), $user->isEmailFormatHtml(),
-                                       rand(), "X.org rulez"));
-            XDB::execute("UPDATE  {$this->_subscriptionTable} as ni
-                             SET  ni.hash = {?}
-                           WHERE  ni.uid = {?}",
-                         $hash, $user->id());
-        }
-
-        $mailer = new PlMailer($this->_tpl);
-        $this->assignData($mailer);
-        $mailer->assign('is_mail', true);
-        $mailer->assign('user', $user);
-        $mailer->assign('prefix',  null);
-        $mailer->assign('hash',    $hash);
-        $mailer->addTo('"' . $user->fullName() . '" <' . $user->bestEmail() . '>');
-        $mailer->send($user->isEmailFormatHtml());
-    }
-
-    protected function getAllRecipients()
-    {
-        global $globals;
-        return  "SELECT  a.uid
-                   FROM  {$this->_subscriptionTable}  AS ni
-             INNER JOIN  accounts AS a ON (ni.uid = a.uid)
-              LEFT JOIN  email_options AS eo ON (eo.uid = a.uid)
-              LEFT JOIN  emails   AS e ON (e.uid = a.uid AND e.flags='active')
-              LEFT JOIN  account_profiles AS ap ON (a.uid = ap.uid AND FIND_IN_SET('owner', ap.perms))
-              LEFT JOIN  profile_display AS pd ON (ap.pid = pd.pid)
-                  WHERE  ni.last < {?} AND ({$this->subscriptionWhere()}) AND
-                         (e.email IS NOT NULL OR FIND_IN_SET('googleapps', eo.storage))
-               GROUP BY  a.uid";
-    }
-
-    public function sendToAll()
-    {
-        $this->setSent();
-        $query = XDB::format($this->getAllRecipients(), $this->_id) . ' LIMIT 60';
-        $emailsCount = 0;
-
-        while (true) {
-            $sent = array();
-            $users = User::getBulkUsersWithUIDs(XDB::fetchColumn($query));
-            if (count($users) == 0) {
-                return $emailsCount;
-            }
-            foreach ($users as $user) {
-                $sent[] = $user->id();
-                $this->sendTo($user, $hash);
-                ++$emailsCount;
-            }
-            XDB::execute("UPDATE  {$this->_subscriptionTable}
-                             SET  last = {?}
-                           WHERE  uid IN {?}", $this->_id, $sent);
-
-            sleep(60);
-        }
-        return $emailsCount;
-    }
-
-    abstract protected function assignData(&$smarty);
-    abstract protected function setSent();
-
-    abstract protected function subscriptionWhere();
-}
-
-// }}}
-// {{{ Functions
-
-function format_text($input, $format, $indent = 0, $width = 68)
-{
-    if ($format == 'text') {
-        return MiniWiki::WikiToText($input, true, $indent, $width, "title");
-    }
-    return MiniWiki::WikiToHTML($input, "title");
-}
-
-// function enriched_to_text($input,$html=false,$just=false,$indent=0,$width=68)
-
-// }}}
-
-// vim:set et sw=4 sts=4 sws=4 enc=utf-8:
-?>
index 4e3aa7f..6a86929 100644 (file)
  *  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA                *
  ***************************************************************************/
 
-require_once("massmailer.inc.php");
+// {{{ class MailNotFound
+
+class MailNotFound extends Exception {
+}
+
+// }}}
 
 // {{{ class NewsLetter
 
-class NewsLetter extends MassMailer
+class NewsLetter
 {
-    public $_date;
-    public $_cats = array();
-    public $_arts = array();
+    public $id;  // ID of the NL (in table newsletters)
+    public $group;  // Short name of the group corresponding to the NL
+    public $group_id;  // ID of that group
+    public $name;  // Name of the NL (e.g "Lettre de Polytechnique.org", ...)
+    public $cats;  // List of all categories for this NL
+    public $criteria;  // PlFlagSet of allowed filters for recipient selection
+
+    protected $custom_css = false;
 
-    function __construct($id = null)
+    // Base name to use instead of the group short name for NLs without a custom CSS
+    const FORMAT_DEFAULT_GROUP = 'default';
+
+    // Diminutif of X.net groups with a specific NL view
+    const GROUP_XORG = 'Polytechnique.org';
+    const GROUP_AX = 'AX';
+    const GROUP_EP = 'Ecole';
+
+    // {{{ Constructor, NewsLetter retrieval (forGroup, getAll)
+
+    public function __construct($id)
     {
-        parent::__construct('newsletter/nl.mail.tpl', 'nl.css', 'nl/show', 'newsletter', 'newsletter_ins');
-        if (isset($id)) {
-            if ($id == 'last') {
-                $res = XDB::query("SELECT MAX(id) FROM newsletter WHERE bits!='new'");
-                $id  = $res->fetchOneCell();
+        // Load NL data
+        $res = XDB::query('SELECT  nls.group_id, g.diminutif AS group_name,
+                                   nls.name AS nl_name, nls.custom_css, nls.criteria
+                             FROM  newsletters AS nls
+                        LEFT JOIN  groups AS g ON (nls.group_id = g.id)
+                            WHERE  nls.id = {?}',
+                            $id);
+        if (!$res->numRows()) {
+            throw new MailNotFound();
+        }
+
+        $data = $res->fetchOneAssoc();
+        $this->id = $id;
+        $this->group_id = $data['group_id'];
+        $this->group = $data['group_name'];
+        $this->name = $data['nl_name'];
+        $this->custom_css = $data['custom_css'];
+        $this->criteria = new PlFlagSet($data['criteria']);
+
+        // Load the categories
+        $res = XDB::iterRow(
+            'SELECT  cid, title
+               FROM  newsletter_cat
+              WHERE  nlid = {?}
+           ORDER BY  pos', $id);
+        while (list($cid, $title) = $res->next()) {
+            $this->cats[$cid] = $title;
+        }
+    }
+
+    /** Retrieve the NL associated with a given group.
+     * @p $group Short name of the group
+     * @return A NewsLetter object, or null if the group doesn't have a NL.
+     */
+    public static function forGroup($group)
+    {
+        $res = XDB::query('SELECT  nls.id
+                             FROM  newsletters AS nls
+                        LEFT JOIN  groups AS g ON (nls.group_id = g.id)
+                            WHERE  g.diminutif = {?}', $group);
+        if (!$res->numRows()) {
+            return null;
+        }
+        return new NewsLetter($res->fetchOneCell());
+    }
+
+    /** Retrieve all newsletters
+     * @return An array of $id => NewsLetter objects
+     */
+    public static function getAll()
+    {
+        $res = XDB::query('SELECT  id
+                             FROM  newsletters');
+        $nls = array();
+        foreach ($res->fetchColumn() as $id) {
+            $nls[$id] = new NewsLetter($id);
+        }
+        return $nls;
+    }
+
+    // }}}
+    // {{{ Issue retrieval
+
+    /** Retrieve all issues which should be sent
+     * @return An array of NLIssue objects to send (i.e state = 'new' and send_before <= today)
+     */
+    public static function getIssuesToSend()
+    {
+        $res = XDB::query('SELECT  id
+                             FROM  newsletter_issues
+                            WHERE  state = \'pending\' AND send_before <= NOW()');
+        $issues = array();
+        foreach ($res->fetchColumn() as $id) {
+            $issues[$id] = new NLIssue($id);
+        }
+        return $issues;
+    }
+
+    /** Retrieve a given issue of this NewsLetter
+     * @p $name Name or ID of the issue to retrieve.
+     * @return A NLIssue object.
+     *
+     * $name may be either a short_name, an ID or the special value 'last' which
+     * selects the latest sent NL.
+     * If $name is null, this will retrieve the current pending NL.
+     */
+    public function getIssue($name = null, $only_sent = true)
+    {
+        if ($name) {
+            if ($name == 'last') {
+                if ($only_sent) {
+                    $where = 'state = \'sent\' AND ';
+                } else {
+                    $where = '';
+                }
+                $res = XDB::query('SELECT  MAX(id)
+                                     FROM  newsletter_issues
+                                    WHERE  ' . $where . ' nlid = {?}',
+                                   $this->id);
+            } else {
+                $res = XDB::query('SELECT  id
+                                     FROM  newsletter_issues
+                                    WHERE  nlid = {?} AND (id = {?} OR short_name = {?})',
+                                  $this->id, $name, $name);
             }
-            $res = XDB::query("SELECT * FROM newsletter WHERE id={?} OR short_name={?} LIMIT 1", $id, $id);
-        } else {
-            $res = XDB::query("SELECT * FROM newsletter WHERE bits='new'");
             if (!$res->numRows()) {
-                NewsLetter::create();
+                throw new MailNotFound();
+            }
+            $id = $res->fetchOneCell();
+        } else {
+            $query = XDB::format('SELECT  id
+                                    FROM  newsletter_issues
+                                   WHERE  nlid = {?} AND state = \'new\'
+                                ORDER BY  id DESC', $this->id);
+            $res = XDB::query($query);
+            if ($res->numRows()) {
+                $id = $res->fetchOneCell();
+            } else {
+                // Create a new, empty issue, and return it
+                $id = $this->createPending();
             }
-            $res = XDB::query("SELECT * FROM newsletter WHERE bits='new' ORDER BY id DESC LIMIT 1");
         }
-        if ($res->numRows() != 1) {
+
+        return new NLIssue($id, &$this);
+    }
+
+    /** Create a new, empty, pending newsletter issue
+     * @p $nlid The id of the NL for which a new pending issue should be created.
+     * @return Id of the newly created issue.
+     */
+    public function createPending()
+    {
+        XDB::execute('INSERT INTO  newsletter_issues
+                              SET  nlid = {?}, state=\'new\', date=NOW(),
+                                   title=\'to be continued\',
+                                   mail_title=\'to be continued\'',
+                                   $this->id);
+        return XDB::insertId();
+    }
+
+    /** Return all sent issues of this newsletter.
+     * @return An array of (id => NLIssue)
+     */
+    public function listSentIssues($check_user = false, $user = null)
+    {
+        if ($check_user && $user == null) {
+            $user = S::user();
+        }
+
+        $res = XDB::query('SELECT  id
+                             FROM  newsletter_issues
+                            WHERE  nlid = {?} AND state = \'sent\'
+                         ORDER BY  date DESC', $this->id);
+        $issues = array();
+        foreach ($res->fetchColumn() as $id) {
+            $issue = new NLIssue($id, $this, false);
+            if (!$check_user || $issue->checkUser($user)) {
+                $issues[$id] = $issue;
+            }
+        }
+        return $issues;
+    }
+
+    /** Return all issues of this newsletter, including invalid and sent.
+     * @return An array of (id => NLIssue)
+     */
+    public function listAllIssues()
+    {
+        $res = XDB::query('SELECT  id
+                             FROM  newsletter_issues
+                            WHERE  nlid = {?}
+                         ORDER BY  FIELD(state, \'pending\', \'new\') DESC, date DESC', $this->id);
+        $issues = array();
+        foreach ($res->fetchColumn() as $id) {
+            $issues[$id] = new NLIssue($id, $this, false);
+        }
+        return $issues;
+    }
+
+    /** Return the latest pending issue of the newsletter.
+     * @return Either null, or a NL object.
+     */
+    public function getPendingIssue()
+    {
+        $res = XDB::query('SELECT  MAX(id)
+                             FROM  newsletter_issues
+                            WHERE  nlid = {?} AND state = \'new\'',
+                            $this->id);
+        if ($res->numRows()) {
+            $id = $res->fetchOneCell();
+            return new NLIssue($id, $this);
+        } else {
+            return null;
+        }
+    }
+
+    // }}}
+    // {{{ Subscription related function
+
+    /** Unsubscribe a user from this newsletter
+     * @p $uid UID to unsubscribe from the newsletter; if null, use current user.
+     * @p $hash True if the uid is actually a hash.
+     * @return True if the user was successfully unsubscribed.
+     */
+    public function unsubscribe($uid = null, $hash = false)
+    {
+        if (is_null($uid) && $hash) {
+            // Unable to unsubscribe from an empty hash
+            return false;
+        }
+        $user = is_null($uid) ? S::user()->id() : $uid;
+        $field = $hash ? 'hash' : 'uid';
+        $res = XDB::query('SELECT  uid
+                             FROM  newsletter_ins
+                            WHERE  nlid = {?} AND ' . $field . ' = {?}',
+                            $this->id, $user);
+        if (!$res->numRows()) {
+            // No subscribed user with that UID/hash
+            return false;
+        }
+        $user = $res->fetchOneCell();
+
+        XDB::execute('DELETE FROM  newsletter_ins
+                            WHERE  nlid = {?} AND uid = {?}',
+                            $this->id, $user);
+        return true;
+    }
+
+    /** Subscribe a user to a newsletter
+     * @p $user User to subscribe to the newsletter; if null, use current user.
+     */
+    public function subscribe($user = null)
+    {
+        if (is_null($user)) {
+            $user = S::user();
+        }
+        if (self::maySubscribe($user)) {
+            XDB::execute('INSERT IGNORE INTO  newsletter_ins (nlid, uid, last, hash)
+                                      VALUES  ({?}, {?}, NULL, hash)',
+                         $this->id, $user->id());
+        }
+    }
+
+    /** Retrieve subscription state of a user
+     * @p $user Target user; if null, use current user.
+     * @return Boolean: true if the user has subscribed to the NL.
+     */
+    public function subscriptionState($user = null)
+    {
+        if (is_null($user)) {
+            $user = S::user();
+        }
+        $res = XDB::query('SELECT  1
+                             FROM  newsletter_ins
+                            WHERE  nlid = {?} AND uid = {?}',
+                          $this->id, $user->id());
+        return ($res->numRows() == 1);
+    }
+
+    /** Get the count of subscribers to the NL.
+     * @return Number of subscribers.
+     */
+    public function subscriberCount()
+    {
+        return XDB::fetchOneCell('SELECT  COUNT(uid)
+                                    FROM  newsletter_ins
+                                   WHERE  nlid = {?}', $this->id);
+    }
+
+    /** Get the number of subscribers to the NL whose last received mailing was $last.
+     * @p $last ID of the issue for which subscribers should be counted.
+     * @return Number of subscribers
+     */
+    public function subscriberCountForLast($last)
+    {
+        return XDB::fetchOneCell('SELECT  COUNT(uid)
+                                    FROM  newsletter_ins
+                                   WHERE  nlid = {?} AND last = {?}', $this->id, $last);
+    }
+
+    /** Retrieve the list of newsletters a user has subscribed to
+     * @p $user User whose subscriptions should be retrieved (if null, use session user).
+     * @return Array of newsletter IDs
+     */
+    public static function getUserSubscriptions($user = null)
+    {
+        if (is_null($user)) {
+            $user = S::user();
+        }
+        $res = XDB::query('SELECT  nlid
+                             FROM  newsletter_ins
+                            WHERE  uid = {?}',
+                          $user->id());
+        return $res->fetchColumn();
+    }
+
+    /** Retrieve the UserFilterBuilder for subscribers to this NL.
+     * This is the place where NL-specific filters may be allowed or prevented.
+     * @p $envprefix Prefix to use for env fields (cf. UserFilterBuilder)
+     * @return A UserFilterBuilder object using the given env prefix
+     */
+    public function getSubscribersUFB($envprefix = '')
+    {
+        require_once 'ufbuilder.inc.php';
+        return new UFB_NewsLetter($this->criteria, $envprefix);
+    }
+
+    // }}}
+    // {{{ Permissions related functions
+
+    /** For later use: check whether a given user may subscribe to this newsletter.
+     * @p $user User whose access should be checked
+     * @return Boolean: whether the user may subscribe to the NL.
+     */
+    public function maySubscribe($user = null)
+    {
+        return true;
+    }
+
+    /** Whether a given user may edit this newsletter
+     * @p $uid UID of the user whose perms should be checked (if null, use current user)
+     * @return Boolean: whether the user may edit the NL
+     */
+    public function mayEdit($user = null)
+    {
+        if (is_null($user)) {
+            $user = S::user();
+        }
+        if ($user->checkPerms('admin')) {
+            return true;
+        }
+        $res = XDB::query('SELECT  perms
+                             FROM  group_members
+                            WHERE  asso_id = {?} AND uid = {?}',
+                            $this->group_id, $user->id());
+        return ($res->numRows() && $res->fetchOneCell() == 'admin');
+    }
+
+    /** Whether a given user may submit articles to this newsletter using X.org validation system
+     * @p $user User whose access should be checked (if null, use current user)
+     * @return Boolean: whether the user may submit articles
+     */
+    public function maySubmit($user = null)
+    {
+        // Submission of new articles is only enabled for the X.org NL (and forbidden when viewing issues on X.net)
+        return ($this->group == self::GROUP_XORG && !isset($GLOBALS['IS_XNET_SITE']));
+    }
+
+    // }}}
+    // {{{ Display-related functions: cssFile, tplFile, prefix, admin_prefix, admin_links_enabled, automatic_mailings_enabled
+
+    /** Get the name of the css file used to display this newsletter.
+     */
+    public function cssFile()
+    {
+        if ($this->custom_css) {
+            $base = $this->group;
+        } else {
+            $base = self::FORMAT_DEFAULT_GROUP;
+        }
+        return 'nl.' . $base . '.css';
+    }
+
+    /** Get the name of the template file used to display this newsletter.
+     */
+    public function tplFile()
+    {
+        if ($this->custom_css) {
+            $base = $this->group;
+        } else {
+            $base = self::FORMAT_DEFAULT_GROUP;
+        }
+        return 'newsletter/nl.' . $base . '.mail.tpl';
+    }
+
+    /** Get the prefix leading to the page for this NL
+     * Only X.org / AX / X groups may be seen on X.org.
+     */
+    public function prefix()
+    {
+        if (!empty($GLOBALS['IS_XNET_SITE'])) {
+            return $this->group . '/nl';
+        }
+        switch ($this->group) {
+        case self::GROUP_XORG:
+            return 'nl';
+        case self::GROUP_AX:
+            return 'ax';
+        case self::GROUP_EP:
+            return 'epletter';
+        default:
+            // Don't display groups NLs on X.org
+            assert(false);
+        }
+    }
+
+    /** Get the prefix to use for all 'admin' pages of this NL.
+     */
+    public function adminPrefix()
+    {
+        if (!empty($GLOBALS['IS_XNET_SITE'])) {
+            return $this->group . '/admin/nl';
+        }
+        switch ($this->group) {
+        case self::GROUP_XORG:
+            return 'admin/newsletter';
+        case self::GROUP_AX:
+            return 'ax/admin';
+        case self::GROUP_EP:
+            return 'epletter/admin';
+        default:
+            // Don't display groups NLs on X.org
+            assert(false);
+        }
+    }
+
+    /** Hack used to remove "admin" links on X.org page on X.net
+     * The 'admin' links are enabled for all pages, except for X.org when accessing NL through X.net
+     */
+    public function adminLinksEnabled()
+    {
+        return ($this->group != self::GROUP_XORG || !isset($GLOBALS['IS_XNET_SITE']));
+    }
+
+    /** Automatic mailings are disabled for X.org NL.
+     */
+    public function automaticMailingEnabled()
+    {
+        return $this->group != self::GROUP_XORG;
+    }
+
+    public function hasCustomCss()
+    {
+        return $this->custom_css;
+    }
+
+    // }}}
+}
+
+// }}}
+
+// {{{ class NLIssue
+
+// A NLIssue is an issue of a given NewsLetter
+class NLIssue
+{
+    protected $nlid;  // Id of the newsletter
+
+    const STATE_NEW = 'new';  // New, currently being edited
+    const STATE_PENDING = 'pending';  // Ready for mailing
+    const STATE_SENT = 'sent';  // Sent
+
+    public $nl;  // Related NL
+
+    public $id;  // Id of this issue of the newsletter
+    public $shortname;  // Shortname for this issue
+    public $title;  // Title of this issue
+    public $title_mail;  // Title of the email
+    public $state;  // State of the issue (one of the STATE_ values)
+    public $sufb;  // Environment to use to generate the UFC through an UserFilterBuilder
+
+    public $date;  // Date at which this issue was sent
+    public $send_before;  // Date at which issue should be sent
+    public $head;  // Foreword of the issue (or body for letters with no articles)
+    public $signature;  // Signature of the letter
+    public $arts = array();  // Articles of the issue
+
+    const BATCH_SIZE = 60;  // Number of emails to send every minute.
+
+    // {{{ Constructor, id-related functions
+
+    /** Build a NewsLetter.
+     * @p $id: ID of the issue (unique among all newsletters)
+     * @p $nl: Optional argument containing an already built NewsLetter object.
+     */
+    function __construct($id, $nl = null, $fetch_articles = true)
+    {
+        return $this->fetch($id, $nl, $fetch_articles);
+    }
+
+    protected function refresh()
+    {
+        return $this->fetch($this->id, $this->nl, false);
+    }
+
+    protected function fetch($id, $nl = null, $fetch_articles = true)
+    {
+        // Load this issue
+        $res = XDB::query('SELECT  nlid, short_name, date, send_before, state, sufb_json,
+                                   title, mail_title, head, signature
+                             FROM  newsletter_issues
+                            WHERE  id = {?}',
+                          $id);
+        if (!$res->numRows()) {
             throw new MailNotFound();
         }
-        $nl = $res->fetchOneAssoc();
+        $issue = $res->fetchOneAssoc();
+        if ($nl && $nl->id == $issue['nlid']) {
+            $this->nl = $nl;
+        } else {
+            $this->nl = new NewsLetter($issue['nlid']);
+        }
+        $this->id = $id;
+        $this->shortname   = $issue['short_name'];
+        $this->date        = $issue['date'];
+        $this->send_before = $issue['send_before'];
+        $this->state       = $issue['state'];
+        $this->title       = $issue['title'];
+        $this->title_mail  = $issue['mail_title'];
+        $this->head        = $issue['head'];
+        $this->signature   = $issue['signature'];
+        $this->sufb        = $this->importJSonStoredUFB($issue['sufb_json']);
 
-        $this->_id         = $nl['id'];
-        $this->_shortname  = $nl['short_name'];
-        $this->_date       = $nl['date'];
-        $this->_title      = $nl['titre'];
-        $this->_title_mail = $nl['titre_mail'];
-        $this->_head       = $nl['head'];
+        if ($fetch_articles) {
+            $this->fetchArticles();
+        }
+    }
 
-        $res = XDB::iterRow("SELECT cid,titre FROM newsletter_cat ORDER BY pos");
-        while (list($cid, $title) = $res->next()) {
-            $this->_cats[$cid] = $title;
+    protected function fetchArticles($force = false)
+    {
+        if (count($this->arts) && !$force) {
+            return;
         }
 
+        // Load the articles
         $res = XDB::iterRow(
-                "SELECT  a.title,a.body,a.append,a.aid,a.cid,a.pos
-                   FROM  newsletter_art AS a
-             INNER JOIN  newsletter     AS n USING(id)
-             LEFT  JOIN  newsletter_cat AS c ON(a.cid=c.cid)
-                  WHERE  a.id={?}
-               ORDER BY  c.pos,a.pos", $this->_id);
+            'SELECT  a.title, a.body, a.append, a.aid, a.cid, a.pos
+               FROM  newsletter_art AS a
+         INNER JOIN  newsletter_issues AS ni USING(id)
+         LEFT  JOIN  newsletter_cat AS c ON (a.cid = c.cid)
+              WHERE  a.id = {?}
+           ORDER BY  c.pos, a.pos',
+           $this->id);
         while (list($title, $body, $append, $aid, $cid, $pos) = $res->next()) {
-            $this->_arts[$cid]["a$aid"] = new NLArticle($title, $body, $append, $aid, $cid, $pos);
+            $this->arts[$cid][$aid] = new NLArticle($title, $body, $append, $aid, $cid, $pos);
+        }
+    }
+
+    protected function importJSonStoredUFB($json = null)
+    {
+        require_once 'ufbuilder.inc.php';
+        $ufb = $this->nl->getSubscribersUFB();
+        if (is_null($json)) {
+            return new StoredUserFilterBuilder($ufb, new PFC_True());
+        }
+        $export = json_decode($json, true);
+        if (is_null($export)) {
+            PlErrorReport::report("Invalid json while reading NL {$this->nlid}, issue {$this->id}: failed to import '''{$json}'''.");
+            return new StoredUserFilterBuilder($ufb, new PFC_True());
+        }
+        $sufb = new StoredUserFilterBuilder($ufb);
+        $sufb->fillFromExport($export);
+        return $sufb;
+    }
+
+    protected function exportStoredUFBAsJSon()
+    {
+        return json_encode($this->sufb->export());
+    }
+
+    public function id()
+    {
+        return is_null($this->shortname) ? $this->id : $this->shortname;
+    }
+
+    protected function selectId($where)
+    {
+        $res = XDB::query("SELECT  IFNULL(ni.short_name, ni.id)
+                             FROM  newsletter_issues AS ni
+                            WHERE  ni.state != 'new' AND ni.nlid = {?} AND ${where}
+                            LIMIT  1", $this->nl->id);
+        if ($res->numRows() != 1) {
+            return null;
+        }
+        return $res->fetchOneCell();
+    }
+
+    /** Delete this issue
+     * @return True if the issue could be deleted, false otherwise.
+     * Related articles will be deleted through cascading FKs.
+     * If this issue was the last issue for at least one subscriber, the deletion will be aborted.
+     */
+    public function delete()
+    {
+        if ($this->state == self::STATE_NEW) {
+            $res = XDB::query('SELECT  COUNT(*)
+                                 FROM  newsletter_ins
+                                WHERE  last = {?}', $this->id);
+            if ($res->fetchOneCell() > 0) {
+                return false;
+            }
+
+            return XDB::execute('DELETE FROM  newsletter_issues
+                                       WHERE  id = {?}', $this->id);
+        } else {
+            return false;
+        }
+    }
+
+    /** Schedule a mailing of this NL
+     * If the 'send_before' field was NULL, it is set to the current time.
+     * @return Boolean Whether the date could be set (false if trying to schedule an already sent NL)
+     */
+    public function scheduleMailing()
+    {
+        if ($this->state == self::STATE_NEW) {
+            $success = XDB::execute('UPDATE  newsletter_issues
+                                        SET  state = \'pending\', send_before = IFNULL(send_before, NOW())
+                                      WHERE  id = {?}',
+                                      $this->id);
+            if ($success) {
+                $this->refresh();
+            }
+            return $success;
+        } else {
+            return false;
+        }
+    }
+
+    /** Cancel the scheduled mailing of this NL
+     * @return Boolean: whether the mailing could be cancelled.
+     */
+    public function cancelMailing()
+    {
+        if ($this->state == self::STATE_PENDING) {
+            $success = XDB::execute('UPDATE  newsletter_issues
+                                        SET  send_before = NULL, state = \'new\'
+                                      WHERE  id = {?}', $this->id);
+            if ($success) {
+                $this->refresh();
+            }
+            return $success;
+        } else {
+            return false;
+        }
+    }
+
+    /** Helper function for smarty templates: is this issue editable ?
+     */
+    public function isEditable()
+    {
+        return $this->state == self::STATE_NEW;
+    }
+
+    /** Helper function for smarty templates: is the mailing of this issue scheduled ?
+     */
+    public function isPending()
+    {
+        return $this->state == self::STATE_PENDING;
+    }
+
+    /** Helper function for smarty templates: has this issue been sent ?
+     */
+    public function isSent()
+    {
+        return $this->state == self::STATE_SENT;
+    }
+
+    // }}}
+    // {{{ Navigation
+
+    private $id_prev = null;
+    private $id_next = null;
+    private $id_last = null;
+
+    /** Retrieve ID of the previous issue
+     * That value, once fetched, is cached in the private $id_prev variable.
+     * @return ID of the previous issue.
+     */
+    public function prev()
+    {
+        if (is_null($this->id_prev)) {
+            $this->id_prev = $this->selectId(XDB::format("ni.id < {?} ORDER BY ni.id DESC", $this->id));
+        }
+        return $this->id_prev;
+    }
+
+    /** Retrieve ID of the following issue
+     * That value, once fetched, is cached in the private $id_next variable.
+     * @return ID of the following issue.
+     */
+    public function next()
+    {
+        if (is_null($this->id_next)) {
+            $this->id_next = $this->selectId(XDB::format("ni.id > {?} ORDER BY ni.id", $this->id));
+        }
+        return $this->id_next;
+    }
+
+    /** Retrieve ID of the last issue
+     * That value, once fetched, is cached in the private $id_last variable.
+     * @return ID of the last issue.
+     */
+    public function last()
+    {
+        if (is_null($this->id_last)) {
+            $this->id_last = $this->nl->getIssue('last')->id;
         }
+        return $this->id_last;
     }
 
+    // }}}
+    // {{{ Edition, articles
+
+    const ERROR_INVALID_SHORTNAME = 'invalid_shortname';
+    const ERROR_INVALID_UFC = 'invalid_ufc';
+    const ERROR_SQL_SAVE = 'sql_error';
+
+    /** Save the global properties of this NL issue (title&co).
+     */
     public function save()
     {
-        XDB::execute('UPDATE newsletter SET date={?},titre={?},titre_mail={?},head={?},short_name={?} WHERE id={?}',
-                     $this->_date, $this->_title, $this->_title_mail, $this->_head, $this->_shortname,$this->_id);
+        $errors = array();
+
+        // Fill the list of fields to update
+        $fields = array(
+            'title' => $this->title,
+            'mail_title' => $this->title_mail,
+            'head' => $this->head,
+            'signature' => $this->signature,
+        );
+
+        if ($this->isEditable()) {
+            $fields['date'] = $this->date;
+            if (!preg_match('/^[-a-z0-9]+$/i', $this->shortname) || is_numeric($this->shortname)) {
+                $errors[] = self::ERROR_INVALID_SHORTNAME;
+            } else {
+                $fields['short_name'] = $this->shortname;
+            }
+            if ($this->sufb->isValid() || $this->sufb->isEmpty()) {
+                $fields['sufb_json'] = json_encode($this->sufb->export()->dict());
+            } else {
+                $errors[] = self::ERROR_INVALID_UFC;
+            }
+
+            if ($this->nl->automaticMailingEnabled()) {
+                $fields['send_before'] = ($this->send_before ? $this->send_before : null);
+            }
+        }
+
+        if (count($errors)) {
+            return $errors;
+        }
+        $field_sets = array();
+        foreach ($fields as $key => $value) {
+            $field_sets[] = XDB::format($key . ' = {?}', $value);
+        }
+        XDB::execute('UPDATE  newsletter_issues
+                         SET  ' . implode(', ', $field_sets) . '
+                       WHERE  id={?}',
+                       $this->id);
+        if (XDB::affectedRows()) {
+            $this->refresh();
+        } else {
+            $errors[] = self::ERROR_SQL_SAVE;
+        }
+        return $errors;
     }
 
+    /** Get an article by number
+     * @p $aid Article ID (among articles of the issue)
+     * @return A NLArticle object, or null if there is no article by that number
+     */
     public function getArt($aid)
     {
-        foreach ($this->_arts as $key=>$artlist) {
-            if (isset($artlist["a$aid"])) {
-                return $artlist["a$aid"];
+        $this->fetchArticles();
+
+        foreach ($this->arts as $category => $artlist) {
+            if (isset($artlist[$aid])) {
+                return $artlist[$aid];
             }
         }
         return null;
     }
 
+    /** Save an article
+     * @p &$a A reference to a NLArticle object (will be modified once saved)
+     */
     public function saveArticle(&$a)
     {
-        $a->_cid = ($a->_cid == 0) ? null : $a->_cid;
-        if ($a->_aid >= 0) {
+        $this->fetchArticles();
+
+        // Prevent cid to be 0 (use NULL instead)
+        $a->cid = ($a->cid == 0) ? null : $a->cid;
+        if ($a->aid >= 0) {
+            // Article already exists in DB
             XDB::execute('UPDATE  newsletter_art
                              SET  cid = {?}, pos = {?}, title = {?}, body = {?}, append = {?}
                            WHERE  id = {?} AND aid = {?}',
-                         $a->_cid, $a->_pos, $a->_title, $a->_body, $a->_append, $this->_id, $a->_aid);
+                         $a->cid, $a->pos, $a->title, $a->body, $a->append, $this->id, $a->aid);
         } else {
+            // New article
             XDB::startTransaction();
             list($aid, $pos) = XDB::fetchOneRow('SELECT  MAX(aid) AS aid, MAX(pos) AS pos
                                                    FROM  newsletter_art AS a
                                                   WHERE  a.id = {?}',
-                                                $this->_id);
-            $a->_aid = ++$aid;
-            $a->_pos = ($a->_pos ? $a->_pos : ++$pos);
+                                                $this->id);
+            $a->aid = ++$aid;
+            $a->pos = ($a->pos ? $a->pos : ++$pos);
             XDB::execute('INSERT INTO  newsletter_art (id, aid, cid, pos, title, body, append)
                                VALUES  ({?}, {?}, {?}, {?}, {?}, {?}, {?})',
-                         $this->_id, $a->_aid, $a->_cid, $a->_pos,
-                         $a->_title, $a->_body, $a->_append);
+                         $this->id, $a->aid, $a->cid, $a->pos,
+                         $a->title, $a->body, $a->append);
             XDB::commit();
         }
-        $this->_arts['a' . $a->_aid] = $a;
+        // Update local ID of article
+        $this->arts[$a->aid] = $a;
     }
 
+    /** Delete an article by its ID
+     * @p $aid ID of the article to delete
+     */
     public function delArticle($aid)
     {
-        XDB::execute('DELETE FROM newsletter_art WHERE id={?} AND aid={?}', $this->_id, $aid);
-        foreach ($this->_arts as $key=>$art) {
-            unset($this->_arts[$key]["a$aid"]);
+        $this->fetchArticles();
+
+        XDB::execute('DELETE FROM newsletter_art WHERE id={?} AND aid={?}', $this->id, $aid);
+        foreach ($this->arts as $key=>$art) {
+            unset($this->arts[$key][$aid]);
         }
     }
 
-    protected function assignData(&$smarty)
+    // }}}
+    // {{{ Display
+
+    /** Retrieve the title of this issue
+     * @p $mail Whether we want the normal title or the email subject
+     * @return Title of the issue
+     */
+    public function title($mail = false)
     {
-        $smarty->assign_by_ref('nl', $this);
+        return $mail ? $this->title_mail : $this->title;
     }
 
-    protected function setSent()
+    /** Retrieve the head of this issue
+     * @p $user User for <dear> customization (may be null: no customization)
+     * @p $type Either 'text' or 'html'
+     * @return Formatted head of the issue.
+     */
+    public function head($user = null, $type = 'text')
     {
-        XDB::execute("UPDATE newsletter  SET bits='sent' WHERE id={?}", $this->_id);
+        if (is_null($user)) {
+            return $this->head;
+        } else {
+            $head = $this->head;
+            $head = str_replace(array('<cher>', '<prenom>', '<nom>'),
+                                array(($user->isFemale() ? 'Chère' : 'Cher'), $user->displayName(), ''),
+                                $head);
+            return format_text($head, $type, 2, 64);
+        }
     }
 
-    static public function subscriptionState($uid = null)
+    /** Retrieve the formatted signature of this issue.
+     */
+    public function signature($type = 'text')
     {
-        $user = is_null($uid) ? S::v('uid') : $uid;
-        $res = XDB::query("SELECT  1
-                             FROM  newsletter_ins
-                            WHERE  uid={?}", $user);
-        return $res->fetchOneCell();
+        return format_text($this->signature, $type, 2, 64);
+    }
+
+    /** Get the title of a given category
+     * @p $cid ID of the category to retrieve
+     * @return Name of the category
+     */
+    public function category($cid)
+    {
+        return $this->nl->cats[$cid];
+    }
+
+    /** Add required data to the given $page for proper CSS display
+     * @p $page Smarty object
+     * @return Either 'true' (if CSS was added to a page) or the raw CSS to add (when $page is null)
+     */
+    public function css(&$page = null)
+    {
+        if (!is_null($page)) {
+            $page->addCssLink($this->nl->cssFile());
+            return true;
+        } else {
+            $css = file_get_contents(dirname(__FILE__) . '/../htdocs/css/' . $this->nl->cssFile());
+            return preg_replace('@/\*.*?\*/@us', '', $css);
+        }
+    }
+
+    /** Set up a smarty page for a 'text' mode render of the issue
+     * @p $page Smarty object (using the $this->nl->tplFile() template)
+     * @p $user User to use when rendering the template
+     */
+    public function toText(&$page, $user)
+    {
+        $this->fetchArticles();
+
+        $this->css($page);
+        $page->assign('prefix', null);
+        $page->assign('is_mail', false);
+        $page->assign('mail_part', 'text');
+        $page->assign('user', $user);
+        $page->assign('hash', null);
+        $this->assignData($page);
     }
 
-    static public function unsubscribe($uid = null)
+    /** Set up a smarty page for a 'html' mode render of the issue
+     * @p $page Smarty object (using the $this->nl->tplFile() template)
+     * @p $user User to use when rendering the template
+     */
+    public function toHtml(&$page, $user)
     {
-        $user = is_null($uid) ? S::v('uid') : $uid;
-        XDB::execute("DELETE FROM  newsletter_ins
-                            WHERE  uid={?}", $user);
+        $this->fetchArticles();
+
+        $this->css($page);
+        $page->assign('prefix', $this->nl->prefix() . '/show/' . $this->id());
+        $page->assign('is_mail', false);
+        $page->assign('mail_part', 'html');
+        $page->assign('user', $user);
+        $page->assign('hash', null);
+        $this->assignData($page);
     }
 
-    static public function subscribe($uid = null)
+    /** Set all 'common' data for the page (those which are required for both web and email rendering)
+     * @p $smarty Smarty object (e.g page) which should be filled
+     */
+    protected function assignData(&$smarty)
     {
-        $user = is_null($uid) ? S::v('uid') : $uid;
-        XDB::execute('INSERT IGNORE INTO  newsletter_ins (uid, last, hash)
-                                  VALUES  ({?}, NULL, NULL)', $user);
+        $this->fetchArticles();
+
+        $smarty->assign_by_ref('issue', $this);
+        $smarty->assign_by_ref('nl', $this->nl);
     }
 
-    protected function subscriptionWhere()
+    // }}}
+    // {{{ Mailing
+    
+    /** Retrieve the 'Send before' date, in a clean format.
+     */
+    public function getSendBeforeDate()
     {
-        return '1';
+        return strftime('%Y-%m-%d', strtotime($this->send_before));
     }
 
-    static public function create()
+    /** Retrieve the 'Send before' time (i.e hour), in a clean format.
+     */
+    public function getSendBeforeTime()
     {
-        XDB::execute("INSERT INTO newsletter
-                              SET bits='new',date=NOW(),titre='to be continued',titre_mail='to be continued'");
+        return strtotime($this->send_before);
     }
 
-    static public function listSent()
+    /** Create a hash based on some additional data
+     * $line Line-specific data (to prevent two hashes generated at the same time to be the same)
+     */
+    protected static function createHash($line)
     {
-        $res = XDB::query("SELECT  IF(short_name IS NULL, id,short_name) as id,date,titre_mail AS titre
-                             FROM  newsletter
-                            WHERE  bits!='new'
-                            ORDER  BY date DESC");
-        return $res->fetchAllAssoc();
+        $hash = implode(time(), $line) . rand();
+        $hash = md5($hash);
+        return $hash;
     }
 
-    static public function listAll()
+    /** Send this issue to the given user, reusing an existing hash if provided.
+     * @p $user User to whom the issue should be mailed
+     * @p $hash Optional hash to use in the 'unsubscribe' link; if null, another one will be generated.
+     */
+    public function sendTo($user, $hash = null)
     {
-        $res = XDB::query("SELECT  IF(short_name IS NULL, id,short_name) as id,date,titre_mail AS titre
-                             FROM  newsletter
-                         ORDER BY  date DESC");
-        return $res->fetchAllAssoc();
+        $this->fetchArticles();
+
+        if (is_null($hash)) {
+            $hash = XDB::fetchOneCell("SELECT  hash
+                                         FROM  newsletter_ins
+                                        WHERE  uid = {?} AND nlid = {?}",
+                                      $user->id(), $this->nl->id);
+        }
+        if (is_null($hash)) {
+            $hash = self::createHash(array($user->displayName(), $user->fullName(),
+                                       $user->isFemale(), $user->isEmailFormatHtml(),
+                                       rand(), "X.org rulez"));
+            XDB::execute("UPDATE  newsletter_ins as ni
+                             SET  ni.hash = {?}
+                           WHERE  ni.uid = {?} AND ni.nlid = {?}",
+                         $hash, $user->id(), $this->nl->id);
+        }
+
+        $mailer = new PlMailer($this->nl->tplFile());
+        $this->assignData($mailer);
+        $mailer->assign('is_mail', true);
+        $mailer->assign('user', $user);
+        $mailer->assign('prefix',  null);
+        $mailer->assign('hash',    $hash);
+        $mailer->sendTo($user);
     }
+
+    /** Select a subset of subscribers which should receive the newsletter.
+     * NL-Specific selections (not yet received, is subscribed) are done when sending.
+     * @return A PlFilterCondition.
+     */
+    protected function getRecipientsUFC()
+    {
+        return $this->sufb->getUFC();
+    }
+
+    /** Check whether a given user may see this issue.
+     * @p $user User whose access should be checked
+     * @return Whether he may access the issue
+     */
+    public function checkUser($user = null)
+    {
+        if ($user == null) {
+            $user = S::user();
+        }
+        $uf = new UserFilter($this->getRecipientsUFC());
+        return $uf->checkUser($user);
+    }
+
+    /** Sent this issue to all valid recipients
+     * @return Number of issues sent
+     */
+    public function sendToAll()
+    {
+        $this->fetchArticles();
+
+        XDB::execute('UPDATE  newsletter_issues
+                         SET  state = \'sent\', date=CURDATE()
+                       WHERE  id = {?}',
+                       $this->id);
+
+        $ufc = new PFC_And($this->getRecipientsUFC(), new UFC_NLSubscribed($this->nl->id, $this->id), new UFC_HasEmailRedirect());
+        $emailsCount = 0;
+        $uf = new UserFilter($ufc, new PlLimit(self::BATCH_SIZE));
+
+        while (true) {
+            $sent = array();
+            $users = $uf->getUsers();
+            if (count($users) == 0) {
+                return $emailsCount;
+            }
+            foreach ($users as $user) {
+                $sent[] = $user->id();
+                $this->sendTo($user, $hash);
+                ++$emailsCount;
+            }
+            XDB::execute("UPDATE  newsletter_ins
+                             SET  last = {?}
+                           WHERE  nlid = {?} AND uid IN {?}", $this->id, $this->nl->id, $sent);
+
+            sleep(60);
+        }
+        return $emailsCount;
+    }
+
+    // }}}
 }
 
 // }}}
@@ -190,45 +1105,48 @@ class NewsLetter extends MassMailer
 
 class NLArticle
 {
+    // Maximum number of lines per article
+    const MAX_LINES_PER_ARTICLE = 9;
+
     // {{{ properties
 
-    var $_aid;
-    var $_cid;
-    var $_pos;
-    var $_title;
-    var $_body;
-    var $_append;
+    public $aid;
+    public $cid;
+    public $pos;
+    public $title;
+    public $body;
+    public $append;
 
     // }}}
     // {{{ constructor
 
     function __construct($title='', $body='', $append='', $aid=-1, $cid=0, $pos=0)
     {
-        $this->_body   = $body;
-        $this->_title  = $title;
-        $this->_append = $append;
-        $this->_aid    = $aid;
-        $this->_cid    = $cid;
-        $this->_pos    = $pos;
+        $this->body   = $body;
+        $this->title  = $title;
+        $this->append = $append;
+        $this->aid    = $aid;
+        $this->cid    = $cid;
+        $this->pos    = $pos;
     }
 
     // }}}
     // {{{ function title()
 
     public function title()
-    { return trim($this->_title); }
+    { return trim($this->title); }
 
     // }}}
     // {{{ function body()
 
     public function body()
-    { return trim($this->_body); }
+    { return trim($this->body); }
 
     // }}}
     // {{{ function append()
 
     public function append()
-    { return trim($this->_append); }
+    { return trim($this->append); }
 
     // }}}
     // {{{ function toText()
@@ -236,8 +1154,8 @@ class NLArticle
     public function toText($hash = null, $login = null)
     {
         $title = '*'.$this->title().'*';
-        $body  = MiniWiki::WikiToText($this->_body, true);
-        $app   = MiniWiki::WikiToText($this->_append,false,4);
+        $body = MiniWiki::WikiToText($this->body, true);
+        $app = MiniWiki::WikiToText($this->append, false, 4);
         $text = trim("$title\n\n$body\n\n$app")."\n";
         if (!is_null($hash) && !is_null($login)) {
             $text = str_replace('%HASH%', "$hash/$login", $text);
@@ -252,9 +1170,9 @@ class NLArticle
 
     public function toHtml($hash = null, $login = null)
     {
-        $title = "<h2 class='xorg_nl'><a id='art{$this->_aid}'></a>".pl_entities($this->title()).'</h2>';
-        $body  = MiniWiki::WikiToHTML($this->_body);
-        $app   = MiniWiki::WikiToHTML($this->_append);
+        $title = "<h2 class='xorg_nl'><a id='art{$this->aid}'></a>".pl_entities($this->title()).'</h2>';
+        $body  = MiniWiki::WikiToHTML($this->body);
+        $app   = MiniWiki::WikiToHTML($this->append);
 
         $art   = "$title\n";
         $art  .= "<div class='art'>\n$body\n";
@@ -276,7 +1194,7 @@ class NLArticle
 
     public function check()
     {
-        $text = MiniWiki::WikiToText($this->_body);
+        $text = MiniWiki::WikiToText($this->body);
         $arr  = explode("\n",wordwrap($text,68));
         $c    = 0;
         foreach ($arr as $line) {
@@ -284,13 +1202,13 @@ class NLArticle
                 $c++;
             }
         }
-        return $c<9;
+        return $c < self::MAX_LINES_PER_ARTICLE;
     }
 
     // }}}
     // {{{ function parseUrlsFromArticle()
 
-    private function parseUrlsFromArticle()
+    protected function parseUrlsFromArticle()
     {
         $email_regex = '([a-z0-9.\-+_\$]+@([\-.+_]?[a-z0-9])+)';
         $url_regex = '((https?|ftp)://[a-zA-Z0-9._%#+/?=&~-]+)';
@@ -346,5 +1264,19 @@ class NLArticle
 
 // }}}
 
+// {{{ Functions
+
+function format_text($input, $format, $indent = 0, $width = 68)
+{
+    if ($format == 'text') {
+        return MiniWiki::WikiToText($input, true, $indent, $width, "title");
+    }
+    return MiniWiki::WikiToHTML($input, "title");
+}
+
+// function enriched_to_text($input,$html=false,$just=false,$indent=0,$width=68)
+
+// }}}
+
 // vim:set et sw=4 sts=4 sws=4 enc=utf-8:
 ?>
index ef07d35..0a385ac 100644 (file)
@@ -60,9 +60,9 @@ class NLReq extends Validate
 
     protected function handle_editor()
     {
-        $this->art->_body   = Env::v('nl_body');
-        $this->art->_title  = Env::v('nl_title');
-        $this->art->_append = Env::v('nl_append');
+        $this->art->body   = Env::v('nl_body');
+        $this->art->title  = Env::v('nl_title');
+        $this->art->append = Env::v('nl_append');
         return true;
     }
 
@@ -106,7 +106,7 @@ class NLReq extends Validate
 
     public function commit()
     {
-        $nl  = new NewsLetter();
+        $nl = NewsLetter::forGroup(NewsLetter::GROUP_XORG)->getPendingIssue();
         $nl->saveArticle($this->art);
         return true;
     }
index d4fb1ed..ac2b4ed 100644 (file)
@@ -1075,7 +1075,7 @@ class AdminModule extends PLModule
     {
         $page->changeTpl('admin/validation.tpl');
         $page->setTitle('Administration - Valider une demande');
-        $page->addCssLink('nl.css');
+        $page->addCssLink('nl.Polytechnique.org.css');
 
         if ($action == 'edit' && !is_null($id)) {
             $page->assign('preview_id', $id);
index 67bdf55..9467dd0 100644 (file)
  *  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA                *
  ***************************************************************************/
 
-class AXLetterModule extends PLModule
+Platal::load('newsletter');
+
+class AXLetterModule extends NewsletterModule
 {
     function handlers()
     {
         return array(
-            'ax'             => $this->make_hook('index',  AUTH_COOKIE),
-            'ax/out'         => $this->make_hook('out',    AUTH_PUBLIC),
-            'ax/show'        => $this->make_hook('show',   AUTH_COOKIE),
-            'ax/edit'        => $this->make_hook('submit', AUTH_MDP),
-            'ax/edit/cancel' => $this->make_hook('cancel', AUTH_MDP),
-            'ax/edit/valid'  => $this->make_hook('valid',  AUTH_MDP),
-            'admin/axletter' => $this->make_hook('admin',  AUTH_MDP, 'admin'),
+            'ax'                   => $this->make_hook('nl',  AUTH_COOKIE),
+            'ax/out'               => $this->make_hook('out',    AUTH_PUBLIC),
+            'ax/show'              => $this->make_hook('nl_show',   AUTH_COOKIE),
+            'ax/admin'             => $this->make_hook('admin_nl', AUTH_MDP),
+            'ax/admin/edit'        => $this->make_hook('admin_nl_edit', AUTH_MDP),
+            'ax/admin/edit/valid'  => $this->make_hook('admin_nl_valid', AUTH_MDP),
+            'ax/admin/edit/cancel' => $this->make_hook('admin_nl_cancel', AUTH_MDP),
+            'ax/admin/edit/delete' => $this->make_hook('admin_nl_delete', AUTH_MDP),
         );
     }
 
+    protected function getNl()
+    {
+        require_once 'newsletter.inc.php';
+        return NewsLetter::forGroup(NewsLetter::GROUP_AX);
+    }
+
     function handler_out(&$page, $hash = null)
     {
         if (!$hash) {
             if (!S::logged()) {
                 return PL_DO_AUTH;
-            } else {
-                return $this->handler_index($page, 'out');
-            }
-        }
-        $this->load('axletter.inc.php');
-        $page->changeTpl('axletter/unsubscribe.tpl');
-        $page->assign('success', AXLetter::unsubscribe($hash, true));
-    }
-
-    function handler_index(&$page, $action = null)
-    {
-        $this->load('axletter.inc.php');
-
-        $page->changeTpl('axletter/index.tpl');
-        $page->setTitle('Envois de l\'AX');
-
-        switch ($action) {
-          case 'in':  AXLetter::subscribe(); break;
-          case 'out': AXLetter::unsubscribe(); break;
-        }
-
-        $perm = AXLetter::hasPerms();
-        if ($perm) {
-            $res = XDB::query("SELECT * FROM axletter_ins");
-            $page->assign('count', $res->numRows());
-            $page->assign('new', AXLetter::awaiting());
-        }
-        $page->assign('axs', AXLetter::subscriptionState());
-        $page->assign('ax_list', AXLetter::listSent());
-        $page->assign('ax_rights', $perm);
-    }
-
-    function handler_submit(&$page, $action = null)
-    {
-        $this->load('axletter.inc.php');
-        if (!AXLetter::hasPerms()) {
-            return PL_FORBIDDEN;
-        }
-
-        $page->changeTpl('axletter/edit.tpl');
-
-        $saved      = Post::i('saved');
-        $new        = false;
-        $id         = Post::i('id');
-        $short_name = trim(Post::v('short_name'));
-        $subject    = trim(Post::v('subject'));
-        $title      = trim(Post::v('title'));
-        $body       = rtrim(Post::v('body'));
-        $signature  = trim(Post::v('signature'));
-        $promo_min  = Post::i('promo_min');
-        $promo_max  = Post::i('promo_max');
-        $subset_to  = preg_split("/[ ,;\:\n\r]+/", Post::v('subset_to'), -1, PREG_SPLIT_NO_EMPTY);
-        $subset     = (count($subset_to) > 0);
-        $subset_rm  = Post::b('subset_rm');
-        $echeance   = Post::has('echeance_date') ?
-              preg_replace('/^(\d\d\d\d)(\d\d)(\d\d)$/', '\1-\2-\3', Post::v('echeance_date')) . ' ' . Post::v('echeance_time')
-            : Post::v('echeance');
-        $echeance_date = Post::v('echeance_date');
-        $echeance_time = Post::v('echeance_time');
-
-        if (!$id) {
-            $res = XDB::query("SELECT * FROM axletter WHERE FIND_IN_SET('new', bits)");
-            if ($res->numRows()) {
-                extract($res->fetchOneAssoc(), EXTR_OVERWRITE);
-                $subset_to = ($subset ? explode("\n", $subset) : array());
-                $subset = (count($subset_to) > 0);
-                $saved = true;
-            } else  {
-                XDB::execute("INSERT INTO axletter SET id = NULL");
-                $id  = XDB::insertId();
-            }
-            if (!$echeance || $echeance == '0000-00-00 00:00:00') {
-                $saved = false;
-                $new   = true;
-            }
-        } elseif (Post::has('valid')) {
-            S::assert_xsrf_token();
-
-            if (!$subject && $title) {
-                $subject = $title;
-            }
-            if (!$title && $subject) {
-                $title = $subject;
-            }
-            if (!$subject || !$title || !$body) {
-                $page->trigError("L'article doit avoir un sujet et un contenu");
-                Post::kill('valid');
-            }
-            if (($promo_min > $promo_max && $promo_max != 0)||
-                ($promo_min != 0 && ($promo_min <= 1900 || $promo_min >= 2020)) ||
-                ($promo_max != 0 && ($promo_max <= 1900 || $promo_max >= 2020)))
-            {
-                $page->trigError("L'intervalle de promotions n'est pas valide");
-                Post::kill('valid');
-            }
-            if (empty($short_name)) {
-                $page->trigError("L'annonce doit avoir un nom raccourci pour simplifier la navigation dans les archives");
-                Post::kill('valid');
-            } elseif (!preg_match('/^[a-z][-a-z0-9]*[a-z0-9]$/', $short_name)) {
-                $page->trigError("Le nom raccourci n'est pas valide, il doit comporter au moins 2 caractères et n'être composé "
-                          . "que de chiffres, lettres et tirets");
-                Post::kill('valid');
-            } elseif ($short_name != Post::v('old_short_name')) {
-                $res = XDB::query("SELECT id FROM axletter WHERE short_name = {?}", $short_name);
-                if ($res->numRows() && $res->fetchOneCell() != $id) {
-                    $page->trigError("Le nom $short_name est déjà utilisé, merci d'en choisir un autre");
-                    $short_name = Post::v('old_short_name');
-                    if (empty($short_name)) {
-                        Post::kill('valid');
-                    }
-                }
-            }
-
-            switch (@Post::v('valid')) {
-              case 'Vérifier les emails':
-                // Same as 'preview', but performs a test of all provided emails
-                if ($subset) {
-                    require_once 'emails.inc.php';
-                    $ids = ids_from_mails($subset_to);
-                    $nb_error = 0;
-                    foreach ($subset_to as $e) {
-                        if (!array_key_exists($e, $ids)) {
-                            if ($nb_error == 0) {
-                                $page->trigError("Emails inconnus :");
-                            }
-                            $nb_error++;
-                            $page->trigError($e);
-                        }
-                    }
-                    if ($nb_error == 0) {
-                        if (count($subset_to) == 1) {
-                            $page->trigSuccess("L'email soumis a été reconnu avec succès.");
-                        } else {
-                            $page->trigSuccess("Les " . count($subset_to) . " emails soumis ont été reconnus avec succès.");
-                        }
-                    } else {
-                        $page->trigError("Total : $nb_error erreur" . ($nb_error > 1 ? "s" : "") . " sur " . count($subset_to) . " adresses mail soumises.");
-                    }
-                    $page->trigSuccess("Les adresses soumises correspondent à un total de " . count(array_unique($ids)) . " camarades.");
-                }
-                // No break here, since Vérifier is a subcase of Aperçu.
-              case 'Aperçu':
-                $this->load('axletter.inc.php');
-                $al = new AXLetter(array($id, $short_name, $subject, $title, $body, $signature,
-                                         $promo_min, $promo_max, $subset, $subset_rm, $echeance, 0, 'new'));
-                $al->toHtml($page, S::user());
-                break;
-
-              case 'Confirmer':
-                XDB::execute('INSERT INTO  axletter (id, short_name, subject, title, body, signature,
-                                                     promo_min, promo_max, echeance, subset, subset_rm)
-                                   VALUES  ({?}, {?}, {?}, {?}, {?}, {?}, {?}, {?}, {?}, {?}, {?})
-                  ON DUPLICATE KEY UPDATE  short_name = VALUES(short_name), subject = VALUES(subject), title = VALUES(title),
-                                           body = VALUES(body), signature = VALUES(signature), promo_min = VALUES(promo_min),
-                                           promo_max = VALUES(promo_max), echeance = VALUES(echeance), subset = VALUES(subset),
-                                           subset_rm = VALUES(subset_rm)',
-                             $id, $short_name, $subject, $title, $body, $signature, $promo_min, $promo_max, $echeance,
-                             $subset ? implode("\n", $subset_to) : null, $subset_rm);
-                if (!$saved) {
-                    global $globals;
-                    $mailer = new PlMailer();
-                    $mailer->setFrom("support@" . $globals->mail->domain);
-                    $mailer->setSubject("Un nouveau projet d'email de l'AX vient d'être proposé");
-                    $mailer->setTxtBody("Un nouvel email vient d'être rédigé en prévision d'un envoi prochain. Vous pouvez "
-                                      . "le modifier jusqu'à ce qu'il soit verrouillé pour l'envoi\n\n"
-                                      . "Le sujet de l'email : $subject\n"
-                                      . "L'échéance d'envoi est fixée à $echeance.\n"
-                                      . "L'email pourra néanmoins partir avant cette échéance si un administrateur de "
-                                      . "Polytechnique.org le valide.\n\n"
-                                      . "Pour modifier, valider ou annuler l'email :\n"
-                                      . "https://www.polytechnique.org/ax/edit\n"
-                                      . "-- \n"
-                                      . "Association Polytechnique.org\n");
-                    $users = User::getBulkUsersWithUIDs(XDB::fetchColumn('SELECT  uid
-                                                                            FROM  axletter_rights'));
-                    foreach ($users as $user) {
-                        $mailer->addTo($user);
-                    }
-                    $mailer->send();
-                }
-                $saved = true;
-                $echeance_date = null;
-                $echeance_time = null;
-                pl_redirect('ax');
-                break;
-            }
-        }
-        $page->assign('id', $id);
-        $page->assign('short_name', $short_name);
-        $page->assign('subject', $subject);
-        $page->assign('title', $title);
-        $page->assign('body', $body);
-        $page->assign('signature', $signature);
-        $page->assign('promo_min', $promo_min);
-        $page->assign('promo_max', $promo_max);
-        $page->assign('subset_to', implode("\n", $subset_to));
-        $page->assign('subset', $subset);
-        $page->assign('subset_rm', $subset_rm);
-        $page->assign('echeance', $echeance);
-        $page->assign('echeance_date', $echeance_date);
-        $page->assign('echeance_time', $echeance_time);
-        $page->assign('saved', $saved);
-        $page->assign('new', $new);
-        $page->assign('is_xorg', S::admin());
-
-        if (!$saved) {
-            $select = '';
-            for ($i = 0 ; $i < 24 ; $i++) {
-                $stamp = sprintf('%02d:00:00', $i);
-                if ($stamp == $echeance_time) {
-                    $sel = ' selected="selected"';
-                } else {
-                    $sel = '';
-                }
-                $select .= "<option value=\"$stamp\"$sel>{$i}h</option>\n";
-            }
-            $page->assign('echeance_time', $select);
-        }
-    }
-
-    function handler_cancel(&$page, $force = null)
-    {
-        $this->load('axletter.inc.php');
-        if (!AXLetter::hasPerms() || !S::has_xsrf_token()) {
-            return PL_FORBIDDEN;
-        }
-
-        $al = AXLetter::awaiting();
-        if (!$al) {
-            $page->kill("Aucune lettre en attente");
-            return;
-        }
-        if (!$al->invalid()) {
-            $page->kill("Une erreur est survenue lors de l'annulation de l'envoi");
-            return;
-        }
-
-        $page->killSuccess("L'envoi de l'annonce {$al->title()} est annulé.");
-    }
-
-    function handler_valid(&$page, $force = null)
-    {
-        $this->load('axletter.inc.php');
-        if (!AXLetter::hasPerms() || !S::has_xsrf_token()) {
-            return PL_FORBIDDEN;
-        }
-
-        $al = AXLetter::awaiting();
-        if (!$al) {
-            $page->kill("Aucune lettre en attente");
-            return;
-        }
-        if (!$al->valid()) {
-            $page->kill("Une erreur est survenue lors de la validation de l'envoi");
-            return;
-        }
-
-        $page->killSuccess("L'envoi de l'annonce aura lieu dans l'heure qui vient.");
-    }
-
-    function handler_show(&$page, $nid = 'last')
-    {
-        $this->load('axletter.inc.php');
-        $page->changeTpl('axletter/show.tpl');
-
-        try {
-            $nl = new AXLetter($nid);
-            $user =& S::user();
-            if (Get::has('text')) {
-                $nl->toText($page, $user);
-            } else {
-                $nl->toHtml($page, $user);
-            }
-            if (Post::has('send')) {
-                $nl->sendTo($user);
-            }
-        } catch (MailNotFound $e) {
-            return PL_NOT_FOUND;
-        }
-    }
-
-    function handler_admin(&$page, $action = null, $uid = null)
-    {
-        $this->load('axletter.inc.php');
-        if (Post::has('action')) {
-            $action = Post::v('action');
-            $uid    = Post::v('uid');
-        }
-        if ($uid) {
-            S::assert_xsrf_token();
-
-            $uids   = preg_split('/ *[,;\: ] */', $uid);
-            foreach ($uids as $uid) {
-                switch ($action) {
-                  case 'add':
-                    $res = AXLetter::grantPerms($uid);
-                    break;
-                  case 'del';
-                    $res = AXLetter::revokePerms($uid);
-                    break;
-                }
-                if (!$res) {
-                    $page->trigError("Personne ne correspond à l'identifiant '$uid'");
-                }
-            }
-        }
-
-        $page->changeTpl('axletter/admin.tpl');
-        $page->assign('admins', User::getBulkUsersWithUIDs(XDB::fetchColumn('SELECT  uid
-                                                                               FROM  axletter_rights')));
-
-        $importer = new CSVImporter('axletter_ins');
-        $importer->registerFunction('uid', 'email vers Id X.org', array($this, 'idFromMail'));
-        $importer->forceValue('hash', array($this, 'createHash'));
-        $importer->apply($page, "admin/axletter", array('uid', 'email', 'prenom', 'nom', 'promo', 'flag', 'hash'));
-    }
-
-    function idFromMail($line, $key, $relation = null)
-    {
-        static $field;
-        global $globals;
-        if (!isset($field)) {
-            $field = array('email', 'mail', 'login', 'bestalias', 'forlife', 'flag');
-            foreach ($field as $fld) {
-                if (isset($line[$fld])) {
-                    $field = $fld;
-                    break;
-                }
             }
         }
-        $uf = new UserFilter(new UFC_Email($line[$field]));
-        $id = $uf->getUIDs();
-        return count($id) == 1 ? $id[0] : 0;
-    }
-
-    function createHash($line, $key, $relation)
-    {
-        $hash = implode(time(), $line) . rand();
-        $hash = md5($hash);
-        return $hash;
+        return $this->handler_nl($page, 'out', $hash);
     }
 }
 
diff --git a/modules/axletter/axletter.inc.php b/modules/axletter/axletter.inc.php
deleted file mode 100644 (file)
index 1d1a52a..0000000
+++ /dev/null
@@ -1,245 +0,0 @@
-<?php
-/***************************************************************************
- *  Copyright (C) 2003-2011 Polytechnique.org                              *
- *  http://opensource.polytechnique.org/                                   *
- *                                                                         *
- *  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.,                                                      *
- *  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA                *
- ***************************************************************************/
-
-require_once("massmailer.inc.php");
-
-class AXLetter extends MassMailer
-{
-    public $_body;
-    public $_signature;
-    public $_promo_min;
-    public $_promo_max;
-    public $_subset;
-    public $_subset_to;
-    public $_subset_rm;
-    public $_echeance;
-    public $_date;
-    public $_bits;
-
-    function __construct($id)
-    {
-        parent::__construct('axletter/letter.mail.tpl', 'ax.css', 'ax/show', 'axletter', 'axletter_ins');
-        $this->_head = '<cher> <prenom>,';
-
-        if (!is_array($id)) {
-            if ($id == 'last') {
-                $res = XDB::query("SELECT  *
-                                     FROM  axletter
-                                    WHERE  FIND_IN_SET('sent', bits)
-                                 ORDER BY  id DESC");
-            } else {
-                $res = XDB::query("SELECT  *
-                                     FROM  axletter
-                                    WHERE  id = {?} OR short_name = {?}", $id, $id);
-            }
-            if (!$res->numRows()) {
-                throw new MailNotFound();
-            }
-            $id = $res->fetchOneRow();
-        }
-        list($this->_id, $this->_shortname, $this->_title_mail, $this->_title,
-             $this->_body, $this->_signature, $this->_promo_min, $this->_promo_max,
-             $this->_subset_to, $this->_subset_rm, $this->_echeance, $this->_date, $this->_bits) = $id;
-        if ($this->_date == '0000-00-00') {
-            $this->_date = 0;
-        }
-        $this->_subset_to = ($this->_subset_to ? explode("\n", $this->_subset_to) : null);
-        $this->_subset = (count($this->_subset_to) > 0);
-    }
-
-    protected function assignData(&$smarty)
-    {
-        $smarty->assign_by_ref('am', $this);
-    }
-
-    public function body($format)
-    {
-        return format_text($this->_body, $format);
-    }
-
-    public function signature($format)
-    {
-        return format_text($this->_signature, $format, 10);
-    }
-
-    public function valid()
-    {
-        return XDB::execute("UPDATE  axletter
-                                SET  echeance = NOW()
-                              WHERE  id = {?}", $this->_id);
-    }
-
-    public function invalid()
-    {
-        return XDB::execute("UPDATE  axletter
-                                SET  bits = 'invalid', date = CURDATE()
-                              WHERE  id = {?}", $this->_id);
-    }
-
-    protected function setSent()
-    {
-        XDB::execute("UPDATE  axletter
-                         SET  bits='sent', date=CURDATE()
-                       WHERE  id={?}", $this->_id);
-    }
-
-    static public function subscriptionState($uid = null)
-    {
-        $user = is_null($uid) ? S::v('uid') : $uid;
-        $res = XDB::query("SELECT  1
-                             FROM  axletter_ins
-                            WHERE  uid={?}", $user);
-        return $res->fetchOneCell();
-    }
-
-    static public function unsubscribe($uid = null, $hash = false)
-    {
-        $user = is_null($uid) ? S::v('uid') : $uid;
-        $field = !$hash ? 'uid' : 'hash';
-        if (is_null($uid) && $hash) {
-            return false;
-        }
-        $res = XDB::query("SELECT uid
-                             FROM axletter_ins
-                            WHERE $field={?}", $user);
-        if ($res->numRows() != 1) {
-            return false;
-        }
-        XDB::execute("DELETE FROM  axletter_ins
-                            WHERE  $field = {?}", $user);
-        return true;
-    }
-
-    static public function subscribe($uid = null)
-    {
-        $user = is_null($uid) ? S::v('uid') : $uid;
-        XDB::execute('INSERT IGNORE INTO  axletter_ins (uid, last)
-                                  VALUES  ({?}, 0)', $user);
-    }
-
-    static public function hasPerms()
-    {
-        if (S::admin()) {
-            return true;
-        }
-        $res = XDB::query("SELECT  COUNT(*)
-                             FROM  axletter_rights
-                            WHERE  uid = {?}", S::i('uid'));
-        return ($res->fetchOneCell() > 0);
-    }
-
-    static public function grantPerms($uid)
-    {
-        if (!is_numeric($uid)) {
-            $res = XDB::query("SELECT uid FROM aliases WHERE alias = {?}", $uid);
-            $uid = $res->fetchOneCell();
-        }
-        if (!$uid) {
-            return false;
-        }
-        return XDB::execute("INSERT IGNORE INTO axletter_rights SET uid = {?}", $uid);
-    }
-
-    static public function revokePerms($uid)
-    {
-        if (!is_numeric($uid)) {
-            $res = XDB::query("SELECT uid FROM aliases WHERE alias = {?}", $uid);
-            $uid = $res->fetchOneCell();
-        }
-        if (!$uid) {
-            return false;
-        }
-        return XDB::execute("DELETE FROM axletter_rights WHERE uid = {?}", $uid);
-    }
-
-    protected function subscriptionWhere()
-    {
-        if (!$this->_promo_min && !$this->_promo_max && !$this->_subset) {
-            return '1';
-        }
-        /* TODO: refines this filter on promotions by using userfilter. */
-        $where = array();
-        if ($this->_promo_min) {
-            $where[] = "((ni.uid = 0 AND ni.promo >= {$this->_promo_min}) OR (ni.uid != 0 AND pd.promo >= 'X{$this->_promo_min}'))";
-        }
-        if ($this->_promo_max) {
-            $where[] = "((ni.uid = 0 AND ni.promo <= {$this->_promo_max}) OR (ni.uid != 0 AND pd.promo <= 'X{$this->_promo_max}'))";
-        }
-        if ($this->_subset) {
-            require_once("emails.inc.php");
-            $ids = ids_from_mails($this->_subset_to);
-            $ids_list = implode(',', $ids);
-            if(count($ids) > 0) {
-                if ($this->_subset_rm) {
-                    $where[] = "ni.uid NOT IN ($ids_list)";
-                } else {
-                    $where[] = "ni.uid IN ($ids_list)";
-                }
-            } else {
-                // No valid email
-                $where[] = "0";
-            }
-        }
-        return implode(' AND ', $where);
-    }
-
-    static public function awaiting()
-    {
-        $res = XDB::query("SELECT  *
-                             FROM  axletter
-                            WHERE  FIND_IN_SET('new', bits)");
-        if ($res->numRows()) {
-            return new AXLetter($res->fetchOneRow());
-        }
-        return null;
-    }
-
-    static public function toSend()
-    {
-        $res = XDB::query("SELECT  *
-                             FROM  axletter
-                            WHERE  FIND_IN_SET('new', bits) AND echeance <= NOW() AND echeance != 0");
-        if ($res->numRows()) {
-            return new AXLetter($res->fetchOneRow());
-        }
-        return null;
-    }
-
-    static public function listSent()
-    {
-        $res = XDB::query("SELECT  IF(short_name IS NULL, id, short_name) as id, date, subject AS titre
-                             FROM  axletter
-                            WHERE  NOT FIND_IN_SET('new', bits) AND NOT FIND_IN_SET('invalid', bits)
-                         ORDER BY  date DESC");
-        return $res->fetchAllAssoc();
-    }
-
-    static public function listAll()
-    {
-        $res = XDB::query("SELECT  IF(short_name IS NULL, id, short_name) as id, date, subject AS titre
-                             FROM  axletter
-                         ORDER BY  date DESC");
-        return $res->fetchAllAssoc();
-    }
-}
-
-// vim:set et sw=4 sts=4 sws=4 enc=utf-8:
-?>
index 051b4ed..0b84a38 100644 (file)
@@ -24,48 +24,67 @@ class NewsletterModule extends PLModule
     function handlers()
     {
         return array(
-            'nl'                           => $this->make_hook('nl',            AUTH_COOKIE),
-            'nl/show'                      => $this->make_hook('nl_show',       AUTH_COOKIE),
-            'nl/submit'                    => $this->make_hook('nl_submit',     AUTH_MDP),
-            'admin/newsletter'             => $this->make_hook('admin_nl',      AUTH_MDP, 'admin'),
-            'admin/newsletter/categories'  => $this->make_hook('admin_nl_cat',  AUTH_MDP, 'admin'),
-            'admin/newsletter/edit'        => $this->make_hook('admin_nl_edit', AUTH_MDP, 'admin'),
+            'nl'                           => $this->make_hook('nl',              AUTH_COOKIE),
+            'nl/show'                      => $this->make_hook('nl_show',         AUTH_COOKIE),
+            'nl/submit'                    => $this->make_hook('nl_submit',       AUTH_MDP),
+            'admin/nls'                    => $this->make_hook('admin_nl_groups', AUTH_MDP, 'admin'),
+            'admin/newsletter'             => $this->make_hook('admin_nl',        AUTH_MDP, 'admin'),
+            'admin/newsletter/categories'  => $this->make_hook('admin_nl_cat',    AUTH_MDP, 'admin'),
+            'admin/newsletter/edit'        => $this->make_hook('admin_nl_edit',   AUTH_MDP, 'admin'),
+            'admin/newsletter/edit/delete' => $this->make_hook('delete',          AUTH_MDP, 'admin'),
+            // Automatic mailing is disabled for X.org NL
+//            'admin/newsletter/edit/cancel' => $this->make_hook('cancel', AUTH_MDP, 'admin'),
+//            'admin/newsletter/edit/valid'  => $this->make_hook('valid',  AUTH_MDP, 'admin'),
         );
     }
 
-    function handler_nl(&$page, $action = null)
+    /** This function should return the adequate NewsLetter object for the current module.
+     */
+    protected function getNl()
     {
         require_once 'newsletter.inc.php';
+        return NewsLetter::forGroup(NewsLetter::GROUP_XORG);
+    }
+
+    function handler_nl(&$page, $action = null, $hash = null)
+    {
+        $nl = $this->getNl();
+        if (!$nl) {
+            return PL_NOT_FOUND;
+        }
 
         $page->changeTpl('newsletter/index.tpl');
         $page->setTitle('Lettres mensuelles');
 
         switch ($action) {
-          case 'out': NewsLetter::unsubscribe(); break;
-          case 'in':  NewsLetter::subscribe(); break;
+          case 'out': $nl->unsubscribe($hash, $hash != null); break;
+          case 'in':  $nl->subscribe(); break;
           default: ;
         }
 
-        $page->assign('nls', NewsLetter::subscriptionState());
-        $page->assign('nl_list', NewsLetter::listSent());
+        $page->assign_by_ref('nl', $nl);
+        $page->assign('nls', $nl->subscriptionState());
+        $page->assign('nl_list', $nl->listSentIssues(true));
     }
 
     function handler_nl_show(&$page, $nid = 'last')
     {
         $page->changeTpl('newsletter/show.tpl');
-
-        require_once 'newsletter.inc.php';
+        $nl = $this->getNl();
+        if (!$nl) {
+            return PL_NOT_FOUND;
+        }
 
         try {
-            $nl = new NewsLetter($nid);
+            $issue = $nl->getIssue($nid);
             $user =& S::user();
             if (Get::has('text')) {
-                $nl->toText($page, $user);
+                $issue->toText($page, $user);
             } else {
-                $nl->toHtml($page, $user);
+                $issue->toHtml($page, $user);
             }
             if (Post::has('send')) {
-                $nl->sendTo($user);
+                $issue->sendTo($user);
             }
         } catch (MailNotFound $e) {
             return PL_NOT_FOUND;
@@ -76,7 +95,11 @@ class NewsletterModule extends PLModule
     {
         $page->changeTpl('newsletter/submit.tpl');
 
-        require_once 'newsletter.inc.php';
+        $nl = $this->getNl();
+        if (!$nl) {
+            return PL_NOT_FOUND;
+        }
+
         $wp = new PlWikiPage('Xorg.LettreMensuelle');
         $wp->buildCache();
 
@@ -92,63 +115,112 @@ class NewsletterModule extends PLModule
             $art->submit();
             $page->assign('submited', true);
         }
-        $page->addCssLink('nl.css');
+        $page->addCssLink($nl->cssFile());
     }
 
     function handler_admin_nl(&$page, $new = false) {
         $page->changeTpl('newsletter/admin.tpl');
         $page->setTitle('Administration - Newsletter : liste');
-        require_once("newsletter.inc.php");
+
+        $nl = $this->getNl();
+        if (!$nl) {
+            return PL_NOT_FOUND;
+        }
 
         if($new) {
-            NewsLetter::create();
-            pl_redirect("admin/newsletter");
+            $id = $nl->createPending();
+            pl_redirect($nl->adminPrefix() . '/edit/' . $id);
         }
 
-        $page->assign('nl_list', NewsLetter::listAll());
+        $page->assign_by_ref('nl', $nl);
+        $page->assign('nl_list', $nl->listAllIssues());
+    }
+
+    function handler_admin_nl_groups(&$page)
+    {
+        require_once 'newsletter.inc.php';
+
+        $page->changeTpl('newsletter/admin_all.tpl');
+        $page->setTitle('Administration - Newsletters : Liste des Newsletters');
+
+        $page->assign('nls', Newsletter::getAll());
     }
 
     function handler_admin_nl_edit(&$page, $nid = 'last', $aid = null, $action = 'edit') {
         $page->changeTpl('newsletter/edit.tpl');
-        $page->addCssLink('nl.css');
+        $page->addCssLink('nl.Polytechnique.org.css');
         $page->setTitle('Administration - Newsletter : Édition');
-        require_once 'newsletter.inc.php';
 
-        $nl = new NewsLetter($nid);
+        $nl = $this->getNl();
+        if (!$nl) {
+            return PL_NOT_FOUND;
+        }
 
-        if($action == 'delete') {
-            $nl->delArticle($aid);
-            pl_redirect("admin/newsletter/edit/$nid");
+        try {
+            $issue = $nl->getIssue($nid, false);
+        } catch (MailNotFound $e) {
+            return PL_NOT_FOUND;
         }
 
+        $ufb = $nl->getSubscribersUFB();
+        $ufb_keepenv = false;  // Will be set to True if there were invalid modification to the UFB.
+
+        // Convert NLIssue error messages to human-readable errors
+        $error_msgs = array(
+            NLIssue::ERROR_INVALID_SHORTNAME => "Le nom court est invalide ou vide.",
+            NLIssue::ERROR_INVALID_UFC => "Le filtre des destinataires est invalide.",
+            NLIssue::ERROR_SQL_SAVE => "Une erreur est survenue en tentant de sauvegarder la lettre, merci de réessayer.",
+        );
+
+        // Update the current issue
         if($aid == 'update') {
-            $nl->_title      = Post::v('title');
-            $nl->_title_mail = Post::v('title_mail');
-            $nl->_date       = Post::v('date');
-            $nl->_head       = Post::v('head');
-            $nl->_shortname  = strlen(Post::v('shortname')) ? Post::v('shortname') : null;
-            if (preg_match('/^[-a-z0-9]*$/i', $nl->_shortname) && !is_numeric($nl->_shortname)) {
-                $nl->save();
-            } else {
-                $page->trigError("Le nom de la NL n'est pas valide.");
-                pl_redirect('admin/newsletter/edit/' . $nl->_id);
+
+            // Save common fields
+            $issue->title      = Post::s('title');
+            $issue->title_mail = Post::s('title_mail');
+            $issue->head       = Post::s('head');
+            $issue->signature  = Post::s('signature');
+
+            if ($issue->isEditable()) {
+                // Date and shortname may only be modified for pending NLs, otherwise all links get broken.
+                $issue->date = Post::s('date');
+                $issue->shortname = strlen(Post::blank('shortname')) ? null : Post::s('shortname');
+                $issue->sufb->updateFromEnv($ufb->getEnv());
+
+                if ($nl->automaticMailingEnabled()) {
+                    $issue->send_before = preg_replace('/^(\d\d\d\d)(\d\d)(\d\d)$/', '\1-\2-\3', Post::v('send_before_date')) . ' ' . Post::i('send_before_time_Hour') . ':00:00';
+                }
+            }
+            $errors = $issue->save();
+            if (count($errors)) {
+                foreach ($errors as $error_code) {
+                    $page->trigError($error_msgs[$error_code]);
+                }
             }
         }
 
+        // Delete an article
+        if($action == 'delete') {
+            $issue->delArticle($aid);
+            pl_redirect($nl->adminPrefix() . "/edit/$nid");
+        }
+
+        // Save an article
         if(Post::v('save')) {
             $art  = new NLArticle(Post::v('title'), Post::v('body'), Post::v('append'),
                                   $aid, Post::v('cid'), Post::v('pos'));
-            $nl->saveArticle($art);
-            pl_redirect("admin/newsletter/edit/$nid");
+            $issue->saveArticle($art);
+            pl_redirect($nl->adminPrefix() . "/edit/$nid");
         }
 
+        // Edit an article
         if ($action == 'edit' && $aid != 'update') {
             $eaid = $aid;
             if (Post::has('title')) {
                 $art  = new NLArticle(Post::v('title'), Post::v('body'), Post::v('append'),
                                       $eaid, Post::v('cid'), Post::v('pos'));
             } else {
-                $art = ($eaid == 'new') ? new NLArticle() : $nl->getArt($eaid);
+                $art = ($eaid == 'new') ? new NLArticle() : $issue->getArt($eaid);
             }
             if ($art && !$art->check()) {
                 $page->trigError("Cet article est trop long.");
@@ -156,12 +228,13 @@ class NewsletterModule extends PLModule
             $page->assign('art', $art);
         }
 
+        // Check blacklisted IPs
         if ($aid == 'blacklist_check') {
             global $globals;
             $ips_to_check = array();
             $blacklist_host_resolution_count = 0;
 
-            foreach ($nl->_arts as $key => $articles) {
+            foreach ($issue->arts as $key => $articles) {
                 foreach ($articles as $article) {
                     $article_ips = $article->getLinkIps($blacklist_host_resolution_count);
                     if (!empty($article_ips)) {
@@ -179,14 +252,109 @@ class NewsletterModule extends PLModule
             }
         }
 
+        if ($issue->state == NLIssue::STATE_SENT) {
+            $page->trigWarning("Cette lettre a déjà été envoyée ; il est recommandé de limiter les modifications au maximum (orthographe, adresses web et mail).");
+        }
+
+        $ufb->setEnv($issue->sufb->getEnv());
         $page->assign_by_ref('nl', $nl);
+        $page->assign_by_ref('issue', $issue);
+    }
+
+    /** This handler will cancel the sending of the currently pending issue
+     * It is disabled for X.org mailings.
+     */
+    function handler_admin_nl_cancel(&$page, $nid, $force = null)
+    {
+        $nl = $this->getNl();
+        if (!$nl) {
+            return PL_NOT_FOUND;
+        }
+
+        if (!$nl->mayEdit() || !S::has_xsrf_token()) {
+            return PL_FORBIDDEN;
+        }
+
+        if (!$nid) {
+            $page->kill("La lettre n'a pas été spécifiée");
+        }
+
+        $issue = $nl->getIssue($nid);
+        if (!$issue) {
+            $page->kill("La lettre {$nid} n'existe pas.");
+        }
+        if (!$issue->cancelMailing()) {
+            $page->trigErrorRedirect("Une erreur est survenue lors de l'annulation de l'envoi.", $nl->adminPrefix());
+        }
+
+        $page->trigSuccessRedirect("L'envoi de l'annonce {$issue->title()} est annulé.", $nl->adminPrefix());
+    }
+
+    /** This handler will enable the sending of the currently pending issue
+     * It is disabled for X.org mailings.
+     */
+    function handler_admin_nl_valid(&$page, $nid, $force = null)
+    {
+        $nl = $this->getNl();
+        if (!$nl) {
+            return PL_NOT_FOUND;
+        }
+
+        if (!$nl->mayEdit() || !S::has_xsrf_token()) {
+            return PL_FORBIDDEN;
+        }
+
+        if (!$nid) {
+            $page->kill("La lettre n'a pas été spécifiée.");
+        }
+
+        $issue = $nl->getIssue($nid);
+        if (!$issue) {
+            $page->kill("La lettre {$nid} n'existe pas.");
+        }
+        if (!$issue->scheduleMailing()) {
+            $page->trigErrorRedirect("Une erreur est survenue lors de la validation de l'envoi.", $nl->adminPrefix());
+        }
+
+        $page->trigSuccessRedirect("L'envoi de la newsletter {$issue->title()} a été validé.", $nl->adminPrefix());
+    }
+
+    /** This handler will remove the given issue.
+     */
+    function handler_admin_nl_delete(&$page, $nid, $force = null)
+    {
+        $nl = $this->getNl();
+        if (!$nl) {
+            return PL_NOT_FOUND;
+        }
+
+        if (!$nl->mayEdit() || !S::has_xsrf_token()) {
+            return PL_FORBIDDEN;
+        }
+
+        if (!$nid) {
+            $page->kill("La lettre n'a pas été spécifiée.");
+        }
+
+        $issue = $nl->getIssue($nid);
+        if (!$issue) {
+            $page->kill("La lettre {$nid} n'existe pas");
+        }
+        if (!$issue->isEditable()) {
+            $page->trigErrorRedirect("La lette a été envoyée ou est en cours d'envoi, elle ne peut être supprimée.", $nl->adminPrefix());
+        }
+        if (!$issue->delete()) {
+            $page->trigErrorRedirect("Une erreur est survenue lors de la suppression de la lettre.", $nl->adminPrefix());
+        }
+
+        $page->trigSuccessRedirect("La lettre a bien été supprimée.", $nl->adminPrefix());
     }
 
     function handler_admin_nl_cat(&$page, $action = 'list', $id = null) {
         $page->setTitle('Administration - Newsletter : Catégories');
         $page->assign('title', 'Gestion des catégories de la newsletter');
         $table_editor = new PLTableEditor('admin/newsletter/categories','newsletter_cat','cid');
-        $table_editor->describe('titre','intitulé',true);
+        $table_editor->describe('title','intitulé',true);
         $table_editor->describe('pos','position',true);
         $table_editor->apply($page, $action, $id);
     }
diff --git a/modules/xnetnl.php b/modules/xnetnl.php
new file mode 100644 (file)
index 0000000..aa769be
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+/***************************************************************************
+ *  Copyright (C) 2003-2011 Polytechnique.org                              *
+ *  http://opensource.polytechnique.org/                                   *
+ *                                                                         *
+ *  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.,                                                      *
+ *  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA                *
+ ***************************************************************************/
+
+Platal::load('newsletter');
+
+class XnetNlModule extends NewsletterModule
+{
+    function handlers()
+    {
+        return array(
+            '%grp/nl'                   => $this->make_hook('nl',                    AUTH_MDP),
+            '%grp/nl/show'              => $this->make_hook('nl_show',               AUTH_MDP),
+            '%grp/admin/nl'             => $this->make_hook('admin_nl',              AUTH_MDP,    'groupadmin'),
+            '%grp/admin/nl/edit'        => $this->make_hook('admin_nl_edit',         AUTH_MDP,    'groupadmin'),
+            '%grp/admin/nl/edit/cancel' => $this->make_hook('admin_nl_cancel',       AUTH_MDP,    'groupadmin'),
+            '%grp/admin/nl/edit/valid'  => $this->make_hook('admin_nl_valid',        AUTH_MDP,    'groupadmin'),
+            '%grp/admin/nl/categories'  => $this->make_hook('admin_nl_cat',          AUTH_MDP,    'groupadmin'),
+        );
+    }
+
+    protected function getNl()
+    {
+       global $globals;
+       $group = $globals->asso('shortname');
+       return NewsLetter::forGroup($group);
+    }
+}
+
+// vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8:
+?>
index 33246be..3764853 100644 (file)
     </td>
   </tr>
   <tr class="pair">
-    <td class="titre">Newsletter</td>
+    <td class="titre">Newsletters</td>
     <td>
-      <a href="admin/newsletter">Liste</a>
+      <a href="admin/nls">Liste des NLs groupes</a>
       &nbsp;&nbsp;|&nbsp;&nbsp;
-      <a href="admin/newsletter/categories">Catégories</a>
+      <a href="admin/newsletter/">NL de X.org</a>
     </td>
   </tr>
   <tr class="impair">
diff --git a/templates/axletter/admin.tpl b/templates/axletter/admin.tpl
deleted file mode 100644 (file)
index 7f43373..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-{**************************************************************************}
-{*                                                                        *}
-{*  Copyright (C) 2003-2011 Polytechnique.org                             *}
-{*  http://opensource.polytechnique.org/                                  *}
-{*                                                                        *}
-{*  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.,                                                     *}
-{*  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA               *}
-{*                                                                        *}
-{**************************************************************************}
-
-<h1>Droits d'administration des lettres de l'AX</h1>
-
-<form action="admin/axletter" method="post">
-  {xsrf_token_field}
-  <table class="tinybicol">
-    <tr>
-      <th>Nom</th>
-      <th>Action</th>
-    </tr>
-    <tr class="pair">
-      <td colspan="2" class="center">
-        <input type="text" name="uid" value="" />
-        <input type="submit" name="action" value="add" />
-      </td>
-    </tr>
-    {foreach item=a from=$admins}
-    <tr class="{cycle values="impair, pair"}">
-      <td>{profile user=$a promo=true}</td>
-      <td class="right"><a href="admin/axletter/del/{$a->login()}?token={xsrf_token}">{icon name=cross title="Retirer"}</a></td>
-    </tr>
-    {/foreach}
-  </table>
-</form>
-
-<h1>Ajout d'utilisateurs</h1>
-
-{include core=csv-importer.tpl}
-
-{* vim:set et sw=2 sts=2 sws=2 enc=utf-8: *}
diff --git a/templates/axletter/edit.tpl b/templates/axletter/edit.tpl
deleted file mode 100644 (file)
index 1bb758b..0000000
+++ /dev/null
@@ -1,121 +0,0 @@
-{**************************************************************************}
-{*                                                                        *}
-{*  Copyright (C) 2003-2011 Polytechnique.org                             *}
-{*  http://opensource.polytechnique.org/                                  *}
-{*                                                                        *}
-{*  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.,                                                     *}
-{*  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA               *}
-{*                                                                        *}
-{**************************************************************************}
-
-<h1>Édition de message</h1>
-
-<form action="{$platal->pl_self()}" method="post">
-  {xsrf_token_field}
-  {if $am}
-  {include file="axletter/letter.mail.tpl"}
-
-  <p class="center">
-    <input type="hidden" name="id" value="{$id}" />
-    <input type="hidden" name="old_short_name" value="{$short_name}" />
-    <input type="hidden" name="saved" value="{$saved}" />
-    {if $echeance}
-    <input type="hidden" name="echeance" value="{$echeance}" />
-    {/if}
-    {if !$new}
-    <input type="submit" name="valid" value="Confirmer" />
-    {/if}
-  </p>
-  {/if}
-
-  <fieldset>
-    <legend>Sujet de l'email&nbsp;: <input type="text" name="subject" value="{$subject}" size="60"/></legend>
-    <p class="center">
-      <a href="wiki_help" class="popup3">
-        {icon name=information title="Syntaxe wiki"} Voir les marqueurs de mise en forme autorisés
-      </a><br />
-      <strong>Titre&nbsp;: </strong><input type="text" name="title" value="{$title}" size="60" /><br />
-      <textarea name="body" rows="30" cols="78">{$body}</textarea><br />
-      <strong>Signature&nbsp;: </strong><input type="text" name="signature" value="{$signature}" size="60" />
-    </p>
-  </fieldset>
-
-  <table class="bicol">
-    <tr>
-      <th colspan="2">Options du message</th>
-    </tr>
-    <tr>
-      <td class="titre">Nom raccourci</td>
-      <td>
-        <input type="text" name="short_name" value="{$short_name}" size="16" maxlength="16" />
-        <span class="smaller">(uniquement lettres, chiffres ou -)</span>
-      </td>
-    </tr>
-    {include file="include/field.promo.tpl" prefix=""}
-    <tr>
-      <td class="titre">Envoyer à une liste d'adresses</td>
-      <td>
-      <textarea name="subset_to" rows="7" cols="78">{$subset_to}</textarea><br />
-      <span class="smaller">Indiquez une liste d'adresses emails&nbsp;: la lettre sera envoyée uniquement aux personnes des promotions sélectionnées, dont l'adresse figure dans la liste, et qui souhaitent recevoir les emails de l'AX.</span>
-      </td>
-    </tr>
-    <tr>
-      <td class="titre">Sélection inversée</td>
-      <td>
-      <input type="checkbox" name="subset_rm" {if $subset_rm}checked{/if} /><span class="smaller">En cochant cette case, la liste sera envoyée à tous les inscrits de l'intervalle de promotions sélectionné, sauf ceux indiqués dans la liste ci-dessus.</span>
-      </td>
-    </tr>
-    {if !$saved}
-    <tr>
-      <td class="titre">Echéance d'envoi</td>
-      <td>
-        le {valid_date name="echeance_date" value=$echeance_date from=3 to=15}
-        vers <select name="echeance_time">{$echeance_time|smarty:nodefaults}</select>
-      </td>
-    </tr>
-    {else}
-    <tr>
-      <td colspan="2" class="center">
-        Envoi au plus tard le {$echeance|date_format:"%x vers %Hh"}<br />
-        {if $is_xorg}
-        [<a href="ax/edit/valid?token={xsrf_token}" onclick="return confirm('Es-tu sûr de vouloir valider l\'envoi de ce message&nbsp;?');">{*
-          *}{icon name=thumb_up} Valider l'envoi</a>]
-        {else}
-        [<a href="ax/edit/cancel?token={xsrf_token}" onclick="return confirm('Es-tu sûr de vouloir annuler l\'envoi de ce message&nbsp;?');">{*
-          *}{icon name=thumb_down} Annuler l'envoi</a>]
-        {/if}
-      </td>
-    </tr>
-    {/if}
-  </table>
-
-  <p class="center">
-    <input type="hidden" name="id" value="{$id}" />
-    <input type="hidden" name="old_short_name" value="{$short_name}" />
-    <input type="hidden" name="saved" value="{$saved}" />
-    {if $echeance}
-    <input type="hidden" name="echeance" value="{$echeance}" />
-    {/if}
-    <input type="submit" name="valid" value="Aperçu" />
-    {if $subset}
-    <input type="submit" name="valid" value="Vérifier les emails" />
-    {/if}
-    {if !$new}
-    <input type="submit" name="valid" value="Confirmer" />
-    {/if}
-  </p>
-</form>
-
-{* vim:set et sw=2 sts=2 sws=2 enc=utf-8: *}
diff --git a/templates/axletter/index.tpl b/templates/axletter/index.tpl
deleted file mode 100644 (file)
index 0260463..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-{**************************************************************************}
-{*                                                                        *}
-{*  Copyright (C) 2003-2011 Polytechnique.org                             *}
-{*  http://opensource.polytechnique.org/                                  *}
-{*                                                                        *}
-{*  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.,                                                     *}
-{*  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA               *}
-{*                                                                        *}
-{**************************************************************************}
-
-
-<h1>
-  Envoi exceptionnel de l'AX
-</h1>
-
-<h2>Ton statut</h2>
-
-{if $axs}
-<p>
-Tu es actuellement inscrit aux envois exceptionnels de l'AX (pour choisir le format HTML ou texte, rends toi sur la page <a href='prefs'>des préférences</a>).
-</p>
-<div class='center'>
-  [<a href='ax/out'>{icon name=delete} me désinscrire des envois exceptionnels</a>]
-</div>
-{else}
-<p>
-Tu n'es actuellement pas inscrit aux envois exceptionnels de l'AX.
-</p>
-<div class='center'>
-  [<a href='ax/in'>{icon name=add} m'inscrire</a>]
-</div>
-{/if}
-
-<h2>Les archives</h2>
-
-<table class="bicol" cellpadding="3" cellspacing="0" summary="liste des NL">
-  <tr>
-    <th>date</th>
-    <th>titre</th>
-  </tr>
-  {if $ax_rights && !$new}
-  <tr class="pair">
-    <td colspan="2" class="center">
-      <a href="ax/edit">{icon name=page_edit} Proposer un nouvel email</a>
-    </td>
-  </tr>
-  {elseif $ax_rights && $new}
-  <tr class="pair">
-    <td><a href="ax/edit">{icon name=page_edit} Éditer la demande</a></td>
-    <td>
-      {if $new->title()}
-      <a href="ax/show/{$new->id()}"><strong>{$new->title(true)}</strong></a>
-      {/if}
-    </td>
-  </tr>
-  {/if}
-  {foreach item=al from=$ax_list}
-  <tr class="{cycle values="impair,pair"}">
-    <td>{$al.date|date_format}</td>
-    <td>
-      <a href="ax/show/{$al.id}">{$al.titre}</a>
-    </td>
-  </tr>
-  {/foreach}
-</table>
-
-{if $ax_rights}
-<p>Il y a actuellement {$count} inscrits aux envois.</p>
-{/if}
-
-{* vim:set et sw=2 sts=2 sws=2 enc=utf-8: *}
diff --git a/templates/axletter/show.tpl b/templates/axletter/show.tpl
deleted file mode 100644 (file)
index bb159fb..0000000
+++ /dev/null
@@ -1,59 +0,0 @@
-{**************************************************************************}
-{*                                                                        *}
-{*  Copyright (C) 2003-2011 Polytechnique.org                             *}
-{*  http://opensource.polytechnique.org/                                  *}
-{*                                                                        *}
-{*  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.,                                                     *}
-{*  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA               *}
-{*                                                                        *}
-{**************************************************************************}
-
-<h1 style="clear: both">
-  {if $am->_date}
-  Lettre de l'AX du {$am->_date|date_format}
-  {else}
-  Lettre de l'AX en préparation
-  {/if}
-</h1>
-
-<p style="float: left">
-{if $smarty.get.text}
-[<a href='{$platal->pl_self()}'>version HTML</a>]
-{else}
-[<a href='{$platal->pl_self()}?text=1'>version Texte</a>]
-{/if}
-{if !$am->_date}
-[<a href='ax/edit'>éditer</a>]
-{/if}
-</p>
-
-{include file="include/massmailer-nav.tpl" mm=$am base=ax}
-
-<form method="post" action="{$platal->path}">
-  <div class='center' style="clear: both">
-    <input type='submit' value="me l'envoyer" name='send' />
-  </div>
-</form>
-
-<table class="bicol">
-  <tr><th>{$am->title(true)}</th></tr>
-  <tr>
-    <td>
-      {include file="axletter/letter.mail.tpl"}
-    </td>
-  </tr>
-</table>
-
-{* vim:set et sw=2 sts=2 sws=2 enc=utf-8: *}
diff --git a/templates/axletter/unsubscribe.tpl b/templates/axletter/unsubscribe.tpl
deleted file mode 100644 (file)
index 51e7042..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-{**************************************************************************}
-{*                                                                        *}
-{*  Copyright (C) 2003-2011 Polytechnique.org                             *}
-{*  http://opensource.polytechnique.org/                                  *}
-{*                                                                        *}
-{*  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.,                                                     *}
-{*  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA               *}
-{*                                                                        *}
-{**************************************************************************}
-
-<h1>Désinscription des envois de l'AX</h1>
-
-{if $success}
-<p>
-  Votre désinscription aux envois exceptionnels de l'AX a été réalisée avec
-  succès. Si vous désirez vous réinscrire, merci de contacter
-  <a href="mailto:info@amicale.polytechnique.org">l'AX</a>. Vous pouvez également
-  le faire en vous <a href="register">inscrivant à Polytechnique.org</a> (pour
-  les X uniquement).
-</p>
-{else}
-<p>
-  Votre inscription aux envois de l'AX n'a pu être résiliée. Merci de contacter
-  au plus vite <a href="mailto:info@amicale.polytechnique.org">l'AX</a> pour faire
-  part de ce problème.
-</p>
-{/if}
-
-{* vim:set et sw=2 sts=2 sws=2 enc=utf-8: *}
index f2b442e..35abde7 100644 (file)
 {*                                                                        *}
 {**************************************************************************}
 
-<input type="text" name="nl_title" size="50" maxlength="200" value="{$valid->art->_title}" />
+<input type="text" name="nl_title" size="50" maxlength="200" value="{$valid->art->title}" />
 <br />
-<textarea rows="10" cols="60" name="nl_body">{$valid->art->_body}</textarea>
+<textarea rows="10" cols="60" name="nl_body">{$valid->art->body}</textarea>
 <br />
-<textarea rows="3" cols="60" name="nl_append">{$valid->art->_append}</textarea>
+<textarea rows="3" cols="60" name="nl_append">{$valid->art->append}</textarea>
 
 
 {* vim:set et sw=2 sts=2 sws=2 enc=utf-8: *}
index b634f6e..3c5a462 100644 (file)
 
 <table style="float: right; text-align: center" id="letter_nav">
   <tr>
-    {if $mm->prev() neq null}
+    {if $issue->prev() neq null}
     <td rowspan="2" style="vertical-align: middle">
-      <a href="{$base}/show/{$mm->prev()}">
+      <a href="{$nl->prefix()}/show/{$issue->prev()}">
       {icon name=resultset_previous title="Lettre précédente"}Lettre précédente
       </a>
     </td>
     {/if}
     <td>
-      [<a href="{$base}">Liste des lettres</a>]
+      [<a href="{$nl->prefix()}">Liste des lettres</a>]
     </td>
-    {if $mm->next() neq null}
+    {if $issue->next() neq null}
     <td rowspan="2" style="vertical-align: middle">
-      <a href="{$base}/show/{$mm->next()}">
+      <a href="{$nl->prefix()}/show/{$issue->next()}">
       Lettre suivante{icon name=resultset_next title="Lettre suivante"}
       </a>
     </td>
     {/if}
   </tr>
-  {if $mm->last() neq null}
+  {if $issue->last() neq null}
   <tr>
     <td>
-      <a href="{$base}/show/{$mm->last()}">
+      <a href="{$nl->prefix()}/show/{$issue->last()}">
         <img src="images/up.png" alt="" title="Liste des lettres" />Dernière lettre<img src="images/up.png" alt="" title="Liste des lettres" />
       </a>
     </td>
index 522403e..e4d1782 100644 (file)
 
 
 <h1>
-  Lettre de Polytechnique.org
+  {$nl->name}
 </h1>
 
 <table class="bicol" cellpadding="3" cellspacing="0" summary="liste des NL">
   <tr>
-    <th>date</th>
-    <th>titre</th>
+    <th>Date</th>
+    <th>État</th>
+    <th>Titre</th>
   </tr>
   <tr>
-    <td colspan='2'><a href='admin/newsletter/new'>Créer une nouvelle lettre</a></td>
+    <td colspan='3'><a href='{$nl->adminPrefix()}/new'>Créer une nouvelle lettre</a></td>
   </tr>
-  {foreach item=nl from=$nl_list}
+  {foreach item=nli from=$nl_list}
   <tr class="{cycle values="pair,impair"}">
-    <td>{$nl.date|date_format}</td>
+    <td>{$nli->date|date_format}</td>
+    <td>{$nli->state}</td>
     <td>
-      <a href="admin/newsletter/edit/{$nl.id}">{$nl.titre|default:"[no title]"}</a>
+      <a href="{$nl->adminPrefix()}/edit/{$nli->id()}">{$nli->title()|default:"[Sans titre]"}</a>
     </td>
   </tr>
   {/foreach}
index 6912d5b..f470439 100644 (file)
 {**************************************************************************}
 
 <h1>
-  Lettre de Polytechnique.org de {$nl->_date|date_format:"%B %Y"}
+  {$nl->name} de {$issue->date|date_format:"%B %Y"}
 </h1>
 
 {if !$art}
 
 <p>
-[<a href="admin/newsletter">liste</a>]
-[<a href="nl/show/{$nl->id()}">visualiser</a>]
+[<a href="{$nl->adminPrefix()}">liste</a>]
+[<a href="{$nl->prefix()}/show/{$issue->id()}">visualiser</a>]
+
 </p>
 
-<form action='admin/newsletter/edit/{$nl->id(true)}/update' method='post'>
+<form action='{$nl->adminPrefix()}/edit/{$issue->id(true)}/update' method='post'>
   <table class="bicol" cellpadding="3" cellspacing="0">
     <tr>
       <th colspan='2'>
     </tr>
     <tr>
       <td class='titre'>
+        État
+      </td>
+      <td>
+{if $issue->isPending()}
+  En attente d'envoi
+  {if $nl->automaticMailingEnabled()}
+    [<a href="{$nl->adminPrefix()}/edit/cancel/{$issue->id()}?token={xsrf_token}" onclick="return confirm('Es-tu sûr de vouloir annuler l\'envoi de ce message&nbsp;?');">{*
+    *}{icon name=delete} Annuler l'envoi</a>]
+  {/if}
+{elseif $issue->isEditable()}
+  En cours d'édition
+
+  {if $nl->automaticMailingEnabled()}
+    [<a href="{$nl->adminPrefix()}/edit/valid/{$issue->id()}?token={xsrf_token}" onclick="return confirm('Es-tu sûr de vouloir déclencher l\'envoi de ce message&nbsp;? Tu ne pourras plus le modifier après cela.');">{*
+    *}{icon name=tick} Valider l'envoi</a>]
+  {/if}
+
+  [<a href="{$nl->adminPrefix()}/edit/delete/{$issue->id()}?token={xsrf_token}" onclick="return confirm('Es-tu sûr de vouloir supprimer cette lettre&nbsp;? Toutes les données en seront perdues.');">{*
+  *}{icon name=cross} Supprimer</a>]
+{else}
+  Envoyée
+{/if}
+
+    <tr>
+      <td class='titre'>
         ID
       </td>
       <td>
-        {$nl->_id}
+        {$issue->id}
       </td>
     </tr>
     <tr>
         Nom
       </td>
       <td>
-        <input type='text' size='16' name='shortname' value="{$nl->_shortname}" />
-        <span class="smaller">(Ex&nbsp;: 2006-06 pour la NL de juin 2006)</span>
+        {if $issue->isEditable()}
+          <input type='text' size='16' name='shortname' value="{$issue->shortname}" />
+          <span class="smaller">(Ex&nbsp;: 2006-06 pour la NL de juin 2006)</span>
+        {else}
+          {$issue->shortname}
+        {/if}
       </td>
     </tr>
     <tr>
@@ -60,7 +90,7 @@
         Titre de l'email
       </td>
       <td>
-        <input type='text' size='60' name='title_mail' value="{$nl->title(true)}" />
+        <input type='text' size='60' name='title_mail' value="{$issue->title(true)}" />
       </td>
     </tr>
     <tr>
         Titre
       </td>
       <td>
-        <input type='text' size='60' name='title' value="{$nl->title()}" />
+        <input type='text' size='60' name='title' value="{$issue->title()}" />
       </td>
     </tr>
     <tr>
       <td class='titre'>
-        Date d'envoi
+        Date
+      </td>
+      <td>
+      {if $issue->isEditable()}
+        {valid_date name="date" value=$issue->date from=0 to=60}
+      {else}
+        {$issue->date}
+      {/if}
+      </td>
+    </tr>
+    <tr>
+      <td class='titre'>
+        Intro de la lettre<br />(ou contenu pour les lettres exceptionnelles)
       </td>
       <td>
-        <input type='text' size='60' name='date' value="{$nl->_date}" />
+        <textarea name='head' cols='60' rows='20'>{$issue->head()}</textarea>
       </td>
     </tr>
     <tr>
       <td class='titre'>
-        Intro de la lettre
+        Signature de la lettre
       </td>
       <td>
-        <textarea name='head' cols='60' rows='6'>{$nl->head()}</textarea>
+        <input type='text' size='60' name='signature' value="{$issue->signature}"</input>
       </td>
     </tr>
+    {if $nl->automaticMailingEnabled() && ($issue->isEditable() || $issue->isPending())}
+    <tr>
+      <td class='titre'>
+        Date d'envoi
+      </td>
+      <td>
+        {if $issue->isEditable()}
+        Le {valid_date name="send_before_date" value=$issue->getSendBeforeDate() from=3 to=15} vers {html_select_time prefix="send_before_time_" time=$issue->getSendBeforeTime() display_hours=true display_minutes=false display_seconds=false display_meridian=false use_24_hours=true} heures
+        {else}
+        Le {$issue->send_before|date_format:"%X vers %Hh"}
+        {/if}
+      </td>
+    </tr>
+    {/if}
+    {if $issue->isEditable()}
+      {if $nl->criteria->hasFlag('promo')}
+      <tr>
+        <td class='titre'>
+          Promotions
+        </td>
+        <td>
+          <script type="text/javascript">/*<![CDATA[*/
+            {literal}
+            function updatepromofields(egal1) {
+              var f = egal1.form;
+              f.egal2.disabled = f.promo2.disabled = egal1.value == '=';
+              f.egal2.readOnly = true;
+              if (f.egal1.value == '>=') {
+                f.egal2.value = '<=';
+              } else {
+                f.egal2.value = '>=';
+              }
+            }
+            $(function() { updatepromofields($('select[name=egal1]')[0]); });
+            {/literal}
+          /*]]>*/</script>
+          <select name="egal1" onchange="updatepromofields(this)" style="text-align:center">
+            <option value="=" {if $smarty.request.egal1 eq "="}selected="selected"{/if}>&nbsp;=&nbsp;</option>
+            <option value="&gt;=" {if $smarty.request.egal1 eq "&gt;="}selected="selected"{/if}>&nbsp;&gt;=&nbsp;</option>
+            <option value="&lt;=" {if $smarty.request.egal1 eq "&lt;="}selected="selected"{/if}>&nbsp;&lt;=&nbsp;</option>
+          </select>
+          <input type="text" name="promo1" size="4" maxlength="4" value="{$smarty.request.promo1}" />
+          &nbsp;et&nbsp;
+          <input type="text" name="egal2" size="1" style="text-align:center" value="{if t($smarty.request.egal2) eq '&lt;'}&lt;{else}&gt;{/if}" readonly="readonly" />
+          <input type="text" name="promo2" size="4" maxlength="4" value="{$smarty.request.promo2}" />
+        </td>
+      </tr>
+      {/if}
+      {if $nl->criteria->hasFlag('axid')}
+      <tr>
+        <td>Matricule AX</td>
+        <td>
+          <textarea name="axid" rows="10" cols="12">{$smarty.request.axid}</textarea>
+          <br />
+          <i>Entrer une liste de matricules AX (un par ligne)</i><br />
+          <input type="checkbox" name="axid_reversed" id="axid_reversed" {if $smarty.request.axid_reversed}checked="checked"{/if} value="1" />
+        </td>
+      </tr>
+      {/if}
+    {/if}
     <tr class='center'>
       <td colspan='2'>
-        <input type='submit' value='sauver' />
+        <input type='submit' value='Sauver' />
       </td>
     </tr>
   </table>
       Créer un nouvel article&hellip;
     </td>
     <td style='vertical-align:middle; border-left: 1px gray solid' class="center">
-      <a href="admin/newsletter/edit/{$nl->_id}/new#edit">{icon name=add title="créer"}</a>
+      <a href="{$nl->adminPrefix()}/edit/{$issue->id}/new#edit">{icon name=add title="créer"}</a>
     </td>
   </tr>
-  {foreach from=$nl->_arts item=arts key=cat}
+  {foreach from=$issue->arts item=arts key=cat}
   <tr>
     <th>
-      {$nl->_cats[$cat]|default:"[no cat]"}
+      {$issue->category($cat)|default:"[no category]"}
     </th>
     <th></th>
   </tr>
       <pre>{$art->toText('%hash%','%login%')}</pre>
     </td>
     <td style="vertical-align: middle; border-left: 1px gray solid; text-align: center">
-      <small><strong>Pos:&nbsp;{$art->_pos}</strong></small><br />
-      <a href="admin/newsletter/edit/{$nl->_id}/{$art->_aid}/edit#edit">
+      <small><strong>Pos:&nbsp;{$art->pos}</strong></small><br />
+      <a href="{$nl->adminPrefix()}/edit/{$issue->id}/{$art->aid}/edit#edit">
         {icon name="page_edit" title="Editer"}
       </a>
       <br /><br /><br />
-      <a href="admin/newsletter/edit/{$nl->_id}/{$art->_aid}/delete"
+      <a href="{$nl->adminPrefix()}/edit/{$issue->id}/{$art->aid}/delete"
          onclick="return confirm('Es-tu sûr de vouloir supprimer cet article&nbsp;?')">
         {icon name="delete" title="Supprimer"}
       </a>
 
 <br />
 
-<form action="admin/newsletter/edit/{$nl->id(true)}/blacklist_check" method="post">
+<form action="{$nl->adminPrefix()}/edit/{$issue->id(true)}/blacklist_check" method="post">
   <table class="bicol" cellpadding="3" cellspacing="0">
     <tr>
       <th colspan="2">
 {else}
 
 <p>
-[<a href="admin/newsletter/edit/{$nl->_id}">retour</a>]
+[<a href="{$nl->adminPrefix()}/edit/{$issue->id}">retour</a>]
 </p>
 
 <table class='bicol'>
 
 <br />
 
-<form action="admin/newsletter/edit/{$nl->_id}/{$art->_aid}/edit#edit" method="post">
+<form action="{$nl->adminPrefix()}/edit/{$issue->id}/{$art->aid}/edit#edit" method="post">
   <table class='bicol'>
     <tr>
       <th colspan='2'>
       <td>
         <select name='cid'>
           <option value='0'>-- none --</option>
-          {foreach from=$nl->_cats item=text key=cid}
-          <option value='{$cid}' {if $art->_cid eq $cid}selected="selected"{/if}>{$text}</option>
+          {foreach from=$nl->cats item=text key=cid}
+          <option value='{$cid}' {if $art->cid eq $cid}selected="selected"{/if}>{$text}</option>
           {/foreach}
         </select>
       </td>
     <tr class="impair">
       <td class='titre'>Position</td>
       <td>
-        <input type='text' value='{$art->_pos}' name='pos' />
+        <input type='text' value='{$art->pos}' name='pos' />
       </td>
     </tr>
     <tr class="pair">
index bda1f5f..4fd362c 100644 (file)
 
 
 <h1>
-  Lettre de Polytechnique.org
+  {$nl->name}
+{if $nl->mayEdit() && $nl->adminLinksEnabled()}
+  [<a href="{$nl->adminPrefix()}">Administrer</a>]
+{/if}
 </h1>
 
-
+{if $nl->maySubmit()}
 <p class="center">
-  <a href="nl/submit">{icon name=page_edit value="Proposer un article"} Proposer un article pour la lettre mensuelle</a>
+  <a href="{$nl->prefix()}/submit">{icon name=page_edit value="Proposer un article"} Proposer un article pour la {$nl->name}</a>
 </p>
+{/if}
 
 <h2>Ton statut</h2>
 
-{if $nls}
+{if $nl->subscriptionState()}
 <p>
-Tu es actuellement inscrit à la lettre mensuelle de Polytechnique.org (pour choisir le format HTML ou texte, rends toi sur la page <a href='prefs'>des préférences</a>).
+Tu es actuellement inscrit à la {$nl->name} (pour choisir le format HTML ou texte, rends toi sur la page <a href='prefs'>des préférences</a>).
 </p>
 <div class='center'>
-  [<a href='nl/out'>{icon name=delete} me désinscrire de la lettre mensuelle</a>]
+  [<a href='{$nl->prefix()}/out'>{icon name=delete} me désinscrire de la {$nl->name}</a>]
 </div>
 {else}
 <p>
-Tu n'es actuellement pas inscrit à la lettre mensuelle de Polytechnique.org.
+Tu n'es actuellement pas inscrit à la {$nl->name}.
 </p>
 <div class='center'>
-  [<a href='nl/in'>{icon name=add} m'inscrire à la lettre mensuelle</a>]
+  [<a href='{$nl->prefix()}/in'>{icon name=add} m'inscrire à la {$nl->name}</a>]
 </div>
 {/if}
 
@@ -55,15 +59,19 @@ Tu n'es actuellement pas inscrit à la lettre mensuelle de Polytechnique.org.
     <th>date</th>
     <th>titre</th>
   </tr>
-  {foreach item=nl from=$nl_list}
+  {foreach item=nli from=$nl_list}
   <tr class="{cycle values="impair,pair"}">
-    <td>{$nl.date|date_format}</td>
+    <td>{$nli.date|date_format}</td>
     <td>
-      <a href="nl/show/{$nl.id}">{$nl.titre}</a>
+      <a href="{$nl->prefix()}/show/{$nli.id}">{$nli.title|default:"[Sans titre]"}</a>
     </td>
   </tr>
   {/foreach}
 </table>
 
+{if $nl->mayEdit()}
+<p>Il y a actuellement {$nl->subscriberCount()} inscrits aux envois.</p>
+{/if}
+
 
 {* vim:set et sw=2 sts=2 sws=2 enc=utf-8: *}
similarity index 89%
rename from templates/axletter/letter.mail.tpl
rename to templates/newsletter/nl.AX.mail.tpl
index ff64695..5a8b0b8 100644 (file)
@@ -23,7 +23,7 @@
 {config_load file="mails.conf" section="mails_ax"}
 {if $mail_part eq 'head'}
 {from full=#from#}
-{subject text=$am->title(true)}
+{subject text=$issue->title(true)}
 {if isset(#replyto#)}{add_header name='Reply-To' value=#replyto#}{/if}
 {if isset(#retpath#)}{add_header name='Return-Path' value=#retpath#}{/if}
 {elseif $mail_part eq 'text'}
 <pre style="width : 72ex; margin: auto">
 {/if}
 ====================================================================
-{$am->title()}
+{$issue->title()}
 ====================================================================
 
-{$am->head($user, 'text')}
+{$issue->head($user, 'text')}
 
-{$am->body('text')}
-
-{$am->signature('text')}
+{$issue->signature('text')}
 
 --------------------------------------------------------------------
 Cette lettre est envoyée par l'AX grâce aux outils de Polytechnique.org.
@@ -66,7 +64,7 @@ ne plus recevoir : &lt;https://www.polytechnique.org/ax/out{if $hash}/{$hash}{/i
       body      { background-color: #ddd; color: #000; }
       {/literal}
     <!--
-      {$am->css()}
+      {$issue->css()}
     -->
     </style>
   </head>
@@ -74,10 +72,9 @@ ne plus recevoir : &lt;https://www.polytechnique.org/ax/out{if $hash}/{$hash}{/i
     <div class="ax_background">
 {/if}
     <div class='ax_mail'>
-      <div class="title">{$am->title()}</div>
-      <div class="intro">{$am->head($user, 'html')|smarty:nodefaults}</div>
-      <div class="body">{$am->body('html')|smarty:nodefaults}</div>
-      <div class="signature">{$am->signature('html')|smarty:nodefaults}</div>
+      <div class="title">{$issue->title()}</div>
+      <div class="intro">{$issue->head($user, 'html')|smarty:nodefaults}</div>
+      <div class="signature">{$issue->signature('html')|smarty:nodefaults}</div>
       <div class="foot1">
         Cette lettre est envoyée par l'AX grâce aux outils de Polytechnique.org.
       </div>
similarity index 85%
rename from templates/newsletter/nl.mail.tpl
rename to templates/newsletter/nl.Polytechnique.org.mail.tpl
index 1d1ea52..43ddf9f 100644 (file)
@@ -23,7 +23,7 @@
 {config_load file="mails.conf" section="newsletter"}
 {if $mail_part eq 'head'}
 {from full=#from#}
-{subject text=$nl->title(true)}
+{subject text=$issue->title(true)}
 {if isset(#replyto#)}{add_header name='Reply-To' value=#replyto#}{/if}
 {if isset(#retpath#)}{add_header name='Return-Path' value=#retpath#}{/if}
 {elseif $mail_part eq 'text'}
 <pre style="width : 72ex; margin: auto">
 {/if}
 ====================================================================
-{$nl->title()}
+{$issue->title()}
 ====================================================================
 
-{$nl->head($user, 'text')}
+{$issue->head($user, 'text')}
 
 
-{foreach from=$nl->_arts key=cid item=arts name=cats}
-{$smarty.foreach.cats.iteration} *{$nl->_cats[$cid]}*
+{foreach from=$issue->arts key=cid item=arts name=cats}
+{$smarty.foreach.cats.iteration} *{$issue->category($cid)}*
 {foreach from=$arts item=art}
 - {$art->title()}
 {/foreach}
 
 {/foreach}
 
-{foreach from=$nl->_arts key=cid item=arts}
+{foreach from=$issue->arts key=cid item=arts}
 --------------------------------------------------------------------
-*{$nl->_cats[$cid]}*
+*{$issue->category($cid)}*
 --------------------------------------------------------------------
 
 {foreach from=$arts item=art}
@@ -84,7 +84,7 @@ ne plus recevoir : &lt;https://www.polytechnique.org/nl/out&gt;
       body      { background-color: #ddd; color: #000; }
       {/literal}
     <!--
-      {$nl->css()}
+      {$issue->css()}
     -->
     </style>
   </head>
@@ -92,21 +92,21 @@ ne plus recevoir : &lt;https://www.polytechnique.org/nl/out&gt;
     <div class='nl_background'>
 {/if}
     <div class='nl'>
-      <div class="title"><a href="{$globals->baseurl}">{$nl->title()}</a></div>
-      <div class="intro">{$nl->head($user, 'html')|smarty:nodefaults}</div>
+      <div class="title"><a href="{$globals->baseurl}">{$issue->title()}</a></div>
+      <div class="intro">{$issue->head($user, 'html')|smarty:nodefaults}</div>
       <a id="top_lnk"></a>
-      {foreach from=$nl->_arts key=cid item=arts name=cats}
+      {foreach from=$issue->arts key=cid item=arts name=cats}
       <div class="lnk">
-        <a href="{$prefix}#cat{$cid}"><strong>{$smarty.foreach.cats.iteration}. {$nl->_cats[$cid]}</strong></a><br />
+        <a href="{$prefix}#cat{$cid}"><strong>{$smarty.foreach.cats.iteration}. {$issue->category($cid)}</strong></a><br />
         {foreach from=$arts item=art}
-        <a href="{$prefix}#art{$art->_aid}">&nbsp;&nbsp;- {$art->title()}</a><br />
+        <a href="{$prefix}#art{$art->aid}">&nbsp;&nbsp;- {$art->title()}</a><br />
         {/foreach}
       </div>
       {/foreach}
 
-      {foreach from=$nl->_arts key=cid item=arts name=cats}
+      {foreach from=$issue->arts key=cid item=arts name=cats}
       <h1 class="xorg_nl"><a id="cat{$cid}"></a>
-        {$nl->_cats[$cid]}
+        {$issue->category($cid)}
       </h1>
       {foreach from=$arts item=art}
         {$art->toHtml($hash, $user->login())|smarty:nodefaults}
index b861e52..39f83a6 100644 (file)
 {**************************************************************************}
 
 <h1 style="clear: both">
-  Lettre de Polytechnique.org du {$nl->_date|date_format}
+  {$issue->nl->name} du {$issue->date|date_format}
 </h1>
 
 <p style="float: left">
   {if $smarty.get.text}
-  [<a href='nl/show/{$nl->id()}'>version HTML</a>]
+  [<a href='nl/show/{$issue->id()}'>version HTML</a>]
   {else}
-  [<a href='nl/show/{$nl->id()}?text=1'>version Texte</a>]
+  [<a href='nl/show/{$issue->id()}?text=1'>version Texte</a>]
   {/if}
-  {if hasPerm('admin')}
-  [<a href='admin/newsletter/edit/{$nl->id()}'>Éditer</a>]
+  {if $nl->mayEdit()}
+  [<a href='{$nl->adminPrefix()}/edit/{$issue->id()}'>Éditer</a>]
   {/if}
 </p>
 
-{include file="include/massmailer-nav.tpl" mm=$nl base=nl}
+{include file="include/massmailer-nav.tpl" issue=$issue nl=$nl}
 
 <form method="post" action="{$platal->path}">
   <div class='center' style="clear: both">
 </form>
 
 <table class="bicol">
-  <tr><th>{$nl->title(true)}</th></tr>
+  <tr><th>{$issue->title(true)}</th></tr>
   <tr>
     <td>
-      {include file="newsletter/nl.mail.tpl" escape=true}
+      {include file=$nl->tplFile() escape=true}
     </td>
   </tr>
 </table>
diff --git a/upgrade/1.1.0/20_newsletter.sql b/upgrade/1.1.0/20_newsletter.sql
new file mode 100644 (file)
index 0000000..24ced53
--- /dev/null
@@ -0,0 +1,171 @@
+DROP TABLE IF EXISTS newsletter_issues;
+DROP TABLE IF EXISTS newsletters;
+
+-----------------
+-- newsletters --
+-----------------
+
+CREATE TABLE newsletters (
+  id int(11) unsigned NOT NULL auto_increment,
+  group_id smallint(5) UNSIGNED NOT NULL,
+  name varchar(255) NOT NULL,
+  custom_css BOOL NOT NULL DEFAULT FALSE,
+  criteria SET('axid', 'promo', 'geo') DEFAUL NULL,
+  PRIMARY KEY (id),
+  UNIQUE KEY  (group_id),
+  FOREIGN KEY (group_id) REFERENCES groups (id)
+                         ON UPDATE CASCADE
+                         ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Lists all newsletters';
+
+-- Filling it with default values for X.org / AX / Ecole
+INSERT INTO newsletters (group_id, name, custom_css)
+      ( SELECT  groups.id, CONCAT("Lettre de ", groups.nom), TRUE
+          FROM  groups
+         WHERE  groups.diminutif IN ('Polytechnique.org', 'Ecole', 'AX')
+      );
+
+-- Set variables for simpler queries later on
+SET @idnl_xorg = (SELECT nls.id FROM newsletters AS nls LEFT JOIN groups AS g ON (g.id = nls.group_id) WHERE g.diminutif = 'Polytechnique.org');
+SET @idnl_ax = (SELECT nls.id FROM newsletters AS nls LEFT JOIN groups AS g ON (g.id = nls.group_id) WHERE g.diminutif = 'AX');
+SET @idnl_ecole = (SELECT nls.id FROM newsletters AS nls LEFT JOIN groups AS g ON (g.id = nls.group_id) WHERE g.diminutif = 'Ecole');
+
+UPDATE newsletters SET name = "Lettre de l'AX", criteria = 'promo,axid' WHERE id = @idnl_ax;
+UPDATE newsletters SET name = "Lettre mensuelle de Polytechnique.org", criteria = 'promo' WHERE id = @idnl_xorg;
+UPDATE newsletters SET name = "DiXit, lettre de l'École polytechnique", criteria = 'promo' WHERE id = @idnl_ecole;
+
+-----------------------
+-- newsletter_issues --
+-----------------------
+
+CREATE TABLE newsletter_issues (
+  nlid int(11) unsigned NOT NULL,
+  id int(11) unsigned NOT NULL auto_increment,
+  date date NOT NULL default '0000-00-00',
+  send_before date default NULL,
+  state enum('sent','new','pending') NOT NULL default 'new',
+  sufb_json text default NULL,
+  title varchar(255) NOT NULL default '',
+  head mediumtext NOT NULL,
+  signature mediumtext NOT NULL,
+  short_name varchar(16) default NULL,
+  mail_title varchar(255) NOT NULL default '',
+  PRIMARY KEY (id),
+  UNIQUE KEY  (nlid, short_name),
+  FOREIGN KEY (nlid) REFERENCES newsletters (id)
+                     ON UPDATE CASCADE
+                     ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Lists issues of all newsletters';
+
+-- Fill with all X.org nls
+INSERT INTO newsletter_issues (nlid, date, title, state, head, short_name, mail_title)
+        (
+          SELECT  @idnl_xorg, n.date, n.titre, n.bits, n.head, n.short_name, n.titre_mail
+            FROM  newsletter AS n
+        );
+
+-- Fill with all AX nls
+INSERT INTO newsletter_issues (nlid, date, title, state, head, signature, short_name, mail_title)
+        (
+          SELECT  @idnl_ax, a.date, a.title, a.bits, CONCAT("<cher> <prenom>,\n\n", a.body), a.signature, a.short_name, a.subject
+            FROM  axletter AS a
+           WHERE  bits != 'invalid'
+        );
+
+--------------------
+-- newsletter_cat --
+--------------------
+
+-- Fix newsletter_cat: add nlid, add FK, rename title
+ALTER TABLE newsletter_cat ADD COLUMN nlid INT(11) UNSIGNED NOT NULL AFTER cid;
+
+UPDATE newsletter_cat SET nlid = @idnl_xorg;
+
+ALTER TABLE newsletter_cat ADD FOREIGN KEY (nlid) REFERENCES newsletters (id)
+                                                  ON UPDATE CASCADE
+                                                  ON DELETE CASCADE;
+ALTER TABLE newsletter_cat CHANGE titre title varchar(128) NOT NULL DEFAULT '';
+
+-- Final state:
+--
+-- CREATE TABLE `newsletter_cat` (
+--   `cid` tinyint(3) unsigned NOT NULL auto_increment,
+--   `nlid` int(11) unsigned NOT NULL,
+--   `pos` tinyint(3) unsigned NOT NULL default '0',
+--   `title` varchar(128) NOT NULL default '',
+--   PRIMARY KEY  (`cid`),
+--   KEY `pos` (`pos`),
+--   KEY `nlid` (`nlid`),
+--   CONSTRAINT `newsletter_cat_ibfk_1` FOREIGN KEY (`nlid`) REFERENCES `newsletters` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
+-- ) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8
+
+
+--------------------
+-- newsletter_art --
+--------------------
+
+-- Fix newsletter_cat: add nlid, add FK, rename title
+ALTER TABLE newsletter_art DROP FOREIGN KEY newsletter_art_ibfk_1;
+   UPDATE  newsletter_art AS na
+LEFT JOIN  newsletter AS n ON na.id = n.id
+LEFT JOIN  newsletter_issues AS ni ON (ni.nlid = @idnl_xorg AND ni.short_name = n.short_name)
+      SET  na.id = ni.id;
+
+ALTER TABLE newsletter_art ADD FOREIGN KEY (id) REFERENCES newsletter_issues (id)
+                                                ON UPDATE CASCADE
+                                                ON DELETE CASCADE
+
+--------------------
+-- newsletter_ins --
+--------------------
+
+-- Fix newsletter_ins: add nlid column, fix FK
+ALTER TABLE newsletter_ins ADD COLUMN nlid INT(11) UNSIGNED NOT NULL AFTER uid;
+
+UPDATE  newsletter_ins SET nlid = @idnl_xorg;
+
+-- We have to drop all FKs in order to update 'last' indexes.
+ALTER TABLE newsletter_ins DROP FOREIGN KEY newsletter_ins_ibfk_1;
+ALTER TABLE newsletter_ins DROP FOREIGN KEY newsletter_ins_ibfk_2;
+ALTER TABLE newsletter_ins DROP PRIMARY KEY;
+
+   UPDATE  newsletter_ins AS ni
+LEFT JOIN  newsletter AS n ON (ni.last = n.id)
+LEFT JOIN  newsletter_issues AS ns ON (n.short_name = ns.short_name)
+      SET  ni.last = ns.id;
+
+ALTER TABLE newsletter_ins ADD PRIMARY KEY (uid, nlid);
+ALTER TABLE newsletter_ins ADD FOREIGN KEY (uid) REFERENCES accounts (uid)
+                                                  ON UPDATE CASCADE
+                                                  ON DELETE CASCADE;
+ALTER TABLE newsletter_ins ADD FOREIGN KEY (last) REFERENCES newsletter_issues (id)
+                                                  ON UPDATE CASCADE
+                                                  ON DELETE CASCADE;
+ALTER TABLE newsletter_ins ADD FOREIGN KEY (nlid) REFERENCES newsletters (id)
+                                                  ON UPDATE CASCADE
+                                                  ON DELETE CASCADE;
+
+-- Add AXletter subscribers.
+INSERT INTO newsletter_ins  (nlid, uid, last, hash)
+        (
+          SELECT  @idnl_ax, ai.uid, MAX(ni.id), ai.hash
+            FROM  axletter_ins AS ai
+       LEFT JOIN  axletter AS a ON (ai.last = a.id)
+       LEFT JOIN  newsletter_issues AS ni ON (ni.nlid = @idnl_ax AND ni.short_name = a.short_name)
+        GROUP BY  ai.uid
+    );
+
+-- Final state:
+--
+-- CREATE TABLE `newsletter_ins` (
+--   `uid` int(11) unsigned NOT NULL default '0',
+--   `nlid` int(11) unsigned NOT NULL,
+--   `last` int(11) unsigned default NULL,
+--   `hash` varchar(32) default NULL,
+--   PRIMARY KEY  (`uid`,`nlid`),
+--   KEY `last` (`last`),
+--   KEY `nlid` (`nlid`),
+--   CONSTRAINT `newsletter_ins_ibfk_1` FOREIGN KEY (`uid`) REFERENCES `accounts` (`uid`) ON DELETE CASCADE ON UPDATE CASCADE,
+--   CONSTRAINT `newsletter_ins_ibfk_2` FOREIGN KEY (`last`) REFERENCES `newsletter_issues` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
+--   CONSTRAINT `newsletter_ins_ibfk_3` FOREIGN KEY (`nlid`) REFERENCES `newsletters` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
+-- ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='liste des abonnés à la newsletter'