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 .= '
' . '
    '; - for ($i = 0; $i < $dis_rows; $i++) { - if ($request->has('page') && $i == $request->input('page', 0)) { - $html .= '
  • '; - } elseif (!$request->has('page') && $i == 0) { - $html .= '
  • '; - } else { - $html .= '
  • '; - } - $html .= '' . ($i + 1) . '
  • '; - } - $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 %} + +
+ {{ csrf() }} + +
+
+ {{ f.input( + 'title', + __('news.edit.subject'), + null, + {'required': true, 'value': news ? news.title : ''} + ) }} +
+
+ {{ f.checkbox('is_meeting', __('news.edit.is_meeting'), news ? news.is_meeting : false) }} +
+
+ +
+
+ {{ 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 %} +
+
+ +
+
+{% 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') }}

+ +
+ {{ csrf() }} + + {{ f.textarea('comment', __('news.comments.message'), {'required': true}) }} + + {{ f.submit() }} +
+
+ {% 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 %} +
+
    + {% for p in range(1, pages) %} + + {{ p }} + + {% endfor %} +
+
+ {% 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); + } +}