diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | README.md | 19 | ||||
-rw-r--r-- | contrib/Dockerfile | 9 | ||||
-rw-r--r-- | contrib/docker-compose.yml | 3 | ||||
-rw-r--r-- | includes/controller/shifts_controller.php | 16 | ||||
-rw-r--r-- | includes/helper/error_helper.php | 11 | ||||
-rw-r--r-- | includes/includes.php | 1 | ||||
-rw-r--r-- | includes/pages/user_atom.php | 16 | ||||
-rw-r--r-- | includes/pages/user_ical.php | 17 | ||||
-rw-r--r-- | package.json | 2 | ||||
-rw-r--r-- | resources/assets/js/forms.js | 35 | ||||
-rw-r--r-- | resources/assets/js/vendor.js | 7 | ||||
-rw-r--r-- | resources/lang/de_DE/default.mo | bin | 46206 -> 0 bytes | |||
-rw-r--r-- | resources/lang/en_US/default.mo | bin | 770 -> 0 bytes | |||
-rw-r--r-- | resources/lang/pt_BR/default.mo | bin | 41129 -> 0 bytes | |||
-rw-r--r-- | resources/views/layouts/parts/navbar.twig | 18 | ||||
-rw-r--r-- | resources/views/pages/login.twig | 8 | ||||
-rw-r--r-- | resources/views/pages/user-shifts.html | 8 | ||||
-rw-r--r-- | src/Helpers/Translation/TranslationServiceProvider.php | 25 | ||||
-rw-r--r-- | tests/Unit/Helpers/Translation/Assets/ba_RR/default.po | 3 | ||||
-rw-r--r-- | tests/Unit/Helpers/Translation/TranslationServiceProviderTest.php | 22 | ||||
-rw-r--r-- | yarn.lock | 2 |
22 files changed, 159 insertions, 64 deletions
@@ -24,6 +24,7 @@ _vimrc_local.vim /public/coverage /coverage /unittests.xml +/resources/lang/*/*.mo # Composer files /vendor/ @@ -58,6 +58,11 @@ The following instructions explain how to get, build and run the latest engelsys ```bash yarn build ``` + * Optionally (for better performance) + * Generate translation files + ```bash + find resources/lang/ -type f -name '*.po' -exec sh -c 'file="{}"; msgfmt "${file%.*}.po" -o "${file%.*}.mo"' \; + ``` ### Configuration and Setup * The webserver must have write access to the ```import``` and ```storage``` directories and read access for all other directories @@ -127,6 +132,20 @@ Import database docker exec -it engelsystem bin/migrate ``` +#### Local development +To use the working directory in the container the docker-compose file has to be changed: +```yaml +[...] + nginx: + volumes: + - ../public/assets:/var/www/public/assets +[...] + engelsystem: + volumes: + - ../:/var/www +[...] +``` + #### Scripts ##### bin/deploy.sh The `bin/deploy.sh` script can be used to deploy the engelsystem. It uses rsync to deploy the application to a server over ssh. diff --git a/contrib/Dockerfile b/contrib/Dockerfile index a7fd403b..013ccf1d 100644 --- a/contrib/Dockerfile +++ b/contrib/Dockerfile @@ -4,7 +4,12 @@ COPY ./ /app/ RUN composer --no-ansi install --no-dev --ignore-platform-reqs RUN composer --no-ansi dump-autoload --optimize -# Intermediate container for less layers +# Intermediate containers for less layers +FROM alpine as translation +RUN apk add gettext +COPY resources/lang/ /data +RUN find /data -type f -name '*.po' -exec sh -c 'file="{}"; msgfmt "${file%.*}.po" -o "${file%.*}.mo"' \; + FROM alpine as data COPY .babelrc .browserslistrc composer.json LICENSE package.json README.md webpack.config.js yarn.lock /app/ COPY bin/ /app/bin @@ -13,11 +18,11 @@ COPY db/ /app/db RUN mkdir /app/import/ COPY includes/ /app/includes COPY public/ /app/public -COPY resources/lang /app/resources/lang COPY resources/views /app/resources/views COPY src/ /app/src COPY storage/ /app/storage +COPY --from=translation /data/ /app/resources/lang COPY --from=composer /app/vendor/ /app/vendor COPY --from=composer /app/composer.lock /app/ diff --git a/contrib/docker-compose.yml b/contrib/docker-compose.yml index 197e2281..4624cce4 100644 --- a/contrib/docker-compose.yml +++ b/contrib/docker-compose.yml @@ -27,12 +27,13 @@ services: depends_on: - database database: - image: mariadb:latest + image: mariadb:10.2 environment: MYSQL_DATABASE: engelsystem MYSQL_USER: engelsystem MYSQL_PASSWORD: engelsystem MYSQL_RANDOM_ROOT_PASSWORD: 1 + MYSQL_INITDB_SKIP_TZINFO: "yes" volumes: - db:/var/lib/mysql networks: diff --git a/includes/controller/shifts_controller.php b/includes/controller/shifts_controller.php index caf124ba..726814cf 100644 --- a/includes/controller/shifts_controller.php +++ b/includes/controller/shifts_controller.php @@ -1,5 +1,6 @@ <?php +use Engelsystem\Http\Exceptions\HttpForbidden; use Engelsystem\ShiftSignupState; /** @@ -348,17 +349,18 @@ function shift_next_controller() function shifts_json_export_controller() { $request = request(); + $user = auth()->apiUser('key'); - if (!$request->has('key') || !preg_match('/^[\da-f]{32}$/', $request->input('key'))) { - engelsystem_error('Missing key.'); + if ( + !$request->has('key') + || !preg_match('/^[\da-f]{32}$/', $request->input('key')) + || !$user + ) { + throw new HttpForbidden('{"error":"Missing or invalid key"}', ['content-type' => 'application/json']); } - $user = auth()->apiUser('key'); - if (!$user) { - engelsystem_error('Key invalid.'); - } if (!auth()->can('shifts_json_export')) { - engelsystem_error('No privilege for shifts_json_export.'); + throw new HttpForbidden('{"error":"Not allowed"}', ['content-type' => 'application/json']); } $shifts = load_ical_shifts(); diff --git a/includes/helper/error_helper.php b/includes/helper/error_helper.php deleted file mode 100644 index 9314a57a..00000000 --- a/includes/helper/error_helper.php +++ /dev/null @@ -1,11 +0,0 @@ -<?php - -/** - * Displays a fatal message and stops execution. - * - * @param string $message - */ -function engelsystem_error($message) -{ - raw_output($message); -} diff --git a/includes/includes.php b/includes/includes.php index 855ff359..601a6ca2 100644 --- a/includes/includes.php +++ b/includes/includes.php @@ -60,7 +60,6 @@ $includeFiles = [ __DIR__ . '/../includes/helper/graph_helper.php', __DIR__ . '/../includes/helper/message_helper.php', - __DIR__ . '/../includes/helper/error_helper.php', __DIR__ . '/../includes/helper/email_helper.php', __DIR__ . '/../includes/mailer/shifts_mailer.php', diff --git a/includes/pages/user_atom.php b/includes/pages/user_atom.php index 8e5b4858..a491fea7 100644 --- a/includes/pages/user_atom.php +++ b/includes/pages/user_atom.php @@ -1,6 +1,7 @@ <?php use Engelsystem\Database\DB; +use Engelsystem\Http\Exceptions\HttpForbidden; /** * Publically available page to feed the news to feed readers @@ -8,17 +9,18 @@ use Engelsystem\Database\DB; function user_atom() { $request = request(); + $user = auth()->apiUser('key'); - if (!$request->has('key') || !preg_match('/^[\da-f]{32}$/', $request->input('key'))) { - engelsystem_error('Missing key.'); + if ( + !$request->has('key') + || !preg_match('/^[\da-f]{32}$/', $request->input('key')) + || empty($user) + ) { + throw new HttpForbidden('Missing or invalid key', ['content-type' => 'text/text']); } - $user = auth()->apiUser('key'); - if (empty($user)) { - engelsystem_error('Key invalid.'); - } if (!auth()->can('atom')) { - engelsystem_error('No privilege for atom.'); + throw new HttpForbidden('Not allowed', ['content-type' => 'text/text']); } $news = DB::select(' diff --git a/includes/pages/user_ical.php b/includes/pages/user_ical.php index ee3a8340..2f3a7ccc 100644 --- a/includes/pages/user_ical.php +++ b/includes/pages/user_ical.php @@ -1,22 +1,25 @@ <?php +use Engelsystem\Http\Exceptions\HttpForbidden; + /** * Controller for ical output of users own shifts or any user_shifts filter. */ function user_ical() { $request = request(); + $user = auth()->apiUser('key'); - if (!$request->has('key') || !preg_match('/^[\da-f]{32}$/', $request->input('key'))) { - engelsystem_error('Missing key.'); + if ( + !$request->has('key') + || !preg_match('/^[\da-f]{32}$/', $request->input('key')) + || !$user + ) { + throw new HttpForbidden('Missing or invalid key', ['content-type' => 'text/text']); } - $user = auth()->apiUser('key'); - if (!$user) { - engelsystem_error('Key invalid.'); - } if (!auth()->can('ical')) { - engelsystem_error('No privilege for ical.'); + throw new HttpForbidden('Not allowed', ['content-type' => 'text/text']); } $ical_shifts = load_ical_shifts(); diff --git a/package.json b/package.json index 5a8a5a44..8a623a3b 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "eonasdan-bootstrap-datetimepicker": "^4.17.47", "jquery": "^3.3.1", "jquery-ui": "^1.11.2", - "moment": "^2.8.2", + "moment": "^2.12.0", "moment-timezone": "^0.4.0", "select2": "^4.0.6-rc.1", "select2-bootstrap-theme": "0.1.0-beta.10" diff --git a/resources/assets/js/forms.js b/resources/assets/js/forms.js index f5818e97..9970b907 100644 --- a/resources/assets/js/forms.js +++ b/resources/assets/js/forms.js @@ -16,7 +16,7 @@ global.checkAll = (id, checked) => { * Sets the checkboxes according to the given type * * @param {string} id The elements ID - * @param {list} shifts_list A list of numbers + * @param {list} shiftsList A list of numbers */ global.checkOwnTypes = (id, shiftsList) => { $('#' + id + ' input[type="checkbox"]').each(function () { @@ -144,10 +144,10 @@ $(function () { elem.children('input').on('click', function (ev) { ev.stopImmediatePropagation(); if (typeof elem.data('DateTimePicker') === 'undefined') { - elem.datetimepicker(opts); - elem.data('DateTimePicker').show(); + elem.datetimepicker(opts); + elem.data('DateTimePicker').show(); } else { - elem.data('DateTimePicker').toggle(); + elem.data('DateTimePicker').toggle(); } }); }); @@ -173,3 +173,30 @@ $(function () { }); }); }); + +/** + * Set the filter selects to latest state + * + * Uses DOMContentLoaded to prevent flickering + */ +window.addEventListener('DOMContentLoaded', () => { + const filter = document.getElementById('collapseShiftsFilterSelect'); + if (!filter || localStorage.getItem('collapseShiftsFilterSelect') !== 'hidden') { + return; + } + + filter.classList.remove('in'); +}); +$(() => { + if (typeof (localStorage) === 'undefined') { + return; + } + + const onChange = (e) => { + localStorage.setItem('collapseShiftsFilterSelect', e.type); + }; + + $('#collapseShiftsFilterSelect') + .on('hidden.bs.collapse', onChange) + .on('shown.bs.collapse', onChange); +}); diff --git a/resources/assets/js/vendor.js b/resources/assets/js/vendor.js index bf3807f7..b4b6487d 100644 --- a/resources/assets/js/vendor.js +++ b/resources/assets/js/vendor.js @@ -10,6 +10,13 @@ require('./forms'); require('./sticky-headers'); require('./moment-countdown'); +moment.updateLocale('en', { + week : { + dow : 1, // Monday is the first day of the week. + doy : 4 // The week that contains Jan 4th is the first week of the year. + } +}); + $.ajaxSetup({ headers: {'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')} }); diff --git a/resources/lang/de_DE/default.mo b/resources/lang/de_DE/default.mo Binary files differdeleted file mode 100644 index fb93d590..00000000 --- a/resources/lang/de_DE/default.mo +++ /dev/null diff --git a/resources/lang/en_US/default.mo b/resources/lang/en_US/default.mo Binary files differdeleted file mode 100644 index 7ef9c3b2..00000000 --- a/resources/lang/en_US/default.mo +++ /dev/null diff --git a/resources/lang/pt_BR/default.mo b/resources/lang/pt_BR/default.mo Binary files differdeleted file mode 100644 index 8b864156..00000000 --- a/resources/lang/pt_BR/default.mo +++ /dev/null diff --git a/resources/views/layouts/parts/navbar.twig b/resources/views/layouts/parts/navbar.twig index 61c6a10b..0b2eee63 100644 --- a/resources/views/layouts/parts/navbar.twig +++ b/resources/views/layouts/parts/navbar.twig @@ -55,16 +55,14 @@ {{ elements.toolbar_item(user.name, url('users', {'action': 'view'}), 'users', 'icon icon-icon_angel') }} {% endif %} - {% if has_permission_to('user_settings') or has_permission_to('logout') %} - <li class="dropdown"> - <a href="#" class="dropdown-toggle" data-toggle="dropdown"> - <span class="caret"></span> - </a> - <ul class="dropdown-menu" role="menu"> - {{ menuUserSubmenu()|join(" ")|raw }} - </ul> - </li> - {% endif %} + <li class="dropdown"> + <a href="#" class="dropdown-toggle" data-toggle="dropdown"> + <span class="caret"></span> + </a> + <ul class="dropdown-menu" role="menu"> + {{ menuUserSubmenu()|join(" ")|raw }} + </ul> + </li> </ul> {% endblock %} diff --git a/resources/views/pages/login.twig b/resources/views/pages/login.twig index da6f4fdf..88326429 100644 --- a/resources/views/pages/login.twig +++ b/resources/views/pages/login.twig @@ -50,7 +50,7 @@ <div class="input-group"> <span class="input-group-addon input-lg">{{ m.glyphicon('lock') }}</span> <input class="form-control input-lg" id="form_password" - type="password" name="password" value="" placeholder="{{ __('Password') }}"> + type="password" name="password" value="" placeholder="{{ __('Password') }}"> </div> </div> @@ -93,8 +93,10 @@ </a> </div> </div> - </div> - {{ m.glyphicon('info-sign') }} {{ __('Please note: You have to activate cookies!') }} + <div class="col-md-12 text-center"> + {{ m.glyphicon('info-sign') }} {{ __('Please note: You have to activate cookies!') }} + </div> + </div> </div> {% endblock %} diff --git a/resources/views/pages/user-shifts.html b/resources/views/pages/user-shifts.html index 9ac501da..d5a98f80 100644 --- a/resources/views/pages/user-shifts.html +++ b/resources/views/pages/user-shifts.html @@ -55,12 +55,12 @@ <div class="col-md-6"> <button class="btn btn-info btn-sm hidden-print" style="margin-top: 20px; margin-bottom:10px" type="button" data-toggle="collapse" - data-target="#collapseRoomSelect" aria-expanded="false" - aria-controls="collapseRoomSelect" + data-target="#collapseShiftsFilterSelect" aria-expanded="true" + aria-controls="collapseShiftsFilterSelect" > collapse/show filters </button> - <div class="collapse in" id="collapseRoomSelect"> + <div class="collapse in" id="collapseShiftsFilterSelect"> <div class="row"> <div class="col-xs-4 col-xxs-12">%room_select%</div> <div class="col-xs-4 col-xxs-12">%type_select%</div> @@ -79,5 +79,5 @@ %shifts_table% <div class="hidden-print"> -%ical_text% + %ical_text% </div>
\ No newline at end of file diff --git a/src/Helpers/Translation/TranslationServiceProvider.php b/src/Helpers/Translation/TranslationServiceProvider.php index 09337dad..62247000 100644 --- a/src/Helpers/Translation/TranslationServiceProvider.php +++ b/src/Helpers/Translation/TranslationServiceProvider.php @@ -5,6 +5,7 @@ namespace Engelsystem\Helpers\Translation; use Engelsystem\Config\Config; use Engelsystem\Container\ServiceProvider; use Gettext\Translations; +use Illuminate\Support\Str; use Symfony\Component\HttpFoundation\Session\Session; class TranslationServiceProvider extends ServiceProvider @@ -67,14 +68,18 @@ class TranslationServiceProvider extends ServiceProvider public function getTranslator(string $locale): GettextTranslator { if (!isset($this->translators[$locale])) { - $file = $this->app->get('path.lang') . '/' . $locale . '/default.mo'; + $file = $this->getFile($locale); /** @var GettextTranslator $translator */ $translator = $this->app->make(GettextTranslator::class); /** @var Translations $translations */ $translations = $this->app->make(Translations::class); - $translations->addFromMoFile($file); + if (Str::endsWith($file, '.mo')) { + $translations->addFromMoFile($file); + } else { + $translations->addFromPoFile($file); + } $translator->loadTranslations($translations); @@ -83,4 +88,20 @@ class TranslationServiceProvider extends ServiceProvider return $this->translators[$locale]; } + + /** + * @param string $locale + * @return string + */ + protected function getFile(string $locale): string + { + $filepath = $file = $this->app->get('path.lang') . '/' . $locale . '/default'; + $file = $filepath . '.mo'; + + if (!file_exists($file)) { + $file = $filepath . '.po'; + } + + return $file; + } } diff --git a/tests/Unit/Helpers/Translation/Assets/ba_RR/default.po b/tests/Unit/Helpers/Translation/Assets/ba_RR/default.po new file mode 100644 index 00000000..887e2daa --- /dev/null +++ b/tests/Unit/Helpers/Translation/Assets/ba_RR/default.po @@ -0,0 +1,3 @@ +# Testing content +msgid "foo.bar" +msgstr "B Arr!" diff --git a/tests/Unit/Helpers/Translation/TranslationServiceProviderTest.php b/tests/Unit/Helpers/Translation/TranslationServiceProviderTest.php index 91307bdd..dc8e8af5 100644 --- a/tests/Unit/Helpers/Translation/TranslationServiceProviderTest.php +++ b/tests/Unit/Helpers/Translation/TranslationServiceProviderTest.php @@ -12,7 +12,7 @@ use Symfony\Component\HttpFoundation\Session\Session; class TranslationServiceProviderTest extends ServiceProviderTest { /** - * @covers \Engelsystem\Helpers\Translation\TranslationServiceProvider::register() + * @covers \Engelsystem\Helpers\Translation\TranslationServiceProvider::register */ public function testRegister(): void { @@ -30,7 +30,7 @@ class TranslationServiceProviderTest extends ServiceProviderTest /** @var TranslationServiceProvider|MockObject $serviceProvider */ $serviceProvider = $this->getMockBuilder(TranslationServiceProvider::class) ->setConstructorArgs([$app]) - ->setMethods(['setLocale']) + ->onlyMethods(['setLocale']) ->getMock(); $app->expects($this->exactly(2)) @@ -71,7 +71,7 @@ class TranslationServiceProviderTest extends ServiceProviderTest } /** - * @covers \Engelsystem\Helpers\Translation\TranslationServiceProvider::getTranslator() + * @covers \Engelsystem\Helpers\Translation\TranslationServiceProvider::getTranslator */ public function testGetTranslator(): void { @@ -87,4 +87,20 @@ class TranslationServiceProviderTest extends ServiceProviderTest // Retry from cache $serviceProvider->getTranslator('fo_OO'); } + + /** + * @covers \Engelsystem\Helpers\Translation\TranslationServiceProvider::getTranslator + * @covers \Engelsystem\Helpers\Translation\TranslationServiceProvider::getFile + */ + public function testGetTranslatorFromPo(): void + { + $app = $this->getApp(['get']); + $this->setExpects($app, 'get', ['path.lang'], __DIR__ . '/Assets'); + + $serviceProvider = new TranslationServiceProvider($app); + + // Get translator using a .po file + $translator = $serviceProvider->getTranslator('ba_RR'); + $this->assertEquals('B Arr!', $translator->gettext('foo.bar')); + } } @@ -3328,7 +3328,7 @@ moment-timezone@^0.4.0: dependencies: moment ">= 2.6.0" -"moment@>= 2.6.0", moment@^2.10, moment@^2.10.2, moment@^2.8.2: +"moment@>= 2.6.0", moment@^2.10, moment@^2.10.2, moment@^2.12.0: version "2.24.0" resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== |