Compare commits

...

10 Commits

8 changed files with 220 additions and 123 deletions

View File

@ -1,6 +1,17 @@
dev
- [ ] literal rank of writer depending of num of com
1.2 - 2023.04.23
- require dotclear 2.26
- use latest dotclear namespace
- use sql statement
- fix plural translations
- fix nullsafe warnings
1.1 - 2023.03.21
- require Dotclear 2.25
- use namespace
1.0 - 2022.12.23
- use dotclear methods for widgets
- fix permissions

View File

@ -3,7 +3,7 @@
[![Release](https://img.shields.io/github/v/release/JcDenis/topWriter)](https://github.com/JcDenis/topWriter/releases)
[![Date](https://img.shields.io/github/release-date/JcDenis/topWriter)](https://github.com/JcDenis/topWriter/releases)
[![Issues](https://img.shields.io/github/issues/JcDenis/topWriter)](https://github.com/JcDenis/topWriter/issues)
[![Dotclear](https://img.shields.io/badge/dotclear-v2.24-blue.svg)](https://fr.dotclear.org/download)
[![Dotclear](https://img.shields.io/badge/dotclear-v2.26-blue.svg)](https://fr.dotclear.org/download)
[![Dotaddict](https://img.shields.io/badge/dotaddict-official-green.svg)](https://plugins.dotaddict.org/dc2/details/topWriter)
[![License](https://img.shields.io/github/license/JcDenis/topWriter)](https://github.com/JcDenis/topWriter/blob/master/LICENSE)
@ -19,7 +19,7 @@ Show most active contributor on a widget.
_topWriter_ requires:
* permissions to manage widgets
* Dotclear 2.24
* Dotclear 2.26
## USAGE

View File

@ -10,7 +10,7 @@
* @copyright Jean-Christian Denis
* @copyright GPL-2.0 https://www.gnu.org/licenses/gpl-2.0.html
*/
if (!defined('DC_RC_PATH')) {
if (!defined('DC_RC_PATH') || is_null(dcCore::app()->auth)) {
return null;
}
@ -18,15 +18,15 @@ $this->registerModule(
'Top writer',
'Ranking of the most prolific writers and/or commentators',
'Jean-Christian Denis, Pierre Van Glabeke',
'1.0',
'1.2',
[
'requires' => [['core', '2.24']],
'requires' => [['core', '2.26']],
'permissions' => dcCore::app()->auth->makePermissions([
dcAuth::PERMISSION_CONTENT_ADMIN,
dcCore::app()->auth::PERMISSION_CONTENT_ADMIN,
]),
'type' => 'plugin',
'support' => 'http://forum.dotclear.org/viewtopic.php?pid=333002#p333002',
'details' => 'http://plugins.dotaddict.org/dc2/details/topWriter',
'repository' => 'https://raw.githubusercontent.com/JcDenis/topWriter/master/dcstore.xml',
'type' => 'plugin',
'support' => 'http://forum.dotclear.org/viewtopic.php?pid=333002#p333002',
'details' => 'http://plugins.dotaddict.org/dc2/details/topWriter',
'repository' => 'https://raw.githubusercontent.com/JcDenis/topWriter/master/dcstore.xml',
]
);

View File

@ -2,11 +2,11 @@
<modules xmlns:da="http://dotaddict.org/da/">
<module id="topWriter">
<name>Top writer</name>
<version>1.0</version>
<version>1.2</version>
<author>Jean-Christian Denis, Pierre Van Glabeke</author>
<desc>Ranking of the most prolific writers and/or commentators</desc>
<file>https://github.com/JcDenis/topWriter/releases/download/v1.0/plugin-topWriter.zip</file>
<da:dcmin>2.24</da:dcmin>
<file>https://github.com/JcDenis/topWriter/releases/download/v1.2/plugin-topWriter.zip</file>
<da:dcmin>2.26</da:dcmin>
<da:details>http://plugins.dotaddict.org/dc2/details/topWriter</da:details>
<da:support>http://forum.dotclear.org/viewtopic.php?pid=333002#p333002</da:support>
</module>

View File

@ -9,26 +9,32 @@
# DOT NOT MODIFY THIS FILE !
#
l10n::$locales['Top writer: entries'] = 'Top rédacteur : billets';
l10n::$locales['Top writer: comments'] = 'Top rédacteur : commentaires';
l10n::$locales['Period:'] = 'Période :';
l10n::$locales['Limit:'] = 'Limite :';
l10n::$locales['List users who write more comments'] = 'Liste les utilisateurs qui ont écrit le plus de commentaires';
l10n::$locales['Top comments'] = 'Top commentaires';
l10n::$locales['Exclude post writer from list'] = 'Exclure de la liste les auteurs de billets';
l10n::$locales['List users who write more posts'] = 'Liste les utilisateurs qui ont écrit le plus de billets';
l10n::$locales['Top entries'] = 'Top billets';
l10n::$locales['Author posts'] = 'Billets de l\'auteur';
l10n::$locales['Author link'] = 'Lien vers l\'auteur';
l10n::$locales['no entries'] = 'aucun billet';
l10n::$locales['one entry'][0] = 'une publication';
l10n::$locales['one entry'][1] = '%s publications';
l10n::$locales['no comments'] = 'aucun commentaire';
l10n::$locales['one comment'][0] = 'un commentaire';
l10n::$locales['one comment'][1] = '%s commentaires';
l10n::$locales['last day'] = 'dernier jour';
l10n::$locales['last week'] = 'dernière semaine';
l10n::$locales['last month'] = 'dernier mois';
l10n::$locales['last year'] = 'dernière année';
l10n::$locales['from begining'] = 'depuis le début';
l10n::$locales['Ranking of the most prolific writers and/or commentators'] = 'Classement des plus prolifiques rédacteurs et/ou commentateurs';
use Dotclear\Helper\L10n;
L10n::$locales['Top writer: entries'] = 'Top rédacteur : billets';
L10n::$locales['Top writer: comments'] = 'Top rédacteur : commentaires';
L10n::$locales['Period:'] = 'Période :';
L10n::$locales['Limit:'] = 'Limite :';
L10n::$locales['List users who write more comments'] = 'Liste les utilisateurs qui ont écrit le plus de commentaires';
L10n::$locales['Top comments'] = 'Top commentaires';
L10n::$locales['Exclude post writer from list'] = 'Exclure de la liste les auteurs de billets';
L10n::$locales['List users who write more posts'] = 'Liste les utilisateurs qui ont écrit le plus de billets';
L10n::$locales['Top entries'] = 'Top billets';
L10n::$locales['Author posts'] = 'Billets de l\'auteur';
L10n::$locales['Author link'] = 'Lien vers l\'auteur';
L10n::$locales['no entries'] = 'aucun billet';
L10n::$locales['one entry'] = [
'une publication',
'%s publications',
];
L10n::$locales['no comments'] = 'aucun commentaire';
L10n::$locales['one comment'] = [
'un commentaire',
'%s commentaires',
];
L10n::$locales['last day'] = 'dernier jour';
L10n::$locales['last week'] = 'dernière semaine';
L10n::$locales['last month'] = 'dernier mois';
L10n::$locales['last year'] = 'dernière année';
L10n::$locales['from begining'] = 'depuis le début';
L10n::$locales['Ranking of the most prolific writers and/or commentators'] = 'Classement des plus prolifiques rédacteurs et/ou commentateurs';

View File

@ -16,8 +16,16 @@ namespace Dotclear\Plugin\topWriter;
use ArrayObject;
use dcCore;
use form;
use html;
use Dotclear\Helper\Html\Form\{
Checkbox,
Div,
Label,
Number,
Para,
Select,
Text
};
use Dotclear\Helper\Html\Html;
/**
* @ingroup DC_PLUGIN_TOPWRITER
@ -41,7 +49,7 @@ class BackendBehaviors
# Display
$__dashboard_items[0][] = '<div class="box small" id="topWriterPostsItems">' .
'<h3>' . html::escapeHTML(__('Top writer: entries')) . '</h3>' .
'<h3>' . Html::escapeHTML(__('Top writer: entries')) . '</h3>' .
'<ul>' . implode('', $li) . '</ul>' .
'</div>';
}
@ -58,7 +66,7 @@ class BackendBehaviors
# Display
$__dashboard_items[0][] = '<div class="box small" id="topWriterCommentsItems">' .
'<h3>' . html::escapeHTML(__('Top writer: comments')) . '</h3>' .
'<h3>' . Html::escapeHTML(__('Top writer: comments')) . '</h3>' .
'<ul>' . implode('', $li) . '</ul>' .
'</div>';
}
@ -70,31 +78,47 @@ class BackendBehaviors
$pref = self::setDefaultPref();
echo
'<div class="fieldset">' .
'<h4>' . __('Top writer: entries') . '</h4>' .
'<p><label class="classic" for="topWriterPostsItems">' .
form::checkbox('topWriterPostsItems', 1, $pref['topWriterPostsItems']) . ' ' .
__('Show') . '</label></p>' .
'<p><label class="classic" for="topWriterPostsPeriod">' . __('Period:') . ' </label>' .
form::combo('topWriterPostsPeriod', Utils::periods(), $pref['topWriterPostsPeriod']) . '</p>' .
'<p><label class="classic" for="topWriterPostsLimit">' . __('Limit:') . ' </label>' .
form::number('topWriterPostsLimit', ['min' => 1, 'max' => 20, 'default' => $pref['topWriterPostsLimit']]) . '</p>' .
'</div>' .
'<div class="fieldset">' .
'<h4>' . __('Top writer: comments') . '</h4>' .
'<p><label class="classic" for="topWriterCommentsItems">' .
form::checkbox('topWriterCommentsItems', 1, $pref['topWriterCommentsItems']) . ' ' .
__('Show') . '</label></p>' .
'<p><label class="classic" for="topWriterCommentsPeriod">' . __('Period:') . ' </label>' .
form::combo('topWriterCommentsPeriod', Utils::periods(), $pref['topWriterCommentsPeriod']) . '</p>' .
'<p><label class="classic" for="topWriterCommentsLimit">' . __('Limit:') . ' </label>' .
form::number('topWriterCommentsLimit', ['min' => 1, 'max' => 20, 'default' => $pref['topWriterCommentsLimit']]) . '</p>' .
'</div>';
(new Div())->items([
(new Div())->class('fieldset')->items([
(new Text('h4', __('Top writer: entries'))),
(new Para())->items([
(new Checkbox('topWriterPostsItems', $pref['topWriterPostsItems']))->value(1),
(new Label(__('Show'), Label::OUTSIDE_LABEL_AFTER))->for('topWriterPostsItems')->class('classic'),
]),
(new Para())->class('field')->items([
(new Label(__('Period:'), Label::OUTSIDE_LABEL_BEFORE))->for('topWriterPostsPeriod'),
(new Select('topWriterPostsPeriod'))->default($pref['topWriterPostsPeriod'])->items(Utils::periods()),
]),
(new Para())->class('field')->items([
(new Label(__('Limit:'), Label::OUTSIDE_LABEL_BEFORE))->for('topWriterPostsLimit'),
(new Number('topWriterPostsLimit'))->min(1)->max(20)->value($pref['topWriterPostsLimit']),
]),
]),
(new Div())->class('fieldset')->items([
(new Text('h4', __('Top writer: comments'))),
(new Para())->items([
(new Checkbox('topWriterCommentsItems', $pref['topWriterCommentsItems']))->value(1),
(new Label(__('Show'), Label::OUTSIDE_LABEL_AFTER))->for('topWriterCommentsItems')->class('classic'),
]),
(new Para())->class('field')->items([
(new Label(__('Period:'), Label::OUTSIDE_LABEL_BEFORE))->for('topWriterCommentsPeriod'),
(new Select('topWriterCommentsPeriod'))->default($pref['topWriterCommentsPeriod'])->items(Utils::periods()),
]),
(new Para())->class('field')->items([
(new Label(__('Limit:'), Label::OUTSIDE_LABEL_BEFORE))->for('topWriterCommentsLimit'),
(new Number('topWriterCommentsLimit'))->min(1)->max(20)->value($pref['topWriterCommentsLimit']),
]),
]),
])->render();
}
public static function adminAfterDashboardOptionsUpdate(?string $user_id): void
{
// nullsafe
if (is_null(dcCore::app()->auth) || is_null(dcCore::app()->auth->user_prefs)) {
return;
}
dcCore::app()->auth->user_prefs->get('dashboard')->put(
'topWriterPostsItems',
!empty($_POST['topWriterPostsItems']),
@ -130,6 +154,11 @@ class BackendBehaviors
private static function setDefaultPref(): array
{
// nullsafe
if (is_null(dcCore::app()->auth) || is_null(dcCore::app()->auth->user_prefs)) {
return [];
}
if (!dcCore::app()->auth->user_prefs->get('dashboard')->prefExists('topWriterPostsItems')) {
dcCore::app()->auth->user_prefs->get('dashboard')->put(
'topWriterPostsItems',

View File

@ -18,42 +18,69 @@ use dcAuth;
use dcBlog;
use dcCore;
use dcUtils;
use dt;
use Dotclear\Database\Statement\{
JoinStatement,
SelectStatement
};
use Dotclear\Helper\Date;
class Utils
{
public static function posts(string $period, int $limit, bool $sort_desc = true): array
{
$req = 'SELECT COUNT(*) AS count, U.user_id ' .
'FROM ' . dcCore::app()->prefix . dcBlog::POST_TABLE_NAME . ' P ' .
'INNER JOIN ' . dcCore::app()->prefix . dcAuth::USER_TABLE_NAME . ' U ON U.user_id = P.user_id ' .
"WHERE blog_id='" . dcCore::app()->con->escape(dcCore::app()->blog->id) . "' " .
'AND post_status=1 AND user_status=1 ' .
self::period('post_dt', $period) .
'GROUP BY U.user_id ' .
'ORDER BY count ' . ($sort_desc ? 'DESC' : 'ASC') . ' , U.user_id ASC ' .
dcCore::app()->con->limit(abs((int) $limit));
// nullsafe
if (is_null(dcCore::app()->blog)) {
return [];
}
$rs = dcCore::app()->con->select($req);
if ($rs->isEmpty()) {
$sql = new SelectStatement();
$sql
->from($sql->as(dcCore::app()->prefix . dcBlog::POST_TABLE_NAME, 'P'))
->columns([
$sql->count('*', 'count'),
'U.user_id',
])
->join(
(new JoinStatement())
->inner()
->from($sql->as(dcCore::app()->prefix . dcAuth::USER_TABLE_NAME, 'U'))
->on('U.user_id = P.user_id')
->statement()
)
->where('blog_id = ' . $sql->quote(dcCore::app()->blog->id))
->and('post_status = ' . dcBlog::POST_PUBLISHED)
->and('user_status = 1')
->group('U.user_id')
->order('count ' . ($sort_desc ? 'DESC' : 'ASC') . ' , U.user_id ASC')
->limit(abs((int) $limit));
self::period($sql, $period, 'post_dt');
$rs = $sql->select();
if (is_null($rs) || $rs->isEmpty()) {
return [];
}
$res = [];
$i = 0;
while ($rs->fetch()) {
$user = dcCore::app()->con->select(
'SELECT * FROM ' . dcCore::app()->prefix . dcAuth::USER_TABLE_NAME . " WHERE user_id='" . $rs->user_id . "' "
);
if ($user->isEmpty()) {
$sql = new SelectStatement();
$user = $sql
->from(dcCore::app()->prefix . dcAuth::USER_TABLE_NAME)
->column('*')
->where('user_id = ' . $sql->quote($rs->f('user_id')))
->select();
if (is_null($user) || $user->isEmpty()) {
continue;
}
$author = dcUtils::getUserCN(
$user->user_id,
$user->user_name,
$user->user_firstname,
$user->user_displayname
$user->f('user_id'),
$user->f('user_name'),
$user->f('user_firstname'),
$user->f('user_displayname')
);
if (empty($author)) {
continue;
@ -62,18 +89,18 @@ class Utils
$i++;
if (dcCore::app()->blog->settings->get('authormode')->get('authormode_active')) {
$res[$i]['author_link'] = '<a href="' .
dcCore::app()->blog->url . dcCore::app()->url->getBase('author') . '/' . $user->user_id . '" ' .
dcCore::app()->blog->url . dcCore::app()->url->getBase('author') . '/' . $user->f('user_id') . '" ' .
'title="' . __('Author posts') . '">' . $author . '</a>';
} elseif ($user->user_url) {
$res[$i]['author_link'] = '<a href="' . $user->user_url . '" title="' .
} elseif ($user->f('user_url')) {
$res[$i]['author_link'] = '<a href="' . $user->f('user_url') . '" title="' .
__('Author link') . '">' . $author . '</a>';
}
$res[$i]['author'] = $author;
if ($rs->count == 0) {
if ((int) $rs->f('count') == 0) {
$res[$i]['count'] = __('no entries');
} else {
$res[$i]['count'] = sprintf(__('one entry', '%s entries', (int) $rs->count), $rs->count);
$res[$i]['count'] = sprintf(__('one entry', '%s entries', (int) $rs->f('count')), $rs->f('count'));
}
}
@ -82,63 +109,87 @@ class Utils
public static function comments(string $period, int $limit, bool $sort_desc = true, bool $exclude = false): array
{
$req = 'SELECT COUNT(*) AS count, comment_email ' .
'FROM ' . dcCore::app()->prefix . dcBlog::POST_TABLE_NAME . ' P, ' . dcCore::app()->prefix . dcBlog::COMMENT_TABLE_NAME . ' C ' .
'WHERE P.post_id=C.post_id ' .
"AND blog_id='" . dcCore::app()->con->escape(dcCore::app()->blog->id) . "' " .
'AND post_status=1 AND comment_status=1 ' .
self::period('comment_dt', $period);
if ($exclude) {
$req .= 'AND comment_email NOT IN (' .
' SELECT U.user_email ' .
' FROM ' . dcCore::app()->prefix . dcAuth::USER_TABLE_NAME . ' U' .
' INNER JOIN ' . dcCore::app()->prefix . dcBlog::POST_TABLE_NAME . ' P ON P.user_id = U.user_id ' .
" WHERE blog_id='" . dcCore::app()->con->escape(dcCore::app()->blog->id) . "' " .
' GROUP BY U.user_email) ';
// nullsafe
if (is_null(dcCore::app()->blog)) {
return [];
}
$req .= 'GROUP BY comment_email ' .
'ORDER BY count ' . ($sort_desc ? 'DESC' : 'ASC') . ' ' .
dcCore::app()->con->limit(abs((int) $limit));
$sql = new SelectStatement();
$sql
->from($sql->as(dcCore::app()->prefix . dcBlog::POST_TABLE_NAME, 'P'))
->from($sql->as(dcCore::app()->prefix . dcBlog::COMMENT_TABLE_NAME, 'C'))
->columns([
$sql->count('*', 'count'),
'comment_email',
])
->where('blog_id = ' . $sql->quote(dcCore::app()->blog->id))
->and('P.post_id = C.post_id')
->and('post_status = ' . dcBlog::POST_PUBLISHED)
->and('comment_status = ' . dcBlog::COMMENT_PUBLISHED)
->group('comment_email')
->order('count ' . ($sort_desc ? 'DESC' : 'ASC'))
->limit(abs((int) $limit))
;
$rs = dcCore::app()->con->select($req);
if ($rs->isEmpty()) {
self::period($sql, $period, 'comment_dt');
if ($exclude) {
$sql->and('comment_email NOT IN (' .
(new SelectStatement())
->from($sql->as(dcCore::app()->prefix . dcAuth::USER_TABLE_NAME, 'U'))
->column('U.user_email')
->join(
(new JoinStatement())
->inner()
->from($sql->as(dcCore::app()->prefix . dcBlog::POST_TABLE_NAME, 'P'))
->on('P.user_id = U.user_id')
->statement()
)
->where('blog_id = ' . $sql->quote(dcCore::app()->blog->id))
->group('U.user_email')
->statement() .
')');
}
$rs = $sql->select();
if (is_null($rs) || $rs->isEmpty()) {
return [];
}
$res = [];
$i = 0;
while ($rs->fetch()) {
$user = dcCore::app()->con->select(
'SELECT * FROM ' . dcCore::app()->prefix . dcBlog::COMMENT_TABLE_NAME . ' ' .
"WHERE comment_email='" . $rs->comment_email . "' " .
'ORDER BY comment_dt DESC'
);
$sql = new SelectStatement();
$user = $sql
->from(dcCore::app()->prefix . dcBlog::COMMENT_TABLE_NAME)
->column('*')
->where('comment_email = ' . $sql->quote($rs->f('comment_email')))
->order('comment_dt DESC')
->select();
if (!$user->comment_author) {
if (is_null($user) || !$user->f('comment_author')) {
continue;
}
$i++;
if ($user->comment_site) {
$res[$i]['author_link'] = '<a href="' . $user->comment_site . '" title="' .
__('Author link') . '">' . $user->comment_author . '</a>';
if ($user->f('comment_site')) {
$res[$i]['author_link'] = '<a href="' . $user->f('comment_site') . '" title="' .
__('Author link') . '">' . $user->f('comment_author') . '</a>';
}
$res[$i]['author'] = $user->comment_author;
$res[$i]['author'] = $user->f('comment_author');
if ($rs->count == 0) {
if ((int) $rs->f('count') == 0) {
$res[$i]['count'] = __('no comments');
} else {
$res[$i]['count'] = sprintf(__('one comment', '%s comments', (int) $rs->count), $rs->count);
$res[$i]['count'] = sprintf(__('one comment', '%s comments', (int) $rs->f('count')), $rs->f('count'));
}
}
return $i ? $res : [];
}
private static function period(string $field, string $period): string
private static function period(SelectStatement $sql, string $period, string $field): void
{
$pattern = '%Y-%m-%d %H:%M:%S';
$time = 0;
@ -164,10 +215,10 @@ class Utils
break;
default:
return '';
return;
}
return "AND $field > TIMESTAMP '" . dt::str($pattern, time() - $time) . "' ";
$sql->and($field . ' > TIMESTAMP ' . $sql->quote(Date::str($pattern, time() - $time)));
}
public static function periods(): array

View File

@ -15,9 +15,9 @@ declare(strict_types=1);
namespace Dotclear\Plugin\topWriter;
use dcCore;
use Dotclear\Helper\Html\Html;
use Dotclear\Plugin\widgets\WidgetsStack;
use Dotclear\Plugin\widgets\WidgetsElement;
use html;
class Widgets
{
@ -133,7 +133,7 @@ class Widgets
(bool) $w->content_only,
'topcomments ' . $w->class,
'',
($w->title ? $w->renderTitle(html::escapeHTML($w->title)) : '') .
($w->title ? $w->renderTitle(Html::escapeHTML($w->title)) : '') .
sprintf('<ul>%s</ul>', implode('', self::lines($lines, 'comments', $w->text)))
);
}
@ -153,7 +153,7 @@ class Widgets
(bool) $w->content_only,
'topentries ' . $w->class,
'',
($w->title ? $w->renderTitle(html::escapeHTML($w->title)) : '') .
($w->title ? $w->renderTitle(Html::escapeHTML($w->title)) : '') .
sprintf('<ul>%s</ul>', implode('', self::lines($lines, 'posts', $w->text)))
);
}