News: Rewrite

This commit is contained in:
Igor Scheller 2020-04-05 16:54:45 +02:00 committed by msquare
parent 72f4839130
commit d323b75501
22 changed files with 1243 additions and 429 deletions

View File

@ -84,7 +84,7 @@ return [
'rewrite_urls' => true,
// Redirect to this site after logging in or when pressing the top-left button
// Must be one of news, user_meetings, user_shifts, angeltypes, user_questions
// Must be one of news, meetings, user_shifts, angeltypes, user_questions
'home_site' => 'news',
// Number of News shown on one site

View File

@ -23,6 +23,12 @@ $route->post('/password/reset/{token:.+}', 'PasswordResetController@postResetPas
$route->get('/metrics', 'Metrics\\Controller@metrics');
$route->get('/stats', 'Metrics\\Controller@stats');
// News
$route->get('/news', 'NewsController@index');
$route->get('/meetings', 'NewsController@meetings');
$route->get('/news/{id:\d+}', 'NewsController@show');
$route->post('/news/{id:\d+}', 'NewsController@comment');
// API
$route->get('/api[/{resource:.+}]', 'ApiController@index');
@ -39,5 +45,12 @@ $route->addGroup(
$route->post('-import', 'Admin\\Schedule\\ImportSchedule@importSchedule');
}
);
$route->addGroup(
'/news',
function (RouteCollector $route) {
$route->get('[/{id:\d+}]', 'Admin\\NewsController@edit');
$route->post('[/{id:\d+}]', 'Admin\\NewsController@save');
}
);
}
);

View File

@ -76,7 +76,6 @@ $includeFiles = [
__DIR__ . '/../includes/pages/guest_login.php',
__DIR__ . '/../includes/pages/user_messages.php',
__DIR__ . '/../includes/pages/user_myshifts.php',
__DIR__ . '/../includes/pages/user_news.php',
__DIR__ . '/../includes/pages/user_questions.php',
__DIR__ . '/../includes/pages/user_settings.php',
__DIR__ . '/../includes/pages/user_shifts.php',

View File

@ -1,86 +0,0 @@
<?php
use Engelsystem\Models\News;
/**
* @return string
*/
function admin_news()
{
$request = request();
if (!$request->has('action')) {
throw_redirect(page_link_to('news'));
}
$html = '<div class="col-md-12"><h1>' . __('Edit news entry') . '</h1>' . msg();
if ($request->has('id') && preg_match('/^\d{1,11}$/', $request->input('id'))) {
$news_id = $request->input('id');
} else {
return error('Incomplete call, missing News ID.', true);
}
$news = News::find($news_id);
if (empty($news)) {
return error('No News found.', true);
}
switch ($request->input('action')) {
case 'edit':
$user_source = $news->user;
if (
!auth()->can('admin_news_html')
&& strip_tags($news->text) != $news->text
) {
$html .= warning(
__('This message contains HTML. After saving the post some formatting will be lost!'),
true
);
}
$html .= form(
[
form_info(__('Date'), $news->created_at->format(__('Y-m-d H:i'))),
form_info(__('Author'), User_Nick_render($user_source)),
form_text('eBetreff', __('Subject'), $news->title),
form_textarea('eText', __('Message'), $news->text),
form_checkbox('eTreffen', __('Meeting'), $news->is_meeting, 1),
form_submit('submit', __('Save'))
],
page_link_to('admin_news', ['action' => 'save', 'id' => $news_id])
);
$html .= '<a class="btn btn-danger" href="'
. page_link_to('admin_news', ['action' => 'delete', 'id' => $news_id])
. '">'
. '<span class="glyphicon glyphicon-trash"></span> ' . __('Delete')
. '</a>';
break;
case 'save':
$text = $request->postData('eText');
if (!auth()->can('admin_news_html')) {
$text = strip_tags($text);
}
$news->title = strip_tags($request->postData('eBetreff'));
$news->text = $text;
$news->is_meeting = $request->has('eTreffen');
$news->save();
engelsystem_log('News updated: ' . $request->postData('eBetreff'));
success(__('News entry updated.'));
throw_redirect(page_link_to('news'));
break;
case 'delete':
$news->delete();
engelsystem_log('News deleted: ' . $news->title);
success(__('News entry deleted.'));
throw_redirect(page_link_to('news'));
break;
default:
throw_redirect(page_link_to('news'));
}
return $html . '</div>';
}

View File

@ -3,6 +3,7 @@
use Engelsystem\Http\Exceptions\HttpForbidden;
use Engelsystem\Models\News;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Collection as SupportCollection;
/**
* Publically available page to feed the news to feed readers
@ -36,7 +37,7 @@ function user_atom()
}
/**
* @param News[]|Collection $news_entries
* @param News[]|Collection|SupportCollection $news_entries
* @return string
*/
function make_atom_entries_from_news($news_entries)
@ -71,11 +72,11 @@ function make_atom_entry_from_news(News $news)
return '
<entry>
<title>' . htmlspecialchars($news->title) . '</title>
<link href="' . page_link_to('news_comments', ['nid' => $news->id]) . '"/>
<link href="' . page_link_to('news/' . $news->id) . '"/>
<id>' . preg_replace(
'#^https?://#',
'',
page_link_to('news_comments', ['nid' => $news->id])
page_link_to('news/' . $news->id)
) . '</id>
<updated>' . $news->updated_at->format('Y-m-d\TH:i:sP') . '</updated>
<summary type="html">' . htmlspecialchars($news->text) . '</summary>

View File

@ -1,247 +0,0 @@
<?php
use Engelsystem\Models\News;
/**
* @return string
*/
function user_news_comments_title()
{
return __('News comments');
}
/**
* @return string
*/
function news_title()
{
return __('News');
}
/**
* @return string
*/
function meetings_title()
{
return __('Meetings');
}
/**
* @return string
*/
function user_meetings()
{
$display_news = config('display_news');
$html = '<div class="container">';
$html .= '<h1>' . meetings_title() . '</h1>' . msg();
$request = request();
if (preg_match('/^\d{1,}$/', $request->input('page', 0))) {
$page = $request->input('page', 0);
} else {
$page = 0;
}
$news = News::whereIsMeeting(true)
->orderBy('created_at', 'DESC')
->limit($display_news)
->offset($page * $display_news)
->get();
foreach ($news as $entry) {
$html .= display_news($entry);
}
$dis_rows = ceil(News::whereIsMeeting(true)->count() / $display_news);
$html .= '<div class="text-center">' . '<ul class="pagination">';
for ($i = 0; $i < $dis_rows; $i++) {
if ($request->has('page') && $i == $request->input('page', 0)) {
$html .= '<li class="active">';
} elseif (!$request->has('page') && $i == 0) {
$html .= '<li class="active">';
} else {
$html .= '<li>';
}
$html .= '<a href="' . page_link_to('user_meetings', ['page' => $i]) . '">' . ($i + 1) . '</a></li>';
}
$html .= '</ul></div></div>';
return $html;
}
/**
* Renders the text content of a news entry
*
* @param News $news
* @return string HTML
*/
function news_text(News $news): string
{
$text = preg_replace("/\r\n\r\n/m", '<br><br>', $news->text);
return $text;
}
/**
* @param News $news
* @return string
*/
function display_news(News $news): string
{
$html = '';
$html .= '<div class="panel' . ($news->is_meeting ? ' panel-info' : ' panel-default') . '">';
$html .= '<div class="panel-heading">';
$html .= '<h3 class="panel-title">' . ($news->is_meeting ? '[Meeting] ' : '') . $news->title . '</h3>';
$html .= '</div>';
$html .= '<div class="panel-body">' . news_text($news) . '</div>';
$html .= '<div class="panel-footer text-muted">';
if (auth()->can('admin_news')) {
$html .= '<div class="pull-right">'
. button_glyph(
page_link_to('admin_news', ['action' => 'edit', 'id' => $news->id]),
'edit',
'btn-xs'
)
. '</div>';
}
$html .= '<span class="glyphicon glyphicon-time"></span> ' . $news->created_at->format(__('Y-m-d H:i')) . '&emsp;';
$html .= User_Nick_render($news->user);
if (current_page() != 'news_comments') {
$html .= '&emsp;<a href="' . page_link_to('news_comments', ['nid' => $news->id]) . '">'
. '<span class="glyphicon glyphicon-comment"></span> '
. __('Comments') . ' &raquo;</a> '
. '<span class="badge">'
. $news->comments()->count()
. '</span>';
}
$html .= '</div>';
$html .= '</div>';
return $html;
}
/**
* @return string
*/
function user_news_comments()
{
$user = auth()->user();
$request = request();
$html = '<div class="container">';
$html .= '<h1>' . user_news_comments_title() . '</h1>';
$nid = $request->input('nid');
if (
$request->has('nid')
&& preg_match('/^\d{1,}$/', $nid)
&& $news = News::find($nid)
) {
if ($request->hasPostData('submit') && $request->has('text')) {
$text = $request->input('text');
$news->comments()->create([
'text' => $text,
'user_id' => $user->id,
]);
engelsystem_log('Created news_comment: ' . $text);
$html .= success(__('Entry saved.'), true);
}
$html .= display_news($news);
foreach ($news->comments as $comment) {
$html .= '<div class="panel panel-default">';
$html .= '<div class="panel-body">' . nl2br(htmlspecialchars($comment->text)) . '</div>';
$html .= '<div class="panel-footer text-muted">';
$html .= '<span class="glyphicon glyphicon-time"></span> ' . $comment->created_at->format(__('Y-m-d H:i')) . '&emsp;';
$html .= User_Nick_render($comment->user);
$html .= '</div>';
$html .= '</div>';
}
$html .= '<hr /><h2>' . __('New Comment:') . '</h2>';
$html .= form([
form_textarea('text', __('Message'), ''),
form_submit('submit', __('Save'))
], page_link_to('news_comments', ['nid' => $news->id]));
} else {
$html .= __('Invalid request.');
}
return $html . '</div>';
}
/**
* @return string
*/
function user_news()
{
$user = auth()->user();
$display_news = config('display_news');
$request = request();
$html = '<div class="container">';
$html .= '<h1>' . news_title() . '</h1>' . msg();
$isMeeting = $request->postData('treffen', false);
if ($request->has('text') && $request->has('betreff') && auth()->can('admin_news')) {
$text = $request->postData('text');
if (!auth()->can('admin_news_html')) {
$text = strip_tags($text);
}
$news = News::create([
'title' => strip_tags($request->postData('betreff')),
'text' => $text,
'user_id' => $user->id,
'is_meeting' => (bool)$isMeeting,
]);
engelsystem_log('Created news: ' . $news->title . ', is meeting: ' . ($news->is_meeting ? 'yes' : 'no'));
success(__('Entry saved.'));
throw_redirect(page_link_to('news'));
}
if (preg_match('/^\d{1,}$/', $request->input('page', 0))) {
$page = $request->input('page', 0);
} else {
$page = 0;
}
$news = News::query()
->orderBy('created_at', 'DESC')
->limit($display_news)
->offset($page * $display_news)
->get();
foreach ($news as $entry) {
$html .= display_news($entry);
}
$dis_rows = ceil(News::query()->count() / $display_news);
$html .= '<div class="text-center">' . '<ul class="pagination">';
for ($i = 0; $i < $dis_rows; $i++) {
if ($request->has('page') && $i == $request->input('page', 0)) {
$html .= '<li class="active">';
} elseif (!$request->has('page') && $i == 0) {
$html .= '<li class="active">';
} else {
$html .= '<li>';
}
$html .= '<a href="' . page_link_to('news', ['page' => $i]) . '">' . ($i + 1) . '</a></li>';
}
$html .= '</ul></div>';
if (auth()->can('admin_news')) {
$html .= '<hr />';
$html .= '<h2>' . __('Create news:') . '</h2>';
$html .= form([
form_text('betreff', __('Subject'), ''),
form_textarea('text', __('Message'), ''),
form_checkbox('treffen', __('Meeting'), false, 1),
form_submit('submit', __('Save'))
]);
}
return $html . '</div>';
}

View File

@ -92,14 +92,14 @@ function make_navigation()
$menu = [];
$pages = [
'news' => __('News'),
'user_meetings' => __('Meetings'),
'meetings' => __('Meetings'),
'user_shifts' => __('Shifts'),
'angeltypes' => __('Angeltypes'),
'user_questions' => __('Ask the Heaven'),
];
foreach ($pages as $menu_page => $title) {
if (auth()->can($menu_page)) {
if (auth()->can($menu_page) || ($menu_page == 'meetings' && auth()->can('user_meetings'))) {
$menu[] = toolbar_item_link(page_link_to($menu_page), '', $title, $menu_page == $page);
}
}

View File

@ -67,3 +67,17 @@ msgstr "Die Minuten vor dem Talk müssen eine Zahl sein."
msgid "validation.minutes-after.int"
msgstr "Die Minuten nach dem Talk müssen eine Zahl sein."
msgid "news.comment.success"
msgstr "Kommentar gespeichert."
msgid "news.edit.success"
msgstr "News erfolgreich aktualisiert."
msgid "news.delete.success"
msgstr "News erfolgreich gelöscht."
msgid "news.edit.contains-html"
msgstr ""
"Diese Nachricht beinhaltet HTML. Wenn du sie speicherst gehen diese "
"Formatierungen verloren!"

View File

@ -220,10 +220,8 @@ msgstr "Passwort wiederholen"
#: resources/views/pages/password/reset-form.twig:14
#: includes/controller/shifts_controller.php:194
#: includes/pages/admin_groups.php:101 includes/pages/admin_news.php:48
#: includes/pages/admin_questions.php:55 includes/pages/admin_rooms.php:177
#: includes/pages/admin_shifts.php:339 includes/pages/user_messages.php:78
#: includes/pages/user_news.php:164 includes/pages/user_news.php:241
#: includes/view/AngelTypes_view.php:120 includes/view/EventConfig_view.php:110
#: includes/view/Questions_view.php:43 includes/view/ShiftEntry_view.php:93
#: includes/view/ShiftEntry_view.php:118 includes/view/ShiftEntry_view.php:141
@ -1089,7 +1087,7 @@ msgid "arrived sum"
msgstr "Summe angekommen"
#: includes/pages/admin_arrive.php:200 includes/pages/admin_arrive.php:215
#: includes/pages/admin_arrive.php:230 includes/pages/admin_news.php:43
#: includes/pages/admin_arrive.php:230
#: includes/pages/user_messages.php:118
msgid "Date"
msgstr "Datum"
@ -1300,48 +1298,15 @@ msgstr "Erledigt!"
msgid "Log"
msgstr "Log"
#: includes/pages/admin_news.php:16
msgid "Edit news entry"
msgstr "News-Eintrag bearbeiten"
#: includes/pages/admin_news.php:36
msgid ""
"This message contains HTML. After saving the post some formatting will be "
"lost!"
msgstr ""
"Diese Nachricht beinhaltet HTML. Wenn du sie speicherst gehen diese "
"Formatierungen verloren!"
#: includes/pages/admin_news.php:44
msgid "Author"
msgstr "Autor"
#: includes/pages/admin_news.php:45 includes/pages/user_news.php:238
msgid "Subject"
msgstr "Betreff"
#: includes/pages/admin_news.php:46 includes/pages/user_messages.php:121
#: includes/pages/user_news.php:163 includes/pages/user_news.php:239
#: includes/pages/user_messages.php:121
msgid "Message"
msgstr "Nachricht"
#: includes/pages/admin_news.php:47 includes/pages/user_news.php:240
msgid "Meeting"
msgstr "Treffen"
#: includes/pages/admin_news.php:56 includes/pages/admin_rooms.php:202
#: includes/pages/admin_rooms.php:202
#: includes/view/User_view.php:129
msgid "Delete"
msgstr "löschen"
#: includes/pages/admin_news.php:72
msgid "News entry updated."
msgstr "News-Eintrag gespeichert."
#: includes/pages/admin_news.php:79
msgid "News entry deleted."
msgstr "News-Eintrag gelöscht."
#: includes/pages/admin_questions.php:11 includes/sys_menu.php:115
msgid "Answer questions"
msgstr "Fragen beantworten"
@ -1778,38 +1743,14 @@ msgstr "Gib bitte einen Schwänz-Kommentar ein!"
msgid "Shift saved."
msgstr "Schicht gespeichert."
#: includes/pages/user_news.php:10
msgid "News comments"
msgstr "News Kommentare"
#: includes/pages/user_news.php:18 includes/sys_menu.php:94
#: includes/sys_menu.php:94
msgid "News"
msgstr "News"
#: includes/pages/user_news.php:26 includes/sys_menu.php:95
#: includes/sys_menu.php:95
msgid "Meetings"
msgstr "Treffen"
#: includes/pages/user_news.php:113
msgid "Comments"
msgstr "Kommentare"
#: includes/pages/user_news.php:146 includes/pages/user_news.php:199
msgid "Entry saved."
msgstr "Eintrag gespeichert."
#: includes/pages/user_news.php:161
msgid "New Comment:"
msgstr "Neuer Kommentar:"
#: includes/pages/user_news.php:167
msgid "Invalid request."
msgstr "Ungültige Abfrage."
#: includes/pages/user_news.php:235
msgid "Create news:"
msgstr "News anlegen:"
#: includes/pages/user_questions.php:11 includes/sys_menu.php:98
#: includes/view/Questions_view.php:40
msgid "Ask the Heaven"
@ -2886,3 +2827,42 @@ msgstr "Titel"
msgid "schedule.import.shift.room"
msgstr "Raum"
msgid "news.title"
msgstr "News"
msgid "news.title.meetings"
msgstr "Treffen"
msgid "news.add"
msgstr "+"
msgid "news.is_meeting"
msgstr "[Treffen]"
msgid "news.updated"
msgstr "Aktualisiert"
msgid "news.comments"
msgstr "Kommentare"
msgid "news.comments.new"
msgstr "Neuer Kommentar"
msgid "news.comments.message"
msgstr "Nachricht"
msgid "news.edit.edit"
msgstr "News bearbeiten"
msgid "news.edit.add"
msgstr "News erstellen"
msgid "news.edit.subject"
msgstr "Betreff"
msgid "news.edit.is_meeting"
msgstr "Treffen"
msgid "news.edit.message"
msgstr "Nachricht"

View File

@ -65,3 +65,15 @@ msgstr "The minutes before the talk have to be an integer."
msgid "validation.minutes-after.int"
msgstr "The minutes after the talk have to be an integer."
msgid "news.comment.success"
msgstr "Comment saved."
msgid "news.edit.success"
msgstr "News successfully updated."
msgid "news.delete.success"
msgstr "News successfully deleted."
msgid "news.edit.contains-html"
msgstr "This message contains HTML. After saving the post some formatting will be lost!"

View File

@ -105,3 +105,42 @@ msgstr "Title"
msgid "schedule.import.shift.room"
msgstr "Room"
msgid "news.title"
msgstr "News"
msgid "news.title.meetings"
msgstr "Meetings"
msgid "news.add"
msgstr "+"
msgid "news.is_meeting"
msgstr "[Meeting]"
msgid "news.updated"
msgstr "Updated"
msgid "news.comments"
msgstr "Comments"
msgid "news.comments.new"
msgstr "New comment"
msgid "news.comments.message"
msgstr "Message"
msgid "news.edit.edit"
msgstr "Edit news"
msgid "news.edit.add"
msgstr "Add news"
msgid "news.edit.subject"
msgstr "Subject"
msgid "news.edit.is_meeting"
msgstr "Meeting"
msgid "news.edit.message"
msgstr "Message"

View File

@ -12,9 +12,9 @@
<link rel="stylesheet" type="text/css" href="{{ asset('assets/theme' ~ theme ~ '.css') }}"/>
<script type="text/javascript" src="{{ asset('assets/vendor.js') }}"></script>
{% if page() in ['news', 'user-meetings', '/'] and is_user() -%}
{% if page() in ['news', 'meetings'] and is_user() -%}
{% set parameters = {'key': user.api_key} -%}
{% if page() == 'user-meetings' -%}
{% if page() == 'meetings' -%}
{% set parameters = parameters|merge({'meetings': 1}) -%}
{% endif %}
<link href="{{ url('atom', parameters) }}" type="application/atom+xml" rel="alternate" title="Atom Feed">

View File

@ -9,3 +9,17 @@
{% macro alert(message, type) %}
<div class="alert alert-{{ type|default('info') }}">{{ message }}</div>
{% endmacro %}
{% macro user(user) %}
<a href="{{ url('users', {'action': 'view', 'user_id': user.id}) }}"
{%- if not user.state.arrived %} class="text-muted"{% endif -%}
>
{{ _self.angel() }} {{ user.name }}
</a>
{% endmacro %}
{% macro button(label, url, type, size) %}
<a href="{{ url }}" class="btn btn-{{ type|default('default') }}{% if size %} btn-{{ size }}{% endif %}">
{{ label }}
</a>
{% endmacro %}

View File

@ -13,6 +13,22 @@
</div>
{%- endmacro %}
{% macro textarea(name, label, opt) %}
<div class="form-group">
{% if label -%}
<label for="{{ name }}">{{ label }}</label>
{%- endif %}
<textarea class="form-control" id="{{ name }}" name="{{ name }}"
{%- if opt.required|default(false) %}
required="required"
{%- endif -%}
{%- if opt.rows|default(0) %}
rows="{{ opt.rows }}"
{%- endif -%}
>{{ opt.value|default('') }}</textarea>
</div>
{%- endmacro %}
{% macro select(name, data, label, selected) %}
<div class="form-group">
{% if label -%}
@ -26,10 +42,30 @@
</div>
{%- endmacro %}
{% macro checkbox(name, label, checked, value) %}
<div class="checkbox">
<label>
<input type="checkbox" id="{{ name }}" name="{{ name }}" value="{{ value|default('1') }}"
{%- if checked|default(false) %} checked="checked"{% endif %}>
{{ label }}
</label>
</div>
{%- endmacro %}
{% macro hidden(name, value) %}
<input type="hidden" id="{{ name }}" name="{{ name }}" value="{{ value }}">
{%- endmacro %}
{% macro submit(label) %}
<button type="submit" class="btn btn-primary">{{ label|default(__('form.submit')) }}</button>
{% macro button(label, opt) %}
<button class="btn btn-{{ opt.btn_type|default('primary') }}"
{%- if opt.type is defined %} type="{{ opt.type }}"{% endif %}
{%- if opt.name is defined %} name="{{ opt.name }}"{% endif %}
{%- if opt.value is defined or opt.name is defined %} value="{{ opt.value|default('1') }}"{% endif -%}
>
{{ label }}
</button>
{%- endmacro %}
{% macro submit(label, opt) %}
{{ _self.button(label|default(__('form.submit')), opt|default({})|merge({'type': 'submit'})) }}
{%- endmacro %}

View File

@ -0,0 +1,62 @@
{% extends 'layouts/app.twig' %}
{% import 'macros/base.twig' as m %}
{% import 'macros/form.twig' as f %}
{% block title %}{{ news ? __('news.edit.edit') : __('news.edit.add') }}{% endblock %}
{% block content %}
<div class="container">
<h1>{{ block('title') }}</h1>
{% include 'layouts/parts/messages.twig' %}
{% if news %}
<div class="row">
<div class="col-md-6">
<p>
{{ m.glyphicon('time') }} {{ news.updated_at.format(__('Y-m-d H:i')) }}
{% if news.updated_at != news.created_at %}
&emsp;{{ __('news.updated') }}
<br>
{{ m.glyphicon('time') }} {{ news.created_at.format(__('Y-m-d H:i')) }}
{% endif %}
&emsp;{{ m.user(news.user) }}
</p>
</div>
</div>
{% endif %}
<form action="" enctype="multipart/form-data" method="post">
{{ csrf() }}
<div class="row">
<div class="col-md-6">
{{ f.input(
'title',
__('news.edit.subject'),
null,
{'required': true, 'value': news ? news.title : ''}
) }}
</div>
<div class="col-md-6">
{{ f.checkbox('is_meeting', __('news.edit.is_meeting'), news ? news.is_meeting : false) }}
</div>
</div>
<div class="row">
<div class="col-md-12">
{{ f.textarea('text', __('news.edit.message'), {'required': true, 'rows': 10, 'value': news ? news.text : ''}) }}
{{ f.submit() }}
{% if news %}
{{ f.submit(m.glyphicon('trash'), {'name': 'delete', 'btn_type': 'danger'}) }}
{% endif %}
</div>
</div>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,45 @@
{% extends 'pages/news/overview.twig' %}
{% import 'macros/base.twig' as m %}
{% import 'macros/form.twig' as f %}
{% block title %}{{ news.title }}{% endblock %}
{% block news %}
{{ _self.news(news) }}
{% endblock %}
{% block comments %}
<div class="col-md-12">
<h2>{{ __('news.comments') }}</h2>
{% for comment in news.comments %}
<div class="panel panel-default">
<div class="panel-body">
{{ comment.text|nl2br }}
</div>
<div class="panel-footer text-muted">
{{ m.glyphicon('time') }}
{{ comment.created_at.format(__('Y-m-d H:i')) }}
{{ m.user(comment.user) }}
</div>
</div>
{% endfor %}
</div>
{% endblock %}
{% block write_comment %}
{% if has_permission_to('news_comments') %}
<div class="col-md-12">
<h3>{{ __('news.comments.new') }}</h3>
<form action="" enctype="multipart/form-data" method="post">
{{ csrf() }}
{{ f.textarea('comment', __('news.comments.message'), {'required': true}) }}
{{ f.submit() }}
</form>
</div>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,93 @@
{% extends 'layouts/app.twig' %}
{% import 'macros/base.twig' as m %}
{% block title %}{{ not only_meetings|default(false) ? __('news.title') : __('news.title.meetings') }}{% endblock %}
{% block content %}
<div class="container">
<h1>
{{ block('title') }}
{%- if has_permission_to('admin_news') and is_overview|default(false) -%}
{{ m.button(__('news.add'), url('admin/news')) }}
{%- endif %}
</h1>
{% include 'layouts/parts/messages.twig' %}
<div class="row">
<div class="col-md-12">
{% block news %}
{% for news in news %}
{{ _self.news(news, true, is_overview) }}
{% endfor %}
{% endblock %}
</div>
{% block comments %}
{% endblock %}
{% block pagination %}
{% if pages|default(0) > 1 %}
<div class="col-md-12 text-center">
<ul class="pagination">
{% for p in range(1, pages) %}
<li{% if p == page %} class="active"{% endif %}>
<a href="{{ url('news', p == 1 ? {} : {'page': p}) }}">{{ p }}</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endblock %}
{% block write_comment %}
{% endblock %}
</div>
</div>
{% endblock %}
{% macro news(news, show_comments_link, is_overview) %}
<div class="panel {% if not news.is_meeting %}panel-default{% else %}panel-info{% endif %}">
{% if is_overview|default(false) %}
<div class="panel-heading">
<h3 class="panel-title">
<a href="{{ url('news/' ~ news.id) }}">
{% if news.is_meeting %}{{ __('news.is_meeting') }}{% endif %}
{{ news.title }}
</a>
</h3>
</div>
{% endif %}
<div class="panel-body">
{{ news.text|raw|nl2br }}
</div>
<div class="panel-footer text-muted">
{{ m.glyphicon('time') }} {{ news.updated_at.format(__('Y-m-d H:i')) }}
{% if news.updated_at != news.created_at and not is_overview %}
&emsp;{{ __('news.updated') }}
<br>
{{ m.glyphicon('time') }} {{ news.created_at.format(__('Y-m-d H:i')) }}
{% endif %}
&emsp;{{ m.user(news.user) }}
{% if show_comments_link|default(false) %}
&ensp;
<a href="{{ url('news/' ~ news.id) }}">
{{ m.glyphicon('comment') }} {{ __('news.comments') }} &raquo;
</a>
<span class="badge">{{ news.comments.count() }}</span>
{% endif %}
{% if has_permission_to('admin_news') %}
<div class="pull-right">
{{ m.button(m.glyphicon('edit'), url('admin/news/' ~ news.id), null, 'xs') }}
</div>
{% endif %}
</div>
</div>
{% endmacro %}

View File

@ -0,0 +1,140 @@
<?php
namespace Engelsystem\Controllers\Admin;
use Engelsystem\Controllers\BaseController;
use Engelsystem\Controllers\HasUserNotifications;
use Engelsystem\Helpers\Authenticator;
use Engelsystem\Http\Redirector;
use Engelsystem\Http\Request;
use Engelsystem\Http\Response;
use Engelsystem\Models\News;
use Psr\Log\LoggerInterface;
class NewsController extends BaseController
{
use HasUserNotifications;
/** @var Authenticator */
protected $auth;
/** @var LoggerInterface */
protected $log;
/** @var News */
protected $news;
/** @var Redirector */
protected $redirect;
/** @var Response */
protected $response;
/** @var array */
protected $permissions = [
'admin_news',
];
/**
* @param Authenticator $auth
* @param LoggerInterface $log
* @param News $news
* @param Redirector $redirector
* @param Response $response
*/
public function __construct(
Authenticator $auth,
LoggerInterface $log,
News $news,
Redirector $redirector,
Response $response
) {
$this->auth = $auth;
$this->log = $log;
$this->news = $news;
$this->redirect = $redirector;
$this->response = $response;
}
/**
* @param Request $request
* @return Response
*/
public function edit(Request $request): Response
{
$id = $request->getAttribute('id');
$news = $this->news->find($id);
if (
$news
&& !$this->auth->can('admin_news_html')
&& strip_tags($news->text) != $news->text
) {
$this->addNotification('news.edit.contains-html', 'warnings');
}
return $this->response->withView(
'pages/news/edit.twig',
['news' => $news] + $this->getNotifications()
);
}
/**
* @param Request $request
* @return Response
*/
public function save(Request $request): Response
{
$id = $request->getAttribute('id');
/** @var News $news */
$news = $this->news->findOrNew($id);
$data = $this->validate($request, [
'title' => 'required',
'text' => 'required',
'is_meeting' => 'optional|checked',
'delete' => 'optional|checked',
]);
if (!is_null($data['delete'])) {
$news->delete();
$this->log->info(
'Deleted {type} "{news}"',
[
'type' => $news->is_meeting ? 'meeting' : 'news',
'news' => $news->title
]
);
$this->addNotification('news.delete.success');
return $this->redirect->to('/news');
}
if (!$this->auth->can('admin_news_html')) {
$data['text'] = strip_tags($data['text']);
}
if (!$news->user) {
$news->user()->associate($this->auth->user());
}
$news->title = $data['title'];
$news->text = $data['text'];
$news->is_meeting = !is_null($data['is_meeting']);
$news->save();
$this->log->info(
'Updated {type} "{news}": {text}',
[
'type' => $news->is_meeting ? 'meeting' : 'news',
'news' => $news->title,
'text' => $news->text,
]
);
$this->addNotification('news.edit.success');
return $this->redirect->to('/news/' . $news->id);
}
}

View File

@ -0,0 +1,181 @@
<?php
namespace Engelsystem\Controllers;
use Engelsystem\Config\Config;
use Engelsystem\Helpers\Authenticator;
use Engelsystem\Http\Redirector;
use Engelsystem\Http\Request;
use Engelsystem\Http\Response;
use Engelsystem\Models\News;
use Engelsystem\Models\NewsComment;
use Psr\Log\LoggerInterface;
class NewsController extends BaseController
{
use HasUserNotifications;
/** @var Authenticator */
protected $auth;
/** @var Config */
protected $config;
/** @var LoggerInterface */
protected $log;
/** @var News */
protected $news;
/** @var Redirector */
protected $redirect;
/** @var Response */
protected $response;
/** @var Request */
protected $request;
/** @var array */
protected $permissions = [
'news',
'meetings' => 'user_meetings',
'comment' => 'news_comments',
];
/**
* @param Authenticator $auth
* @param Config $config
* @param LoggerInterface $log
* @param News $news
* @param Redirector $redirector
* @param Response $response
* @param Request $request
*/
public function __construct(
Authenticator $auth,
Config $config,
LoggerInterface $log,
News $news,
Redirector $redirector,
Response $response,
Request $request
) {
$this->auth = $auth;
$this->config = $config;
$this->log = $log;
$this->news = $news;
$this->redirect = $redirector;
$this->response = $response;
$this->request = $request;
}
/**
* @return Response
*/
public function index()
{
return $this->showOverview();
}
/**
* @return Response
*/
public function meetings(): Response
{
return $this->showOverview(true);
}
/**
* @param Request $request
* @return Response
*/
public function show(Request $request): Response
{
$news = $this->news
->with('user')
->with('comments')
->findOrFail($request->getAttribute('id'));
return $this->renderView('pages/news/news.twig', ['news' => $news]);
}
/**
* @param Request $request
* @return Response
*/
public function comment(Request $request): Response
{
$data = $this->validate($request, [
'comment' => 'required',
]);
$user = $this->auth->user();
$news = $this->news
->findOrFail($request->getAttribute('id'));
/** @var NewsComment $comment */
$comment = $news->comments()->create([
'text' => $data['comment'],
'user_id' => $user->id,
]);
$this->log->info(
'Created news comment for "{news}": {comment}',
[
'news' => $news->title,
'comment' => $comment->text,
]
);
$this->addNotification('news.comment.success');
return $this->redirect->back();
}
/**
* @param bool $onlyMeetings
* @return Response
*/
protected function showOverview(bool $onlyMeetings = false): Response
{
$query = $this->news;
$page = $this->request->get('page', 1);
$perPage = $this->config->get('display_news');
if ($onlyMeetings) {
$query = $query->where('is_meeting', true);
}
$news = $query
->with('user')
->withCount('comments')
->orderByDesc('updated_at')
->limit($perPage)
->offset(($page - 1) * $perPage)
->get();
$pagesCount = ceil($query->count() / $perPage);
return $this->renderView(
'pages/news/overview.twig',
[
'news' => $news,
'pages' => max(1, $pagesCount),
'page' => max(1, min($page, $pagesCount)),
'only_meetings' => $onlyMeetings,
'is_overview' => true,
]
);
}
/**
* @param string $page
* @param array $data
* @return Response
*/
protected function renderView(string $page, array $data): Response
{
$data += $this->getNotifications();
return $this->response->withView($page, $data);
}
}

View File

@ -95,19 +95,15 @@ class LegacyMiddleware implements MiddlewareInterface
*/
protected function loadPage($page)
{
$title = ucfirst($page);
switch ($page) {
/** @noinspection PhpMissingBreakStatementInspection */
case 'ical':
require_once realpath(__DIR__ . '/../../includes/pages/user_ical.php');
user_ical();
break;
/** @noinspection PhpMissingBreakStatementInspection */
case 'atom':
require_once realpath(__DIR__ . '/../../includes/pages/user_atom.php');
user_atom();
break;
/** @noinspection PhpMissingBreakStatementInspection */
case 'shifts_json_export':
require_once realpath(__DIR__ . '/../../includes/controller/shifts_controller.php');
shifts_json_export_controller();
@ -134,19 +130,6 @@ class LegacyMiddleware implements MiddlewareInterface
return [$title, $content];
case 'rooms':
return rooms_controller();
case 'news':
$title = news_title();
$content = user_news();
return [$title, $content];
case 'news_comments':
require_once realpath(__DIR__ . '/../../includes/pages/user_news.php');
$title = user_news_comments_title();
$content = user_news_comments();
return [$title, $content];
case 'user_meetings':
$title = meetings_title();
$content = user_meetings();
return [$title, $content];
case 'user_myshifts':
$title = myshifts_title();
$content = user_myshifts();
@ -193,10 +176,6 @@ class LegacyMiddleware implements MiddlewareInterface
$title = admin_free_title();
$content = admin_free();
return [$title, $content];
case 'admin_news':
require_once realpath(__DIR__ . '/../../includes/pages/admin_news.php');
$content = admin_news();
return [$title, $content];
case 'admin_rooms':
$title = admin_rooms_title();
$content = admin_rooms();

View File

@ -0,0 +1,265 @@
<?php
namespace Engelsystem\Test\Unit\Controllers\Admin;
use Engelsystem\Controllers\Admin\NewsController;
use Engelsystem\Helpers\Authenticator;
use Engelsystem\Http\Exceptions\ValidationException;
use Engelsystem\Http\Request;
use Engelsystem\Http\Response;
use Engelsystem\Http\Validation\Validator;
use Engelsystem\Models\News;
use Engelsystem\Models\User\User;
use Engelsystem\Test\Unit\HasDatabase;
use Engelsystem\Test\Unit\TestCase;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Collection;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\Test\TestLogger;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
class NewsControllerTest extends TestCase
{
use HasDatabase;
/** @var Authenticator|MockObject */
protected $auth;
/** @var array */
protected $data = [
[
'title' => 'Foo',
'text' => '<b>foo</b>',
'is_meeting' => false,
'user_id' => 1,
]
];
/** @var TestLogger */
protected $log;
/** @var Response|MockObject */
protected $response;
/** @var Request */
protected $request;
/**
* @covers \Engelsystem\Controllers\Admin\NewsController::edit
*/
public function testEditHtmlWarning()
{
$this->request->attributes->set('id', 1);
$this->response->expects($this->once())
->method('withView')
->willReturnCallback(function ($view, $data) {
$this->assertEquals('pages/news/edit.twig', $view);
/** @var Collection $warnings */
$warnings = $data['warnings'];
$this->assertNotEmpty($data['news']);
$this->assertTrue($warnings->isNotEmpty());
$this->assertEquals('news.edit.contains-html', $warnings->first());
return $this->response;
});
$this->addUser();
/** @var NewsController $controller */
$controller = $this->app->make(NewsController::class);
$controller->edit($this->request);
}
/**
* @covers \Engelsystem\Controllers\Admin\NewsController::__construct
* @covers \Engelsystem\Controllers\Admin\NewsController::edit
*/
public function testEdit()
{
$this->request->attributes->set('id', 1);
$this->response->expects($this->once())
->method('withView')
->willReturnCallback(function ($view, $data) {
$this->assertEquals('pages/news/edit.twig', $view);
/** @var Collection $warnings */
$warnings = $data['warnings'];
$this->assertNotEmpty($data['news']);
$this->assertTrue($warnings->isEmpty());
return $this->response;
});
$this->auth->expects($this->once())
->method('can')
->with('admin_news_html')
->willReturn(true);
/** @var NewsController $controller */
$controller = $this->app->make(NewsController::class);
$controller->edit($this->request);
}
/**
* @covers \Engelsystem\Controllers\Admin\NewsController::save
*/
public function testSaveCreateInvalid()
{
/** @var NewsController $controller */
$controller = $this->app->make(NewsController::class);
$controller->setValidator(new Validator());
$this->expectException(ValidationException::class);
$controller->save($this->request);
}
/**
* @return array
*/
public function saveCreateEditProvider(): array
{
return [
['Some <b>test</b>', true, true, 'Some <b>test</b>'],
['Some <b>test</b>', false, false, 'Some test'],
['Some <b>test</b>', false, true, 'Some <b>test</b>', 1],
['Some <b>test</b>', true, false, 'Some test', 1],
];
}
/**
* @covers \Engelsystem\Controllers\Admin\NewsController::save
* @dataProvider saveCreateEditProvider
*
* @param string $text
* @param bool $isMeeting
* @param bool $canEditHtml
* @param string $result
* @param int|null $id
*/
public function testSaveCreateEdit(
string $text,
bool $isMeeting,
bool $canEditHtml,
string $result,
int $id = null
) {
$this->request->attributes->set('id', $id);
$id = $id ?: 2;
$this->request = $this->request->withParsedBody([
'title' => 'Some Title',
'text' => $text,
'is_meeting' => $isMeeting ? '1' : null,
]);
$this->addUser();
$this->auth->expects($this->once())
->method('can')
->with('admin_news_html')
->willReturn($canEditHtml);
$this->response->expects($this->once())
->method('redirectTo')
->with('/news/' . $id)
->willReturn($this->response);
/** @var NewsController $controller */
$controller = $this->app->make(NewsController::class);
$controller->setValidator(new Validator());
$controller->save($this->request);
$this->assertTrue($this->log->hasInfoThatContains('Updated'));
/** @var Session $session */
$session = $this->app->get('session');
$messages = $session->get('messages');
$this->assertEquals('news.edit.success', $messages[0]);
$news = (new News())->find($id);
$this->assertEquals($result, $news->text);
$this->assertEquals($isMeeting, (bool)$news->is_meeting);
}
/**
* @covers \Engelsystem\Controllers\Admin\NewsController::save
*/
public function testSaveDelete()
{
$this->request->attributes->set('id', 1);
$this->request = $this->request->withParsedBody([
'title' => '.',
'text' => '.',
'delete' => '1',
]);
$this->response->expects($this->once())
->method('redirectTo')
->with('/news')
->willReturn($this->response);
/** @var NewsController $controller */
$controller = $this->app->make(NewsController::class);
$controller->setValidator(new Validator());
$controller->save($this->request);
$this->assertTrue($this->log->hasInfoThatContains('Deleted'));
/** @var Session $session */
$session = $this->app->get('session');
$messages = $session->get('messages');
$this->assertEquals('news.delete.success', $messages[0]);
}
/**
* Setup environment
*/
public function setUp(): void
{
parent::setUp();
$this->initDatabase();
$this->request = new Request();
$this->app->instance(Request::class, $this->request);
$this->app->instance(ServerRequestInterface::class, $this->request);
$this->response = $this->createMock(Response::class);
$this->app->instance(Response::class, $this->response);
$this->log = new TestLogger();
$this->app->instance(LoggerInterface::class, $this->log);
$this->app->instance('session', new Session(new MockArraySessionStorage()));
$this->auth = $this->createMock(Authenticator::class);
$this->app->instance(Authenticator::class, $this->auth);
(new News([
'title' => 'Foo',
'text' => '<b>foo</b>',
'is_meeting' => false,
'user_id' => 1,
]))->save();
}
/**
* Creates a new user
*/
protected function addUser()
{
$user = new User([
'name' => 'foo',
'password' => '',
'email' => '',
'api_key' => '',
'last_login_at' => null,
]);
$user->forceFill(['id' => 42]);
$user->save();
$this->auth->expects($this->any())
->method('user')
->willReturn($user);
}
}

View File

@ -0,0 +1,274 @@
<?php
namespace Engelsystem\Test\Unit\Controllers;
use Engelsystem\Config\Config;
use Engelsystem\Controllers\NewsController;
use Engelsystem\Helpers\Authenticator;
use Engelsystem\Http\Exceptions\ValidationException;
use Engelsystem\Http\Request;
use Engelsystem\Http\Response;
use Engelsystem\Http\Validation\Validator;
use Engelsystem\Models\News;
use Engelsystem\Models\NewsComment;
use Engelsystem\Models\User\User;
use Engelsystem\Test\Unit\HasDatabase;
use Engelsystem\Test\Unit\TestCase;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Collection;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\Test\TestLogger;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
class NewsControllerTest extends TestCase
{
use HasDatabase;
/** @var Authenticator|MockObject */
protected $auth;
/** @var array */
protected $data = [
[
'title' => 'Foo',
'text' => 'foo',
'is_meeting' => false,
'user_id' => 1,
],
[
'title' => 'Bar',
'text' => 'bar',
'is_meeting' => false,
'user_id' => 1,
],
[
'title' => 'baz',
'text' => 'baz',
'is_meeting' => true,
'user_id' => 1,
],
[
'title' => 'Lorem',
'text' => 'lorem',
'is_meeting' => false,
'user_id' => 1,
],
[
'title' => 'Ipsum',
'text' => 'ipsum',
'is_meeting' => true,
'user_id' => 1,
],
[
'title' => 'Dolor',
'text' => 'test',
'is_meeting' => true,
'user_id' => 1,
],
];
/** @var TestLogger */
protected $log;
/** @var Response|MockObject */
protected $response;
/** @var Request */
protected $request;
/**
* @covers \Engelsystem\Controllers\NewsController::__construct
* @covers \Engelsystem\Controllers\NewsController::index
* @covers \Engelsystem\Controllers\NewsController::meetings
* @covers \Engelsystem\Controllers\NewsController::showOverview
* @covers \Engelsystem\Controllers\NewsController::renderView
*/
public function testIndex()
{
$this->request->attributes->set('page', 2);
/** @var NewsController $controller */
$controller = $this->app->make(NewsController::class);
$n = 1;
$this->response->expects($this->exactly(3))
->method('withView')
->willReturnCallback(
function (string $page, array $data) use (&$n) {
$this->assertEquals('pages/news/overview.twig', $page);
/** @var Collection $news */
$news = $data['news'];
switch ($n) {
case 1:
// Show everything
$this->assertFalse($data['only_meetings']);
$this->assertTrue($news->isNotEmpty());
$this->assertEquals(3, $data['pages']);
$this->assertEquals(2, $data['page']);
break;
case 2:
// Show meetings
$this->assertTrue($data['only_meetings']);
$this->assertTrue($news->isNotEmpty());
$this->assertEquals(1, $data['pages']);
$this->assertEquals(1, $data['page']);
break;
default:
// No news found
$this->assertTrue($news->isEmpty());
$this->assertEquals(1, $data['pages']);
$this->assertEquals(1, $data['page']);
}
$n++;
return $this->response;
}
);
$controller->index();
$controller->meetings();
News::query()->truncate();
$controller->index();
}
/**
* @covers \Engelsystem\Controllers\NewsController::show
*/
public function testShow()
{
$this->request->attributes->set('id', 1);
$this->response->expects($this->once())
->method('withView')
->with('pages/news/news.twig')
->willReturn($this->response);
/** @var NewsController $controller */
$controller = $this->app->make(NewsController::class);
$controller->show($this->request);
}
/**
* @covers \Engelsystem\Controllers\NewsController::show
*/
public function testShowNotFound()
{
$this->request->attributes->set('id', 42);
/** @var NewsController $controller */
$controller = $this->app->make(NewsController::class);
$this->expectException(ModelNotFoundException::class);
$controller->show($this->request);
}
/**
* @covers \Engelsystem\Controllers\NewsController::comment
*/
public function testCommentInvalid()
{
/** @var NewsController $controller */
$controller = $this->app->make(NewsController::class);
$controller->setValidator(new Validator());
$this->expectException(ValidationException::class);
$controller->comment($this->request);
}
/**
* @covers \Engelsystem\Controllers\NewsController::comment
*/
public function testCommentNewsNotFound()
{
$this->request->attributes->set('id', 42);
$this->request = $this->request->withParsedBody(['comment' => 'Foo bar!']);
$this->addUser();
/** @var NewsController $controller */
$controller = $this->app->make(NewsController::class);
$controller->setValidator(new Validator());
$this->expectException(ModelNotFoundException::class);
$controller->comment($this->request);
}
/**
* @covers \Engelsystem\Controllers\NewsController::comment
*/
public function testComment()
{
$this->request->attributes->set('id', 1);
$this->request = $this->request->withParsedBody(['comment' => 'Foo bar!']);
$this->addUser();
$this->response->expects($this->once())
->method('redirectTo')
->willReturn($this->response);
/** @var NewsController $controller */
$controller = $this->app->make(NewsController::class);
$controller->setValidator(new Validator());
$controller->comment($this->request);
$this->log->hasInfoThatContains('Created news comment');
/** @var NewsComment $comment */
$comment = NewsComment::whereNewsId(1)->first();
$this->assertEquals('Foo bar!', $comment->text);
}
/**
* Setup environment
*/
public function setUp(): void
{
parent::setUp();
$this->initDatabase();
$this->request = new Request();
$this->app->instance(Request::class, $this->request);
$this->app->instance(ServerRequestInterface::class, $this->request);
$this->response = $this->createMock(Response::class);
$this->app->instance(Response::class, $this->response);
$this->app->instance(Config::class, new Config(['display_news' => 2]));
$this->log = new TestLogger();
$this->app->instance(LoggerInterface::class, $this->log);
$this->app->instance('session', new Session(new MockArraySessionStorage()));
$this->auth = $this->createMock(Authenticator::class);
$this->app->instance(Authenticator::class, $this->auth);
foreach ($this->data as $news) {
(new News($news))->save();
}
}
/**
* Creates a new user
*/
protected function addUser()
{
$user = new User([
'name' => 'foo',
'password' => '',
'email' => '',
'api_key' => '',
'last_login_at' => null,
]);
$user->forceFill(['id' => 42]);
$user->save();
$this->auth->expects($this->any())
->method('user')
->willReturn($user);
}
}