diff --git a/.github/workflows/linting.yaml b/.github/workflows/linting.yaml new file mode 100644 index 0000000..144e9ee --- /dev/null +++ b/.github/workflows/linting.yaml @@ -0,0 +1,25 @@ +name: CI +on: + pull_request: null + push: + branches: + - main +jobs: + tests: + runs-on: ubuntu-latest + strategy: + matrix: + php: ['8.1'] + + name: Linting - PHP ${{ matrix.php }} + steps: + - uses: actions/checkout@v3 + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + extensions: intl + - run: composer install --no-progress + - run: composer validate --strict --no-check-version + - run: composer codestyle-check + - run: composer phpstan diff --git a/.gitignore b/.gitignore index b8a2625..f40090b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,174 +1,5 @@ -# Created by https://www.toptal.com/developers/gitignore/api/phpstorm,composer,symfony - -### Composer ### -composer.phar -/vendor/ - -# Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control -# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file +.idea +vendor/ composer.lock - -### PhpStorm ### -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf - -# Generated files -.idea/**/contentModel.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/artifacts -# .idea/compiler.xml -# .idea/jarRepositories.xml -# .idea/modules.xml -# .idea/*.iml -# .idea/modules -# *.iml -# *.ipr - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format -*.iws - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser - -### PhpStorm Patch ### -# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 - -# *.iml -# modules.xml -# .idea/misc.xml -# *.ipr - -# Sonarlint plugin -# https://plugins.jetbrains.com/plugin/7973-sonarlint -.idea/**/sonarlint/ - -# SonarQube Plugin -# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin -.idea/**/sonarIssues.xml - -# Markdown Navigator plugin -# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced -.idea/**/markdown-navigator.xml -.idea/**/markdown-navigator-enh.xml -.idea/**/markdown-navigator/ - -# Cache file creation bug -# See https://youtrack.jetbrains.com/issue/JBR-2257 -.idea/$CACHE_FILE$ - -# CodeStream plugin -# https://plugins.jetbrains.com/plugin/12206-codestream -.idea/codestream.xml - -### Symfony ### -# Cache and logs (Symfony2) -/app/cache/* -/app/logs/* -!app/cache/.gitkeep -!app/logs/.gitkeep - -# Email spool folder -/app/spool/* - -# Cache, session files and logs (Symfony3) -/var/cache/* -/var/logs/* -/var/sessions/* -!var/cache/.gitkeep -!var/logs/.gitkeep -!var/sessions/.gitkeep - -# Logs (Symfony4) -/var/log/* -!var/log/.gitkeep - -# Parameters -/app/config/parameters.yml -/app/config/parameters.ini - -# Managed by Composer -/app/bootstrap.php.cache -/var/bootstrap.php.cache -/bin/* -!bin/console -!bin/symfony_requirements - -# Assets and user uploads -/web/bundles/ -/web/uploads/ - -# PHPUnit -/app/phpunit.xml -/phpunit.xml - -# Build data -/build/ - -# Composer PHAR -/composer.phar - -# Backup entities generated with doctrine:generate:entities command -**/Entity/*~ - -# Embedded web-server pid file -/.web-server-pid - -### Symfony Patch ### -/web/css/ -/web/js/ - - -### SharedProjectTimesheetsBundle ### -/tests/coverage/ -.phpunit.result.cache \ No newline at end of file +.php-cs-fixer.cache +.disabled diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..9d28b45 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,175 @@ +setRiskyAllowed(true) + ->setRules([ + 'encoding' => true, + 'full_opening_tag' => true, + 'blank_line_after_namespace' => true, + 'control_structure_braces' => true, + 'control_structure_continuation_position' => ['position' => 'same_line'], + 'declare_parentheses' => true, + 'no_multiple_statements_per_line' => true, + 'statement_indentation' => true, + 'class_definition' => true, + 'elseif' => true, + 'function_declaration' => true, + 'indentation_type' => true, + 'line_ending' => true, + 'constant_case' => ['case' => 'lower'], + 'lowercase_keywords' => true, + 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'], + 'header_comment' => ['header' => $fileHeaderComment, 'separate' => 'both'], + 'no_php4_constructor' => true, + 'ordered_imports' => true, + 'no_break_comment' => true, + 'no_closing_tag' => true, + 'no_spaces_after_function_name' => true, + 'no_spaces_inside_parenthesis' => true, + 'no_trailing_whitespace' => true, + 'no_trailing_whitespace_in_comment' => true, + 'single_blank_line_at_eof' => true, + 'single_class_element_per_statement' => ['elements' => ['property']], + 'single_import_per_statement' => true, + 'single_line_after_imports' => true, + 'switch_case_semicolon_to_colon' => true, + 'switch_case_space' => true, + 'array_syntax' => [ + 'syntax' => 'short' + ], + 'binary_operator_spaces' => true, + 'blank_line_after_opening_tag' => true, + 'blank_line_before_statement' => [ + 'statements' => ['return'], + ], + 'cast_spaces' => true, + 'class_attributes_separation' => ['elements' => ['method' => 'one']], + 'concat_space' => ['spacing' => 'one'], + 'declare_equal_normalize' => true, + 'function_typehint_space' => true, + 'include' => true, + 'lowercase_cast' => true, + 'lowercase_static_reference' => true, + 'magic_constant_casing' => true, + 'native_function_casing' => true, + 'new_with_braces' => true, + 'no_blank_lines_after_class_opening' => true, + 'no_blank_lines_after_phpdoc' => true, + 'no_empty_comment' => true, + 'no_empty_phpdoc' => true, + 'no_empty_statement' => true, + 'no_extra_blank_lines' => ['tokens' => [ + 'curly_brace_block', + 'extra', + 'parenthesis_brace_block', + 'square_brace_block', + 'throw', + 'use', + ]], + 'no_leading_import_slash' => true, + 'no_leading_namespace_whitespace' => true, + 'no_mixed_echo_print' => ['use' => 'echo'], + 'no_multiline_whitespace_around_double_arrow' => true, + 'no_short_bool_cast' => true, + 'no_singleline_whitespace_before_semicolons' => true, + 'no_spaces_around_offset' => true, + 'no_trailing_comma_in_singleline' => true, + 'no_unneeded_curly_braces' => true, + 'no_unneeded_final_method' => true, + 'no_unused_imports' => true, + 'no_whitespace_before_comma_in_array' => true, + 'no_whitespace_in_blank_line' => true, + 'normalize_index_brace' => true, + 'object_operator_without_whitespace' => true, + 'php_unit_fqcn_annotation' => true, + 'phpdoc_align' => [ + 'align' => 'left', + 'tags' => [ + 'method', + 'param', + 'property', + 'return', + 'throws', + 'type', + 'var', + ], + ], + 'phpdoc_annotation_without_dot' => true, + 'phpdoc_indent' => true, + 'phpdoc_inline_tag_normalizer' => true, + 'phpdoc_no_access' => true, + 'phpdoc_no_alias_tag' => true, + 'phpdoc_no_empty_return' => false, + 'phpdoc_no_package' => true, + 'phpdoc_no_useless_inheritdoc' => true, + 'phpdoc_return_self_reference' => true, + 'phpdoc_scalar' => true, + 'phpdoc_separation' => false, + 'phpdoc_single_line_var_spacing' => true, + 'phpdoc_summary' => false, + 'phpdoc_to_comment' => true, + 'phpdoc_trim' => true, + 'phpdoc_types' => true, + 'phpdoc_var_without_name' => true, + 'protected_to_private' => true, + 'return_type_declaration' => true, + 'semicolon_after_instruction' => true, + 'short_scalar_cast' => true, + 'single_blank_line_before_namespace' => true, + 'single_line_comment_style' => [ + 'comment_types' => ['hash'], + ], + 'single_quote' => true, + 'space_after_semicolon' => [ + 'remove_in_empty_for_expressions' => true, + ], + 'standardize_increment' => true, + 'standardize_not_equals' => true, + 'ternary_operator_spaces' => true, + 'trailing_comma_in_multiline' => false, + 'trim_array_spaces' => true, + 'unary_operator_spaces' => true, + 'whitespace_after_comma_in_array' => true, + 'yoda_style' => false, + 'ternary_to_null_coalescing' => true, + 'visibility_required' => ['elements' => [ + 'const', + 'method', + 'property', + ]], + 'native_function_invocation' => [ + 'include' => [ + '@compiler_optimized' + ], + 'scope' => 'namespaced' + ], + 'native_function_type_declaration_casing' => true, + 'no_alias_functions' => [ + 'sets' => [ + '@internal' + ] + ], + ]) + ->setFinder( + PhpCsFixer\Finder::create() + ->in([ + __DIR__ + ])->exclude([ + __DIR__ . '/Resources/', + __DIR__ . '/vendor/', + __DIR__ . '/.github/', + ]) + ) + ->setFormat('checkstyle') +; + +return $fixer; diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e51fc4..832814c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,22 +1,7 @@ # Changelog -## 2.1.0 +## 3.0.0 -- Reworked options to open and copy link #17 +Compatibility: requires minimum Kimai 2.0.26 -## 2.0.1 - -- Added missing translations - -## 2.0.0 - -- Added charts to public shared timesheet view #7, #8 -- Fixed db migration compatibility bug #19 - -Compatible with Kimai 1.15+ - -## 1.0.0 - -- Initial version - -Compatible with Kimai 1.11+ +- Compatibility with Kimai 2 diff --git a/Command/InstallCommand.php b/Command/InstallCommand.php index 88ce08a..b422061 100644 --- a/Command/InstallCommand.php +++ b/Command/InstallCommand.php @@ -1,6 +1,7 @@ pluginDir = $pluginDir; - } - - /** - * {@inheritdoc} - */ - protected function configure() - { - $this - ->setName(self::getDefaultName()) - ->setDescription('Install bundle/plugin ' . self::BUNDLE_IDENTIFIER) - ->setHelp('This command will install the ' . self::BUNDLE_NAME . ' database.') - ; - } - - /** - * @param InputInterface $input - * @param OutputInterface $output - * @return int|null - */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function getBundleCommandNamePart(): string { - $io = new SymfonyStyle($input, $output); - - try { - $this->importMigrations($io, $output); - } catch (Exception $ex) { - $io->error('Failed to install ' . self::BUNDLE_NAME . ' database: ' . $ex->getMessage()); - return 1; - } - - $io->success('Congratulations! ' . self:: BUNDLE_NAME . ' was successful installed!'); - return 0; + return 'shared-project-timesheets'; } - protected function importMigrations(SymfonyStyle $io, OutputInterface $output) + protected function getMigrationConfigFilename(): ?string { - $config = $this->pluginDir . '/' . self::BUNDLE_NAME . '/Migrations/' . self::BUNDLE_IDENTIFIER . '.yaml'; - - // prevent windows from breaking - $config = str_replace('/', DIRECTORY_SEPARATOR, $config); - - $command = $this->getApplication()->find('doctrine:migrations:migrate'); - $cmdInput = new ArrayInput(['--allow-no-migration' => true, '--configuration' => $config]); - $cmdInput->setInteractive(false); - $command->run($cmdInput, $output); - - $io->writeln(''); + return __DIR__ . '/../Migrations/shared-project-timesheets.yaml'; } - -} \ No newline at end of file +} diff --git a/Controller/ManageController.php b/Controller/ManageController.php index 9bbf9f9..21b8eba 100644 --- a/Controller/ManageController.php +++ b/Controller/ManageController.php @@ -1,6 +1,7 @@ shareProjectTimesheetRepository = $shareProjectTimesheetRepository; - $this->manageService = $manageService; - $this->translator = $translator; } - /** - * @Route(path="", name="manage_shared_project_timesheets", methods={"GET"}) - * @return \Symfony\Component\HttpFoundation\Response - */ - public function index() + #[Route(path: '', name: 'manage_shared_project_timesheets', methods: ['GET'])] + public function index(): Response { - $sharedProjects = $this->shareProjectTimesheetRepository->findAll(); - - return $this->render( - '@SharedProjectTimesheets/manage/index.html.twig', - [ - 'sharedProjects' => $sharedProjects, - 'mergeModeNone' => RecordMergeMode::MODE_NONE, - 'mergeModeMerge' => RecordMergeMode::MODE_MERGE, - 'mergeModeUseFirst' => RecordMergeMode::MODE_MERGE_USE_FIRST_OF_DAY, - 'mergeModeUseLast' => RecordMergeMode::MODE_MERGE_USE_LAST_OF_DAY, - 'RecordMergeMode' => RecordMergeMode::getModes(), - ] - ); + $query = new BaseQuery(); + + $sharedProjects = $this->shareProjectTimesheetRepository->findAllSharedProjects($query); + + $table = new DataTable('shared_project_timesheets_manage', $query); + $table->setPagination($sharedProjects); + $table->setReloadEvents('kimai.sharedProject'); + + $table->addColumn('name', ['class' => 'alwaysVisible', 'orderBy' => false]); + $table->addColumn('url', ['class' => 'alwaysVisible', 'orderBy' => false]); + $table->addColumn('password', ['class' => 'd-none', 'orderBy' => false]); + $table->addColumn('record_merge_mode', ['class' => 'd-none text-center w-min', 'orderBy' => false, 'title' => 'shared_project_timesheets.manage.table.record_merge_mode']); + $table->addColumn('entry_user_visible', ['class' => 'd-none text-center w-min', 'orderBy' => false, 'title' => 'shared_project_timesheets.manage.table.entry_user_visible']); + $table->addColumn('entry_rate_visible', ['class' => 'd-none text-center w-min', 'orderBy' => false, 'title' => 'shared_project_timesheets.manage.table.entry_rate_visible']); + $table->addColumn('annual_chart_visible', ['class' => 'd-none text-center w-min', 'orderBy' => false, 'title' => 'shared_project_timesheets.manage.table.annual_chart_visible']); + $table->addColumn('monthly_chart_visible', ['class' => 'd-none text-center w-min', 'orderBy' => false, 'title' => 'shared_project_timesheets.manage.table.monthly_chart_visible']); + + $table->addColumn('actions', ['class' => 'actions alwaysVisible']); + + $page = new PageSetup('shared_project_timesheets.manage.title'); + $page->setActionName('shared_projects'); + $page->setDataTable($table); + + return $this->render('@SharedProjectTimesheets/manage/index.html.twig', [ + 'page_setup' => $page, + 'dataTable' => $table, + 'RecordMergeMode' => RecordMergeMode::getModes(), + ]); } - /** - * @Route(path="/create", name="create_shared_project_timesheets", methods={"GET","POST"}) - * @param Request $request - * @return \Symfony\Component\HttpFoundation\Response - */ - public function create(Request $request) + #[Route(path: '/create', name: 'create_shared_project_timesheets', methods: ['GET', 'POST'])] + public function create(Request $request): Response { $sharedProject = new SharedProjectTimesheet(); - $form = $this->createForm(SharedProjectFormType::class, $sharedProject); + $form = $this->createForm(SharedProjectFormType::class, $sharedProject, [ + 'method' => 'POST', + 'action' => $this->generateUrl('create_shared_project_timesheets') + ]); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { try { $this->manageService->create($sharedProject, $form->get('password')->getData()); - $this->flashSuccess($this->translator->trans('shared_project_timesheets.manage.persist.success')); + $this->flashSuccess('action.update.success'); + return $this->redirectToRoute('manage_shared_project_timesheets'); - } catch (OptimisticLockException | ORMException $e) { - $this->logException($e); - $this->flashError($this->translator->trans('shared_project_timesheets.manage.persist.error')); + } catch (\Exception $e) { + $this->flashUpdateException($e); } } - return $this->render( - '@SharedProjectTimesheets/manage/edit.html.twig', - [ - 'form' => $form->createView(), - 'type' => 'create', - ] - ); + return $this->render('@SharedProjectTimesheets/manage/edit.html.twig', [ + 'entity' => $sharedProject, + 'form' => $form->createView(), + ]); } - /** - * @Route(path="/{projectId}/{shareKey}", name="update_shared_project_timesheets", methods={"GET", "POST"}) - * @param Request $request - * @return \Symfony\Component\HttpFoundation\Response - */ - public function update(Request $request) + #[Route(path: '/{projectId}/{shareKey}', name: 'update_shared_project_timesheets', methods: ['GET', 'POST'])] + public function update(string $projectId, string $shareKey, Request $request): Response { - $projectId = $request->get('projectId'); - $shareKey = $request->get('shareKey'); - if ($projectId == null || $shareKey == null) { - throw new NotFoundHttpException("Project not found"); + throw $this->createNotFoundException('Project not found'); } - /* @var $sharedProject SharedProjectTimesheet */ + /** @var SharedProjectTimesheet $sharedProject */ $sharedProject = $this->shareProjectTimesheetRepository->findOneBy(['project' => $projectId, 'shareKey' => $shareKey]); if ($sharedProject === null) { - throw new NotFoundHttpException("Project not found"); + throw $this->createNotFoundException('Given project not found'); } // Store data in temporary SharedProjectTimesheet object - $form = $this->createForm(SharedProjectFormType::class, $sharedProject); + $form = $this->createForm(SharedProjectFormType::class, $sharedProject, [ + 'method' => 'POST', + 'action' => $this->generateUrl('update_shared_project_timesheets', ['projectId' => $projectId, 'shareKey' => $shareKey]) + ]); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { try { $this->manageService->update($sharedProject, $form->get('password')->getData()); - $this->flashSuccess($this->translator->trans('shared_project_timesheets.manage.persist.success')); + $this->flashSuccess('action.update.success'); + return $this->redirectToRoute('manage_shared_project_timesheets'); - } catch (OptimisticLockException | ORMException $e) { - $this->logException($e); - $this->flashError($this->translator->trans('shared_project_timesheets.manage.persist.error')); + } catch (\Exception $e) { + $this->flashUpdateException($e); } - } else if ( !$form->isSubmitted() ) { - if ( !empty($sharedProject->getPassword()) ) { + } elseif (!$form->isSubmitted()) { + if (!empty($sharedProject->getPassword())) { $form->get('password')->setData(ManageService::PASSWORD_DO_NOT_CHANGE_VALUE); } } - return $this->render( - '@SharedProjectTimesheets/manage/edit.html.twig', - [ - 'form' => $form->createView(), - 'type' => 'update', - ] - ); + return $this->render('@SharedProjectTimesheets/manage/edit.html.twig', [ + 'entity' => $sharedProject, + 'form' => $form->createView(), + ]); } - /** - * @Route(path="/{projectId}/{shareKey}/remove", name="remove_shared_project_timesheets", methods={"GET"}) - * @param Request $request - * @return \Symfony\Component\HttpFoundation\Response - */ - public function remove(Request $request) + #[Route(path: '/{projectId}/{shareKey}/remove', name: 'remove_shared_project_timesheets', methods: ['GET', 'POST'])] + public function remove(Request $request): Response { $projectId = $request->get('projectId'); $shareKey = $request->get('shareKey'); if ($projectId == null || $shareKey == null) { - throw new NotFoundHttpException("Project not found"); + throw $this->createNotFoundException('Project not found'); } - /* @var $sharedProject SharedProjectTimesheet */ + /** @var SharedProjectTimesheet $sharedProject */ $sharedProject = $this->shareProjectTimesheetRepository->findOneBy(['project' => $projectId, 'shareKey' => $shareKey]); if (!$sharedProject || $sharedProject->getProject() === null || $sharedProject->getShareKey() === null) { - throw new NotFoundHttpException("Project not found"); + throw $this->createNotFoundException('Given project not found'); } try { $this->shareProjectTimesheetRepository->remove($sharedProject); - $this->flashSuccess($this->translator->trans('shared_project_timesheets.manage.persist.success')); - } catch (OptimisticLockException | ORMException $e) { - $this->logException($e); - $this->flashError($this->translator->trans('shared_project_timesheets.manage.persist.error')); + $this->flashSuccess('action.delete.success'); + } catch (\Exception $ex) { + $this->flashDeleteException($ex); } return $this->redirectToRoute('manage_shared_project_timesheets'); } - } diff --git a/Controller/ViewController.php b/Controller/ViewController.php index 5a036ba..e720ed0 100644 --- a/Controller/ViewController.php +++ b/Controller/ViewController.php @@ -1,6 +1,7 @@ viewService = $viewService; - $this->sharedProjectTimesheetRepository = $sharedProjectTimesheetRepository; } - /** - * @Route(path="/{projectId}/{shareKey}", name="view_shared_project_timesheets", methods={"GET","POST"}) - * @param string $projectId - * @param string $shareKey - * @param Request $request - * @return \Symfony\Component\HttpFoundation\Response - */ - public function indexAction(string $projectId, string $shareKey, Request $request) + #[Route(path: '/{id}/{shareKey}', name: 'view_shared_project_timesheets', methods: ['GET', 'POST'])] + public function indexAction(Project $project, string $shareKey, Request $request): Response { - // Receive parameters. $givenPassword = $request->get('spt-password'); - $year = (int)$request->get('year', date('Y')); - $month = (int)$request->get('month', date('m')); + $year = (int) $request->get('year', date('Y')); + $month = (int) $request->get('month', date('m')); $detailsMode = $request->get('details', 'table'); // Get project. $sharedProject = $this->sharedProjectTimesheetRepository->findByProjectAndShareKey( - $projectId, + $project->getId(), $shareKey ); if ($sharedProject === null) { - throw new NotFoundHttpException("Project not found."); - } - - if ($sharedProject->getShareKey() !== $shareKey) { - return $this->render( - '@SharedProjectTimesheets/view/error.html.twig', - ['error' => 'shared_project_timesheets.view.error.access_denied'] - ); + throw $this->createNotFoundException('Project not found'); } // Check access. if (!$this->viewService->hasAccess($sharedProject, $givenPassword)) { - return $this->render( - '@SharedProjectTimesheets/view/auth.html.twig', - [ - 'project' => $sharedProject->getProject(), - 'invalidPassword' => $request->isMethod("POST") && $givenPassword !== null, - ] - ); + return $this->render('@SharedProjectTimesheets/view/auth.html.twig', [ + 'project' => $sharedProject->getProject(), + 'invalidPassword' => $request->isMethod('POST') && $givenPassword !== null, + ]); } // Get time records. @@ -100,8 +66,8 @@ public function indexAction(string $projectId, string $shareKey, Request $reques // Define currency. $currency = 'EUR'; - $customer = $sharedProject->getProject()->getCustomer(); - if ( $customer !== null ) { + $customer = $sharedProject->getProject()?->getCustomer(); + if ($customer !== null) { $currency = $customer->getCurrency(); } @@ -113,22 +79,18 @@ public function indexAction(string $projectId, string $shareKey, Request $reques $statsPerDay = ($monthlyChartVisible && $detailsMode === 'chart') ? $this->viewService->getMonthlyStats($sharedProject, $year, $month) : null; - return $this->render( - '@SharedProjectTimesheets/view/timesheet.html.twig', - [ - 'sharedProject' => $sharedProject, - 'timeRecords' => $timeRecords, - 'rateSum' => $rateSum, - 'durationSum' => $durationSum, - 'year' => $year, - 'month' => $month, - 'currency' => $currency, - 'statsPerMonth' => $statsPerMonth, - 'monthlyChartVisible' => $monthlyChartVisible, - 'statsPerDay' => $statsPerDay, - 'detailsMode' => $detailsMode, - ] - ); + return $this->render('@SharedProjectTimesheets/view/timesheet.html.twig', [ + 'sharedProject' => $sharedProject, + 'timeRecords' => $timeRecords, + 'rateSum' => $rateSum, + 'durationSum' => $durationSum, + 'year' => $year, + 'month' => $month, + 'currency' => $currency, + 'statsPerMonth' => $statsPerMonth, + 'monthlyChartVisible' => $monthlyChartVisible, + 'statsPerDay' => $statsPerDay, + 'detailsMode' => $detailsMode, + ]); } - } diff --git a/DependencyInjection/SharedProjectTimesheetsExtension.php b/DependencyInjection/SharedProjectTimesheetsExtension.php index bf5b65a..ad04457 100644 --- a/DependencyInjection/SharedProjectTimesheetsExtension.php +++ b/DependencyInjection/SharedProjectTimesheetsExtension.php @@ -1,6 +1,7 @@ load('services.yaml'); } + public function prepend(ContainerBuilder $container): void + { + $container->prependExtensionConfig('kimai', [ + 'permissions' => [ + 'roles' => [ + 'ROLE_SUPER_ADMIN' => [ + 'shared_projects', + ], + ], + ], + ]); + + $container->prependExtensionConfig('security', [ + 'password_hashers' => [ + 'shared_projects' => 'auto', + ], + ]); + } } diff --git a/Entity/SharedProjectTimesheet.php b/Entity/SharedProjectTimesheet.php index 1f8a613..f8ec597 100644 --- a/Entity/SharedProjectTimesheet.php +++ b/Entity/SharedProjectTimesheet.php @@ -1,6 +1,7 @@ id; + } - /** - * @return Project - */ public function getProject(): ?Project { return $this->project; } - /** - * @param Project $project - */ - public function setProject(Project $project): SharedProjectTimesheet + public function setProject(Project $project): void { $this->project = $project; - - return $this; } - /** - * @return string - */ public function getShareKey(): ?string { return $this->shareKey; } - /** - * @param string $shareKey - * @return SharedProjectTimesheet - */ - public function setShareKey(string $shareKey): SharedProjectTimesheet + public function setShareKey(string $shareKey): void { $this->shareKey = $shareKey; - - return $this; } - /** - * @return string|null - */ public function getPassword(): ?string { return $this->password; } - /** - * @param string|null $password - * @return SharedProjectTimesheet - */ - public function setPassword(?string $password): SharedProjectTimesheet + public function setPassword(?string $password): void { $this->password = $password; - - return $this; } - /** - * @return bool - */ public function isEntryUserVisible(): bool { return $this->entryUserVisible; } - /** - * @param bool $entryUserVisible - * @return SharedProjectTimesheet - */ - public function setEntryUserVisible(bool $entryUserVisible): SharedProjectTimesheet + public function setEntryUserVisible(bool $entryUserVisible): void { $this->entryUserVisible = (bool) $entryUserVisible; - - return $this; } - /** - * @return bool - */ public function isEntryRateVisible(): bool { return $this->entryRateVisible; } - /** - * @param bool $entryRateVisible - * @return SharedProjectTimesheet - */ - public function setEntryRateVisible(bool $entryRateVisible): SharedProjectTimesheet + public function setEntryRateVisible(bool $entryRateVisible): void { $this->entryRateVisible = (bool) $entryRateVisible; + } - return $this; + public function hasRecordMerging(): bool + { + return $this->recordMergeMode !== RecordMergeMode::MODE_NONE; } - /** - * @return string - */ public function getRecordMergeMode(): string { return $this->recordMergeMode; } - /** - * @param string $recordMergeMode - * @return SharedProjectTimesheet - */ - public function setRecordMergeMode(string $recordMergeMode): SharedProjectTimesheet + public function setRecordMergeMode(string $recordMergeMode): void { $this->recordMergeMode = $recordMergeMode; - - return $this; } - /** - * @return bool - */ public function isAnnualChartVisible(): bool { return $this->annualChartVisible; } - /** - * @param bool $annualChartVisible - * @return SharedProjectTimesheet - */ - public function setAnnualChartVisible(bool $annualChartVisible): SharedProjectTimesheet + public function setAnnualChartVisible(bool $annualChartVisible): void { $this->annualChartVisible = $annualChartVisible; - - return $this; } - /** - * @return bool - */ public function isMonthlyChartVisible(): bool { return $this->monthlyChartVisible; } - /** - * @param bool $monthlyChartVisible - * @return SharedProjectTimesheet - */ - public function setMonthlyChartVisible(bool $monthlyChartVisible): SharedProjectTimesheet + public function setMonthlyChartVisible(bool $monthlyChartVisible): void { $this->monthlyChartVisible = $monthlyChartVisible; - - return $this; } - } diff --git a/EventSubscriber/MenuSubscriber.php b/EventSubscriber/MenuSubscriber.php index 4bc5ca9..0bd8b74 100644 --- a/EventSubscriber/MenuSubscriber.php +++ b/EventSubscriber/MenuSubscriber.php @@ -1,6 +1,7 @@ security = $security; } - /** - * @return array - */ public static function getSubscribedEvents(): array { return [ @@ -40,16 +28,14 @@ public static function getSubscribedEvents(): array ]; } - /** - * @param \App\Event\ConfigureMainMenuEvent $event - */ - public function onMenuConfigure(ConfigureMainMenuEvent $event) + public function onMenuConfigure(ConfigureMainMenuEvent $event): void { - if ($this->security->isGranted('IS_AUTHENTICATED_REMEMBERED') && - $this->security->isGranted('ROLE_SUPER_ADMIN')) { - $event->getSystemMenu()->addChild( - new MenuItemModel('manage_shared_project_timesheets', 'shared_project_timesheets.menu.title', 'manage_shared_project_timesheets', [], 'fas fa-receipt') - ); + if (!$this->security->isGranted('shared_projects')) { + return; } + + $event->getAppsMenu()->addChild( + new MenuItemModel('manage_shared_project_timesheets', 'shared_project_timesheets.menu.title', 'manage_shared_project_timesheets', [], 'fas fa-receipt') + ); } } diff --git a/EventSubscriber/SharedProjectSubscriber.php b/EventSubscriber/SharedProjectSubscriber.php new file mode 100644 index 0000000..2d99ab3 --- /dev/null +++ b/EventSubscriber/SharedProjectSubscriber.php @@ -0,0 +1,43 @@ +getPayload(); + + if (!\is_array($payload) || !\array_key_exists('shared_project', $payload)) { + return; + } + + /** @var SharedProjectTimesheet $sharedProject */ + $sharedProject = $payload['shared_project']; + + if ($sharedProject->getId() === null || $sharedProject->getProject() === null) { + return; + } + + $event->addAction('edit', ['url' => $this->path('update_shared_project_timesheets', ['projectId' => $sharedProject->getProject()->getId(), 'shareKey' => $sharedProject->getShareKey()])]); + $event->addAction('project', ['url' => $this->path('project_details', ['id' => $sharedProject->getProject()->getId()])]); + $event->addDelete($this->path('remove_shared_project_timesheets', ['projectId' => $sharedProject->getProject()->getId(), 'shareKey' => $sharedProject->getShareKey()]), false); + } +} diff --git a/EventSubscriber/SharedProjectsSubscriber.php b/EventSubscriber/SharedProjectsSubscriber.php new file mode 100644 index 0000000..276c62c --- /dev/null +++ b/EventSubscriber/SharedProjectsSubscriber.php @@ -0,0 +1,27 @@ +addCreate($this->path('create_shared_project_timesheets')); + } +} diff --git a/Form/SharedProjectFormType.php b/Form/SharedProjectFormType.php index 89c82dc..d1c22a5 100644 --- a/Form/SharedProjectFormType.php +++ b/Form/SharedProjectFormType.php @@ -1,6 +1,7 @@ $mergeRecordTypes, ]) ->add('password', PasswordType::class, [ - 'label' => 'label.password', + 'label' => 'password', 'required' => false, 'always_empty' => false, 'mapped' => false, @@ -54,11 +53,11 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'inherit_data' => true, 'required' => false, ]) - ->add('entryUserVisible', CheckboxType::class, [ + ->add('entryUserVisible', YesNoType::class, [ 'label' => 'shared_project_timesheets.manage.form.entry_user_visible', 'required' => false, ]) - ->add('entryRateVisible', CheckboxType::class, [ + ->add('entryRateVisible', YesNoType::class, [ 'label' => 'shared_project_timesheets.manage.form.entry_rate_visible', 'required' => false, ]) @@ -70,32 +69,31 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'inherit_data' => true, 'required' => false, ]) - ->add('annualChartVisible', CheckboxType::class, [ + ->add('annualChartVisible', YesNoType::class, [ 'label' => 'shared_project_timesheets.manage.form.annual_chart_visible', 'required' => false, ]) - ->add('monthlyChartVisible', CheckboxType::class, [ + ->add('monthlyChartVisible', YesNoType::class, [ 'label' => 'shared_project_timesheets.manage.form.monthly_chart_visible', 'required' => false, ]) - ) - ->add('save', SubmitType::class, [ - 'label' => 'action.save', - ]); + ); } public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'data_class' => SharedProjectTimesheet::class, + 'attr' => [ + 'data-form-event' => 'kimai.sharedProject' + ], ]); } - public function finishView(FormView $view, FormInterface $form, array $options) + public function finishView(FormView $view, FormInterface $form, array $options): void { if (!empty($form->get('password')->getData())) { $view['password']->vars['value'] = ManageService::PASSWORD_DO_NOT_CHANGE_VALUE; } } - } diff --git a/Migrations/Version2020120600000.php b/Migrations/Version2020120600000.php index 8f0b66b..2c5e615 100644 --- a/Migrations/Version2020120600000.php +++ b/Migrations/Version2020120600000.php @@ -1,59 +1,54 @@ hasTable(self::SHARED_PROJECT_TIMESHEETS_TABLE_NAME)) { - $table = $schema->createTable(self::SHARED_PROJECT_TIMESHEETS_TABLE_NAME); - $table->addColumn('id', Types::INTEGER, ['autoincrement' => true, 'notnull' => true]); - $table->addColumn('project_id', Types::INTEGER, ['notnull' => true]); - $table->addColumn('share_key', Types::STRING, ['length' => 20, 'notnull' => true]); - $table->addColumn('password', Types::STRING, ['length' => 255, 'default' => null, 'notnull' => false]); - $table->addColumn('entry_user_visible', Types::BOOLEAN, ['default' => false, 'notnull' => true]); - $table->addColumn('entry_rate_visible', Types::BOOLEAN, ['default' => false, 'notnull' => true]); - - $table->setPrimaryKey(['id']); - $table->addIndex(['share_key']); - $table->addUniqueIndex(['project_id', 'share_key']); - $table->addForeignKeyConstraint( - 'kimai2_projects', - ['project_id'], - ['id'], - [ - 'onUpdate' => 'CASCADE', - 'onDelete' => 'CASCADE', - ] - ); - } + $table = $schema->createTable(self::SHARED_PROJECT_TIMESHEETS_TABLE_NAME); + $table->addColumn('id', Types::INTEGER, ['autoincrement' => true, 'notnull' => true]); + $table->addColumn('project_id', Types::INTEGER, ['notnull' => true]); + $table->addColumn('share_key', Types::STRING, ['length' => 20, 'notnull' => true]); + $table->addColumn('password', Types::STRING, ['length' => 255, 'default' => null, 'notnull' => false]); + $table->addColumn('entry_user_visible', Types::BOOLEAN, ['default' => false, 'notnull' => true]); + $table->addColumn('entry_rate_visible', Types::BOOLEAN, ['default' => false, 'notnull' => true]); + + $table->setPrimaryKey(['id']); + $table->addIndex(['share_key']); + $table->addUniqueIndex(['project_id', 'share_key']); + $table->addForeignKeyConstraint( + 'kimai2_projects', + ['project_id'], + ['id'], + [ + 'onUpdate' => 'CASCADE', + 'onDelete' => 'CASCADE', + ] + ); } public function down(Schema $schema): void { - if ($schema->hasTable(self::SHARED_PROJECT_TIMESHEETS_TABLE_NAME)) { - $schema->dropTable(self::SHARED_PROJECT_TIMESHEETS_TABLE_NAME); - } + $schema->dropTable(self::SHARED_PROJECT_TIMESHEETS_TABLE_NAME); } } diff --git a/Migrations/Version2020120920000.php b/Migrations/Version2020120920000.php index 09587f0..c225af7 100644 --- a/Migrations/Version2020120920000.php +++ b/Migrations/Version2020120920000.php @@ -1,45 +1,40 @@ hasTable(Version2020120600000::SHARED_PROJECT_TIMESHEETS_TABLE_NAME)) { - $table = $schema->getTable(Version2020120600000::SHARED_PROJECT_TIMESHEETS_TABLE_NAME); - $table->addColumn( - 'record_merge_mode', - Types::STRING, - ['length' => 50, 'notnull' => true, 'default' => RecordMergeMode::MODE_NONE] - ); - } + $table = $schema->getTable(Version2020120600000::SHARED_PROJECT_TIMESHEETS_TABLE_NAME); + $table->addColumn( + 'record_merge_mode', + Types::STRING, + ['length' => 50, 'notnull' => true, 'default' => RecordMergeMode::MODE_NONE] + ); } public function down(Schema $schema): void { - if ($schema->hasTable(Version2020120600000::SHARED_PROJECT_TIMESHEETS_TABLE_NAME)) { - $table = $schema->getTable(Version2020120600000::SHARED_PROJECT_TIMESHEETS_TABLE_NAME); - $table->dropColumn('record_merge_mode'); - } + $table = $schema->getTable(Version2020120600000::SHARED_PROJECT_TIMESHEETS_TABLE_NAME); + $table->dropColumn('record_merge_mode'); } } diff --git a/Migrations/Version2020122010000.php b/Migrations/Version2020122010000.php index 4b1fe5a..da0a7fd 100644 --- a/Migrations/Version2020122010000.php +++ b/Migrations/Version2020122010000.php @@ -1,45 +1,39 @@ hasTable(Version2020120600000::SHARED_PROJECT_TIMESHEETS_TABLE_NAME)) { - $table = $schema->getTable(Version2020120600000::SHARED_PROJECT_TIMESHEETS_TABLE_NAME); + $table = $schema->getTable(Version2020120600000::SHARED_PROJECT_TIMESHEETS_TABLE_NAME); - $table->addColumn('annual_chart_visible', Types::BOOLEAN, ['default' => false, 'notnull' => true]); - $table->addColumn('monthly_chart_visible', Types::BOOLEAN, ['default' => false, 'notnull' => true]); - } + $table->addColumn('annual_chart_visible', Types::BOOLEAN, ['default' => false, 'notnull' => true]); + $table->addColumn('monthly_chart_visible', Types::BOOLEAN, ['default' => false, 'notnull' => true]); } public function down(Schema $schema): void { - if ($schema->hasTable(Version2020120600000::SHARED_PROJECT_TIMESHEETS_TABLE_NAME)) { - $table = $schema->getTable(Version2020120600000::SHARED_PROJECT_TIMESHEETS_TABLE_NAME); + $table = $schema->getTable(Version2020120600000::SHARED_PROJECT_TIMESHEETS_TABLE_NAME); - $table->dropColumn('annual_chart_visible'); - $table->dropColumn('monthly_chart_visible'); - } + $table->dropColumn('annual_chart_visible'); + $table->dropColumn('monthly_chart_visible'); } - } diff --git a/Migrations/shared-project-timesheets.yaml b/Migrations/shared-project-timesheets.yaml index 6d56bcd..6afe1a4 100644 --- a/Migrations/shared-project-timesheets.yaml +++ b/Migrations/shared-project-timesheets.yaml @@ -4,9 +4,10 @@ # # Execute all migrations with: # bin/console kimai:bundle:shared-project-timesheets:install +# bin/console doctrine:migrations:migrate --em=default --configuration=var/plugins/SharedProjectTimesheetsBundle/Migrations/shared-project-timesheets.yaml # -------------------------------------------------------------------------------------------------- table_storage: - table_name: bundle_migration_shared_project_timesheets + table_name: 'bundle_migration_shared_project_timesheets' migrations_paths: - KimaiPlugin\SharedProjectTimesheetsBundle\Migrations: var/plugins/SharedProjectTimesheetsBundle/Migrations + KimaiPlugin\SharedProjectTimesheetsBundle\Migrations: 'var/plugins/SharedProjectTimesheetsBundle/Migrations' diff --git a/Model/ChartStat.php b/Model/ChartStat.php index 7766ee5..336cb75 100644 --- a/Model/ChartStat.php +++ b/Model/ChartStat.php @@ -1,6 +1,7 @@ duration = $resultRow !== null && isset($resultRow['duration']) ? $resultRow['duration'] : 0; - $this->rate = $resultRow !== null && isset($resultRow['rate']) ? $resultRow['rate'] : 0; + $this->duration = (int) ($resultRow !== null && isset($resultRow['duration']) ? $resultRow['duration'] : 0); + $this->rate = (float) ($resultRow !== null && isset($resultRow['rate']) ? $resultRow['rate'] : 0.0); } - /** - * @return mixed - */ - public function getDuration() + public function getDuration(): int { return $this->duration; } - /** - * @return mixed - */ - public function getRate() + public function getRate(): float { return $this->rate; } - -} \ No newline at end of file +} diff --git a/Model/RecordMergeMode.php b/Model/RecordMergeMode.php index f6f8097..55559da 100644 --- a/Model/RecordMergeMode.php +++ b/Model/RecordMergeMode.php @@ -1,6 +1,7 @@ + */ + public static function getModes(): array + { return [ self::MODE_NONE => 'shared_project_timesheets.model.merge_record_mode.none', self::MODE_MERGE => 'shared_project_timesheets.model.merge_record_mode.merge', @@ -25,5 +29,4 @@ public static function getModes() { self::MODE_MERGE_USE_LAST_OF_DAY => 'shared_project_timesheets.model.merge_record_mode.merge_use_last_of_day', ]; } - } diff --git a/Model/TimeRecord.php b/Model/TimeRecord.php index a76101e..b57d94e 100644 --- a/Model/TimeRecord.php +++ b/Model/TimeRecord.php @@ -1,6 +1,7 @@ */ + public const VALID_MERGE_MODES = [ RecordMergeMode::MODE_MERGE, RecordMergeMode::MODE_MERGE_USE_FIRST_OF_DAY, RecordMergeMode::MODE_MERGE_USE_LAST_OF_DAY, ]; - /** - * Create time record of timesheet entity. - * @param Timesheet $timesheet - * @param string $mergeMode - * @return TimeRecord - */ - public static function fromTimesheet(Timesheet $timesheet, $mergeMode = RecordMergeMode::MODE_MERGE): TimeRecord { - if ( !in_array($mergeMode, self::VALID_MERGE_MODES) ) { + public static function fromTimesheet(Timesheet $timesheet, string $mergeMode = RecordMergeMode::MODE_MERGE): TimeRecord + { + if (!\in_array($mergeMode, self::VALID_MERGE_MODES)) { throw new \InvalidArgumentException("Invalid merge mode given: $mergeMode"); } - return (new TimeRecord($timesheet->getBegin(), $timesheet->getUser(), $mergeMode)) - ->addTimesheet($timesheet); - } - - /** - * @var DateTime - */ - private $date; - - /** - * @var string - */ - private $description = null; - - /** - * @var float[] - */ - private $hourlyRates = []; - - /** - * @var float - */ - private $rate = 0.0; - - /** - * @var int - */ - private $duration = 0; + $record = new TimeRecord($timesheet->getBegin(), $timesheet->getUser(), $mergeMode); + $record->addTimesheet($timesheet); - /** - * @var User - */ - private $user; + return $record; + } + private ?\DateTimeInterface $date = null; + private ?string $description = null; /** - * @var string + * @var array> */ - private $mergeMode; + private array $hourlyRates = []; + private float $rate = 0.0; + private int $duration = 0; + private ?User $user = null; + private ?string $mergeMode = null; - /** - * Private constructor, use fromTimesheet() to create instances. - * @param DateTime $date - * @param User $user - * @param string $mergeMode - */ - private function __construct(DateTime $date, User $user, string $mergeMode) + private function __construct(\DateTimeInterface $date, User $user, string $mergeMode) { $this->date = $date; $this->user = $user; $this->mergeMode = $mergeMode; } - /** - * @return DateTime - */ - public function getDate(): DateTime + public function getDate(): \DateTimeInterface { return $this->date; } - /** - * @return string - */ public function getDescription(): ?string { return $this->description; } /** - * @return int[] + * @return array> */ - public function getHourlyRates() + public function getHourlyRates(): array { return $this->hourlyRates; } - /** - * @return float - */ - public function getRate() + public function getRate(): float { return $this->rate; } - /** - * @return int - */ - public function getDuration() + public function getDuration(): int { return $this->duration; } - /** - * @return User - */ public function getUser(): User { return $this->user; @@ -139,24 +90,22 @@ public function getUser(): User // Helper methods - public function hasDifferentHourlyRates() + public function hasDifferentHourlyRates(): bool { - return count($this->hourlyRates) > 1; + return \count($this->hourlyRates) > 1; } - public function addTimesheet(Timesheet $timesheet) + public function addTimesheet(Timesheet $timesheet): void { - $this->addHourlyRate($timesheet->getHourlyRate(), $timesheet->getDuration()) - ->addRate($timesheet->getRate()) - ->addDuration($timesheet->getDuration()) - ->setDescription($timesheet); - - return $this; + $this->addHourlyRate($timesheet->getHourlyRate(), $timesheet->getDuration()); + $this->addRate($timesheet->getRate()); + $this->addDuration($timesheet->getDuration()); + $this->setDescription($timesheet); } - protected function addHourlyRate($hourlyRate, $duration) + protected function addHourlyRate(?float $hourlyRate, ?int $duration): void { - if ( $hourlyRate > 0 && $duration > 0 ) { + if ($hourlyRate > 0 && $duration > 0) { $entryIndex = null; foreach ($this->hourlyRates as $index => $info) { if ($info['hourlyRate'] === $hourlyRate) { @@ -174,51 +123,42 @@ protected function addHourlyRate($hourlyRate, $duration) $this->hourlyRates[$entryIndex]['duration'] += $duration; } } - - return $this; } - private function addRate(?float $rate) + private function addRate(?float $rate): void { - if ( $rate !== null ) { + if ($rate !== null) { $this->rate += $rate; } - - return $this; } - private function addDuration(?int $duration) + private function addDuration(?int $duration): void { - if ( $duration !== null ) { + if ($duration !== null) { $this->duration += $duration; } - - return $this; } - protected function setDescription(Timesheet $timesheet) + protected function setDescription(Timesheet $timesheet): void { $description = $timesheet->getDescription(); // Merge description dependent on record merge mode if ($this->description === null) { $this->description = $description; - } else if ($this->mergeMode === RecordMergeMode::MODE_MERGE_USE_LAST_OF_DAY && $this->getDate() < $timesheet->getBegin()) { + } elseif ($this->mergeMode === RecordMergeMode::MODE_MERGE_USE_LAST_OF_DAY && $this->getDate() < $timesheet->getBegin()) { // Override description on last $this->description = $timesheet->getDescription(); - } else if ($this->mergeMode === RecordMergeMode::MODE_MERGE) { + } elseif ($this->mergeMode === RecordMergeMode::MODE_MERGE) { // MODE_MERGE - if ($description !== null && strlen($description) > 0) { + if ($description !== null && \strlen($description) > 0) { $this->description = ( - implode(PHP_EOL, [ + implode(PHP_EOL, [ $this->getDescription(), $description ]) ); } } - - return $this; } - -} \ No newline at end of file +} diff --git a/README.md b/README.md index 634fd6c..e90107c 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,17 @@ -# Shared Project Timesheets Bundle - Kimai 2 Plugin +# Shared Project Timesheets Bundle -A Kimai 2 plugin that allows you to share your project timesheets with anyone you want to grant access to. +**THIS IS A CLONE OF https://github.com/dexterity42/SharedProjectTimesheetsBundle** + +The original author seems to have abandoned the plugin and is not responding. +Due to popular demand it was forked and extended and **IS working with Kimai 2.0+**. + +---- + +You can share your project report (budgets, timesheets) with anyone by a public URL and optional password. ## Features -- Create publicly accessible urls for the project timesheets you want to share +- Create publicly accessible URL for a project - Access control feature - protect the shared project timesheets with a password - View control feature @@ -17,10 +24,16 @@ A Kimai 2 plugin that allows you to share your project timesheets with anyone yo ## Installation +This plugin is compatible with the following Kimai releases: + +| Bundle version | Minimum Kimai version | +|----------------|-----------------------| +| 3.0.0 | 2.0.26 | + First clone this plugin to your Kimai installation `plugins` directory: ``` cd /kimai/var/plugins/ -git clone https://github.com/dexterity42/SharedProjectTimesheetsBundle.git +git clone https://github.com/Keleo/SharedProjectTimesheetsBundle.git ``` Go back to the root of your Kimai installation and clear the cache: @@ -35,8 +48,8 @@ Execute database migrations: bin/console kimai:bundle:shared-project-timesheets:install ``` -You're done. Open up your browser and navigate to "Shared project timesheets". +You're done. Open up your browser and navigate to "Applications > Shared project timesheets". ## Permissions -Currently, there are no specific plugin permissions. The role `ROLE_SUPER_ADMIN` is required to manage the shared project timesheets. +The permission `shared_projects` is required to manage the "shared project timesheets", which is assigned to the role `ROLE_SUPER_ADMIN` by default. diff --git a/Repository/SharedProjectTimesheetRepository.php b/Repository/SharedProjectTimesheetRepository.php index 7e1d7e6..8d5faa2 100644 --- a/Repository/SharedProjectTimesheetRepository.php +++ b/Repository/SharedProjectTimesheetRepository.php @@ -1,6 +1,7 @@ createQueryBuilder('spt') + $qb = $this->createQueryBuilder('spt') ->join(Project::class, 'p', Join::WITH, 'spt.project = p') - ->orderBy('p.name, spt.shareKey', 'ASC') - ->getQuery() - ->execute(); + ->orderBy('p.name, spt.shareKey', 'ASC'); + + $loader = new LoaderPaginator(new DefaultLoader(), $qb, $this->count([])); + + $paginator = new Pagination($loader); + $paginator->setMaxPerPage($query->getPageSize()); + $paginator->setCurrentPage($query->getPage()); + + return $paginator; } - /** - * @param SharedProjectTimesheet $sharedProject - * @throws \Doctrine\ORM\ORMException - * @throws \Doctrine\ORM\OptimisticLockException - */ - public function save(SharedProjectTimesheet $sharedProject) + public function save(SharedProjectTimesheet $sharedProject): void { $em = $this->getEntityManager(); $em->persist($sharedProject); $em->flush(); } - /** - * @param SharedProjectTimesheet $sharedProject - * @throws \Doctrine\ORM\ORMException - * @throws \Doctrine\ORM\OptimisticLockException - */ - public function remove(SharedProjectTimesheet $sharedProject) + public function remove(SharedProjectTimesheet $sharedProject): void { $em = $this->getEntityManager(); $em->remove($sharedProject); $em->flush(); } - /** - * @param Project|int|null $project - * @param string|null $shareKey - * @return SharedProjectTimesheet|null - */ - public function findByProjectAndShareKey($project, ?string $shareKey) + public function findByProjectAndShareKey(Project|int|null $project, ?string $shareKey): ?SharedProjectTimesheet { try { return $this->createQueryBuilder('spt') @@ -76,5 +67,4 @@ public function findByProjectAndShareKey($project, ?string $shareKey) return null; } } - } diff --git a/Resources/config/services.yaml b/Resources/config/services.yaml index 354d34f..41a7ce9 100644 --- a/Resources/config/services.yaml +++ b/Resources/config/services.yaml @@ -6,7 +6,12 @@ services: KimaiPlugin\SharedProjectTimesheetsBundle\: resource: '../../*' - exclude: '../../{Resources}' + exclude: + - '../../Entity/' + - '../../Migrations/' + - '../../Model/' + - '../../Resources/' + - '../../tests/' KimaiPlugin\SharedProjectTimesheetsBundle\Controller\: resource: '../../Controller' diff --git a/Resources/translations/messages.de.yaml b/Resources/translations/messages.de.yaml index 91ebfcc..dcaa33d 100644 --- a/Resources/translations/messages.de.yaml +++ b/Resources/translations/messages.de.yaml @@ -7,7 +7,6 @@ shared_project_timesheets: open: Öffnen copy: Kopieren copied: Kopiert - subtitle: Verwalten Sie Ihre geteilten Projektzeiten create_link_label: Fügen Sie einen Eintrag hinzu, um Projektzeiten zu teilen create: subtitle: Eintrag hinzufügen diff --git a/Resources/translations/messages.en.yaml b/Resources/translations/messages.en.yaml index 8dea586..bdff50b 100644 --- a/Resources/translations/messages.en.yaml +++ b/Resources/translations/messages.en.yaml @@ -7,7 +7,6 @@ shared_project_timesheets: open: Open copy: Copy copied: Copied - subtitle: Manage your shared project timesheets create_link_label: Create an entry to share your project timesheets create: subtitle: Create entry diff --git a/Resources/views/manage/actions.html.twig b/Resources/views/manage/actions.html.twig index 39b89a7..920fe1d 100644 --- a/Resources/views/manage/actions.html.twig +++ b/Resources/views/manage/actions.html.twig @@ -1,25 +1,2 @@ -{% macro sharedProjectIndex(view) %} - {% import "macros/widgets.html.twig" as widgets %} - {% set actions = {} %} - {% set actions = actions|merge({'create': path('create_shared_project_timesheets')}) %} - - {% set event = trigger('actions.sharedProjectIndex', {'actions': actions, 'view': view}) %} - {{ widgets.page_actions(event.payload.actions) }} -{% endmacro %} - -{% macro sharedProjectChild(view) %} - {% import "macros/widgets.html.twig" as widgets %} - {% set actions = {} %} - {% set actions = actions|merge({'back': path('manage_shared_project_timesheets')}) %} - {% set event = trigger('actions.sharedProjectChild', {'actions': actions, 'view': view}) %} - {{ widgets.page_actions(event.payload.actions) }} -{% endmacro %} - -{% macro removeSharedProject(sharedProject, view) %} - {% import "macros/widgets.html.twig" as widgets %} - {% set actions = {} %} - {% set actions = actions|merge({'edit': path('update_shared_project_timesheets', {projectId: sharedProject.project.id, shareKey: sharedProject.shareKey})}) %} - {% set actions = actions|merge({'trash': path('remove_shared_project_timesheets', {projectId: sharedProject.project.id, shareKey: sharedProject.shareKey})}) %} - {% set event = trigger('actions.removeSharedProject', {'actions': actions, 'view': view}) %} - {{ widgets.table_actions(event.payload.actions) }} +{% macro sharedProject(sharedProject) %} {% endmacro %} diff --git a/Resources/views/manage/edit.html.twig b/Resources/views/manage/edit.html.twig index c3666ae..68d5b1d 100644 --- a/Resources/views/manage/edit.html.twig +++ b/Resources/views/manage/edit.html.twig @@ -1,15 +1,12 @@ -{% extends 'base.html.twig' %} -{% import "@SharedProjectTimesheets/manage/actions.html.twig" as actions %} -{% import "macros/widgets.html.twig" as widgets %} - -{% block page_title %}{{ 'shared_project_timesheets.manage.title' | trans }}{% endblock %} -{% block page_subtitle %}{{ ('shared_project_timesheets.manage.' ~ type ~ '.subtitle') | trans }}{% endblock %} -{% block page_actions %}{{ actions.sharedProjectChild('create') }}{% endblock %} +{% extends kimai_context.modalRequest ? 'form.html.twig' : 'base.html.twig' %} {% block main %} -
-
- {{ form(form) }} -
-
+ {% set formEditTemplate = kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig' %} + {% set formOptions = { + 'title': (entity.id is null ? 'create'|trans : 'edit'|trans({}, 'actions')), + 'form': form, + 'back': path('manage_shared_project_timesheets') + } %} + {% embed formEditTemplate with formOptions %} + {% endembed %} {% endblock %} diff --git a/Resources/views/manage/index.html.twig b/Resources/views/manage/index.html.twig index 5f05464..8991dd0 100644 --- a/Resources/views/manage/index.html.twig +++ b/Resources/views/manage/index.html.twig @@ -1,146 +1,39 @@ -{% extends 'base.html.twig' %} -{% import "@SharedProjectTimesheets/manage/actions.html.twig" as actions %} -{% import "macros/datatables.html.twig" as tables %} +{% extends 'datatable.html.twig' %} {% import "macros/widgets.html.twig" as widgets %} +{% import "@SharedProjectTimesheets/manage/actions.html.twig" as actions %} -{% set columns = { - 'name': {'class': 'alwaysVisible'}, - 'url': {'class': 'alwaysVisible'}, - 'password': {'class': 'hidden-xs hidden-sm text-center'}, - 'record_merge_mode': {'class': 'hidden-xs hidden-sm text-center'}, - 'entry_user_visible': {'class': 'hidden-xs hidden-sm text-center'}, - 'entry_rate_visible': {'class': 'hidden-xs hidden-sm text-center'}, - 'annual_chart_visible': {'class': 'hidden-xs hidden-sm text-center'}, - 'monthly_chart_visible': {'class': 'hidden-xs hidden-sm text-center'}, - 'actions': {'class': 'hidden-xs hidden-sm'}, -} %} - -{% block page_title %}{{ 'shared_project_timesheets.manage.title' | trans }}{% endblock %} -{% block page_subtitle %}{{ 'shared_project_timesheets.manage.subtitle' | trans }}{% endblock %} -{% block page_actions %}{{ actions.sharedProjectIndex('index') }}{% endblock %} - -{% set tableName = 'shared_project_timesheets_manage' %} - -{% block main_before %} - {{ tables.data_table_column_modal(tableName, columns) }} -{% endblock %} - -{% block main %} - {{ tables.datatable_header(tableName, columns, null, {'translationPrefix': 'shared_project_timesheets.manage.table.'}) }} +{% block datatable_row_attr %} class="modal-ajax-form open-edit" data-href="{{ url('update_shared_project_timesheets', {projectId: entry.project.id, shareKey: entry.shareKey}) }}"{% endblock %} - {% for sharedProject in sharedProjects %} - - {{ sharedProject.project.name }} - - {% if sharedProject.shareKey %} - - {% else %} - - - {% endif %} - - - - - - - {% if sharedProject.recordMergeMode == mergeModeMerge %} - - {% else %} - {% if sharedProject.recordMergeMode != mergeModeNone %} - - {% endif %} - {% endif %} - - - - - - - - - - - - - - {{ actions.removeSharedProject(sharedProject, 'index') }} - - {% endfor %} - {% if sharedProjects is empty %} - - - - - {{ 'shared_project_timesheets.manage.create_link_label' | trans }} +{% block datatable_column_value %} + {% if column == 'name' %} + {{ entry.project.name }} + {% elseif column == 'url' %} + {% if entry.shareKey %} + {% set p_url = url('view_shared_project_timesheets', {id: entry.project.id, shareKey: entry.shareKey}) %} + + {{ p_url }} - - + {% else %} + - + {% endif %} + {% elseif column == 'password' %} + {{ widgets.label_boolean(entry.password != null) }} + {% elseif column == 'record_merge_mode' %} + {% if entry.hasRecordMerging() %} + {{ widgets.label('yes'|trans, 'success', (RecordMergeMode[entry.recordMergeMode] | trans) ) }} + {% else %} + {{ widgets.label_boolean(entry.hasRecordMerging()) }} + {% endif %} + {% elseif column == 'entry_user_visible' %} + {{ widgets.label_boolean(entry.entryUserVisible) }} + {% elseif column == 'entry_rate_visible' %} + {{ widgets.label_boolean(entry.entryRateVisible) }} + {% elseif column == 'annual_chart_visible' %} + {{ widgets.label_boolean(entry.annualChartVisible) }} + {% elseif column == 'monthly_chart_visible' %} + {{ widgets.label_boolean(entry.monthlyChartVisible) }} + {% elseif column == 'actions' %} + {% set event = actions(app.user, 'shared_project', 'index', {'shared_project': entry}) %} + {{ widgets.table_actions(event.actions) }} {% endif %} - - {{ tables.data_table_footer() }} -{% endblock %} - -{% block stylesheets %} -{{ parent() }} - -{% endblock %} - -{% block javascripts %} -{{ parent() }} - - - {% endblock %} diff --git a/Resources/views/top-nav-layout/base.html.twig b/Resources/views/top-nav-layout/base.html.twig deleted file mode 100644 index cbb15b5..0000000 --- a/Resources/views/top-nav-layout/base.html.twig +++ /dev/null @@ -1,142 +0,0 @@ -{% extends '@SharedProjectTimesheets/top-nav-layout/layout.html.twig' %} - -{% block body_start %} - data-title="{{- get_title() -}}" -{% endblock %} - -{% block after_body_start %} - {% embed 'embeds/modal.html.twig' %} - {% block modal_id %}remote_form_modal{% endblock %} - {% block modal_title %}{% endblock %} - {% block modal_body %}{% endblock %} - {% block modal_footer %}{% endblock %} - {% endembed %} -{% endblock %} - -{% block page_content_before %} - {% set event = trigger(constant('App\\Event\\ThemeEvent::CONTENT_BEFORE')) %} - {{ event.content|raw }} -
- {% block main_before %}{% endblock %} -
-{% endblock %} - -{% block page_content_after %} - {% block main_after %}{% endblock %} - {% set event = trigger(constant('App\\Event\\ThemeEvent::CONTENT_AFTER')) %} - {{ event.content|raw }} -{% endblock %} - -{% block page_content %} - {% set event = trigger(constant('App\\Event\\ThemeEvent::CONTENT_START')) %} - {{ event.content|raw }} - {% block main %}{% endblock %} - {% set event = trigger(constant('App\\Event\\ThemeEvent::CONTENT_END')) %} - {{ event.content|raw }} -{% endblock %} - -{% block title %} - {{- get_title() -}} -{% endblock %} - -{% block page_subtitle %}{% endblock %} - -{% block logo_mini %} - {% if not kimai_context.branding.mini is empty %} - {{ kimai_context.branding.mini|raw }} - {% else %} - KTT - {% endif %} -{% endblock %} - -{% block logo_large %} - {% if not kimai_context.branding.company is empty %} - {{ kimai_context.branding.company|raw }} - {% else %} - Kimai - Time Tracking - {% endif %} -{% endblock %} - -{% block footer %} - -{% endblock %} - -{% block navbar_start %}{% endblock %} - - -{# these blocks and the hook-in logic by the AdminTheme could be re-used by Kimai or an extension at some point #} -{% block navbar_messages %}{% endblock %} -{% block navbar_notifications %}{% endblock %} -{% block navbar_tasks %}{% endblock %} -{% block navbar_end %}{% endblock %} - -{# deactivated blocks, as Kimai does not ship the sidebar for UX reasons #} -{% block sidebar_user %}{% endblock %} -{% block sidebar_search %}{% endblock %} - -{% block navbar_user %}{% endblock %} - -{% block breadcrumb %} - {% block page_search %}{% endblock %} - {% block page_actions %}{% endblock %} -{% endblock %} - -{% block stylesheets %} - {# we do not call parent() as we use a custom built for the frontend assets and don't want the default #} - {{ encore_entry_link_tags('app') }} - {% set event = trigger(constant('App\\Event\\ThemeEvent::STYLESHEET')) %} - {{ event.content|raw }} -{% endblock %} - -{% block head %} - {{ parent() }} - {{ encore_entry_script_tags('app') }} - {% include 'partials/head.html.twig' %} - {% set event = trigger(constant('App\\Event\\ThemeEvent::HTML_HEAD')) %} - {{ event.content|raw }} -{% endblock %} - -{% block javascripts %} - {# no call to parent(), as we use a custom built for the frontend assets and don't want the default - {% set event = trigger(constant('App\\Event\\ThemeEvent::JAVASCRIPT')) %} - {{ event.content|raw }} -{% endblock %} diff --git a/Resources/views/top-nav-layout/layout.html.twig b/Resources/views/top-nav-layout/layout.html.twig deleted file mode 100644 index 3a93fee..0000000 --- a/Resources/views/top-nav-layout/layout.html.twig +++ /dev/null @@ -1,116 +0,0 @@ -{# - Use this as your new starter template page, use it to start your new project, - by adding this code to your own base template: - - {% extends '@AdminLTE/layout/top-nav-layout.html.twig' %} - - Enjoy your AdminLTE theme! -#} - - - - {% block head %} - - - - {% endblock %} - {% block title %}{{ block('page_title') }}{% endblock %} - {% block stylesheets %} - - {% endblock %} - -{# -Apply one of the following classes for the skin: -skin-blue, skin-black, skin-purple, skin-yellow, skin-red, skin-green -#} - -{% block after_body_start %}{% endblock %} -
- -
- -
- -
- {% block content_header %} -
-

- {% block page_title %}{{ 'Admin LTE'|trans({}, 'AdminLTEBundle') }}{% endblock %} - {% block page_subtitle %}{{ 'A short subtitle for your page'|trans({}, 'AdminLTEBundle') }}{% endblock %} -

- - {% block breadcrumb %} - {% if admin_lte_context.knp_menu.enable %} - {% include '@AdminLTE/Breadcrumb/knp-breadcrumb.html.twig' %} - {% else %} - {{ render(controller('KevinPapst\\AdminLTEBundle\\Controller\\BreadcrumbController::breadcrumbAction', {'request':app.request})) }} - {% endif %} - {% endblock %} -
- {% endblock %} - - {% block page_content_before %}{% endblock %} - -
- {% block page_content_start %}{{ include('@AdminLTE/Partials/_flash_messages.html.twig') }}{% endblock %} - {% block page_content %}{% endblock %} - {% block page_content_end %}{% endblock %} -
- - {% block page_content_after %}{% endblock %} -
- - {% block footer %} - {% include '@AdminLTE/Partials/_footer.html.twig' %} - {% endblock %} - {% block control_sidebar %} - {% if admin_lte_context.control_sidebar %} - {% include '@AdminLTE/Partials/_control-sidebar.html.twig' %} - {% endif %} - {% endblock %} - -
- -{% block javascripts %} - -{% endblock %} - - - diff --git a/Resources/views/view/auth.html.twig b/Resources/views/view/auth.html.twig index 76b980b..788c661 100644 --- a/Resources/views/view/auth.html.twig +++ b/Resources/views/view/auth.html.twig @@ -1,24 +1,26 @@ -{% extends '@SharedProjectTimesheets/top-nav-layout/base.html.twig' %} +{% extends '@theme/fullpage.html.twig' %} -{% block page_title %}{{ 'shared_project_timesheets.view.auth.title' | trans }} {{ project.name }}{% endblock %} -{% block page_subtitle %}{% endblock %} +{% block page_content %} +
+
+

{{ 'shared_project_timesheets.view.auth.title' | trans }}
{{ project.name }}

+
-{% block main %} -
-
-
- {% if invalidPassword %} -
{{ 'shared_project_timesheets.view.auth.invalid_password' | trans }}
- {% endif %} -

{{ 'shared_project_timesheets.view.auth.description' | trans }}

-

- - -

-
- +{% endblock %} diff --git a/Resources/views/view/chart/annual-chart.html.twig b/Resources/views/view/chart/annual-chart.html.twig index 08c0c88..e203fc3 100644 --- a/Resources/views/view/chart/annual-chart.html.twig +++ b/Resources/views/view/chart/annual-chart.html.twig @@ -1,9 +1,9 @@ {% set chartId = 'statsPerMonthChart' %} -{% set backgroundColor = kimai_context.chart.background_color %} -{% set gridColor = kimai_context.chart.grid_color %} +{% set backgroundColor = config('theme.chart.background_color') %} +{% set gridColor = config('theme.chart.grid_color') %}
- +
\ No newline at end of file + diff --git a/Resources/views/view/chart/monthly-chart.html.twig b/Resources/views/view/chart/monthly-chart.html.twig index fcb1ffe..21b7821 100644 --- a/Resources/views/view/chart/monthly-chart.html.twig +++ b/Resources/views/view/chart/monthly-chart.html.twig @@ -1,9 +1,9 @@ {% set chartId = 'statsPerDayChart' %} -{% set backgroundColor = kimai_context.chart.background_color %} -{% set gridColor = kimai_context.chart.grid_color %} +{% set backgroundColor = config('theme.chart.background_color') %} +{% set gridColor = config('theme.chart.grid_color') %} -
- +
+
\ No newline at end of file + diff --git a/Resources/views/view/error.html.twig b/Resources/views/view/error.html.twig deleted file mode 100644 index 898d5d6..0000000 --- a/Resources/views/view/error.html.twig +++ /dev/null @@ -1,12 +0,0 @@ -{% extends '@SharedProjectTimesheets/top-nav-layout/base.html.twig' %} - -{% block page_title %}{{ 'shared_project_timesheets.view.error.title' | trans }} {{ project.name }}{% endblock %} -{% block page_subtitle %}{% endblock %} - -{% block main %} -
-
-

{{ error | trans }}

-
-
-{% endblock %} \ No newline at end of file diff --git a/Resources/views/view/timesheet.html.twig b/Resources/views/view/timesheet.html.twig index f55ffb8..0ecbf8b 100644 --- a/Resources/views/view/timesheet.html.twig +++ b/Resources/views/view/timesheet.html.twig @@ -1,187 +1,173 @@ -{% extends '@SharedProjectTimesheets/top-nav-layout/base.html.twig' %} +{% extends '@theme/fullpage.html.twig' %} +{% from "macros/widgets.html.twig" import nothing_found %} -{% block page_title %}{{ 'shared_project_timesheets.view.title' | trans }} {{ sharedProject.project.name }}{% endblock %} -{% block page_subtitle %}{{ 'shared_project_timesheets.view.subtitle' | trans }}{% endblock %} +{% block title %}{{ 'shared_project_timesheets.view.title' | trans }} {{ sharedProject.project.name }}{% endblock %} {% block stylesheets %} {{ parent() }} + {{ encore_entry_link_tags('app') }} {{ encore_entry_link_tags('chart') }} - {% endblock %} {% block head %} {{ parent() }} + {{ encore_entry_script_tags('app') }} {{ encore_entry_script_tags('chart') }} {% endblock %} -{% block main %} -
-
-
-
-

{{ 'shared_project_timesheets.view.selection_title' | trans }}

-
-
-
- -
-
- - - -
- - - -
- - - -
-
-
-
+{% block page_classes %}page{% endblock %} + +{% block page_content %} +
+ +

+ {{ sharedProject.project.name }} + + - {% if statsPerMonth != null %} -
-
-
-

{{ 'shared_project_timesheets.view.chart.per_month_title' | trans({'%year%': year}) }}

+ +
+ + + +
+ + + +
+ + + +
+

+ + {% if statsPerMonth != null %} +
+
+
+
+

{{ 'shared_project_timesheets.view.chart.per_month_title' | trans({'%year%': year}) }}

-
+
{% include '@SharedProjectTimesheets/view/chart/annual-chart.html.twig' with {year: year, month: month, statsPerMonth: statsPerMonth} %}
- {% endif %}
+ {% endif %} -
-
-

{{ 'shared_project_timesheets.view.table.title' | trans }}

+
+
+

{{ 'shared_project_timesheets.view.table.title' | trans }}

{% if monthlyChartVisible %} - +
+ +
{% endif %}
-
- {% if statsPerDay == null %} - - - - - - {% if sharedProject.entryUserVisible %} - - {% endif %} - - {% if sharedProject.entryRateVisible %} - - - {% endif %} - - - {% for record in timeRecords %} - - - - {% if sharedProject.entryUserVisible %} - - {% endif %} - - {% if sharedProject.entryRateVisible %} - {% if record.differentHourlyRates %} - - {% else %} - +
+ {% if timeRecords is empty %} + {{ nothing_found() }} + {% elseif statsPerDay == null %} +
{{ 'shared_project_timesheets.view.table.date' | trans }}{{ 'shared_project_timesheets.view.table.description' | trans }}{{ 'shared_project_timesheets.view.table.user' | trans }}{{ 'shared_project_timesheets.view.table.duration' | trans }}{{ 'shared_project_timesheets.view.table.rate_hour' | trans }}{{ 'shared_project_timesheets.view.table.rate_total' | trans }}
{{ record.date | date_short }}{{ record.description | e | nl2br }}{{ record.user.displayName }}{{ record.duration | duration }} - {% for info in record.hourlyRates %} -
{{ info.duration | duration }} - {{ info.hourlyRate | format_currency(currency) }}
- {% endfor %} -
{{ record.hourlyRates[0].hourlyRate | format_currency(currency) }}
+ + + + {% if sharedProject.entryUserVisible %} + + {% endif %} + + + {% if sharedProject.entryRateVisible %} + + + {% endif %} + + + {% for record in timeRecords %} + + + {% if sharedProject.entryUserVisible %} + + {% endif %} + + + {% if sharedProject.entryRateVisible %} + {% if record.differentHourlyRates %} + + {% else %} + + {% endif %} + {% endif %} - - {% endif %} - - {% endfor %} - {% if timeRecords is not empty %} - - - - - {% if sharedProject.entryUserVisible %} - - {% endif %} - - {% if sharedProject.entryRateVisible %} - - - {% endif %} - - - {% endif %} -
{{ 'shared_project_timesheets.view.table.date' | trans }}{{ 'shared_project_timesheets.view.table.user' | trans }}{{ 'shared_project_timesheets.view.table.description' | trans }}{{ 'shared_project_timesheets.view.table.duration' | trans }}{{ 'hourlyRate' | trans }}{{ 'total_rate' | trans }}
{{ record.date | date_short }}{{ record.user.displayName }}{{ record.description | e | nl2br }}{{ record.duration | duration }} + {% for info in record.hourlyRates %} +
{{ info.duration | duration }} - {{ info.hourlyRate | format_currency(currency) }}
+ {% endfor %} +
+ {% if record.hourlyRates is not empty %} + {{ record.hourlyRates[0].hourlyRate | format_currency(currency) }} + {% endif %} + {{ record.rate | format_currency(currency) }}{{ record.rate | format_currency(currency) }}
{{ durationSum | duration }}{{ rateSum | format_currency(currency) }}
+ + {% endfor %} + {% if timeRecords is not empty %} + + + + {% if sharedProject.entryUserVisible %} + + {% endif %} + + {{ durationSum | duration }} + {% if sharedProject.entryRateVisible %} + + {{ rateSum | format_currency(currency) }} + {% endif %} + + + {% endif %} + {% else %} {% include '@SharedProjectTimesheets/view/chart/monthly-chart.html.twig' with {year: year, month: month, statsPerDay: statsPerDay} %} {% endif %} - - {% if timeRecords is empty %} -

{{ 'shared_project_timesheets.view.table.empty' | trans }}

- {% endif %}
+ +
{% endblock %} \ No newline at end of file diff --git a/Service/ManageService.php b/Service/ManageService.php index 6265fd0..e950dfb 100644 --- a/Service/ManageService.php +++ b/Service/ManageService.php @@ -1,6 +1,7 @@ sharedProjectTimesheetRepository = $sharedProjectTimesheetRepository; + public function __construct(private SharedProjectTimesheetRepository $sharedProjectTimesheetRepository, private PasswordHasherFactoryInterface $passwordHasherFactory) + { + } - $this->encoder = new NativePasswordEncoder(); + private function getPasswordHasher(): PasswordHasherInterface + { + return $this->passwordHasherFactory->getPasswordHasher('shared_projects'); } /** @@ -48,10 +40,10 @@ public function __construct(SharedProjectTimesheetRepository $sharedProjectTimes public function create(SharedProjectTimesheet $sharedProjectTimesheet, ?string $password = null): SharedProjectTimesheet { // Set share key - if ( $sharedProjectTimesheet->getShareKey() === null ) { + if ($sharedProjectTimesheet->getShareKey() === null) { do { $sharedProjectTimesheet->setShareKey( - substr(preg_replace("/[^A-Za-z0-9]+/", "", $this->getUuidV4()), 0, 12) + substr(preg_replace('/[^A-Za-z0-9]+/', '', $this->getUuidV4()), 0, 12) ); $existingEntry = $this->sharedProjectTimesheetRepository->findByProjectAndShareKey( @@ -74,7 +66,7 @@ public function update(SharedProjectTimesheet $sharedProjectTimesheet, ?string $ { // Check if updatable if ($sharedProjectTimesheet->getShareKey() === null) { - throw new \InvalidArgumentException("Cannot update shared project timesheet with share key equals null"); + throw new \InvalidArgumentException('Cannot update shared project timesheet with share key equals null'); } // Ensure project @@ -85,10 +77,10 @@ public function update(SharedProjectTimesheet $sharedProjectTimesheet, ?string $ // Handle password $currentHashedPassword = $sharedProjectTimesheet !== null && !empty($sharedProjectTimesheet->getPassword()) ? $sharedProjectTimesheet->getPassword() : null; - if ( $newPassword !== self::PASSWORD_DO_NOT_CHANGE_VALUE ) { + if ($newPassword !== self::PASSWORD_DO_NOT_CHANGE_VALUE) { if (!empty($newPassword)) { // Change password - $encodedPassword = $this->encoder->encodePassword($newPassword, null); + $encodedPassword = $this->getPasswordHasher()->hash($newPassword); $sharedProjectTimesheet->setPassword($encodedPassword); } else { // Reset password if exists @@ -99,18 +91,22 @@ public function update(SharedProjectTimesheet $sharedProjectTimesheet, ?string $ } $this->sharedProjectTimesheetRepository->save($sharedProjectTimesheet); + return $sharedProjectTimesheet; } /** - * @link https://www.php.net/manual/en/function.uniqid.php#94959 + * @see https://www.php.net/manual/en/function.uniqid.php#94959 * @return string */ - private function getUuidV4() { - return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + private function getUuidV4(): string + { + return sprintf( + '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', // 32 bits for "time_low" - mt_rand(0, 0xffff), mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + mt_rand(0, 0xffff), // 16 bits for "time_mid" mt_rand(0, 0xffff), @@ -125,8 +121,9 @@ private function getUuidV4() { mt_rand(0, 0x3fff) | 0x8000, // 48 bits for "node" - mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) + mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + mt_rand(0, 0xffff) ); } - } diff --git a/Service/ViewService.php b/Service/ViewService.php index 66026ee..d7e3461 100644 --- a/Service/ViewService.php +++ b/Service/ViewService.php @@ -1,6 +1,7 @@ timesheetRepository = $timesheetRepository; - $this->session = $session; - $this->encoder = ($encoder instanceof PasswordEncoderInterface) ? $encoder : new NativePasswordEncoder(); + return $this->passwordHasherFactory->getPasswordHasher('shared_projects'); } /** * Check if the user has access to the given shared project timesheet. - * @param SharedProjectTimesheet $sharedProject - * @param $givenPassword - * @return bool */ - public function hasAccess(SharedProjectTimesheet $sharedProject, $givenPassword): bool + public function hasAccess(SharedProjectTimesheet $sharedProject, ?string $givenPassword): bool { $hashedPassword = $sharedProject->getPassword(); @@ -72,13 +49,13 @@ public function hasAccess(SharedProjectTimesheet $sharedProject, $givenPassword) $sessionPasswordKey = "spt-authed-$projectId-$shareKey-$passwordMd5"; - if (!$this->session->has($sessionPasswordKey)) { + if (!$this->request->getSession()->has($sessionPasswordKey)) { // Check given password - if (empty($givenPassword) || !$this->encoder->isPasswordValid($hashedPassword, $givenPassword, null)) { + if (empty($givenPassword) || !$this->getPasswordHasher()->verify($hashedPassword, $givenPassword)) { return false; } - $this->session->set($sessionPasswordKey, true); + $this->request->getSession()->set($sessionPasswordKey, true); } } @@ -116,22 +93,22 @@ public function getTimeRecords(SharedProjectTimesheet $sharedProject, int $year, $timeRecords = []; $mergeMode = $sharedProject->getRecordMergeMode(); foreach ($timesheets as $timesheet) { - $dateKey = $timesheet->getBegin()->format("Y-m-d"); - if (!array_key_exists($dateKey, $timeRecords)) { + $dateKey = $timesheet->getBegin()->format('Y-m-d'); + if (!\array_key_exists($dateKey, $timeRecords)) { $timeRecords[$dateKey] = []; } - $userKey = preg_replace("/[^a-z0-9]/", "", strtolower($timesheet->getUser()->getDisplayName())); + $userKey = preg_replace('/[^a-z0-9]/', '', strtolower($timesheet->getUser()->getDisplayName())); if ($mergeMode !== RecordMergeMode::MODE_NONE) { // Assume that records from one user will be merged into one - if (!array_key_exists($userKey, $timeRecords[$dateKey])) { + if (!\array_key_exists($userKey, $timeRecords[$dateKey])) { $timeRecords[$dateKey][$userKey] = [TimeRecord::fromTimesheet($timesheet, $mergeMode)]; } else { $timeRecords[$dateKey][$userKey][0]->addTimesheet($timesheet); } } else { // One user can be assigned to multiple records per day - $time = $timesheet->getBegin()->format("H-i-s"); + $time = $timesheet->getBegin()->format('H-i-s'); $timeRecords[$dateKey][$userKey][$time] = TimeRecord::fromTimesheet($timesheet); } } @@ -193,6 +170,7 @@ public function getAnnualStats(SharedProjectTimesheet $sharedProject, int $year) } ksort($stats); + return $stats; } @@ -242,7 +220,7 @@ public function getMonthlyStats(SharedProjectTimesheet $sharedProject, int $year } ksort($stats); + return $stats; } - } diff --git a/SharedProjectTimesheetsBundle.php b/SharedProjectTimesheetsBundle.php index 891b47e..5010f5e 100644 --- a/SharedProjectTimesheetsBundle.php +++ b/SharedProjectTimesheetsBundle.php @@ -1,6 +1,7 @@ getDuration()); self::assertEquals(2.2, $chartStat->getRate()); } - -} \ No newline at end of file +} diff --git a/tests/Model/RecordMergeModeTest.php b/tests/Model/RecordMergeModeTest.php index 49e6264..5587459 100644 --- a/tests/Model/RecordMergeModeTest.php +++ b/tests/Model/RecordMergeModeTest.php @@ -1,6 +1,7 @@ setDescription($description); } - function testInvalidTimesheet(): void + public function testInvalidTimesheet(): void { - $this->expectErrorMessage("null given"); + $this->expectErrorMessage('null given'); TimeRecord::fromTimesheet(new Timesheet()); } - function testValidEmptyTimesheet(): void + public function testValidEmptyTimesheet(): void { $begin = new DateTime(); $user = new User(); @@ -68,13 +67,13 @@ function testValidEmptyTimesheet(): void self::assertEquals([], $timeRecord->getHourlyRates()); } - function testValidFilledTimesheet(): void + public function testValidFilledTimesheet(): void { $hours = 2.1; $hourlyRate = 123.456; $rate = $hours * $hourlyRate; $duration = $hours * 60 * 60; - $description = "description"; + $description = 'description'; $timeRecord = TimeRecord::fromTimesheet( self::createTimesheet(new DateTime(), new User(), $hourlyRate, $duration, $description) @@ -86,11 +85,11 @@ function testValidFilledTimesheet(): void self::assertNotEmpty($timeRecord->getHourlyRates()); self::assertEquals(false, $timeRecord->hasDifferentHourlyRates()); - self::assertEquals($hourlyRate, $timeRecord->getHourlyRates()[0]["hourlyRate"]); - self::assertEquals($duration, $timeRecord->getHourlyRates()[0]["duration"]); + self::assertEquals($hourlyRate, $timeRecord->getHourlyRates()[0]['hourlyRate']); + self::assertEquals($duration, $timeRecord->getHourlyRates()[0]['duration']); } - function testMergeModeNull(): void + public function testMergeModeNull(): void { $this->expectException(\InvalidArgumentException::class); @@ -100,7 +99,7 @@ function testMergeModeNull(): void ); } - function testMergeModeNone(): void + public function testMergeModeNone(): void { $this->expectException(\InvalidArgumentException::class); @@ -110,7 +109,7 @@ function testMergeModeNone(): void ); } - function testMergeModeRandom(): void + public function testMergeModeRandom(): void { $this->expectException(\InvalidArgumentException::class); @@ -120,19 +119,19 @@ function testMergeModeRandom(): void ); } - function testMergeModeDefaultSameRate(): void + public function testMergeModeDefaultSameRate(): void { $hourlyRate = 123.456; $firstRecordHours = 2.1; $firstRecordRate = $firstRecordHours * $hourlyRate; $firstRecordDuration = $firstRecordHours * 60 * 60; - $firstRecordDescription = "description-first"; + $firstRecordDescription = 'description-first'; $secondRecordHours = 3.8; $secondRecordRate = $secondRecordHours * $hourlyRate; $secondRecordDuration = $secondRecordHours * 60 * 60; - $secondRecordDescription = "description-second"; + $secondRecordDescription = 'description-second'; $timesheet1 = self::createTimesheet(new DateTime(), new User(), $hourlyRate, $firstRecordDuration, $firstRecordDescription); $timesheet2 = self::createTimesheet(new DateTime(), new User(), $hourlyRate, $secondRecordDuration, $secondRecordDescription); @@ -146,23 +145,23 @@ function testMergeModeDefaultSameRate(): void self::assertNotEmpty($timeRecord->getHourlyRates()); self::assertEquals(false, $timeRecord->hasDifferentHourlyRates()); - self::assertEquals($hourlyRate, $timeRecord->getHourlyRates()[0]["hourlyRate"]); - self::assertEquals($firstRecordDuration + $secondRecordDuration, $timeRecord->getHourlyRates()[0]["duration"]); + self::assertEquals($hourlyRate, $timeRecord->getHourlyRates()[0]['hourlyRate']); + self::assertEquals($firstRecordDuration + $secondRecordDuration, $timeRecord->getHourlyRates()[0]['duration']); } - function testMergeModeUseFirstSameRate(): void + public function testMergeModeUseFirstSameRate(): void { $hourlyRate = 123.456; $firstRecordHours = 2.1; $firstRecordRate = $firstRecordHours * $hourlyRate; $firstRecordDuration = $firstRecordHours * 60 * 60; - $firstRecordDescription = "description-first"; + $firstRecordDescription = 'description-first'; $secondRecordHours = 3.8; $secondRecordRate = $secondRecordHours * $hourlyRate; $secondRecordDuration = $secondRecordHours * 60 * 60; - $secondRecordDescription = "description-second"; + $secondRecordDescription = 'description-second'; $timesheet1 = self::createTimesheet(new DateTime(), new User(), $hourlyRate, $firstRecordDuration, $firstRecordDescription); $timesheet2 = self::createTimesheet(new DateTime(), new User(), $hourlyRate, $secondRecordDuration, $secondRecordDescription); @@ -176,23 +175,23 @@ function testMergeModeUseFirstSameRate(): void self::assertNotEmpty($timeRecord->getHourlyRates()); self::assertEquals(false, $timeRecord->hasDifferentHourlyRates()); - self::assertEquals($hourlyRate, $timeRecord->getHourlyRates()[0]["hourlyRate"]); - self::assertEquals($firstRecordDuration + $secondRecordDuration, $timeRecord->getHourlyRates()[0]["duration"]); + self::assertEquals($hourlyRate, $timeRecord->getHourlyRates()[0]['hourlyRate']); + self::assertEquals($firstRecordDuration + $secondRecordDuration, $timeRecord->getHourlyRates()[0]['duration']); } - function testMergeModeUseLastSameRate(): void + public function testMergeModeUseLastSameRate(): void { $hourlyRate = 123.456; $firstRecordHours = 2.1; $firstRecordRate = $firstRecordHours * $hourlyRate; $firstRecordDuration = $firstRecordHours * 60 * 60; - $firstRecordDescription = "description-first"; + $firstRecordDescription = 'description-first'; $secondRecordHours = 3.8; $secondRecordRate = $secondRecordHours * $hourlyRate; $secondRecordDuration = $secondRecordHours * 60 * 60; - $secondRecordDescription = "description-second"; + $secondRecordDescription = 'description-second'; $timesheet1 = self::createTimesheet(new DateTime(), new User(), $hourlyRate, $firstRecordDuration, $firstRecordDescription); $timesheet2 = self::createTimesheet(new DateTime(), new User(), $hourlyRate, $secondRecordDuration, $secondRecordDescription); @@ -206,23 +205,23 @@ function testMergeModeUseLastSameRate(): void self::assertNotEmpty($timeRecord->getHourlyRates()); self::assertEquals(false, $timeRecord->hasDifferentHourlyRates()); - self::assertEquals($hourlyRate, $timeRecord->getHourlyRates()[0]["hourlyRate"]); - self::assertEquals($firstRecordDuration + $secondRecordDuration, $timeRecord->getHourlyRates()[0]["duration"]); + self::assertEquals($hourlyRate, $timeRecord->getHourlyRates()[0]['hourlyRate']); + self::assertEquals($firstRecordDuration + $secondRecordDuration, $timeRecord->getHourlyRates()[0]['duration']); } - function testMergeModeDefaultDifferentRate(): void + public function testMergeModeDefaultDifferentRate(): void { $firstRecordHours = 2.1; $firstRecordHourlyRate = 123.456; $firstRecordRate = $firstRecordHours * $firstRecordHourlyRate; $firstRecordDuration = $firstRecordHours * 60 * 60; - $firstRecordDescription = "description-first"; + $firstRecordDescription = 'description-first'; $secondRecordHours = 3.8; $secondRecordHourlyRate = 234.567; $secondRecordRate = $secondRecordHours * $secondRecordHourlyRate; $secondRecordDuration = $secondRecordHours * 60 * 60; - $secondRecordDescription = "description-second"; + $secondRecordDescription = 'description-second'; $timesheet1 = self::createTimesheet(new DateTime(), new User(), $firstRecordHourlyRate, $firstRecordDuration, $firstRecordDescription); $timesheet2 = self::createTimesheet(new DateTime(), new User(), $secondRecordHourlyRate, $secondRecordDuration, $secondRecordDescription); @@ -236,25 +235,25 @@ function testMergeModeDefaultDifferentRate(): void self::assertNotEmpty($timeRecord->getHourlyRates()); self::assertEquals(true, $timeRecord->hasDifferentHourlyRates()); - self::assertEquals($firstRecordHourlyRate, $timeRecord->getHourlyRates()[0]["hourlyRate"]); - self::assertEquals($firstRecordDuration, $timeRecord->getHourlyRates()[0]["duration"]); - self::assertEquals($secondRecordHourlyRate, $timeRecord->getHourlyRates()[1]["hourlyRate"]); - self::assertEquals($secondRecordDuration, $timeRecord->getHourlyRates()[1]["duration"]); + self::assertEquals($firstRecordHourlyRate, $timeRecord->getHourlyRates()[0]['hourlyRate']); + self::assertEquals($firstRecordDuration, $timeRecord->getHourlyRates()[0]['duration']); + self::assertEquals($secondRecordHourlyRate, $timeRecord->getHourlyRates()[1]['hourlyRate']); + self::assertEquals($secondRecordDuration, $timeRecord->getHourlyRates()[1]['duration']); } - function testMergeModeUseFirstDifferentRate(): void + public function testMergeModeUseFirstDifferentRate(): void { $firstRecordHours = 2.1; $firstRecordHourlyRate = 123.456; $firstRecordRate = $firstRecordHours * $firstRecordHourlyRate; $firstRecordDuration = $firstRecordHours * 60 * 60; - $firstRecordDescription = "description-first"; + $firstRecordDescription = 'description-first'; $secondRecordHours = 3.8; $secondRecordHourlyRate = 234.567; $secondRecordRate = $secondRecordHours * $secondRecordHourlyRate; $secondRecordDuration = $secondRecordHours * 60 * 60; - $secondRecordDescription = "description-second"; + $secondRecordDescription = 'description-second'; $timesheet1 = self::createTimesheet(new DateTime(), new User(), $firstRecordHourlyRate, $firstRecordDuration, $firstRecordDescription); $timesheet2 = self::createTimesheet(new DateTime(), new User(), $secondRecordHourlyRate, $secondRecordDuration, $secondRecordDescription); @@ -268,25 +267,25 @@ function testMergeModeUseFirstDifferentRate(): void self::assertNotEmpty($timeRecord->getHourlyRates()); self::assertEquals(true, $timeRecord->hasDifferentHourlyRates()); - self::assertEquals($firstRecordHourlyRate, $timeRecord->getHourlyRates()[0]["hourlyRate"]); - self::assertEquals($firstRecordDuration, $timeRecord->getHourlyRates()[0]["duration"]); - self::assertEquals($secondRecordHourlyRate, $timeRecord->getHourlyRates()[1]["hourlyRate"]); - self::assertEquals($secondRecordDuration, $timeRecord->getHourlyRates()[1]["duration"]); + self::assertEquals($firstRecordHourlyRate, $timeRecord->getHourlyRates()[0]['hourlyRate']); + self::assertEquals($firstRecordDuration, $timeRecord->getHourlyRates()[0]['duration']); + self::assertEquals($secondRecordHourlyRate, $timeRecord->getHourlyRates()[1]['hourlyRate']); + self::assertEquals($secondRecordDuration, $timeRecord->getHourlyRates()[1]['duration']); } - function testMergeModeUseLastDifferentRate(): void + public function testMergeModeUseLastDifferentRate(): void { $firstRecordHours = 2.1; $firstRecordHourlyRate = 123.456; $firstRecordRate = $firstRecordHours * $firstRecordHourlyRate; $firstRecordDuration = $firstRecordHours * 60 * 60; - $firstRecordDescription = "description-first"; + $firstRecordDescription = 'description-first'; $secondRecordHours = 3.8; $secondRecordHourlyRate = 234.567; $secondRecordRate = $secondRecordHours * $secondRecordHourlyRate; $secondRecordDuration = $secondRecordHours * 60 * 60; - $secondRecordDescription = "description-second"; + $secondRecordDescription = 'description-second'; $timesheet1 = self::createTimesheet(new DateTime(), new User(), $firstRecordHourlyRate, $firstRecordDuration, $firstRecordDescription); $timesheet2 = self::createTimesheet(new DateTime(), new User(), $secondRecordHourlyRate, $secondRecordDuration, $secondRecordDescription); @@ -300,10 +299,9 @@ function testMergeModeUseLastDifferentRate(): void self::assertNotEmpty($timeRecord->getHourlyRates()); self::assertEquals(true, $timeRecord->hasDifferentHourlyRates()); - self::assertEquals($firstRecordHourlyRate, $timeRecord->getHourlyRates()[0]["hourlyRate"]); - self::assertEquals($firstRecordDuration, $timeRecord->getHourlyRates()[0]["duration"]); - self::assertEquals($secondRecordHourlyRate, $timeRecord->getHourlyRates()[1]["hourlyRate"]); - self::assertEquals($secondRecordDuration, $timeRecord->getHourlyRates()[1]["duration"]); + self::assertEquals($firstRecordHourlyRate, $timeRecord->getHourlyRates()[0]['hourlyRate']); + self::assertEquals($firstRecordDuration, $timeRecord->getHourlyRates()[0]['duration']); + self::assertEquals($secondRecordHourlyRate, $timeRecord->getHourlyRates()[1]['hourlyRate']); + self::assertEquals($secondRecordDuration, $timeRecord->getHourlyRates()[1]['duration']); } - -} \ No newline at end of file +} diff --git a/tests/Service/ManageServiceTest.php b/tests/Service/ManageServiceTest.php index 432e8e9..b29a287 100644 --- a/tests/Service/ManageServiceTest.php +++ b/tests/Service/ManageServiceTest.php @@ -1,6 +1,7 @@ createMock(SharedProjectTimesheetRepository::class); - $repository->method('save') - ->willReturnArgument(0); + $repository->method('save')->willReturnArgument(0); + + $factory = $this->createMock(PasswordHasherFactoryInterface::class); + $hasher = $this->createMock(PasswordHasherInterface::class); + $factory->method('getPasswordHasher')->willReturn($hasher); - $this->service = new ManageService($repository); + $this->service = new ManageService($repository, $factory); } public function testCreateSuccess(): void { - $sharedProjectTimesheet = (new SharedProjectTimesheet()) - ->setProject(new Project()) - ->setRecordMergeMode(RecordMergeMode::MODE_MERGE) - ->setEntryUserVisible(true) - ->setEntryRateVisible(true) - ->setAnnualChartVisible(true) - ->setMonthlyChartVisible(true); + $sharedProjectTimesheet = new SharedProjectTimesheet(); + $sharedProjectTimesheet->setProject(new Project()); + $sharedProjectTimesheet->setRecordMergeMode(RecordMergeMode::MODE_MERGE); + $sharedProjectTimesheet->setEntryUserVisible(true); + $sharedProjectTimesheet->setEntryRateVisible(true); + $sharedProjectTimesheet->setAnnualChartVisible(true); + $sharedProjectTimesheet->setMonthlyChartVisible(true); $saved = $this->service->create($sharedProjectTimesheet); @@ -57,6 +59,7 @@ public function testCreateSuccess(): void self::assertTrue($saved->isAnnualChartVisible()); self::assertTrue($saved->isMonthlyChartVisible()); } + public function testDefaultValues(): void { $sharedProjectTimesheet = new SharedProjectTimesheet(); @@ -72,10 +75,10 @@ public function testDefaultValues(): void public function testCreatePassword(): void { - $sharedProjectTimesheet = (new SharedProjectTimesheet()) - ->setProject(new Project()); + $sharedProjectTimesheet = (new SharedProjectTimesheet()); + $sharedProjectTimesheet->setProject(new Project()); - $saved = $this->service->create($sharedProjectTimesheet, "password"); + $saved = $this->service->create($sharedProjectTimesheet, 'password'); self::assertNotNull($saved->getPassword()); } @@ -88,21 +91,21 @@ public function testCreateInvalidProject(): void public function testUpdateSuccess(): void { - $sharedProjectTimesheet = (new SharedProjectTimesheet()) - ->setShareKey("sharekey") - ->setProject(new Project()) - ->setPassword("password") - ->setRecordMergeMode(RecordMergeMode::MODE_MERGE) - ->setEntryUserVisible(true) - ->setEntryRateVisible(true) - ->setAnnualChartVisible(true) - ->setMonthlyChartVisible(true); - - $saved = $this->service->update($sharedProjectTimesheet, "newPassword"); - - self::assertEquals("sharekey", $saved->getShareKey()); + $sharedProjectTimesheet = (new SharedProjectTimesheet()); + $sharedProjectTimesheet->setShareKey('sharekey'); + $sharedProjectTimesheet->setProject(new Project()); + $sharedProjectTimesheet->setPassword('password'); + $sharedProjectTimesheet->setRecordMergeMode(RecordMergeMode::MODE_MERGE); + $sharedProjectTimesheet->setEntryUserVisible(true); + $sharedProjectTimesheet->setEntryRateVisible(true); + $sharedProjectTimesheet->setAnnualChartVisible(true); + $sharedProjectTimesheet->setMonthlyChartVisible(true); + + $saved = $this->service->update($sharedProjectTimesheet, 'newPassword'); + + self::assertEquals('sharekey', $saved->getShareKey()); self::assertNotNull($saved->getProject()); - self::assertNotEquals("newPassword", $saved->getPassword()); + self::assertNotEquals('newPassword', $saved->getPassword()); self::assertEquals(RecordMergeMode::MODE_MERGE, $saved->getRecordMergeMode()); self::assertTrue($saved->isEntryUserVisible()); self::assertTrue($saved->isEntryRateVisible()); @@ -112,28 +115,28 @@ public function testUpdateSuccess(): void public function testUpdatePasswordDoesNotChange(): void { - $sharedProjectTimesheet = (new SharedProjectTimesheet()) - ->setShareKey("sharekey") - ->setProject(new Project()) - ->setPassword("password"); + $sharedProjectTimesheet = (new SharedProjectTimesheet()); + $sharedProjectTimesheet->setShareKey('sharekey'); + $sharedProjectTimesheet->setProject(new Project()); + $sharedProjectTimesheet->setPassword('password'); $saved = $this->service->update($sharedProjectTimesheet, ManageService::PASSWORD_DO_NOT_CHANGE_VALUE); - self::assertEquals("sharekey", $saved->getShareKey()); + self::assertEquals('sharekey', $saved->getShareKey()); self::assertNotNull($saved->getProject()); - self::assertEquals("password", $saved->getPassword()); + self::assertEquals('password', $saved->getPassword()); } public function testUpdatePasswordReset(): void { - $sharedProjectTimesheet = (new SharedProjectTimesheet()) - ->setShareKey("sharekey") - ->setProject(new Project()) - ->setPassword("password"); + $sharedProjectTimesheet = (new SharedProjectTimesheet()); + $sharedProjectTimesheet->setShareKey('sharekey'); + $sharedProjectTimesheet->setProject(new Project()); + $sharedProjectTimesheet->setPassword('password'); $saved = $this->service->update($sharedProjectTimesheet, null); - self::assertEquals("sharekey", $saved->getShareKey()); + self::assertEquals('sharekey', $saved->getShareKey()); self::assertNotNull($saved->getProject()); self::assertNull($saved->getPassword()); } @@ -143,5 +146,4 @@ public function testUpdateInvalidProject(): void $this->expectException(\InvalidArgumentException::class); $this->service->update(new SharedProjectTimesheet()); } - -} \ No newline at end of file +} diff --git a/tests/Service/ViewServiceTest.php b/tests/Service/ViewServiceTest.php index 267cd46..4839744 100644 --- a/tests/Service/ViewServiceTest.php +++ b/tests/Service/ViewServiceTest.php @@ -1,6 +1,7 @@ createMock(TimesheetRepository::class); + $request = new RequestStack(); $this->session = $this->createPartialMock(SessionInterface::class, []); - $this->encoder = $this->createMock(PasswordEncoderInterface::class); - $this->service = new ViewService($timesheetRepository, $this->session, $this->encoder); + $factory = $this->createMock(PasswordHasherFactoryInterface::class); + $this->encoder = $this->createMock(PasswordHasherInterface::class); + $factory->method('getPasswordHasher')->willReturn($this->encoder); + + $this->service = new ViewService($timesheetRepository, $this->session, $factory); } - private function createSharedProjectTimesheet() + private function createSharedProjectTimesheet(): SharedProjectTimesheet { $project = $this->createMock(Project::class); $project->method('getId') ->willReturn(1); - return (new SharedProjectTimesheet()) - ->setProject($project) - ->setShareKey("sharekey"); + $tmp = new SharedProjectTimesheet(); + $tmp->setProject($project); + $tmp->setShareKey('sharekey'); + + return $tmp; } - public function testNoPassword() + public function testNoPassword(): void { $sharedProjectTimesheet = $this->createSharedProjectTimesheet(); - $hasAccess = $this->service->hasAccess($sharedProjectTimesheet, null); + $hasAccess = $this->service->hasAccess($sharedProjectTimesheet, ''); self::assertTrue($hasAccess); } - public function testValidPassword() + public function testValidPassword(): void { $this->encoder->method('isPasswordValid') - ->willReturnCallback(function($hashedPassword, $givenPassword) { + ->willReturnCallback(function ($hashedPassword, $givenPassword) { return $hashedPassword === $givenPassword; }); - $sharedProjectTimesheet = ($this->createSharedProjectTimesheet()) - ->setPassword("password"); + $sharedProjectTimesheet = $this->createSharedProjectTimesheet(); + $sharedProjectTimesheet->setPassword('password'); - $hasAccess = $this->service->hasAccess($sharedProjectTimesheet, "password"); + $hasAccess = $this->service->hasAccess($sharedProjectTimesheet, 'password'); self::assertTrue($hasAccess); } - public function testInvalidPassword() + public function testInvalidPassword(): void { $this->encoder->method('isPasswordValid') - ->willReturnCallback(function($hashedPassword, $givenPassword) { + ->willReturnCallback(function ($hashedPassword, $givenPassword) { return $hashedPassword === $givenPassword; }); - $sharedProjectTimesheet = ($this->createSharedProjectTimesheet()) - ->setPassword("password"); + $sharedProjectTimesheet = $this->createSharedProjectTimesheet(); + $sharedProjectTimesheet->setPassword('password'); - $hasAccess = $this->service->hasAccess($sharedProjectTimesheet, "wrong"); + $hasAccess = $this->service->hasAccess($sharedProjectTimesheet, 'wrong'); self::assertFalse($hasAccess); } - public function testPasswordRemember() + public function testPasswordRemember(): void { // Mock session behaviour $this->session->expects($this->exactly(1)) ->method('set') - ->willReturnCallback(function($key) { + ->willReturnCallback(function ($key) { $this->sessionKey = $key; }); $this->session->expects($this->exactly(2)) ->method('has') - ->willReturnCallback(function($key) { + ->willReturnCallback(function ($key) { return $key === $this->sessionKey; }); // Expect the encoder->isPasswordValid method is called only once $this->encoder->expects($this->exactly(1)) ->method('isPasswordValid') - ->willReturnCallback(function($hashedPassword, $givenPassword) { + ->willReturnCallback(function ($hashedPassword, $givenPassword) { return $hashedPassword === $givenPassword; }); - $sharedProjectTimesheet = ($this->createSharedProjectTimesheet()) - ->setPassword("test"); + $sharedProjectTimesheet = $this->createSharedProjectTimesheet(); + $sharedProjectTimesheet->setPassword('test'); - $this->service->hasAccess($sharedProjectTimesheet, "test"); - $this->service->hasAccess($sharedProjectTimesheet, "test"); + $this->service->hasAccess($sharedProjectTimesheet, 'test'); + $this->service->hasAccess($sharedProjectTimesheet, 'test'); } - public function testPasswordChange() + public function testPasswordChange(): void { // Mock session behaviour $this->session->expects($this->exactly(1)) ->method('set') - ->willReturnCallback(function($key) { + ->willReturnCallback(function ($key) { $this->sessionKey = $key; }); $this->session->expects($this->exactly(2)) ->method('has') - ->willReturnCallback(function($key) { + ->willReturnCallback(function ($key) { return $key === $this->sessionKey; }); // Expect the encoder->isPasswordValid method is called only once $this->encoder->expects($this->exactly(2)) ->method('isPasswordValid') - ->willReturnCallback(function($hashedPassword, $givenPassword) { + ->willReturnCallback(function ($hashedPassword, $givenPassword) { return $hashedPassword === $givenPassword; }); - $sharedProjectTimesheet = ($this->createSharedProjectTimesheet()) - ->setPassword("test"); + $sharedProjectTimesheet = $this->createSharedProjectTimesheet(); + $sharedProjectTimesheet->setPassword('test'); - $hasAccess = $this->service->hasAccess($sharedProjectTimesheet, "test"); + $hasAccess = $this->service->hasAccess($sharedProjectTimesheet, 'test'); self::assertTrue($hasAccess); - $sharedProjectTimesheet = ($this->createSharedProjectTimesheet()) - ->setPassword("changed"); + $sharedProjectTimesheet = $this->createSharedProjectTimesheet(); + $sharedProjectTimesheet->setPassword('changed'); - $hasAccess = $this->service->hasAccess($sharedProjectTimesheet, "test"); + $hasAccess = $this->service->hasAccess($sharedProjectTimesheet, 'test'); self::assertFalse($hasAccess); } - }