diff --git a/config/config.default.php b/config/config.default.php
index 5b146553..ea94cce8 100644
--- a/config/config.default.php
+++ b/config/config.default.php
@@ -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
diff --git a/config/routes.php b/config/routes.php
index f8b9feed..a524a196 100644
--- a/config/routes.php
+++ b/config/routes.php
@@ -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');
+ }
+ );
}
);
diff --git a/includes/includes.php b/includes/includes.php
index 7ec1f768..017e5c3e 100644
--- a/includes/includes.php
+++ b/includes/includes.php
@@ -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',
diff --git a/includes/pages/admin_news.php b/includes/pages/admin_news.php
deleted file mode 100644
index b96aaedb..00000000
--- a/includes/pages/admin_news.php
+++ /dev/null
@@ -1,86 +0,0 @@
-has('action')) {
- throw_redirect(page_link_to('news'));
- }
-
- $html = '
' . __('Edit news entry') . '
' . 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 .= '
'
- . ' ' . __('Delete')
- . '';
- 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 . '
';
-}
diff --git a/includes/pages/user_atom.php b/includes/pages/user_atom.php
index 9a4d65a5..bbd7e7d4 100644
--- a/includes/pages/user_atom.php
+++ b/includes/pages/user_atom.php
@@ -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 '
' . htmlspecialchars($news->title) . '
-
+
' . preg_replace(
'#^https?://#',
'',
- page_link_to('news_comments', ['nid' => $news->id])
+ page_link_to('news/' . $news->id)
) . '
' . $news->updated_at->format('Y-m-d\TH:i:sP') . '
' . htmlspecialchars($news->text) . '
diff --git a/includes/pages/user_news.php b/includes/pages/user_news.php
deleted file mode 100644
index 771311fe..00000000
--- a/includes/pages/user_news.php
+++ /dev/null
@@ -1,247 +0,0 @@
-';
- $html .= '' . meetings_title() . '
' . 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 .= '' . '
';
-
- 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", '
', $news->text);
- return $text;
-}
-
-/**
- * @param News $news
- * @return string
- */
-function display_news(News $news): string
-{
- $html = '';
- $html .= '';
- $html .= '
';
- $html .= '
' . ($news->is_meeting ? '[Meeting] ' : '') . $news->title . '
';
- $html .= '';
- $html .= '
' . news_text($news) . '
';
-
- $html .= '';
- $html .= '
';
- return $html;
-}
-
-/**
- * @return string
- */
-function user_news_comments()
-{
- $user = auth()->user();
- $request = request();
-
- $html = '';
- $html .= '
' . user_news_comments_title() . '
';
- $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 .= '
';
- $html .= '
' . nl2br(htmlspecialchars($comment->text)) . '
';
- $html .= '';
- $html .= '
';
- }
-
- $html .= '
' . __('New Comment:') . '
';
- $html .= form([
- form_textarea('text', __('Message'), ''),
- form_submit('submit', __('Save'))
- ], page_link_to('news_comments', ['nid' => $news->id]));
- } else {
- $html .= __('Invalid request.');
- }
-
- return $html . '
';
-}
-
-/**
- * @return string
- */
-function user_news()
-{
- $user = auth()->user();
- $display_news = config('display_news');
- $request = request();
-
- $html = '';
- $html .= '
' . news_title() . '
' . 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 .= '
' . '
';
-
- if (auth()->can('admin_news')) {
- $html .= '
';
- $html .= '
' . __('Create news:') . '
';
-
- $html .= form([
- form_text('betreff', __('Subject'), ''),
- form_textarea('text', __('Message'), ''),
- form_checkbox('treffen', __('Meeting'), false, 1),
- form_submit('submit', __('Save'))
- ]);
- }
- return $html . '
';
-}
diff --git a/includes/sys_menu.php b/includes/sys_menu.php
index f883a797..7c422890 100644
--- a/includes/sys_menu.php
+++ b/includes/sys_menu.php
@@ -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);
}
}
diff --git a/resources/lang/de_DE/additional.po b/resources/lang/de_DE/additional.po
index ffbd7792..429d6093 100644
--- a/resources/lang/de_DE/additional.po
+++ b/resources/lang/de_DE/additional.po
@@ -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!"
diff --git a/resources/lang/de_DE/default.po b/resources/lang/de_DE/default.po
index 20299c2c..75dc4a80 100644
--- a/resources/lang/de_DE/default.po
+++ b/resources/lang/de_DE/default.po
@@ -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"
diff --git a/resources/lang/en_US/additional.po b/resources/lang/en_US/additional.po
index fa49ffdf..f898cd39 100644
--- a/resources/lang/en_US/additional.po
+++ b/resources/lang/en_US/additional.po
@@ -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!"
diff --git a/resources/lang/en_US/default.po b/resources/lang/en_US/default.po
index 4ee92c78..bafb2621 100644
--- a/resources/lang/en_US/default.po
+++ b/resources/lang/en_US/default.po
@@ -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"
diff --git a/resources/views/layouts/app.twig b/resources/views/layouts/app.twig
index dc02e3ed..17d9f34b 100644
--- a/resources/views/layouts/app.twig
+++ b/resources/views/layouts/app.twig
@@ -12,9 +12,9 @@
- {% 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 %}
diff --git a/resources/views/macros/base.twig b/resources/views/macros/base.twig
index 94287bd4..6339be91 100644
--- a/resources/views/macros/base.twig
+++ b/resources/views/macros/base.twig
@@ -9,3 +9,17 @@
{% macro alert(message, type) %}
{{ message }}
{% endmacro %}
+
+{% macro user(user) %}
+
+ {{ _self.angel() }} {{ user.name }}
+
+{% endmacro %}
+
+{% macro button(label, url, type, size) %}
+
+ {{ label }}
+
+{% endmacro %}
diff --git a/resources/views/macros/form.twig b/resources/views/macros/form.twig
index ece85fcf..8965e92a 100644
--- a/resources/views/macros/form.twig
+++ b/resources/views/macros/form.twig
@@ -13,6 +13,22 @@
{%- endmacro %}
+{% macro textarea(name, label, opt) %}
+
+ {% if label -%}
+
+ {%- endif %}
+
+
+{%- endmacro %}
+
{% macro select(name, data, label, selected) %}
{% if label -%}
@@ -26,10 +42,30 @@
{%- endmacro %}
+{% macro checkbox(name, label, checked, value) %}
+
+
+
+{%- endmacro %}
+
{% macro hidden(name, value) %}
{%- endmacro %}
-{% macro submit(label) %}
-
+{% macro button(label, opt) %}
+
+{%- endmacro %}
+
+{% macro submit(label, opt) %}
+ {{ _self.button(label|default(__('form.submit')), opt|default({})|merge({'type': 'submit'})) }}
{%- endmacro %}
diff --git a/resources/views/pages/news/edit.twig b/resources/views/pages/news/edit.twig
new file mode 100644
index 00000000..52b2aaae
--- /dev/null
+++ b/resources/views/pages/news/edit.twig
@@ -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 %}
+
+
{{ block('title') }}
+
+ {% include 'layouts/parts/messages.twig' %}
+
+ {% if news %}
+
+
+
+ {{ m.glyphicon('time') }} {{ news.updated_at.format(__('Y-m-d H:i')) }}
+
+ {% if news.updated_at != news.created_at %}
+ {{ __('news.updated') }}
+
+ {{ m.glyphicon('time') }} {{ news.created_at.format(__('Y-m-d H:i')) }}
+ {% endif %}
+
+ {{ m.user(news.user) }}
+
+
+
+ {% endif %}
+
+
+
+{% endblock %}
diff --git a/resources/views/pages/news/news.twig b/resources/views/pages/news/news.twig
new file mode 100644
index 00000000..f426dfa0
--- /dev/null
+++ b/resources/views/pages/news/news.twig
@@ -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 %}
+
+
{{ __('news.comments') }}
+
+ {% for comment in news.comments %}
+
+
+ {{ comment.text|nl2br }}
+
+
+
+ {% endfor %}
+
+{% endblock %}
+
+{% block write_comment %}
+ {% if has_permission_to('news_comments') %}
+
+
{{ __('news.comments.new') }}
+
+
+
+ {% endif %}
+{% endblock %}
diff --git a/resources/views/pages/news/overview.twig b/resources/views/pages/news/overview.twig
new file mode 100644
index 00000000..d14d853d
--- /dev/null
+++ b/resources/views/pages/news/overview.twig
@@ -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 %}
+
+
+ {{ block('title') }}
+ {%- if has_permission_to('admin_news') and is_overview|default(false) -%}
+ {{ m.button(__('news.add'), url('admin/news')) }}
+ {%- endif %}
+
+
+ {% include 'layouts/parts/messages.twig' %}
+
+
+
+ {% block news %}
+ {% for news in news %}
+ {{ _self.news(news, true, is_overview) }}
+ {% endfor %}
+ {% endblock %}
+
+
+ {% block comments %}
+ {% endblock %}
+
+ {% block pagination %}
+ {% if pages|default(0) > 1 %}
+
+
+
+ {% endif %}
+ {% endblock %}
+
+ {% block write_comment %}
+ {% endblock %}
+
+
+{% endblock %}
+
+{% macro news(news, show_comments_link, is_overview) %}
+
+ {% if is_overview|default(false) %}
+
+ {% endif %}
+
+
+ {{ news.text|raw|nl2br }}
+
+
+
+
+{% endmacro %}
diff --git a/src/Controllers/Admin/NewsController.php b/src/Controllers/Admin/NewsController.php
new file mode 100644
index 00000000..05f1ea3c
--- /dev/null
+++ b/src/Controllers/Admin/NewsController.php
@@ -0,0 +1,140 @@
+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);
+ }
+}
diff --git a/src/Controllers/NewsController.php b/src/Controllers/NewsController.php
new file mode 100644
index 00000000..4d0b6cb8
--- /dev/null
+++ b/src/Controllers/NewsController.php
@@ -0,0 +1,181 @@
+ '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);
+ }
+}
diff --git a/src/Middleware/LegacyMiddleware.php b/src/Middleware/LegacyMiddleware.php
index 5e08858d..bb2e73f4 100644
--- a/src/Middleware/LegacyMiddleware.php
+++ b/src/Middleware/LegacyMiddleware.php
@@ -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();
diff --git a/tests/Unit/Controllers/Admin/NewsControllerTest.php b/tests/Unit/Controllers/Admin/NewsControllerTest.php
new file mode 100644
index 00000000..3a8f0d30
--- /dev/null
+++ b/tests/Unit/Controllers/Admin/NewsControllerTest.php
@@ -0,0 +1,265 @@
+ 'Foo',
+ 'text' => 'foo',
+ '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 test', true, true, 'Some test'],
+ ['Some test', false, false, 'Some test'],
+ ['Some test', false, true, 'Some test', 1],
+ ['Some test', 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' => 'foo',
+ '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);
+ }
+}
diff --git a/tests/Unit/Controllers/NewsControllerTest.php b/tests/Unit/Controllers/NewsControllerTest.php
new file mode 100644
index 00000000..3301042b
--- /dev/null
+++ b/tests/Unit/Controllers/NewsControllerTest.php
@@ -0,0 +1,274 @@
+ '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);
+ }
+}